JavaScript/Canvas
Canvas API[1] を使ってウェブブラウザにグラフィックスを描いてみましょう。
長方形の描画
[編集]- 矩形を描画するプログラムの例
<!DOCTYPE html> <html lang='ja'> <meta charset="utf-8"> <title>Canvas 要素に JavaScript で四角形を描く</title> <canvas id="myCanvas" width="300" height="300">HTML5のCANVAS要素に対応したウェブブラウザで御覧ください。</canvas> <script> // Canvas要素を取得 const myCanvas = document.querySelector("#myCanvas"); // 2Dコンテキストを取得 const context = myCanvas.getContext("2d"); // strokeRectメソッドを使って四角形を描画 // (20, 30)から始まり、幅150px、高さ200pxの四角形を描画 context.strokeRect(20, 30, 150, 200); </script> </html>
- 文書型宣言はHTML5を指定します。
- 言語は日本語
- エンコーディングは UTF-8
- タイトルは必須要素です
- 今回の主役のCANVAS要素;サポートしていないウェブブラウザのケアも忘れずに[2]
- JavaScriptのプログラムはSCRIPT要素の中に書きます
- myCanvas は、CANVAS要素のElementオブジェクト[3]、id属性を持つ要素はid属性の値を変数名としてJavaScriptからアクセスできます。継承関係 HTMLCanvasElement < HTMLElement < Element
- 描画コンテキストを得ます[4]。今回は "2d" コンテキストタイプを使いますが他に "wgl" や "wgl2" があります
- strokeRectメソッド[5]は対角の座標を与えて矩形(Rectangle)を描画します
文字列の描画
[編集]Canvasには、文字列を画像として描画することも出来ます。
文字列を描画するプログラムの例
<!DOCTYPE html>
<html lang='ja'>
<meta charset="utf-8">
<title>Canvas 要素に JavaScript で文字列を描く</title>
<canvas id="canvasTest" width="300" height="300">HTML5のCANVAS要素に対応したウェブブラウザで御覧ください。</canvas>
<script>
const context = canvasTest.getContext("2d"),
text = "こんにちは Text";
context.font = "32px serif"
context.fillText(text, 30, 50);
</script>
</html>
imageData へのアクセスを使った高速化
[編集]マンデルブロ集合の計算(と描画)は、 演算性能を評価するベンチマークテストにも使われるほど膨大な計算時間を必要とするとともに、 可視化するとフラクタルの持つ特有の美しさを持ち頻繁にプログラミングの題材とされます。
JavaScriptで描いてみます。
マンデルブロ集合を描画するプログラム
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='utf-8'>
<title>JavaScriptでマンデルブロー集合を描画してみた</title>
<meta content='CANVAS要素のimageDataに直描するというやや野蛮な方法でマンデルブロ集合を描画してみました。' name='description'>
</head>
<body onload='init()'>
<h1>JavaScriptでマンデルブロー集合を描画してみた</h1>
<canvas height='800' id='canvas' width='800'>Canvas対応ブラウザを使用してください。</canvas>
<div style='text-align: right; width: 800px'>
<span id='laptime'></span>
<a href='#' id='link'>画像を保存</a>
</div>
<script>
function init() {
let t = Date.now();
draw();
document.querySelector("#laptime").innerHTML = `描画処理時間: ${(Date.now() - t) / 1000}秒`;
function draw() {
const canvas = document.querySelector("#canvas"),
context = canvas.getContext("2d"),
{clientWidth, clientHeight} = canvas,
rMin = -2, rMax = 2, rSpan = rMax - rMin,
iMin = -2, iMax = 2, iSpan = iMax - iMin,
nmax = 300, /* 収束判定の上限回数 */
zLimit = 4, /* 収束判定条件 */
n = 5;
let imageData = // Canvas要素から描画用ImageDataを取得
context.createImageData(clientWidth, clientHeight),
index = 0; // buffer のインデックス左上が0
for (let j = 0; j < clientHeight; j++) {
let ci = iMin + iSpan * j / (clientHeight - 1);
for (let i = 0; i < clientWidth; i++) {
const cr = rMin + rSpan * i / (clientWidth - 1);
let r = 0, im = 0;
let k = 0, z2 = 0;
for (k = 0; k < nmax; k++) {
const zr = r * r - im * im + cr,
zi = 2 * r * im + ci;
r = zr;
im = zi;
z2 = r * r + im * im;
if (z2 >= zLimit) {
break;
}
}
if (k == nmax) { /* 収束しなかった */
imageData.data[index++] = z2 * n * 4 // Red
imageData.data[index++] = z2 * n * 16 // Green
imageData.data[index++] = z2 * n * 64 // Bule
}
else { /* 収束した */
imageData.data[index++] = z2 * n * 2 // Red
imageData.data[index++] = z2 * n * 4 // Green
imageData.data[index++] = z2 * n * 8 // Bule
}
imageData.data[index++] = 255; // Alpha
}
}
context.putImageData(imageData, 0, 0);
document.querySelector("#link").setAttribute("href", canvas.toDataURL());
}
}
</script>
</body>
</html>
まとまった長さのプログラムなのでステップ・バイ・ステップの解説は行いませんが、要点だけ。
<body onload='init()'>
Loadが終わるまで(DOMが準備完了に成るまで)Canvasは使えないのでBODY要素のonload属性に初期化メソッドinitを設定します。
{clientWidth, clientHeight} = canvas,
Canvasとは直接関係ありませんが、オブジェクトのプロパティの値を同名の変数に代入するイディオムです。
clientWidth = canvas.clientWidth, clientHeight = canvas.clientHeight,
と同じです。
短くかけるとともに、ミスタイプの入り込む余地がなく元のオブジェクトのプロパティ名の変数の名付けを行う動機づけになります(別の名前の変数にプロパティを代入する構文もあります)。
let imageData = // Canvas要素から描画用ImageDataを取得
context.createImageData(clientWidth, clientHeight),
index = 0; // buffer のインデックス左上が0
Canvas オブジェクトの領域にあるピクセルデータ(ImageData オブジェクト)を得ています[8]。 imageData.dataプロパティはUint8ClampedArrayオブジェクトです。 このUint8ClampedArraオブジェクトは型付き配列の一種で、 U:符号なし int8:8ビット整数う Clamped:? Array:配列、というネーミングで符号なし整数の配列であろうということはわかりますがClamped が問題です。 Clamped は「強制された」と訳されますが、この場合の強制は
255以上の値が要素に代入されたら255に丸める。0未満の値が要素に代入されたら0に丸める。
という挙動を示します。のみならず、
浮動小数点数が代入されたら、端数が0.5より小さいなら切り捨て/端数が0.5より大きいならは切り上げ/端数がちょうど0.5なら切り捨て、と切り上げのうち結果が偶数となる方へ丸める
という動作をします(最近接偶数丸め)。
このUint8ClampedArraオブジェクトのimageData.dataは
[
R(0,0), G(0,0), B(0,0), A(0,0), R(1,0), G(1,0), B(1,0), A(1,0), ... R(width-1,0), G(width-1,0), B(width-1,0), A(width-1,0),
R(0,1), G(0,1), B(0,1), A(0,1), R(1,1), G(1,1), B(1,1), A(1,1), ... R(width-1,1), G(width-1,1), B(width-1,1), A(width-1,1),
:
:
R(0,height-1), G(0,height-1), B(0,height-1), A(0,height-1), ... R(width-1,height-1), G(width-1,height-1), B(width-1,height-1), A(width-1,height-1),
]
という(二次元ではなく)一次元配列で、赤 緑 青 透過率 の4バイトの対が左から右・上から下の順で並んでいます。
context.putImageData(imageData, 0, 0);
document.querySelector("#link").setAttribute("href", canvas.toDataURL());
imageDataに操作を行ったあと、putImageDataメソッドでCanvasで反映します。
次の行のtoDataURLメソッドはCanvasの中のイメージを data: スキームで(ディフォルトでは img/png で)返します。
この事で「名前をつけて画像を保存」を実現しています。
HAMLソース
[編集]このプログラムは、HAMLでコーディングしHTMLに変換しました。
!!!
:ruby
CANVAS_DIM, RANGE = 800, 2
%html{:lang => "ja"}
%head
%meta{charset: "utf-8"}/
%title JavaScriptでマンデルブロー集合を描画してみた
%meta{content: 'CANVAS要素のimageDataに直描するというやや野蛮な方法でマンデルブロ集合を描画してみました。', name: 'description'}
%body{onload: "init()"}
%h1 JavaScriptでマンデルブロー集合を描画してみた
%canvas#canvas{height: CANVAS_DIM, width: CANVAS_DIM} Canvas対応ブラウザを使用してください。
%div{:style => "text-align: right; width: #{CANVAS_DIM}px"}
%span#laptime
%a#link{:href => "#"} 画像を保存
:javascript
function init() {
let t = Date.now();
draw();
document.querySelector("#laptime").innerHTML = `描画処理時間: ${(Date.now() - t) / 1000}秒`;
function draw() {
const canvas = document.querySelector("#canvas"),
context = canvas.getContext("2d"),
{clientWidth, clientHeight} = canvas,
rMin = -#{RANGE}, rMax = #{RANGE}, rSpan = rMax - rMin,
iMin = -#{RANGE}, iMax = #{RANGE}, iSpan = iMax - iMin,
nmax = 300, /* 収束判定の上限回数 */
zLimit = 4, /* 収束判定条件 */
n = 5;
let imageData = // Canvas要素から描画用ImageDataを取得
context.createImageData(clientWidth, clientHeight),
index = 0; // buffer のインデックス左上が0
for (let j = 0; j < clientHeight; j++) {
let ci = iMin + iSpan * j / (clientHeight - 1);
for (let i = 0; i < clientWidth; i++) {
const cr = rMin + rSpan * i / (clientWidth - 1);
let r = 0, im = 0;
let k = 0, z2 = 0;
for (k = 0; k < nmax; k++) {
const zr = r * r - im * im + cr,
zi = 2 * r * im + ci;
r = zr;
im = zi;
z2 = r * r + im * im;
if (z2 >= zLimit) {
break;
}
}
if (k == nmax) { /* 収束しなかった */
imageData.data[index++] = z2 * n * 4 // Red
imageData.data[index++] = z2 * n * 16 // Green
imageData.data[index++] = z2 * n * 64 // Bule
}
else { /* 収束した */
imageData.data[index++] = z2 * n * 2 // Red
imageData.data[index++] = z2 * n * 4 // Green
imageData.data[index++] = z2 * n * 8 // Bule
}
imageData.data[index++] = 255; // Alpha
}
}
context.putImageData(imageData, 0, 0);
document.querySelector("#link").setAttribute("href", canvas.toDataURL());
}
}
マンデルブロ集合を描画するプログラム:実行例
[編集]Canvas要素へのアニメーションの描画
[編集]requestAnimationFrameのコールバックに、Canvas要素への描画関数を渡すとアニメーションを行うことができます。
- balls.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas Animation Example</title> <style> body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } canvas { border: 1px solid black; width: 100vh; height: 100vh; } </style> </head> <body> <canvas id="myCanvas" width="200" height="200"></canvas> <script> class Ball { constructor(x, y, speed, angle, radius, ctx, canvas) { this.x = x; this.y = y; this.speed = speed; this.angle = angle; this.radius = radius; this.ctx = ctx; this.canvas = canvas; this.gravity = 0.098; // 重力加速度 this.bounceFactor = 0.99; // 反射係数 } draw() { this.ctx.beginPath(); this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); const hue = this.angle; // 運動の角度を色相の角度に const saturation = 80; // 彩度を一定に設定(0~100 の範囲) let brightness = this.speed * 100 / 5; // 速度を明度に反映 brightness = brightness > 80 ? 80 : brightness; // 明度>80でクリップ brightness = brightness < 20 ? 20 : brightness; // 明度<20でクリップ const color = `hsl(${hue}, ${saturation}%, ${brightness}%)`; // HSL 色空間で色を表現 this.ctx.fillStyle = color; this.ctx.fill(); this.ctx.closePath(); } update() { // 角度をラジアンに変換して移動量を計算 let radians = this.angle * Math.PI / 180; let vx = Math.cos(radians) * this.speed; let vy = Math.sin(radians) * this.speed; // 重力による垂直方向の速度の変化 vy += this.gravity; // 新しい位置を計算 this.x += vx; this.y += vy; // 壁に対する反射 if (this.x + this.radius > this.canvas.width) { this.x = this.canvas.width - this.radius; vx *= -this.bounceFactor; // 水平成分の反射 } else if (this.x - this.radius < 0) { this.x = this.radius; vx *= -this.bounceFactor; // 水平成分の反射 } // 床に対する反射 if (this.y + this.radius > this.canvas.height) { this.y = this.canvas.height - this.radius; vy *= -this.bounceFactor; // 垂直成分の反射 } else if (this.y - this.radius < 0) { this.y = this.radius; vy *= -this.bounceFactor; // 垂直成分の反射 } // 新しい速度と角度を反映 this.speed = Math.hypot(vx,vy); this.angle = Math.atan2(vy, vx) * 180 / Math.PI; } } // end class Ball const canvas = document.querySelector('#myCanvas'); const ctx = canvas.getContext('2d'); const balls = []; // 初期設定 const numBalls = 5; // ボールの数 const radius = 10; // ボールの半径 for (let i = 0; i < numBalls; i++) { // ランダムな初期位置と速度を設定 const initialX = Math.random() * (canvas.width - 2 * radius) + radius; const initialY = Math.random() * (canvas.height - 2 * radius) + radius; const initialSpeed = Math.random() * 5 + 2; // 2 から 7 のランダムな速度 const initialAngle = Math.random() * 360; // 0 から 360 度のランダムな角度 // ボールインスタンスを生成し、配列に追加 balls.push(new Ball(initialX, initialY, initialSpeed, initialAngle, radius, ctx, canvas)); } const startTime = Date.now(); let lastFrameTime = startTime; let frames = 0; function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 時間経過を計算 const now = Date.now(); const elapsedTime = now - startTime; lastFrameTime = now; frames++; const fps = 1000 * frames / elapsedTime; // fps を表示 ctx.font = '16px sans-serif'; ctx.fillStyle = 'black'; ctx.fillText(`${fps.toFixed(2)} fps`, 10, 20); balls.forEach(ball => { ball.update(); ball.draw(); }); requestAnimationFrame(animate); } animate(); </script> </body> </html>
解説
[編集]HTML部分
[編集]<head>
セクションに基本的なメタデータとCSSスタイルが含まれています。body
をflex
でセンタリングし、canvas
に枠を付けています。<canvas>
要素はアニメーションを描画する領域として使用します。
JavaScript部分
[編集]Ballクラス
[編集]Ball
クラスのコンストラクタは、ボールの位置(x
とy
)、速度(speed
)、角度(angle
)、半径(radius
)、描画コンテキスト(ctx
)およびキャンバス(canvas
)を初期化します。draw
メソッドは、ボールの色を計算し描画します。色相(hue
)は角度に基づき、明度(brightness
)は速度に基づいて計算されます。update
メソッドは、ボールの位置を更新し、壁や床に衝突した場合の反射を処理します。速度成分(vx
とvy
)を使って新しい位置を計算し、重力を垂直速度に追加します。
アニメーションの実行
[編集]balls
配列に複数のBall
インスタンスを生成します。各ボールはランダムな初期位置、速度、角度を持ちます。animate
関数は、requestAnimationFrame
を使ってアニメーションをループさせます。各フレームでキャンバスをクリアし、全てのボールの位置を更新し描画します。
このコードにより、ボールがランダムな方向に動き、速度と角度に基づいて色が変わるアニメーションが実現されます。ボールはキャンバスの端に達すると反射し、重力の影響を受けて動き続けます。
水の流れアニメーション
[編集]Y軸だけずらしたサイン波を描画してX軸でスクロールします。
- waves.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>水の流れアニメーション</title> <style> body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } canvas { border: 1px solid black; width: 100vh; height: 100vh; } </style> </head> <body> <canvas id="canvas"></canvas> <script> const canvas = document.querySelector('canvas'); const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; const waveCount = 10; // 波線の数 const waveWidth = width / waveCount; // 波線の幅 const waves = []; for (let i = 0; i < waveCount; i++) { const y = i * waveWidth; const amplitude = 16; // 波線の振幅 const speed = 0.2; // 波線の速度 waves.push({ y: y, amplitude: amplitude, speed: speed, phase: 0, }); } function draw() { // キャンバスをクリア ctx.clearRect(0, 0, width, height); // 波線を描画 for (const wave of waves) { ctx.beginPath(); ctx.moveTo(0, wave.y); for (let x = 0; x < width; x++) { const y = wave.y + wave.amplitude * Math.sin(2 * Math.PI * x / waveWidth + wave.phase); ctx.lineTo(x, y); } ctx.strokeStyle = 'blue'; ctx.stroke(); // 波線を更新 wave.phase += wave.speed; } // 次のフレームを要求 requestAnimationFrame(draw); } // 最初のフレームを描画 draw(); </script> </body> </html>
以下に、上記の「水の流れアニメーション」のHTMLとJavaScriptコードの解説を示します。
HTML 部分
[編集]<head>
セクションに基本的なメタデータとCSSスタイルが含まれています。body
をflex
でセンタリングし、canvas
に枠を付けています。<canvas>
要素はアニメーションを描画する領域として使用します。
JavaScript 部分
[編集]キャンバスとコンテキストの初期化
[編集]canvas
要素を取得し、その描画コンテキスト(ctx
)を取得します。- キャンバスの幅(
width
)と高さ(height
)を変数に保存します。
波の設定
[編集]waveCount
は、描画する波の数を指定します。waveWidth
は、各波の幅をキャンバスの幅を波の数で割ることで計算します。waves
配列には、各波の初期設定を格納します。- それぞれの波に対して、y位置、振幅(
amplitude
)、速度(speed
)、および初期位相(phase
)を設定します。
波の描画と更新
[編集]draw
関数は、キャンバスをクリアし、波を描画し、波の位相を更新します。ctx.clearRect(0, 0, width, height)
でキャンバス全体をクリアします。- 各波について、パスを開始し(
ctx.beginPath()
)、波の開始位置を移動します(ctx.moveTo(0, wave.y)
)。 for
ループでキャンバスの幅にわたって各ポイントの y 座標を計算し、lineTo
メソッドでパスを描画します。波の y 座標は、基本の y 座標に振幅と位相を考慮した正弦波の値を加えることで計算します。- 波の色を設定し(
ctx.strokeStyle = 'blue'
)、パスを描画します(ctx.stroke()
)。 - 各波の位相を更新し、次のフレームを要求します(
requestAnimationFrame(draw)
)。
このコードは、波の流れを模倣するアニメーションをキャンバス上に描画します。波の振幅、速度、そして位相がそれぞれ異なるため、自然な水の動きを再現しています。
複数の形状
[編集]Shapeクラスを作り、それを継承し様々な形状クラスを導出。
- shapes.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas Animation Example</title> <style> body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } canvas { border: 1px solid black; width: 100vh; height: 100vh; } </style> </head> <body> <canvas id="myCanvas" width="500" height="500"></canvas> <script> class Shape { constructor(x, y, angle, ctx) { this.x = x; this.y = y; this.angle = angle; this.ctx = ctx; this.rotationSpeed = Math.random() * 0.04 - 0.02; // ランダムな回転速度 } draw() { // 抽象メソッドとして定義 throw new Error('draw() メソッドが実装されていません'); } update() { this.angle += this.rotationSpeed; } } class Square extends Shape { constructor(x, y, size, angle, ctx) { super(x, y, angle, ctx); this.size = size; } draw() { this.ctx.save(); this.ctx.translate(this.x, this.y); this.ctx.rotate(this.angle); this.ctx.fillStyle = 'blue'; this.ctx.fillRect(-this.size / 2, -this.size / 2, this.size, this.size); this.ctx.restore(); } } class Triangle extends Shape { constructor(x, y, size, angle, ctx) { super(x, y, angle, ctx); this.size = size; } draw() { this.ctx.save(); this.ctx.translate(this.x, this.y); this.ctx.rotate(this.angle); this.ctx.fillStyle = 'green'; this.ctx.beginPath(); this.ctx.moveTo(0, -this.size / 2); this.ctx.lineTo(this.size / 2, this.size / 2); this.ctx.lineTo(-this.size / 2, this.size / 2); this.ctx.closePath(); this.ctx.fill(); this.ctx.restore(); } } class Circle extends Shape { constructor(x, y, radius, angle, ctx) { super(x, y, angle, ctx); this.radius = radius; } draw() { this.ctx.save(); this.ctx.translate(this.x, this.y); this.ctx.rotate(this.angle); const hue = (this.angle * 180 / Math.PI) % 360; // 角度に基づいて色相を変更 this.ctx.fillStyle = `hsl(${hue}, 100%, 50%)`; this.ctx.beginPath(); this.ctx.arc(0, 0, this.radius, 0, Math.PI * 2); this.ctx.fill(); this.ctx.restore(); } } const canvas = document.querySelector('#myCanvas'); const ctx = canvas.getContext('2d'); const shapes = []; const numShapes = 9; for (let i = 0; i < numShapes; i++) { const x = Math.random() * canvas.width; const y = Math.random() * canvas.height; const angle = Math.random() * 360; if (i % 3 === 0) { shapes.push(new Square(x, y, 50, angle, ctx)); } else if (i % 3 === 1) { shapes.push(new Triangle(x, y, 50, angle, ctx)); } else { shapes.push(new Circle(x, y, 25, angle, ctx)); } } function drawShapes() { ctx.clearRect(0, 0, canvas.width, canvas.height); shapes.forEach(shape => { shape.update(); shape.draw(); }); requestAnimationFrame(drawShapes); } drawShapes(); </script> </body> </html>
コードの解説
[編集]- HTMLヘッダ部分
- HTMLのヘッダ部分では、基本的なメタデータとスタイルシートを定義しています。
body
タグにflex
を使って、キャンバスを中央に配置します。canvas
要素に黒い枠線を付けています。
- キャンバスの設定
- キャンバス要素を
id
属性を使って設定し、幅と高さを500ピクセルに設定しています。
- キャンバス要素を
- Shapeクラスの定義
- 基底クラス
Shape
を定義しています。 x
とy
は図形の位置、angle
は回転角度、ctx
はキャンバスのコンテキストです。rotationSpeed
はランダムな回転速度を持ちます。draw
メソッドは派生クラスで実装されるべき抽象メソッドとして定義されています。update
メソッドで図形の回転角度を更新します。
- 基底クラス
- Squareクラスの定義
Shape
クラスを継承するSquare
クラスを定義。draw
メソッドで四角形を描画し、位置と回転を考慮しています。
- Triangleクラスの定義
Shape
クラスを継承するTriangle
クラスを定義。draw
メソッドで三角形を描画し、位置と回転を考慮しています。
- Circleクラスの定義
Shape
クラスを継承するCircle
クラスを定義。draw
メソッドで円を描画し、色相を回転角度に基づいて変更しています。
- キャンバスと図形の初期化
- キャンバスの要素を取得し、2Dコンテキストを設定します。
shapes
配列に9つの図形(正方形、三角形、円)をランダムな位置と角度で追加します。
- アニメーションの描画
drawShapes
関数はキャンバスをクリアし、各図形のupdate
とdraw
メソッドを呼び出します。requestAnimationFrame
を使って、次のフレームを描画し続けます。
このコードにより、複数の図形(正方形、三角形、円)がキャンバス上で回転するアニメーションが実現されます。円は回転しても見た目に変化がないため、回転角度に基づいて色相を変えることで、視覚的な変化を示しています。
まとめ
[編集]Canvas は様々なグラフィックインスタンスや効果をJavaScriptから操作する機能があります。 これを使いこなすには DOMの知識が必須です。 また、Canvasのイメージバッファを直接操作する事ができます。 この場合は、型付き配列の特性(特にUint8ClampedArrayの特性)を理解することが、処理速度の向上やコードの見通しの良さに繋がります。
脚注
[編集]- ^ https://momdo.github.io/html/canvas.html#the-canvas-element
- ^ https://momdo.github.io/html/dom.html#fallback-content
- ^ https://momdo.github.io/html/canvas.html#htmlcanvaselement
- ^ https://momdo.github.io/html/canvas.html#dom-canvas-getcontext
- ^ https://momdo.github.io/html/canvas.html#dom-context-2d-strokerect
- ^ https://momdo.github.io/html/canvas.html#dom-context-2d-font
- ^ https://momdo.github.io/html/canvas.html#dom-context-2d-filltext
- ^ https://momdo.github.io/html/canvas.html#imagedata