文化祭のHPとLINE@のマップ機能を実装した話 (3/4)

はじめに

学校の文化祭で技術系の開発をする団体ができたので、活動の一環としてHPとLINE@の製作に携わりました。
このページでは主にCreateJSの基礎的な部分、開発に使用した技術等を書いています。
文化祭の話については、4つのページに分けています。

1.実装した機能(HP/LINE@)全体の概要

sho0126hiro.hatenablog.com

2.LINE@のmap機能について

sho0126hiro.hatenablog.com

3.HPのmapページについて(概要とCreateJSやHTML,JSの基本事項について)(今回)

4.HPのmapページについて(実装方法、実装時の注意点などについて)

sho0126hiro.hatenablog.com

HPのマップ機能について、分量がかなり多くなったので2つの記事に分けました。

今回はHP-Mapの概要と、基本的な文法について

最後の記事で基本的な文法を使って、どのように実装したかについて書きます。

目次

HP-map機能の概要

HPのマップ画面は、フロントエンドに関しては全て自分で処理しました。 2018年度、夏のインターンシップでCreateJSというJavaScriptライブラリのスイートについて学ばせていただき、HPに応用できると考えたため、これを用いて作成しました。

(その1)に記したように、レスポンシブ対応時の描画に問題があり、スマートフォンに対応することができませんでした。しかし、PCによる直感的操作によってエリアの遷移や模擬店情報の詳細表示等を容易に行うシステムを実現しました。

実装した機能

  • 使い方の表示
  • マップの移動は直観的。クリックしたエリア、フロアなどにジャンプする。
  • もちろん戻ることも可能。
  • 他のページとの関連性について
    • 模擬店一覧ページで選択した模擬店から、詳細なMAP場所を表示させる
    • 研究室公開の一覧のページから、詳細マップを表示する。
    • マップピンをクリックすると、クリックした場所の模擬店情報の詳細が表示
    • 模擬店の情報はリアルタイムで更新される
  • スマートフォンでアクセスするとLINE@招待ページに飛ぶ

参考画像

f:id:sho0126hiro:20181106233259p:plain f:id:sho0126hiro:20181106233304p:plain

CreateJSとは

CreateJSは、HTML5リッチコンテンツを制作するためのJavaScriptライブラリのスイート(特定用途のソフトウェアを詰め合わせたパッケージ)です。 参考:https://ics.media/tutorial-createjs/basic.html

超ざっくりいうと、Flashの新しいやつ、みたいな感じです。
WebGLを使うことができ、使うとかなりサクサク動作します。
HTML5Canvas要素に描画するために使われます。

CreateJSは4つのJavaScriptライブラリからなっています。

  • EaselJS

    HTML5 Canvasでの制作を扱うためのライブラリ

  • TweenJS

    主に、アニメーションを実装できます。

  • SoundJS

    音声再生等の実装ができます。

  • PreloadJS

    ファイル、画像、音声等の先読みができます。

実際に主に使ったのは、EaselJS、TweenJS、PreloadJSです。

Canvasについて

Canvasは図形等のオブジェクトを描くことに特化しています。図形や画像、テキストをオブジェクトとして描くことができるため、オブジェクト(図形等)ごとに座標指定やイベントを設定することができるためHTMLを単に書くより、リッチなサイトを作ることができます。 実際にCanvasYoutubeやGoogleMapにも使われているHTMLの要素です。

オブジェクトの描画

図形、画像、テキストの3つを主に使用したので、それらについて、実際の描画方法について書いていきます。

準備

HelloWorld等はこのサイトを見るとわかり易いです。詳細はこちらを見て下さい。 https://ics.media/tutorial-createjs/quickstart.html

注意:このURLにStageGLについては触れられていません。

ここではポイントだけを書きます。

<!-- import createjs -->
<script src="https://code.createjs.com/1.0.0/createjs.min.js"></script>
<!-- jsファイルのインポート -->
<script src="map.js"></script>
<!-- bodyにcanvas要素を置く -->
<body>
    <canvas id = "myCanvas"></canvas>
</body>

ファイルを分割していますが、<script>タグ内にJavaScriptコードを書くこともできます。

<script>
    window.addEventListener("load", init);
    // 略
    }
  </script>

これから後は、map.jsに書くことを前提として説明します。

Stage,StageGL

Canvasの上に、createjs.Stageクラスを作り、その上でオブジェクトの処理を行います。 しかし、EaselJS 1.0.0に新たなWebGLへの対応として、StageGLクラスが導入されました。http://www.fumiononaka.com/Business/html5/FN1704008.html

こちらは最近の物で、なかなか記事が見つかりませんでした。 StageGLを用いることでStageを用いるより軽い動作を実現します。

参考:https://qiita.com/FumioNonaka/items/d10d74e40a564f06d32f

Stageを描画する

// htmlでid=myCanvasと定義したところの要素をcanvasElementに入れる
var canvasElement   = document.getElementById("myCanvas");
// Stageの定義(createjs.StageGLクラスを用いています。)
var stage = new createjs.StageGL(canvasElement);
// stage定義時には、背景が灰色に設定されているため、白に変更します。
stage.setClearColor('#FFFFFF');

図形

図形を描画してみましょう。 図形描画には、createjs.Shapeクラスを使用します。

var shape = new Shape();
shape.graphics.beginFill("DarkRed"); // 赤色で描画するように設定
shape.graphics.drawCircle(0, 0, 100); //半径100pxの円を描画
stage.addChild(shape); // stageの子要素に入れる。

stageの子要素としてshapeを入れることで初めて描画されます。 忘れないようにしましょう。

stage.addChild(shape);

同じように四角形、星などの図形を描くことができます。 参考:https://ics.media/tutorial-createjs/shape_draw.html

図形描画でよく用いたプロパティ―

次のメソッドは図形オブジェクトに対して共通で使えます。

// obj : new Shape();をして生成したオブジェクト
obj.graphics.beginStroke(color); // 枠線色指定
obj.setStrokeStyle(5); // 枠線の太さ指定

画像

画像ファイルは、PNG画像/JPEG画像/GIF画像/SVG画像を画面に表示することができます。
createjs.Bitmapクラスを使用します。

var bmp = new createjs.Bitmap("sample.png");
stage.addChild(bmp);

テキスト

Canvas内にテキストを表示するときは、createjs.Textクラスを用います。

//new createjs.Text(テキスト, フォント, 色);
var t = new createjs.Text("Hello World!", "24px serif", "DarkRed");
stage.addChild(t);

textAlgin,textBaselineプロパティ―を使うと、水平、垂直方向に文字を揃えることができます。 参考:https://createjs.com/docs/easeljs/classes/Text.html#property_textAlign

オブジェクトの描画でよく用いたプロパティ―

次のメソッドは、上記全てのオブジェクトに対して、設定できます。

// obj : new createjs.*****();をして生成したオブジェクト
obj.x = x; // x座標の指定 
obj.y = y; // y座標の指定 
obj.alpha = alpha; // 透明度の指定
obj.scaleX = scaleX; // x座標の倍率を指定
obj.scaleY = scaleY; // 枠線の太さ指定

これらは、次のコンテナでも使用可能です。

コンテナ

実際のプログラムでは、画像、図形、テキスト等のオブジェクトを大量に生成します。多くなると開発者側にとってもわかりにくいし、全部のオブジェクトに同じ操作をするときにコードが長くなるでしょう。 コンテナを用いることで、親子関係を作ることができます。

コンテナは複数の要素を子要素として、まとめます。

また、親子関係を構築すると、親の位置や角度、スケール、色等の設定が全て子孫に継承されるようになります。

例えば、一度に大量のオブジェクトを非表示(透明度100%)に設定したい場合、大量のオブジェクトを親要素の子要素として、親要素の透明度だけを100%に設定すると、子要素のオブジェクトが全て透明度100%になります。

createjs.Containerクラスを使用します。

var Container = new createjs.Container();
stage.addChild(Container);
Container.addChild(shape); // shape : 表示オブジェクト

このコードの場合、次のような構造になります。

□ stage
  └□ container
    └□ shape

Container.removeChild()でコンテナの要素から外すこともできます。

Container.removeChild(obj);

参考:https://ics.media/tutorial-createjs/nest.html

また、親要素の中身はContainer.childrenで参照できます。

// 中身は例
// コンテナに入れた順番に、配列で格納されていきます。
Container.children = [
                        SampleContainer,
                        text1,
                        text2,
                        shape,
                        bmp ...];

ただ、childrenを使うと、可読性がかなり悪いコードが生まれてしまうので、控えたほうが良いかもしれないです。

設計ミスで、参照先の情報がうまく取れなかったので、次のようなやばいコードが生まれてしまいました。。。

// 赤ピンのオブジェクトをとるだけなのにここまでしないといけない。。。
var redPin = BuildingFloorContainers[building][floor].children[1].children[3*pinNum];

イベント

HPを見ている人がHPに対して何かしらの操作をすることを「イベント」といいます。 例:マウスクリック、キーボードの操作等

javaScriptのaddEventListenerメソッドを使うと、イベントが起こったを取得し、それに対する処理をすることができます。 参考:https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener

次のように使います。

対象要素.addEventListener(種類,イベントが起こったときに実行する関数);
function func(){
    ...
}
target.addEventListener(type,func);

無名関数を用いた記法や、ES2015による関数記法も使用できます。

// 無名関数
target.addEventListener(type,function(){
    ...
});
// ES2015
target.addEventListener(type,()=>{

});

あるオブジェクト(obj)がクリックされたときに、何かしらの処理をする場合、次のように書きます。

// イベントを登録
obj.addEventListener("click", handleClick);
// クリックしたときに入る
function handleClick(event){
    // 処理
}

アニメーション

createJSについての基本事項はこれで最後です。アニメーションを使うとHPがよりリッチになります。

開発したHPにも、画面の切り替えなどで多くアニメーションを実装しています。

TweenJSを用いると、比較的簡単にアニメーションを実装することができます。

createjs.Tweenクラスを使用することで、動きを設定することができます。

createjs.Tween.get(対象オブジェクト)
     .to(変化後のパラメーター, アニメーションの時間[ms]);

次のように使用します。

createjs.Tween.get(target).to({alpha:0},changetime);

参考:https://createjs.com/docs/tweenjs/classes/Tween.html

TimeLine

createjs.TimeLineクラスは、先ほどのコンテナのような役割を果たします。すなわち、アニメーションにも親子構造を作ります。 (正しいことは調べ切れてませんが、そのように理解すると早いです。) このようにして、TimeLineにアニメーションをのせます。

 TimeLine.addTween(アニメーション);

アニメーションを作成し、TimeLineに乗せ、アニメーションを実行するには次のようなコードを書きます。

// createjs.TimeLineクラスを用いて、タイムラインを作成する。
var TimeLine = new createjs.Timeline();
// アニメーションの追加
// override:true は、「もしアニメーション中に他のアニメーションが発生した時、今動いているアニメーション以外も動かしていいか」みたいな設定です。
TimeLine.addTween(
        createjs.Tween.get(target1,{override:true})
          .to({alpha:0},changetime)
      );
TimeLine.addTween(
        createjs.Tween.get(target2,{override:true})
          .to({alpha:1},changetime)
      );
// addしたアニメーションを再生します。
TimeLine.gotoAndPlay("start");

JSからHTMLを操作する

HTMLやXMLを自由に操作する仕組みをDOM(Document Object Model)といいます。 createjsとはあまり関係がありませんが、JSの知識として重要です。 今回のmap機能では、特定のピンを押したときに、DOMを指定の値に書き換え、そのピンの模擬店情報を表示しました。

次のようにして、DOMを「information」に書き換えます。

// <p id = "infotxt">等で、タグ付けした中身を持ってきます。
var infotxt       = document.getElementById("infotxt");
infotxt.innerHTML = "information";

レスポンシブ化

Canvasのレスポンシブ化は、かなり難しかったです。 stageGLについてのリサイズ処理の詳細が多くはないですが、参考としてコードを上げておきます。 話を簡単にするために、画面のフルスクリーンにCanvasのサイズを合わせるための手順を貼ります。

<!-- htmlでのcanvasのstyleは次のようなものです -->
<style>
        canvas#myCanvas{
            display: block;
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
        }
    </style>
// stageの定義
let stage = new createjs.StageGL("myCanvas");
stage.setClearColor('#FFFFFF');
window.addEventListener("resize", handleResize);
handleResize(); // 起動時にもリサイズしておく
// リサイズ処理
function handleResize(event) {
    // Canvas要素の大きさを画面幅・高さに合わせる
    stage.canvas.width = window.innerWidth;
    stage.canvas.height = window.innerHeight;
    // viewportの設定を更新する
    stage.updateViewport(stage.canvas.width,stage.canvas.height);
    stage.update();
}

ポイントは、 stage.updateViewport(w,h);です。 Viewportをupdateしないと、stageGLの場合、正しくリサイズされません。

コード全体はこちら

Retina対応

Retina対応は一つの大きな壁といえるでしょう。 これも難しかったです。

モバイル端末ではRetinaディスプレイが使われています。 Retinaは高解像度のディスプレイのため、画像や図形などをそのまま表示すると、ボケて表示されてしまいます。

実際、モバイル端末で動作するときにCanvasオブジェクトの画質が極端に落ち、モバイル端末での実装を断念しました。

画質が落ちた時の画像

開発途中の画像を1枚。 Canvas要素(マップが表示されているページ)に、若干ボケがかかっているのがわかります。

f:id:sho0126hiro:20190306232947j:plain:w300

改善策

100%理解したわけではないので、ざっくりとした説明です。 Retinaに対応したコードを書きました。

これも、フルスクリーンにリサイズをかけ、レスポンシブ対応させています。 参考にしてみてください。 コード全体はこちら https://github.com/sho0126hiro/CreateJS/blob/master/retina(responsive)/fullscreen/responsive.js

自分が作成したRetina対応させたサイトのURLを二つ貼っておきます。 ズームには対応させていないので、ズームすると画面が崩れますが、きれいな表示ができているのではないでしょうか。 Retina対応したサイト https://sho0126hiro.github.io/CreateJS/retina(responsive)/fullscreen/responsive.html

ポイントはdevicePixelRatioです。 詳細はこちらの記事を参考にしてください。 https://qiita.com/uto-usui/items/5ee0634d5107f3da86e8

HPのMAP画面がうまく表示できなかった原因はRetinaディスプレイだったことだけではない

Retinaでは綺麗に描画されますが、HPでうまく表示できなかった原因としてCSSの描画サイズ等も挙げられます。

styleのwidthとCanvasのwidthや、その他親要素のstyleの値が干渉しあったとも言えます。 Retinaだけが原因ではないということに注意が必要です。

また、画像自体に問題があった可能性もあります。 上のURLは猫の綺麗な画像ですが、MAPに用いたのは、MAPの2次元画像で白黒、さらに細線であったことも原因として挙げられます。 上のURLの画像をMAP画像に置き換えると、うまく表示されませんでした。

その他にも、Hexoとの関係等いろいろ原因は考えられます。

Retinaについての反省点

かなり多くのバグの原因が干渉しあっていたので、実は単純なバグでも開発側から見えなくなることはよくあります。

今回は実装までの時間がかなり少なかったことと、挙げられる原因の数が多すぎたことで、LINEBOTに切り替えました。

プログラムを頑張って作っても表示系でのバグは見れなくなるまでダメージを受けてしまうので、プログラムや数値を誤ると表示できなくなるということを知っておくと良いと思います。

また、今回の場合だけかもしれませんが、他のプラットフォームで動作するかは分からないということも知っておくと良いです。

PCでは綺麗に動作していたのでモバイル端末でも動作していると誤信していました。誤信したまま開発を続けた結果、気づいた時には残り2週間しかないという。。。

もっと細かく表示テストをしておけば、発見が早かったかもしれないですね。。。

当時はめちゃめちゃダメージを受けていたけど凄く良い勉強になりました(笑)

Modal

ModalはCreateJSとは関係がないので、丁寧には説明しませんが、サクッと説明しておきます。 クリックしたときに、新しい画面が一つ上に出てくるみたいなものを「モーダル」といいます。

f:id:sho0126hiro:20181106233304p:plain

w3CSSのmodalを使いました。 参考:https://www.w3schools.com/w3css/w3css_modal.asp

<!-- こんな感じで使います。参考として -->
<div class="w3-container">
        <button onclick="document.getElementById('button').style.display='block'" class="w3-button w3-black">
            <p id="buttontxt">使い方を表示</p>
        </button>
        <div id="button" class="w3-modal">
        <div class="w3-modal-content">
            <div class="w3-container">
            <span onclick="document.getElementById('button').style.display='none'" class="w3-button w3-display-topright">&times;</span>
            <div  class="mapdata" >
                <h1>Information</h1>
                <p id = "infotxt"></p>
                <p id = "boothdata"></p>
                </div>
            </div>
        </div>
        </div>
    </div>

かなり長くなったので、いったんここで切ります。 次回は、上の基本事項をどうやってmapページに応用したか

どのような構成か、プログラムの流れはどんな感じか、みたいなことと、 JavaScript開発をしていて早く知っておいたほうが良かったこととか、今後のこととかを書項と思います。