【canvasで塗り絵機能を実装】画像からオリジナル塗り絵を作って塗り絵ができるアプリを作った

今週はずっと前に作った[塗り絵ツクール](https://nurie-maker.com/)のアップデートということで、塗り絵機能をつけました。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">塗り絵ツクールで公開されてる塗り絵やシェアされたURLから作成した塗り絵画像に色をつけることができるようにしました！（スマホ対応してますが、現在はPC推奨です）<a href="https://twitter.com/hashtag/%E5%A1%97%E3%82%8A%E7%B5%B5%E3%83%84%E3%82%AF%E3%83%BC%E3%83%AB?src=hash&amp;ref_src=twsrc%5Etfw">#塗り絵ツクール</a><a href="https://t.co/bKEDtJe3Ih">https://t.co/bKEDtJe3Ih</a> <a href="https://t.co/LzDjbayrli">pic.twitter.com/LzDjbayrli</a></p>&mdash; Yui 🌕 Yuiko Ito (@yui_active) <a href="https://twitter.com/yui_active/status/1429759478639972357?ref_src=twsrc%5Etfw">August 23, 2021</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

今回はこの塗り絵機能に関して書きます。
参考になったと思えばLGTM :thumbsup: してもらえれば励みになります。

## canvasに背景画像をつける

まずはcanvasエレメントを用意して、canvasに背景画像を描画します。

canvasに背景画像を描画する際に少し時間がかかるので、その間はローディングを出すようにしています。
そのため、Promiseで画像が完全に読み込まれるのを待ってからoverlayフラグをfalseに戻します。

```vuejs
<script>
export default {
  async asyncData({ params }) {
    return {
      url: `${process.env.BASE_URL}/nurie/${params.id}`,
      image: `${process.env.AWS_IMAGE_URL}/nurie/${params.id}.jpg`,
      twitterImage: `${process.env.AWS_IMAGE_URL}/nurie/${params.id}.jpg`,
    }
  },
  data() {
    return {
      canvas: null,
      ctx: null,
      noPicture: require('@/assets/img/noPic.png'),
      overlay: true,
    }
  },
  mounted() {
    this.init()
  },
  methods: {
    async init() {
      this.canvas = this.$refs.canvas
      this.ctx = this.canvas.getContext('2d')
      const wrapper = this.$refs.wrapper
      await this.loadImage(this.image).then((res) => {
        const scale = wrapper.clientWidth / res.naturalWidth
        this.canvas.width = res.naturalWidth * scale
        this.canvas.height = res.naturalHeight * scale
        this.ctx.drawImage(res, 0, 0, this.canvas.width, this.canvas.height)
      })
      this.overlay = false
    },
    loadImage(src) {
      return new Promise((resolve) => {
        const img = new Image()
        img.src = src
        img.onload = () => resolve(img)
        img.onerror = () => {
          img.src = this.noPicture
        }
      })
    },
  }
}
</script>
```

これでcanvasに画像が描画できました。

## canvasで描画後の画像が荒くなる

上記で描画された画像を見ると少し解像度が荒くなってしまいましたので、その部分を修正します。
具体的にはcanvasで画像を読み込む際に2倍の大きさで読み込んでおき、canvasのstyleを1/2にすることで、実際のcanvasの描画サイズは上記と同じなのですが、解像度が細かくなりきれいに描画できます。

```javascript
      await this.loadImage(this.image).then((res) => {
        this.ctx.scale(2, 2)
        const scale = wrapper.clientWidth / res.naturalWidth
        this.canvas.width = res.naturalWidth * scale * 2
        this.canvas.height = res.naturalHeight * scale * 2
        this.canvas.style.width = this.canvas.width / 2 + 'px'
        this.canvas.style.height = this.canvas.height / 2 + 'px'
        this.ctx.drawImage(res, 0, 0, this.canvas.width, this.canvas.height)
      })
```

## お絵かき機能を実装する

ユーザーがクリックする際の操作はPCだと``mousedown``、ドラッグは``mousemove``、ドラッグが終わったタイミングは``mouseup``（もしくは``mouseout``）でイベントが探知できるので、それを利用します。

```vue
<canvas
  ref="canvas"
  @mousedown.prevent="dragStart"
  @mouseup.prevent="dragEnd"
  @mouseout.prevent="dragEnd"
  @mousemove.prevent="draw"
>
</canvas>
```

※``lineWidth``と``currentColor``はユーザーの操作によって変動できるようにしています。

```javascript
    dragStart() {
      this.ctx.beginPath()
      this.isDrag = true
    },
    draw(e) {
      const x = (e.clientX - this.canvas.getBoundingClientRect().left) * 2
      const y = (e.clientY - this.canvas.getBoundingClientRect().top) * 2
      if (!this.isDrag) {
        return
      }
      this.ctx.lineCap = 'round'
      this.ctx.lineJoin = 'round'
      this.ctx.lineWidth = this.lineWidth
      this.ctx.strokeStyle = this.currentColor
      if (this.lastPosition.x === null || this.lastPosition.y === null) {
        this.ctx.moveTo(x, y)
      } else {
        this.ctx.moveTo(this.lastPosition.x, this.lastPosition.y)
      }
      this.ctx.lineTo(x, y)
      this.ctx.stroke()
      this.lastPosition.x = x
      this.lastPosition.y = y
    },
    dragEnd() {
      this.ctx.closePath()
      this.isDrag = false
      this.lastPosition.x = null
      this.lastPosition.y = null
      this.isErase = false
    },
```

## 線の色を変える
線の色を変える部分は[vue-color](https://www.npmjs.com/package/vue-color)というライブラリを利用しました。

```
yarn add vue-color
```

plugins配下にvueColor.jsを作ってvue-colorの読み込み部分を書きます。
今回はSketchというデザインを使うのでSketchだけを読み込んでます。

```javascript
import Vue from 'vue'
import { Sketch } from 'vue-color'

Vue.component('Sketch', Sketch)
```

nuxt.config.jsに以下追加

```javascript:nuxt.config.js
  plugins: [{ src: '@/plugins/vueColor.js', mode: 'client' }],
```

後はこれを該当のページ部分で描画するだけです。
このままでも描画は問題ないのですが、warningが出てしまうので、``<client-only>``の中で動かします。

```vue
  <client-only>
    <Sketch :value="colors" @input="updateValue"></Sketch>
  </client-only>
```

colorsには初期値を設定しておきます。hexでもrgbaでもどちらでも設定できます。
色が変更されたら以下の関数で色を取得できます。

```javascript
    updateValue(e) {
      this.currentColor = e.hex
    },
```

めちゃくちゃ使い勝手がよく、楽ちんでした :smiley: 

## 塗り絵をしたcanvasをダウンロードする

後はボタンをクリックするとcanvasをダウンロードする機能を実装します。
適当にボタンを用意して、以下の関数でcanvasをダウンロードします。

```javascript
    download() {
      let link = document.createElement('a')
      link.href = this.canvas.toDataURL('image/jpeg')
      link.download = 'nurie-' + new Date().getTime() + '.jpg'
      link.click()
    },
```

...が、このままではcanvasがダウンロードできません。

## CORSが原因でtoDataURLの実行に失敗する

実は今回、canvasの背景画像に関してはuuidからs3に保存してあるURLを参照するようにしていたので、その部分のCORSが原因でエラーが発生してしまいました。

```javascript
image: `${process.env.AWS_IMAGE_URL}/nurie/${params.id}.jpg`
```

imageを↑のように読み込んでいるので、現在のURLのドメインと画像のURLのドメインが異なるため、以下のエラーが発生。

```
tainted canvases may not be exported
```

汚染されてるcanvas...oh...

ということで、今回CORS対策を[@nuxtjs/proxy](https://github.com/nuxt-community/proxy-module)で解決しました。

※当初AWSでCORS対策をしてたのですが、なんでか画像によってうまくいったりいかなかったりという事が起きたのでAWSでのCORS設定に加えて、@nuxtjs/proxyでも設定するようにしました。

```
yarn add @nuxtjs/proxy
```

nuxt.config.jsに追記。

```javascript:nuxt.config.js
  modules: ['@nuxtjs/dotenv', '@nuxtjs/proxy'],
  proxy: {
    '/nurie/': {
      target: AWS_IMAGE_URL,
      changeOrigin: true,
      secure: false,
    },
  },
```

これで``${process.env.AWS_IMAGE_URL}/nurie/${params.id}.jpg``と書いていた部分はサイトのURLを使って置き換えることができるようになりました。
image部分を以下に変更します。

```javascript
image: `${process.env.BASE_URL}/nurie/${params.id}.jpg`,
```

## スマホで塗り絵をできるようにする

このままではスマホで塗り絵ができないので、スマホのイベントも追加します。
スマホでは``touchstart``, ``touchmove``, ``touchend``でイベントを探知できます。

```vue
<canvas
  ref="canvas"
  @mousedown.prevent="dragStart"
  @touchstart.prevent="dragStart"
  @touchend.prevent="dragEnd"
  @mouseup.prevent="dragEnd"
  @mouseout.prevent="dragEnd"
  @mousemove.prevent="draw"
  @touchmove.prevent="spDraw"
>
</canvas>
```
線を書く部分だけスマホだとPCと探知できるイベントが異なるので、以下で調整します。
要はPCだとタッチしてる場所などは``e.clientX``等で取得できますが、スマホだと``e.changedTouches[0].clientX``になるのでその対策です。（一本指の場合）

```javascript
    spDraw(e) {
      if (e.changedTouches.length == 1) {
        this.draw(e.changedTouches[0])
      }
    },
```

今回は一本指にしか対応してませんが、もし複数指に対応させる場合はforでchangedTouchesを回して、``this.draw(e.changedTouches[i])``にでもすればOKだと思います。

[JavaScript タッチイベントの取得（マルチ対応） サンプルコード](https://qiita.com/yamazaki3104/items/1f550c589b13febade82)の記事を非常に参考にさせて頂きました。

これで塗り絵機能ができました！

## あとがき

これで14週目の週イチ発信となりました。
良ければこれまでの週イチ発信も見て下さい！
ではでは〜。

- [【React + Typescriptで顔認識】tensorflowを使って画像にマスクをかけるアプリを作った](https://qiita.com/Yui_active/items/b8062b9f89f72c6865ed)
- [【React + Typescript】ボタン一つでコンポーネントのscssをコピーできるサイトを作った](https://qiita.com/Yui_active/items/1288fed0dcc24c71e159)
- [【アップデート】ui-componentsに18個のコンポーネントを追加した](https://qiita.com/yuiko_ito/items/c1d0599e1e7da7fa42e0)
- [【Nuxt.js × Tailwind CSS】ボタン一つで有名絵画風の画像にできるサービスをリリースした！](https://qiita.com/yuikoito/items/126b0e2d36a707bfd36e)
- [【GASでLINE Bot作成】現在地の近くのおすすめのごはん屋さんを教えてくれるLINE Botを作った](https://qiita.com/yuikoito/items/c1aef147cef4458f9d2e)
- [【動的OGP】Next.js + TypeScript + Vercelデプロイで動的OGPを実現する](https://qiita.com/yuikoito/items/619120c592d99f9d3053)
- [【LambdaでOpenCVを利用】AWSとOpenCVを利用してポケモン画像でアスキーアート風に変換するAPIを作った](https://qiita.com/yuikoito/items/7dcf1deeed46f46b2f2e)
- [ポケモン画像でアスキーアート風に変換するwebアプリを作った](https://qiita.com/yuikoito/items/da5954a5d78da9de6a4e)
- [ボタン一つで漫画風の画像にできるサービスを作った](https://qiita.com/yuikoito/items/a76ea5bf05f04a44df8d)
- [【Next.js + TensorFlowでweb cameraにバーチャル背景をつける】バーチャル旅行を体験できるアプリを作った](https://qiita.com/yuikoito/items/bb9c40bbb673e4f71abe)
- [オリンピックを盛り上げるためにピクトグラムさんになれるアプリを作った【Next.js+TypeScriptでTensorFlow.jsを使って姿勢検出】](https://qiita.com/yuikoito/items/274f926b5c5e59b43ce9)
- [毎週アプリをリリースして3ヶ月経ったので振り返る](https://qiita.com/yuikoito/items/eede538b17d72685ea02)