DEV Community

Yuiko Ito
Yuiko Ito

Posted on

Create a coloring book in canvas: Developed an app that you can create and enjoy your own original coloring from an image.

Hey guys,

I developed an app that you can create and enjoy your own original coloring book from an image.

Look this!

Angry

You can also play coloring in this app.

Image from Gyazo

The part about turning the image into a coloring book is almost the same as this article I wrote before.

So, in this article, I will explain how to develop this coloring functions using canvas.

DEMO→https://nurie-maker.com/
github→https://github.com/yuikoito/nurie-creator

※I'm using Nuxt.js here. Please replace it as necessary.

Draw a background image on the canvas

First, we need to prepare a canvas element and draw a background image on the canvas.

It takes a little time to draw the background image on the canvas, and I want to make sure to emit loading during that time.
Therefore, I wait here for the image to be fully loaded in Promise.

<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>
Enter fullscreen mode Exit fullscreen mode

Now we can draw an image on canvas.

Use Retina Canvas Support

If you look at the image drawn above, you will see that the resolution is a little rough, so we will fix that part.

Specifically, when loading an image in canvas, load it at twice the size and set the canvas style to 1/2. The actual canvas drawing size will be the same as above, but the resolution will be finer and the image will be drawn beautifully.

      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)
      })
Enter fullscreen mode Exit fullscreen mode

Implement a drawing function.

When a user clicks on the screen, the event can be detected by mousedown on a PC, mousemove for dragging, and mouseup (or mouseout) for the timing when the dragging is finished.

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

※The lineWidth and currentColor can be changed by the user.

    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
    },
Enter fullscreen mode Exit fullscreen mode

Change color of the line

I used vue-color to change color.

yarn add vue-color
Enter fullscreen mode Exit fullscreen mode

Create vueColor.js under plugins.
In this case, we are using a design called Sketch, so we are only loading Sketch.

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

Vue.component('Sketch', Sketch)
Enter fullscreen mode Exit fullscreen mode

Add in nuxt.config.js.

  plugins: [{ src: '@/plugins/vueColor.js', mode: 'client' }],
Enter fullscreen mode Exit fullscreen mode

Then draw this component in the page.
The drawing is fine as it is, but you will get a warning, so move it inside <client-only>.

  <client-only>
    <Sketch :value="colors" @input="updateValue"></Sketch>
  </client-only>
Enter fullscreen mode Exit fullscreen mode

Set a default value for colors.
When a user change color, you will get the color by the following function.

    updateValue(e) {
      this.currentColor = e.hex
    },
Enter fullscreen mode Exit fullscreen mode

This library is easy and useful!

Download the canvas

Then, we will implement a function to download the canvas when the button is clicked.

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

... However, we can't download canvas at this point.

Execution of toDataURL fails due to CORS.

In fact, this time, for the background image of canvas, I was referring to the URL stored in s3 from uuid, so the error occurred due to CORS in that part.

image: `${process.env.AWS_IMAGE_URL}/nurie/${params.id}.jpg`
Enter fullscreen mode Exit fullscreen mode

Since the image is loaded as shown above, the domain of the current URL and the domain of the image URL are different, causing the following error.

tainted canvases may not be exported
Enter fullscreen mode Exit fullscreen mode

Then I used @nuxtjs/proxy to fix this problem.

yarn add @nuxtjs/proxy
Enter fullscreen mode Exit fullscreen mode

Add nuxt.config.js.

  modules: ['@nuxtjs/dotenv', '@nuxtjs/proxy'],
  proxy: {
    '/nurie/': {
      target: AWS_IMAGE_URL,
      changeOrigin: true,
      secure: false,
    },
  },
Enter fullscreen mode Exit fullscreen mode

Now, you can replace ${process.env.AWS_IMAGE_URL}/nurie/${params.id}.jpg with the URL of your site.
Change the image part to the following.

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

Enable to play coloring on the phone

As it is, we can't do coloring on the phone, so we will also add events for the phone.
On the phone, you can detect events with touchstart, touchmove, and 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>
Enter fullscreen mode Exit fullscreen mode

The events that can be detected on a smartphone differ from those on a PC only for the part where the line is drawn, so adjust the following.
The point is that on a PC you can get the location of the touch by e.clientX, etc., but on a smartphone it is e.changedTouches[0].clientX. (In the case of a single finger)

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

Finish!

That's it!

Thanks for reading.
I am very happy if you enjoy it!

🍎🍎🍎🍎🍎🍎

Please send me a message if you need.

yuiko.dev@gmail.com
https://twitter.com/yui_active

🍎🍎🍎🍎🍎🍎

Discussion (0)