1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

今週はずっと前に作った塗り絵ツクールのアップデートということで、塗り絵機能をつけました。

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

canvasに背景画像をつける

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

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

<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の描画サイズは上記と同じなのですが、解像度が細かくなりきれいに描画できます。

      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)でイベントが探知できるので、それを利用します。

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

lineWidthcurrentColorはユーザーの操作によって変動できるようにしています。

    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というライブラリを利用しました。

yarn add vue-color

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

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

Vue.component('Sketch', Sketch)

nuxt.config.jsに以下追加

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

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

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

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

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

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

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

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

    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が原因でエラーが発生してしまいました。

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

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

tainted canvases may not be exported

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

ということで、今回CORS対策を@nuxtjs/proxyで解決しました。

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

yarn add @nuxtjs/proxy

nuxt.config.jsに追記。

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部分を以下に変更します。

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

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

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

<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になるのでその対策です。(一本指の場合)

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

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

JavaScript タッチイベントの取得(マルチ対応) サンプルコードの記事を非常に参考にさせて頂きました。

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

あとがき

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?