HTML Canvasでボケないペンツールpx-brushを作りました

## はじめに

HTMLのCanvasは便利なAPIがたくさん用意されていて簡単に描画ツールなどを自作できるのですが、通常の方法ではアンチエイリアスが強制的にかかってしまい、敢えてピクセル感のあるいわゆる「ジャギる」線を書くことはできません。

StackOverflowでもこのことが質問として上がっています。
https://stackoverflow.com/questions/195262/can-i-turn-off-antialiasing-on-an-html-canvas-element

## なめらかな線しか書けない

![普通に実装したペンの例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673/db25d8f4-49cb-0f5e-f1d2-e8f56f170018.gif)

こちらが普通にCanvas上に実装したペンの例です。
ここでは `context.imageSmoothingEnabled` というAPIを使用していますが、これは画像の拡大縮小にしようするものであり、ペンの実装には全く効果がないことがわかります。

## ジャギらせたい

https://www.pixilart.com/draw#

![Pixilart](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673/fe7750a2-a474-4bbf-414c-5b2a5d665f38.gif)

Pixilartというサービスがありまして、ちょっとレトロな雰囲気で絵がかけるサービスです。
ここでは四角形のペンを実装していますので、描画時の座標から浮動小数点数を除去すれば確かにこれで可能なんですよね。
ただし、四角ではなく丸いペン先のツールを実装したいのです。

## スタンプみたいにしてみてはどうか？

![最初のスタンプ型実装](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673/cd8b56ab-439f-e16b-5dbc-a3abe66199db.gif)

Pixilartから学べたことはペン先をスタンプにする。要するに画像をマウスの動きに合わせて連続して描画すればいけるんじゃないかと考えました。
しかし、ゆっくり動かした場合は問題ないんですが、すばやく動かすとスタンプの隙間ができてしまい線になりませんでした。

## 隙間を計算して埋める

というわけで他のHTML Canvasを使用しているサービスがどうやってこの隙間問題を解決しているのか調べたところ、以下の記事を発見しました。

http://perfectionkills.com/exploring-canvas-drawing-techniques/

いろんなペンの実装があって非常に参考になるのですが、まさに隙間問題を解決する方法が載ってまして、要するにマウスの動きの__角度と距離を計算して隙間にもスタンプを描画する__という手法でした。

実際のコードがこちらで

```js
function distanceBetween (point1, point2) {
  return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2))
}
function angleBetween (point1, point2) {
  return Math.atan2(point2.x - point1.x, point2.y - point1.y)
}
const dist = distanceBetween(beginPosition, destPosition)
const angle = angleBetween(beginPosition, destPosition)
for (let i = 0; i < dist; i += 5) {
  const x = beginPosition.x + (Math.sin(angle) * i) - halfSizeOfBrush
  const y = beginPosition.y + (Math.cos(angle) * i) - halfSizeOfBrush
  this.context.drawImage(stampImage, Math.round(x), Math.round(y))
}
```

これを適用したペン実装がこちらです

![角度を距離を計算して隙間を埋めた](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673/d0de1f90-1727-08ac-70d6-7898465b4ea8.gif)

大変いい感じです。まさに求めていた__ジャギる__ペンツールの可能性が見えてきました。

## サイズと色を動的にしたい

ここまでの実装ではフォトショップでGIF画像を作成し、それをスタンプとして使っていましたが、これではサイズも色も固定になってしまっています。
サイズと色を動的に変更できないものか、とググっていたところ以下のページを発見しました。

http://members.chello.at/easyfilter/canvas.html

どうやら__Bresenham’s line algorithm__というものでアンチエイリアスのかかっていない円形の描画ができるようです。

![Bresenham’s line algorithmを使ったサービス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673/d7997c54-e506-b992-797e-694e300e7900.png)

こんな感じですね。

## 動的なスタンプ生成がついにできた

できちゃいましたね。アルゴリズムのコードを参考に実装してみたところ若干苦戦しましたが実現できました。

![動的にペン先を生成する例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673/a1c4cdc9-98ca-fab9-e87f-73864e97b79b.gif)

サイズを変更するスライダーを動かすと次々にペン先が生成されています。
完全に勝ちました。

## Retinaディスプレイだとボケる

ずっと外部モニタで開発していて、MBPの画面で人に説明しようとしたら突然ボケだしたのでめちゃくちゃ焦りましたが、Retinaディスプレイのせいで `window.devicePixelRatio` の値が2になっていたせいでした。
これはcanvas要素のwidth,height属性を２倍したあと、styleで元のサイズまで縮小して表示させ、さらにcontextのscaleを２倍にするという意味のわからないことをやると解決します。

こんな感じです。

```js
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const dpr = window.devicePixelRatio

const { width, height } = canvas

// canvas要素のwidth,heightをdevicePixelRatio倍する
canvas.width = width * dpr
canvas.height = height * dpr

// styleで元のサイズに戻して見た目は大きくしないようする
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`

// contextのscaleをdevicePixelRatioに合わせておく
context.scale(dpr, dpr)
```

## px-brushというnpmパッケージにしました

というわけで「こんな感じにじっそうしたら、いい感じに動かないかな〜」と考えてやってみた結果、超うまくいったので世界中の皆さんにも使っていただきたくnpmパッケージにしました。

https://www.npmjs.com/package/px-brush

バグや要望などありましたらどしどしPull Request送ってきてください。