10
3

More than 5 years have passed since last update.

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

Posted at

はじめに

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

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

最初のスタンプ型実装

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というものでアンチエイリアスのかかっていない円形の描画ができるようです。

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送ってきてください。

10
3
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
10
3