Canvasでドットの円と楕円を描画するサンプル
すみません。ゲーム用に調べてて作ったサンプルなのですが、動くけど中身ぜんぜん分かってません。
ドットの円と楕円のサンプル
まず、円を描画するアルゴリズムを調べました。すると英語版WikipediaにJavaScriptのソースが載ってたので、変数名とかを自分に分かるように書き直して使いました。
Midpoint circle algorithm - Wikipedia
よくわからないのですが、dy
の値が1だと半径が小さいときに円というより四角になってしまったので、いろいろ触ってみて2に変更することにしました。
次に、楕円を描画するアルゴリズムを調べました。こちらはStack Overflowに貼られていたものを参考にしました。触ってみても1ミリしか解らなくて魂が砕けました。ただ、この方法だと縦横の長さを入れ替えて90度回転させたとき、完全に一致する楕円にはならないらしい。
javascript - Is there a midpoint ellipse algorithm? - Stack Overflow
これらをお借りして、ドットで線を引けるようにいじったサンプルが以下のものです。
See the Pen multitouch by napoporitataso (@napoporitataso) on CodePen.
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="robots" content="noarchive" />
<title>Canvas ドットの円・楕円描画サンプル その1</title>
<style>
html {
background-color: #000;
color: #eee;
}
#canvas {
display: block;
box-sizing: border-box;
margin: 5px 0 10px;
border: 1px solid #666;
}
</style>
</head>
<body>
種類:
<select id="draw-type">
<option value="circle">円</option>
<option value="ellipse-horizontal">楕円(横)</option>
<option value="ellipse-vertical">楕円(縦)</option>
</select>
ドットの大きさ:
<select id="pixel-size">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="6">6</option>
<option value="8">8</option></select
>px
<canvas id="canvas"></canvas>
<script src="script.js"></script>
</body>
</html>
(() => {
const CANVAS_WIDTH = 320;
const CANVAS_HEIGHT = 320;
const STROKE_COLOR = [255, 255, 255, 255];
/**
* Canvasにサンプルを描画
* @param {CanvasRenderingContext2D} ctx Canvasのコンテキスト
* @param {string} type 種類 (circle|ellipse-horizontal|ellipse-vertical)
* @param {number} pixelSize ドットの大きさ
*/
const draw = (ctx, type, pixelSize) => {
// 黒で塗りつぶす
ctx.fillStyle = 'rgba(0, 0, 0, 255)';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 値をドットの大きさの整数倍に補正する
const pixelize = function (value) {
return value - (value % pixelSize);
};
const x = pixelize(160);
const y = pixelize(160);
// ImageData取得して書き込み
// 同心円状に6つの丸を描画
const imageData = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
const rasiuses = [10, 30, 50, 80, 110, 140];
switch (type) {
case 'circle':
rasiuses.forEach((radius) => {
putPixelCircle(imageData, x, y, pixelize(radius), pixelSize);
});
break;
case 'ellipse-horizontal':
rasiuses.forEach((radius) => {
putPixelEllipse(imageData, x, y, pixelize(radius), pixelize(radius / 2), pixelSize);
});
break;
case 'ellipse-vertical':
rasiuses.forEach((radius) => {
putPixelEllipse(imageData, x, y, pixelize(radius / 2), pixelize(radius), pixelSize);
});
break;
}
// ImageDataをcanvasに反映
ctx.putImageData(imageData, 0, 0);
};
/**
* ImageDataにドットの円を描画する
* https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
* @param {ImageData} imageData
* @param {number} cx 中心X座標
* @param {number} cy 中心Y座標
* @param {number} radius 半径
* @param {number} pixelSize ドットの大きさ
*/
const putPixelCircle = (imageData, cx, cy, radius, pixelSize) => {
const diameter = radius * 2; // 直径
let x = radius; // 半径 - 線幅
let y = 0;
let dx = 1;
let dy = 2; // わかんないけど1だと半径が小さい時に四角になっちゃうから2にした
let decisionOver2 = dx - diameter;
// 0・90・180・270度の地点から両側に向かって線を伸ばしていき、計8本の弧を書く
while (x >= y) {
putPixel(imageData, cx + x, cy - y, pixelSize); // 0 -> 45
putPixel(imageData, cx + y, cy - x, pixelSize); // 45 <- 90
putPixel(imageData, cx - y, cy - x, pixelSize); // 90 -> 135
putPixel(imageData, cx - x, cy - y, pixelSize); // 135 <- 180
putPixel(imageData, cx - x, cy + y, pixelSize); // 180 -> 225
putPixel(imageData, cx - y, cy + x, pixelSize); // 225 <- 270
putPixel(imageData, cx + y, cy + x, pixelSize); // 270 -> 315
putPixel(imageData, cx + x, cy + y, pixelSize); // 315 <- 360
// わかんない
if (decisionOver2 <= 0) {
y += pixelSize;
decisionOver2 += dy * pixelSize;
dy += 2 * pixelSize;
}
if (decisionOver2 > 0) {
x -= pixelSize;
dx += 2 * pixelSize;
decisionOver2 += (-diameter + dx) * pixelSize;
}
}
};
/**
* ImageDataにドットの楕円を描画する
* https://stackoverflow.com/questions/15474122/is-there-a-midpoint-ellipse-algorithm
* @param {ImageData} imageData
* @param {number} cx 中心X座標
* @param {number} cy 中心Y座標
* @param {number} radiusX 横の長さ
* @param {number} radiusY タテの長さ
* @param {number} pixelSize ドットの大きさ
*/
const putPixelEllipse = (imageData, cx, cy, radiusX, radiusY, pixelSize) => {
const aa = radiusX * radiusX;
const bb = radiusY * radiusY;
const aa2 = 2 * aa;
const bb2 = 2 * bb;
let p;
let x = 0;
let y = radiusY;
let px = 0;
let py = aa2 * y;
// 最初の点を描画
putPixel(imageData, cx + x, cy + y, pixelSize);
putPixel(imageData, cx - x, cy + y, pixelSize);
putPixel(imageData, cx + x, cy - y, pixelSize);
putPixel(imageData, cx - x, cy - y, pixelSize);
// 上下を描画 (わかんない)
p = bb - aa * radiusY + 0.25 * aa;
while (px < py) {
x += pixelSize;
px += bb2 * pixelSize;
if (p < 0) {
p += bb + px * pixelSize;
} else {
y -= pixelSize;
py -= aa2 * pixelSize;
p += (bb + px - py) * pixelSize;
}
putPixel(imageData, cx + x, cy + y, pixelSize);
putPixel(imageData, cx - x, cy + y, pixelSize);
putPixel(imageData, cx + x, cy - y, pixelSize);
putPixel(imageData, cx - x, cy - y, pixelSize);
}
// 左右を描画 (わかんない)
p = bb * (x + 0.5) * (x + 0.5) + aa * (y - 1) * (y - 1) - aa * bb;
while (y > 0) {
y -= pixelSize;
py -= aa2 * pixelSize;
if (p > 0) {
p += (aa - py) * pixelSize;
} else {
x += pixelSize;
px += bb2 * pixelSize;
p += (aa - py + px) * pixelSize;
}
putPixel(imageData, cx + x, cy + y, pixelSize);
putPixel(imageData, cx - x, cy + y, pixelSize);
putPixel(imageData, cx + x, cy - y, pixelSize);
putPixel(imageData, cx - x, cy - y, pixelSize);
}
};
/**
* ImageDataにドットを描画する
* @param {ImageData} imageData
* @param {number} x X座標
* @param {number} y Y座標
* @param {number} pixelSize ドットの大きさ
*/
const putPixel = (imageData, x, y, pixelSize) => {
// はみ出たら描画しない
if (x < 0 || CANVAS_WIDTH <= x || y < 0 || CANVAS_HEIGHT < y) {
return;
}
// 描画開始座標をドットごとのマス目の左上に補正
x = x - (x % pixelSize);
y = y - (y % pixelSize);
// ImageDataにドット書き込み
for (let offsetY = 0; offsetY < pixelSize; ++offsetY) {
for (let offsetX = 0; offsetX < pixelSize; ++offsetX) {
const i = 4 * ((y + offsetY) * CANVAS_WIDTH + (x + offsetX));
imageData.data[i] = STROKE_COLOR[0];
imageData.data[i + 1] = STROKE_COLOR[1];
imageData.data[i + 2] = STROKE_COLOR[2];
imageData.data[i + 3] = STROKE_COLOR[3];
}
}
};
// 初期化
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
// ドットの大きさ1pxで円を描画
draw(ctx, 'circle', 1);
// 種類・ドットの大きさ変更時に再描画するように
document.getElementById('draw-type').addEventListener('change', (e) => {
const type = e.target.value;
const pixelSize = parseInt(document.getElementById('pixel-size').value, 10);
draw(ctx, type, pixelSize);
});
document.getElementById('pixel-size').addEventListener('change', (e) => {
const type = document.getElementById('draw-type').value;
const pixelSize = parseInt(e.target.value, 10);
draw(ctx, type, pixelSize);
});
})();
ペイントっぽいサンプル
ペイントソフトよろしく、好きな大きさの丸を描けるサンプルも作ってみました。こっちだと、先述の縦横の挙動が一致しない感じがわりと分かります。横長で高さ1pxの丸は描けないけど、縦長で幅1pxの線が描けるあたりで顕著です。
同じ理由から、楕円の描画処理を使ってそのまま円を描こうとすると若干歪むので、縦横の長さが等しい時だけは円のアルゴリズムに切り替えるようにしてみました。
See the Pen pixel-ellipse-in-canvas-2 by napoporitataso (@napoporitataso) on CodePen.
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="robots" content="noarchive" />
<title>Canvas ドットの円・楕円描画サンプル その2</title>
<style>
html {
background-color: #000;
color: #eee;
}
#canvas {
display: block;
box-sizing: border-box;
margin: 5px 0 10px;
border: 1px solid #666;
}
</style>
</head>
<body>
マウスドラッグで丸を描けます。(タッチ操作非対応)<br />
ドットの大きさ:
<select id="pixel-size">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="6">6</option>
<option value="8">8</option></select
>px
<canvas id="canvas"></canvas>
<script src="script.js"></script>
</body>
</html>
(() => {
const CANVAS_WIDTH = 320;
const CANVAS_HEIGHT = 320;
const STROKE_COLOR = [255, 255, 255, 255];
// Canvasの大きさ設定
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
// 黒で塗りつぶし
ctx.fillStyle = 'rgba(0, 0, 0, 255)';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 描画開始時の情報記憶用
let imageData_org = null;
const startPos = { x: null, y: null };
// ドットの大きさ設定
let pixelSize = parseInt(document.getElementById('pixel-size').value, 10);
// ドットの大きさ変更時
document.getElementById('pixel-size').addEventListener('change', (e) => {
pixelSize = parseInt(e.target.value, 10);
});
// 描画開始時
canvas.addEventListener('mousedown', (e) => {
if (imageData_org !== null) {
return;
}
e.preventDefault();
const imageData = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// Canvas内でのイベント発生座標を算出
const bounds = e.target.getBoundingClientRect();
const x = e.clientX - bounds.left;
const y = e.clientY - bounds.top;
// 描画開始時の座標とピクセル情報を保持しておく
startPos.x = x;
startPos.y = y;
imageData_org = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// イベント座標からImageDataに楕円を描画
putPixelEllipseByEventCoords(imageData, startPos.x, startPos.y, x, y, pixelSize);
ctx.putImageData(imageData, 0, 0);
});
// 描画開始後にカーソル移動時
canvas.addEventListener('mousemove', (e) => {
if (imageData_org === null) {
return;
}
e.preventDefault();
const imageData = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 描画確定してない楕円を消す
copyImageData(imageData_org, imageData);
// Canvas内でのイベント発生座標を算出
const bounds = e.target.getBoundingClientRect();
const x = e.clientX - bounds.left;
const y = e.clientY - bounds.top;
// イベント座標からImageDataに楕円を描画
putPixelEllipseByEventCoords(imageData, startPos.x, startPos.y, x, y, pixelSize);
ctx.putImageData(imageData, 0, 0);
});
// 描画確定時
canvas.addEventListener('mouseup', (e) => {
if (imageData_org === null) {
return;
}
e.preventDefault();
const imageData = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 描画確定してない楕円を消す
copyImageData(imageData_org, imageData);
// Canvas内でのイベント発生座標を算出
const bounds = e.target.getBoundingClientRect();
const x = e.clientX - bounds.left;
const y = e.clientY - bounds.top;
// イベント座標からImageDataに楕円を描画
putPixelEllipseByEventCoords(imageData, startPos.x, startPos.y, x, y, pixelSize);
ctx.putImageData(imageData, 0, 0);
// 描画開始時に保存した値を破棄
startPos.x = startPos.y = null;
imageData_org = null;
});
/**
* ImageDataのピクセル情報をコピーする
* @param {ImageData} src コピー元ImageData
* @param {ImageData} dst コピー先ImageData
*/
const copyImageData = (src, dst) => {
for (let i = 0, len = dst.data.length; i < len; ++i) {
dst.data[i] = src.data[i];
}
};
/**
* イベント座標からドットの楕円を描画
* @param {ImageData} imageData
* @param {number} startX 描画開始座標X
* @param {number} startY 描画開始座標Y
* @param {number} x イベント座標X
* @param {number} y イベント座標Y
* @param {number} pixelSize ドットの大きさ
*/
const putPixelEllipseByEventCoords = (imageData, startX, startY, x, y, pixelSize) => {
// クリック座標とクリック開始座標から横とタテの長さを算出
let a = (x - startX) / 2;
let b = (y - startY) / 2;
// クリック座標・横・タテの長さをドットの大きさの整数倍に補正
x = x - (x % pixelSize);
y = y - (y % pixelSize);
a = Math.floor(a - (a % pixelSize));
b = Math.floor(b - (b % pixelSize));
// 中心座標算出
const cx = Math.floor(startX + a);
const cy = Math.floor(startY + b);
// b辺の長さがマイナス (カーソルを上方向に動かした時) だったら補正
b = b < 0 ? -1 * b : b;
if (a === b) {
// 縦横の長さが同じなら円を描画 (こっちの方が綺麗だから)
putPixelCircle(imageData, cx, cy, a, pixelSize);
} else {
// 楕円を描画
putPixelEllipse(imageData, cx, cy, a, b, pixelSize);
}
};
/**
* ImageDataにドットの円を描画する
* https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
* @param {ImageData} imageData
* @param {number} cx 中心X座標
* @param {number} cy 中心Y座標
* @param {number} radius 半径
* @param {number} pixelSize ドットの大きさ
*/
const putPixelCircle = (imageData, cx, cy, radius, pixelSize) => {
const diameter = radius * 2; // 直径
let x = radius; // 半径 - 線幅
let y = 0;
let dx = 1;
let dy = 2; // わかんないけど1だと半径が小さい時に四角になっちゃうから2にした
let decisionOver2 = dx - diameter;
// 0・90・180・270度の地点から両側に向かって線を伸ばしていき、計8本の弧を書く
while (x >= y) {
putPixel(imageData, cx + x, cy - y, pixelSize); // 0 -> 45
putPixel(imageData, cx + y, cy - x, pixelSize); // 45 <- 90
putPixel(imageData, cx - y, cy - x, pixelSize); // 90 -> 135
putPixel(imageData, cx - x, cy - y, pixelSize); // 135 <- 180
putPixel(imageData, cx - x, cy + y, pixelSize); // 180 -> 225
putPixel(imageData, cx - y, cy + x, pixelSize); // 225 <- 270
putPixel(imageData, cx + y, cy + x, pixelSize); // 270 -> 315
putPixel(imageData, cx + x, cy + y, pixelSize); // 315 <- 360
// わかんない
if (decisionOver2 <= 0) {
y += pixelSize;
decisionOver2 += dy * pixelSize;
dy += 2 * pixelSize;
}
if (decisionOver2 > 0) {
x -= pixelSize;
dx += 2 * pixelSize;
decisionOver2 += (-diameter + dx) * pixelSize;
}
}
};
/**
* ImageDataにドットの楕円を描画する
* https://stackoverflow.com/questions/15474122/is-there-a-midpoint-ellipse-algorithm
* @param {ImageData} imageData
* @param {number} cx 中心X座標
* @param {number} cy 中心Y座標
* @param {number} radiusX 横の長さ
* @param {number} radiusY タテの長さ
* @param {number} pixelSize ドットの大きさ
*/
const putPixelEllipse = (imageData, cx, cy, radiusX, radiusY, pixelSize) => {
const aa = radiusX * radiusX;
const bb = radiusY * radiusY;
const aa2 = 2 * aa;
const bb2 = 2 * bb;
let p;
let x = 0;
let y = radiusY;
let px = 0;
let py = aa2 * y;
// 最初の点を描画
putPixel(imageData, cx + x, cy + y, pixelSize);
putPixel(imageData, cx - x, cy + y, pixelSize);
putPixel(imageData, cx + x, cy - y, pixelSize);
putPixel(imageData, cx - x, cy - y, pixelSize);
// 上下を描画 (わかんない)
p = bb - aa * radiusY + 0.25 * aa;
while (px < py) {
x += pixelSize;
px += bb2 * pixelSize;
if (p < 0) {
p += bb + px * pixelSize;
} else {
y -= pixelSize;
py -= aa2 * pixelSize;
p += (bb + px - py) * pixelSize;
}
putPixel(imageData, cx + x, cy + y, pixelSize);
putPixel(imageData, cx - x, cy + y, pixelSize);
putPixel(imageData, cx + x, cy - y, pixelSize);
putPixel(imageData, cx - x, cy - y, pixelSize);
}
// 左右を描画 (わかんない)
p = bb * (x + 0.5) * (x + 0.5) + aa * (y - 1) * (y - 1) - aa * bb;
while (y > 0) {
y -= pixelSize;
py -= aa2 * pixelSize;
if (p > 0) {
p += (aa - py) * pixelSize;
} else {
x += pixelSize;
px += bb2 * pixelSize;
p += (aa - py + px) * pixelSize;
}
putPixel(imageData, cx + x, cy + y, pixelSize);
putPixel(imageData, cx - x, cy + y, pixelSize);
putPixel(imageData, cx + x, cy - y, pixelSize);
putPixel(imageData, cx - x, cy - y, pixelSize);
}
};
/**
* ImageDataにドットを描画する
* @param {ImageData} imageData
* @param {number} x X座標
* @param {number} y Y座標
* @param {number} pixelSize ドットの大きさ
*/
const putPixel = (imageData, x, y, pixelSize) => {
// はみ出たら描画しない
if (x < 0 || CANVAS_WIDTH <= x || y < 0 || CANVAS_HEIGHT < y) {
return;
}
// 描画開始座標をドットごとのマス目の左上に補正
x = x - (x % pixelSize);
y = y - (y % pixelSize);
// ImageDataにドット書き込み
for (let offsetY = 0; offsetY < pixelSize; ++offsetY) {
for (let offsetX = 0; offsetX < pixelSize; ++offsetX) {
const i = 4 * ((y + offsetY) * CANVAS_WIDTH + (x + offsetX));
imageData.data[i] = STROKE_COLOR[0];
imageData.data[i + 1] = STROKE_COLOR[1];
imageData.data[i + 2] = STROKE_COLOR[2];
imageData.data[i + 3] = STROKE_COLOR[3];
}
}
};
})();
そのほか
他に参考にさせていただいたところでは、以下のサイト様の解説は懇切丁寧で嬉しかったです。
伝説のお茶の間 No007-09(1) 円の描画(1) MichenerとBresenham
次のサイト様のソースコードは、シンプルなのに意味不明で、流したら本当に○が描けて恐ろしかったです。
以下に、当該のコードをJavaScriptで動かすサンプルを置いておきます。
See the Pen pixel-ellipse-in-canvas-wakaran by napoporitataso (@napoporitataso) on CodePen.
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="robots" content="noarchive" />
<title>わからん</title>
<style>
html {
background-color: #000;
color: #eee;
}
#canvas {
display: block;
box-sizing: border-box;
margin: 5px 0 10px;
border: 1px solid #666;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="script.js"></script>
</body>
</html>
/**
* 【参考】
* 円の描画(アルゴリズム)
* https://www.kazetest.com/vcmemo/drawcircle/drawcircle.htm
*/
const CANVAS_WIDTH = 320;
const CANVAS_HEIGHT = 320;
const STROKE_COLOR = [255, 255, 255, 255];
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
const imageData = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
const putPixel = (x, y) => {
var i = 4 * (y * CANVAS_WIDTH + x);
imageData.data[i] = STROKE_COLOR[0];
imageData.data[i + 1] = STROKE_COLOR[1];
imageData.data[i + 2] = STROKE_COLOR[2];
imageData.data[i + 3] = STROKE_COLOR[3];
};
const drawCircle = (cx, cy, r) => {
var xx = r << 7;
var yy = 0;
var x = 0;
var y = 0;
while (yy <= xx) {
x = xx >> 7;
y = yy >> 7;
putPixel(cx + x, cy + y);
putPixel(cx - x, cy - y);
putPixel(cx - x, cy + y);
putPixel(cx + x, cy - y);
putPixel(cx + y, cy + x);
putPixel(cx - y, cy - x);
putPixel(cx - y, cy + x);
putPixel(cx + y, cy - x);
yy += xx >> 7;
xx -= yy >> 7;
}
ctx.putImageData(imageData, 0, 0);
};
const drawEllipse = (cx, cy, a, b) => {
var xx = a << 6;
var yy = 0;
var x = 0;
var y = 0;
while (xx >= 0) {
x = xx >> 6;
y = yy >> 6;
putPixel(cx + x, cy + y);
putPixel(cx - x, cy - y);
putPixel(cx - x, cy + y);
putPixel(cx + x, cy - y);
yy += ((xx * b) / a) >> 6;
xx -= ((yy * a) / b) >> 6;
}
ctx.putImageData(imageData, 0, 0);
};
drawCircle(180, 170, 16);
drawCircle(160, 160, 52);
drawEllipse(160, 160, 14, 34);
drawEllipse(160, 160, 124, 62);