はじめに
HTMLのCanvasは便利なAPIがたくさん用意されていて簡単に描画ツールなどを自作できるのですが、通常の方法ではアンチエイリアスが強制的にかかってしまい、敢えてピクセル感のあるいわゆる「ジャギる」線を書くことはできません。
StackOverflowでもこのことが質問として上がっています。
https://stackoverflow.com/questions/195262/can-i-turn-off-antialiasing-on-an-html-canvas-element
なめらかな線しか書けない
こちらが普通にCanvas上に実装したペンの例です。
ここでは context.imageSmoothingEnabled
というAPIを使用していますが、これは画像の拡大縮小にしようするものであり、ペンの実装には全く効果がないことがわかります。
ジャギらせたい
Pixilartというサービスがありまして、ちょっとレトロな雰囲気で絵がかけるサービスです。
ここでは四角形のペンを実装していますので、描画時の座標から浮動小数点数を除去すれば確かにこれで可能なんですよね。
ただし、四角ではなく丸いペン先のツールを実装したいのです。
スタンプみたいにしてみてはどうか?
Pixilartから学べたことはペン先をスタンプにする。要するに画像をマウスの動きに合わせて連続して描画すればいけるんじゃないかと考えました。
しかし、ゆっくり動かした場合は問題ないんですが、すばやく動かすとスタンプの隙間ができてしまい線になりませんでした。
隙間を計算して埋める
というわけで他のHTML Canvasを使用しているサービスがどうやってこの隙間問題を解決しているのか調べたところ、以下の記事を発見しました。
いろんなペンの実装があって非常に参考になるのですが、まさに隙間問題を解決する方法が載ってまして、要するにマウスの動きの角度と距離を計算して隙間にもスタンプを描画するという手法でした。
実際のコードがこちらで
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))
}
これを適用したペン実装がこちらです
大変いい感じです。まさに求めていたジャギるペンツールの可能性が見えてきました。
サイズと色を動的にしたい
ここまでの実装ではフォトショップでGIF画像を作成し、それをスタンプとして使っていましたが、これではサイズも色も固定になってしまっています。
サイズと色を動的に変更できないものか、とググっていたところ以下のページを発見しました。
どうやらBresenham’s line algorithmというものでアンチエイリアスのかかっていない円形の描画ができるようです。
こんな感じですね。
動的なスタンプ生成がついにできた
できちゃいましたね。アルゴリズムのコードを参考に実装してみたところ若干苦戦しましたが実現できました。
サイズを変更するスライダーを動かすと次々にペン先が生成されています。
完全に勝ちました。
Retinaディスプレイだとボケる
ずっと外部モニタで開発していて、MBPの画面で人に説明しようとしたら突然ボケだしたのでめちゃくちゃ焦りましたが、Retinaディスプレイのせいで window.devicePixelRatio
の値が2になっていたせいでした。
これはcanvas要素のwidth,height属性を2倍したあと、styleで元のサイズまで縮小して表示させ、さらにcontextのscaleを2倍にするという意味のわからないことをやると解決します。
こんな感じです。
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パッケージにしました。
バグや要望などありましたらどしどしPull Request送ってきてください。