2021年版キャンドルナイトプロダクト開発のあゆみ(Candle Night @ Shinjuku) ～canvasとの闘いを乗り越えて～

こんにちは。GxPの杉森です。
この記事は、[グロースエクスパートナーズ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/gxp) 25日目、最終日の記事です。
今回は、プロダクトデザイン・プロダクトオーナー及びPJ全体の取りまとめを務めさせていただいた杉森と、今回のプロダクト１番の目玉機能を開発したGxP富岡との共作記事として書かせていただきます。

# :candle:イントロ:candle:
昨年に引き続き、今年も新宿のまちづくり・魅力創出の取り組みの一環として、
Candle Night@Shinjuku イベントにオンラインプロダクトを提供させていただきました。
昨年の記事もぜひご覧ください。
[『新宿中央公園キャンドルナイトの塗り絵用投稿・鑑賞サイトについて』](https://qiita.com/rando-gxp/items/979a79371d79f08e7472)

小田急電鉄さんが2018年に始められた「Candle Night @ Shinjuku Central Park」イベントも今年で4回目を迎えました。
今年は、これまでの西新宿に閉じたイベントから、東新宿にもイベントフィールドを拡大し、『Candle Night @ Shinjuku』というイベントをデザインしました。
イベントは2週に渡って、
前半を『**Candle Night @ Shinjuku East Square**』(新宿駅東口 駅前広場)
後半を『**Candle Night @ Shinjuku Central Park**』(新宿中央公園 水の広場)
という2部構成で形作ることとなりました。

更に今年は、
『**これから始まる、まちとつながり、多様な活動を生み出す次世代のターミナル「新宿グランドターミナル」の実現に向けて、生まれ変わるこのまちを多くの人に体感してほしい**』という思いから、
・京王電鉄株式会社
・西武鉄道株式会社
・東京地下鉄株式会社
・東日本旅客鉄道株式会社
の鉄道４社が加わり、鉄道事業者5社が初めて共同でのイベントを開催する形となりました。

下記のイベントチラシは、工学院大学公認学生団体『まち開発プロジェクト -Smart Tech-』のメンバーによって作成されたもので、今年もとても素敵なデザインを作成頂きました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912430/3a49aebd-957f-2668-6a9e-eddab7bec756.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912430/7e1f0929-1648-c8d4-9519-cba4b1e3520e.png)

本記事では、今年も上記イベントを形作る要素の１つである、オンラインプロダクトについてご紹介させていただきたいと思います！

# :candle:ぬり絵機能開発:candle:
実装を担当したGxP富岡です。
今回のプロダクトの目玉であるぬり絵機能の開発秘話です。
※掲載しているコードは説明の為簡略化していますのでそのままでは動きません

### 機能概要
昨年は紙の台紙を塗ったものを撮影して写真を投稿する形でした。
ただオンラインで完結しないため参加のハードルが高くなってしまっていました。
それを改善するため今年はオンラインで完結するようにオンラインでぬり絵して投稿できるようにアップデートしました。

ぬり絵～投稿の流れ
「ぬり絵にチャレンジ！」でぬり絵開始
![DCN1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912430/094e628e-ea85-b638-137d-8a56544198e5.png)

好きな台紙を選ぶ
![DCN2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912430/86f46367-7c81-6557-171a-5dbb5d8ef794.png)

タップやクリックで線を引いて色をぬる
![DCN3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912430/cd3375a5-5dda-3220-723b-883215118e2b.png)

![DCN4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912430/0ea370f8-da62-8317-ba64-169ca3eb5683.png)

規約に同意して投稿する
![DCN5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912430/a8603529-e4e1-e4e3-8e1c-09a2413a1dec.png)

投稿完了
![DCN6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912430/ae35c7fb-f245-29d2-d1c3-186acdebc5bf.png)


### 実現方法
HTMLでグラフィカルな表示をさせることができるcanvas要素で実現しました。
台紙の画像をcanvasに描画、クリック座標を取得して線を引く、という感じです。
こちらは検索してみると参考になる記事がありますので本記事では割愛します。
実装で参考にした記事：https://zenn.dev/kitchy/articles/ecea795f11cddf

### 小さい画面でも塗りやすく
操作性を上げるために施した数々の工夫をピックアップして紹介します。
まずひとつめは、台紙の線をぬった線で上書きしないようにさせることです。
canvasを重ねてレイヤーを実現し、一番上のレイヤーに台紙の透過画像を描画することで上書きしていないように見せています。
実装としては、divでcanvasをラップしてdivに重なるようにz-indexを指定します。
全3枚で一番下が台紙、真ん中が塗るcanvas、一番上が透過画像です。
クリックイベントはdivに発生するのでこれを拾って塗るレイヤーのcanvasに描画します。

<details>
<summary>レイヤーのコード</summary><div>

```html
<div id="canvas-wrap">
    <canvas id="canvas_overlay"></canvas>
    <canvas id="canvas_drawing"></canvas>
    <canvas id="canvas_base"></canvas>
</div>

<style>
#canvas-wrap {
  position: relative;
  border: 1px solid white;
}
#canvas-wrap > canvas {
  position: absolute;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
}
#canvas_base {
  z-index: 1;
}
#canvas_drawing {
  z-index: 2;
}
#canvas_overlay {
  z-index: 3;
}
</style>
```

</div></details>


これだけではcanvasに描画されているものがバラバラなので最後投稿する際の画像化処理で3つのcanvasを合成しています。

<details>
<summary>レイヤー合成コード</summary><div>

```javascript
      // 合成用canvasを取得
      const mergeCanvas = document.querySelector('#canvas_merge')
      // 台紙レイヤーを合成
      mergeCanvas.drawImage(document.querySelector('#canvas_base'), 0, 0)
      // ぬり絵レイヤーを合成
      mergeCanvas.drawImage(document.querySelector('#canvas_drawing'), 0, 0)
      // 透過レイヤーを合成
      mergeCanvas.drawImage(document.querySelector('#canvas_overlay'), 0, 0)
      // 合成用canvasを画像化
      const canvasMergeImage = mergeCanvas.toDataURL()
```

</div></details>

ふたつめは、スマホ前提のプロダクトであるため塗る領域をできるだけ広くとることです。
まず、canvasをレスポンシブ対応してできるだけ大きく表示されるようにしました。
具体的には、CSSでアスペクト比を維持しつつ端末によってサイズが変わるようにしました。
canvasには表示サイズ(CSSで指定するサイズ)と描画サイズ(canvasタグのwidth/height属性)の2つがあり、描画サイズは表示サイズに合わせてスケールされます。これにより場合によっては意図しない挙動になってしまうので描画サイズと表示サイズは同じになるように指定する必要があります。具体的にはJSで表示サイズを取得して描画サイズ（canvasのwidth/heightプロパティ）を更新します。
canvasのサイズは台紙のアスペクト比を維持する形で端末によってサイズが変わるようにCSSを適用（実際は台紙毎にサイズが違うのでjsで算出して指定）し、初回描画時に描画サイズを表示サイズに合わせるようなjsを書きます。

<details>
<summary>canvasサイズ指定のコード</summary><div>

```javascript
    const image = new Image()
    image.onload = e => {
      // position: relative;にしているラップのdivに対してサイズ指定する
      const canvasWrap = document.querySelector('#canvas-wrap')
      // 台紙のアスペクト比を導出する
      const aspect = image.height / image.width * 100
      const aspectRound = Math.floor(aspect)
      canvasWrap.style.paddingTop = aspectRound + '%'
      // それぞれのcanvasにも適用する
      const width = canvasWrap.clientWidth
      const height = canvasWrap.clientHeight
      document.querySelector('#canvas_base').width = width
      document.querySelector('#canvas_base').height = height
      document.querySelector('#canvas_drawing').width = width
      document.querySelector('#canvas_drawing').height = height
      document.querySelector('#canvas_overlay').width = width
      document.querySelector('#canvas_overlay').height = height
    }
    // 台紙画像を読み込む
    image.src = selectedNurie.baseImagePath
```

</div></details>

また、横画面にした場合の広さを最大限活用しつつ操作性を上げるために縦画面と横画面で（正しくは画面幅によって）レイアウトを変えるようにしました。こちらはCSSのorderプロパティで表示する順序を変えているだけです。


### 難敵canvasとの闘い
小さい画面でぬりやすくする工夫で触れているcanvasのレスポンシブ対応です。
実現するにあたって問題がいくつかありました。
アプローチとしては、文書（ビュー）の大きさが変更されたときに発火されるJSのリサイズイベントを拾いcanvasのサイズ変更を行います。
1点はcanvasの仕様として描画サイズ（canvasのwidth/heightプロパティ）を更新するとcanvasの描画がクリアされてしまうことです。つまりリサイズ時に変えるとそれまで塗っていたぬり絵が全て無に帰されてしまうという問題が発生しました。こちらはサイズ変更前にcanvasを画像化して保持しておきサイズ変更後にスケールを設定して画像を描画させることで対応できました。
2点目はJSのリサイズイベントが実はブラウザによっては端末回転以外でも発生してしまうことです。どういうことかというと、ブラウザによってはスクロールするとURLバーが引っ込んだり出てきたりする動作をしますが、その際にもリサイズイベントが発火されてしまうのです。前述の描画サイズ更新でクリアされる問題と合わさってとても塗れる状況ではない（塗ってるそばからクリアされる）でした。スクロール時のURLバーによって変更されるの縦幅なので、横幅が変わったときだけ処理するようにガードをいれて端末回転時だけ動作するように対応できました。

上記2点対応後、クリック座標と線が引かれる位置がずれる問題が発生しました。
リサイズのサイズ指定が失敗していると思い原因を探りましたがなかなかわからずレスポンシブ対応断念しそうになりました。
色々いじっていたところ、1点目で入れたリサイズ後の状態復帰処理（元の画像描画処理）に原因があることがわかりました。

<details>
<summary>リサイズ後の状態復帰処理（問題解消前）</summary><div>

```javascript
      // リサイズcanvasがクリアされるため塗るレイヤーを画像化して描画データを保持しておく
      const canvasDrawing = document.querySelector('#canvas_drawing')
      const drawingImg = canvasDrawing.toDataURL()
      // リサイズ処理（省略）
      // 塗るレイヤーの状態復帰
      img.onload = (e) => {
        const contextDrawing = canvasDrawing.getContext('2d')
        // リサイズ前後のサイズからスケール率を導出
        var scale = canvasDrawing.width / img.width
        // スケール設定
        contextDrawing.setTransform(scale, 0, 0, scale, 0, 0)
        // 描画データ（画像）を描画
        contextDrawing.drawImage(img, 0, 0)
      }
      // 保持しておいた描画データ（画像）を読む
      img.src = drawingImg
```
</div></details>

canvasのsetTransformメソッドでスケール設定をいれることでリサイズ前の画像をリサイズ後のサイズに合うように描画させていましたが、setTransformで設定した内容はそのままになるようで、描画後にスケールを等倍にリセットすることで解決しました。

<details>
<summary>リサイズ後の状態復帰処理（問題解消後）</summary><div>

```javascript
      // リサイズcanvasがクリアされるため塗るレイヤーを画像化して描画データを保持しておく
      const canvasDrawing = document.querySelector('#canvas_drawing')
      const drawingImg = canvasDrawing.toDataURL()
      // リサイズ処理（省略）
      // 塗るレイヤーの状態復帰
      img.onload = (e) => {
        const contextDrawing = canvasDrawing.getContext('2d')
        // リサイズ前後のサイズからスケール率を導出
        var scale = canvasDrawing.width / img.width
        // スケール設定
        contextDrawing.setTransform(scale, 0, 0, scale, 0, 0)
        // 描画データ（画像）を描画
        contextDrawing.drawImage(img, 0, 0)
        // スケールを等倍にリセット（これを追加）
        contextDrawing.setTransform(1, 0, 0, 1, 0, 0)
      }
      // 保持しておいた描画データ（画像）を読む
      img.src = drawingImg
```
</div></details>

### 実装してみて
canvasが複雑であることがわかりました。その分知識を増やすことができました。
自社サービスであるため自分自身も使う目線に立ってプロダクトの機能に追及できるのがとても面白かったです。
完成した後普通に遊んでしまいました(笑)

それぐらい開発者も気に入っている渾身の機能なのでぜひ遊んでみてください！

https://dcn-s.gxp.jp/odkdcn-web/#/

# :candle:締め:candle:
最後まで記事に目を通していただきありがとうございます。
Candle Night @ Shinjukuイベントは、これからも毎年続けていく恒例イベントとして関係者一同考えています。
そしてオンラインプロダクトも、毎年よりオンラインでも楽しんで参加していただけるよう成長をさせていく予定でいます。
来年はオンラインプロダクトが、そしてイベント全体がどういう成長・変化をしていくのか？！ぜひ毎年の楽しみにしていただけますと幸いです。