はじめに
この記事は、以下の記事で扱っていた HTML-in-Canvas に関するものです。今回、HTML-in-Canvas を p5.js Web Editor で使ってみました。
●前に見かけていた HTML-in-Canvas を少し試してみる(公式情報・関連情報を調べつつ) - Qiita
https://qiita.com/youtoy/items/e2b2e36e395c33bb4c00
実際に試してみる
HTML-in-Canvas に関する情報は既に前回の記事に書いているので、今回は省略します。
以下では、実際に p5.js Web Editor で HTML-in-Canvas を試した時の流れを書いていきます。
p5.js と HTML-in-Canvas を組み合わせる話について
今回の内容を試していくにあたり、「p5.js と HTML-in-Canvas を組み合わせる」というのを実現する実装を、少し確認しました。
p5.js で HTML-in-Canvas を扱う場合について
p5.js で HTML-in-Canvas を扱う場合、drawElementImage() が p5.js の API ではないため、p5.js の drawingContext を用いる必要があります。
それと今回、HTML要素は createDiv() や createButton() を用いて作るのですが、それを Canvas の直接の子要素にする処理を行います。
そして、HTML要素を子要素とする Canvas に、layoutsubtree を設定します。これは、HTML-in-Canvas では、描画対象が layoutsubtree を持つ Canvas の直接の子要素である必要があるためです(layoutsubtree の話は、WICG のリポジトリに記載があります)。
実行手順について
この後で示しているコードを、Chrome で実行するための下準備は以下となります。
- Chrome で chrome://flags/#canvas-draw-element を Enabled にしていない場合は、それを Enabled にする
- Chrome を再起動する
このフラグの有効化が実際に反映されているかどうかを確認する方法については、よろしければ前回の記事をご参照ください。
今回用いるコード
コードで実現する処理内容
今回用いるコードについて、実行した際に描画される内容は、前回用いたコードで描画したものと同じです。具体的には、以下のような描画を行います。
コードの内容
コードの内容は、以下のとおりです。これを実行する際には、p5.js Web Editor の sketch.js の内容を、この内容に置きかえて実行すれば OK です。
let cnv;
let ctx;
let blueCard;
let pinkCard;
let yellowButton;
let animationTime = 0;
let htmlInCanvasSupported = false;
function setup() {
pixelDensity(1);
cnv = createCanvas(800, 500);
cnv.elt.setAttribute("layoutsubtree", "");
/*
* drawingContextを使うのはp5.jsのAPIに存在しない以下の3つだけ。
*
* 1. drawElementImage() … HTML-in-Canvas本体
* 2. requestPaint() … HTML-in-Canvas本体
* 3. ctx.filter … 「次の描画1回だけ」へのフィルター適用
*/
ctx = drawingContext;
createHtmlElements();
htmlInCanvasSupported =
typeof ctx.drawElementImage === "function" &&
typeof cnv.elt.requestPaint === "function";
console.table({
drawElementImage: typeof ctx.drawElementImage === "function",
requestPaint: typeof cnv.elt.requestPaint === "function",
});
if (!htmlInCanvasSupported) {
console.error(
"HTML-in-Canvas APIが利用できません。\n" +
"chrome://flags/#canvas-draw-element をEnabledにして、" +
"Chromeを再起動してください。"
);
background(15);
fill(255, 100, 100);
noStroke();
textSize(18);
textAlign(CENTER, CENTER);
text(
"HTML-in-Canvas APIが利用できません\n" +
"chrome://flags/#canvas-draw-element を確認してください",
width / 2,
height / 2
);
noLoop();
return;
}
/*
* HTML-in-Canvasの描画はpaintイベント内で行う
*/
cnv.elt.addEventListener("paint", paintHtmlElements);
/*
* p5.jsの通常のdraw()は使わず、
* requestAnimationFrameからrequestPaint()を呼ぶ。
*/
noLoop();
requestAnimationFrame(animate);
}
function createHtmlElements() {
// --------------------------------
// 青いカード
// --------------------------------
blueCard = createDiv(`
<h2 style="
margin:0 0 8px;
font-size:26px;
">
Blue Card
</h2>
<p style="
margin:0;
line-height:1.5;
">
p5.jsの回転とせん断で<br>
動かしています。
</p>
`);
setStyles(blueCard, {
width: "220px",
padding: "22px",
boxSizing: "border-box",
borderRadius: "18px",
color: "white",
background: "linear-gradient(135deg, #2563eb, #06b6d4)",
boxShadow: "0 14px 35px rgba(6, 182, 212, 0.35)",
transformOrigin: "top left",
});
// --------------------------------
// ピンクのカード
// --------------------------------
pinkCard = createDiv(`
<h2 style="
margin:0 0 8px;
font-size:26px;
">
Pink Card
</h2>
<p style="
margin:0;
line-height:1.5;
">
横方向に分割して<br>
波打たせています。
</p>
`);
setStyles(pinkCard, {
width: "220px",
padding: "22px",
boxSizing: "border-box",
borderRadius: "18px",
color: "white",
background: "linear-gradient(135deg, #ec4899, #7c3aed)",
boxShadow: "0 14px 35px rgba(236, 72, 153, 0.35)",
transformOrigin: "top left",
});
// --------------------------------
// 黄色いボタン
// --------------------------------
yellowButton = createButton("HTML Button");
setStyles(yellowButton, {
padding: "16px 30px",
border: "none",
borderRadius: "999px",
background: "linear-gradient(135deg, #fde047, #f59e0b)",
color: "#422006",
fontSize: "22px",
fontWeight: "bold",
cursor: "pointer",
transformOrigin: "top left",
});
yellowButton.mousePressed(() => {
if (yellowButton.html() === "Clicked!") {
yellowButton.html("HTML Button");
} else {
yellowButton.html("Clicked!");
}
cnv.elt.requestPaint();
});
/*
* HTML-in-Canvasでは描画対象がCanvasの直接の子要素である必要がある。
* p5.jsがbodyへ追加した要素をCanvasへ移動する。
*/
cnv.elt.appendChild(blueCard.elt);
cnv.elt.appendChild(pinkCard.elt);
cnv.elt.appendChild(yellowButton.elt);
}
function setStyles(element, styles) {
Object.assign(element.elt.style, styles);
}
/*
* HTML-in-Canvasのpaintイベント
*/
function paintHtmlElements() {
resetMatrix();
// p5.jsで背景を描画
background(2, 6, 23);
drawGrid();
drawBlueCard();
drawPinkWaveCard();
drawYellowButton();
}
/*
* 青いカード
*
* p5.jsのtranslate、rotate、shearXを使用。
*/
function drawBlueCard() {
push();
translate(80, 80);
const rotation = sin(animationTime * 1.4) * 0.12;
const shearAmount = sin(animationTime) * 0.32;
rotate(rotation);
shearX(shearAmount);
const transform = ctx.drawElementImage(blueCard.elt, 0, 0);
pop();
/*
* Canvas上の位置と本物のDOM要素の位置を同期する。
* これによりクリック判定やアクセシビリティ上の位置を合わせられる。
*/
blueCard.elt.style.transform = transform.toString();
}
/*
* ピンクのカード
*
* p5.jsのsin()で横方向のずれを計算し、p5.jsのclip()で細い帯だけ表示する。
*/
function drawPinkWaveCard() {
const baseX = 485;
const baseY = 75;
const cardWidth = 220;
const cardHeight = 150;
const sliceHeight = 5;
for (let sourceY = 0; sourceY < cardHeight; sourceY += sliceHeight) {
const offsetX = sin(animationTime * 3 + sourceY * 0.075) * 18;
push();
clip(() => {
rect(baseX + offsetX, baseY + sourceY, cardWidth, sliceHeight + 1);
});
ctx.drawElementImage(pinkCard.elt, baseX + offsetX, baseY);
pop();
}
/*
* 波形は1つの行列では表現できないので、
* DOMのクリック領域は基準位置だけ合わせる。
*/
pinkCard.elt.style.transform = `translate(${baseX}px, ${baseY}px)`;
}
/*
* 黄色いボタン
*
* p5.jsで位置と拡大率を変更し、
* drawingContext.filterで発光させる。
*/
function drawYellowButton() {
push();
translate(295, 350);
const pulse = 1 + sin(animationTime * 4) * 0.06;
scale(pulse);
/*
* p5.jsのfilter()は「描画済みのキャンバス全体」に
* 後からかける処理なので、
* 「この1回の描画にだけ」フィルターを適用するには
* CanvasRenderingContext2D.filterを使うしかない。
* drawElementImage()にだけフィルターを適用する。
*/
ctx.filter =
"blur(0.2px) " +
"drop-shadow(0 0 8px rgba(253, 224, 71, 0.9)) " +
"drop-shadow(0 0 24px rgba(249, 115, 22, 0.8))";
const transform = ctx.drawElementImage(yellowButton.elt, 0, 0);
// 次の描画へfilterを持ち越さない
ctx.filter = "none";
pop();
yellowButton.elt.style.transform = transform.toString();
}
function drawGrid() {
stroke(56, 189, 248, 32);
strokeWeight(1);
for (let x = 0; x <= width; x += 40) {
line(x, 0, x, height);
}
for (let y = 0; y <= height; y += 40) {
line(0, y, width, y);
}
noStroke();
fill(148, 163, 184);
textSize(15);
textAlign(LEFT, TOP);
text("p5.js + HTML-in-Canvas", 20, 18);
}
/*
* アニメーションループ
*
* p5.jsのdraw()を使わずにHTML-in-Canvasへpaintを要求する。
*/
function animate(milliseconds) {
animationTime = milliseconds / 1000;
if (htmlInCanvasSupported) {
cnv.elt.requestPaint();
}
requestAnimationFrame(animate);
}
上記のコードを実行した結果
上記のコードを p5.js Web Editor で動作させた時の様子を掲載します。
まずは、わざと HTML-in-Canvas が有効ではない環境で実行してみました。その結果、以下のように HTML-in-Canvas が使えない場合の表示が出ることを確認できました。
あとは、前の記事を書いたときにも使った HTML-in-Canvas を有効にしている環境で、上記を実行してみます。その結果、以下のように前回の記事で試したのと同様の描画が行われることを確認できました。

