動画とHTMLを同一領域に統合表示した
動画とHTMLコンテンツを、同じ領域に配置する必要があった。
コンポーネント志向でカプセル化。それぞれ独立した処理にした。
なぜ作ったか
midori253まで、3D空間にHTMLを表示できるようになった。
でも、動画は別扱いだった。
HTMLエリアと動画エリアが分離している。
一緒に表示したい場面があった。
説明文と動画を、同じパネルに配置。
ナレーション付きデモ。
これまでの実装では、無理だった。
統合する必要があった。
統合の方針
同一Canvas上に描画
HTMLも動画も、同じCanvasに描画。
class UnifiedDisplay {
constructor(width, height) {
this.canvas = new OffscreenCanvas(width, height);
this.ctx = this.canvas.getContext('2d');
this.htmlRenderer = null;
this.videoRenderer = null;
}
render() {
// HTML部分を描画
if (this.htmlRenderer) {
this.htmlRenderer.draw(this.ctx, 0, 0);
}
// 動画部分を描画
if (this.videoRenderer) {
this.videoRenderer.draw(this.ctx, 0, 200);
}
}
}
1つのCanvasに、全てを統合。
位置を指定して、配置。
レイヤー構造
HTMLと動画を、レイヤーとして扱う。
const layers = [
{ type: 'html', y: 0, height: 200 },
{ type: 'video', y: 200, height: 400 },
{ type: 'html', y: 600, height: 200 }
];
柔軟に配置できる。
HTMLの間に動画を挟むこともできる。
コンポーネント化
各レンダラーを独立したクラスに。
HTMLレンダラー
class HtmlRenderer {
constructor(component) {
this.component = component;
this.svgCache = null;
}
async draw(ctx, x, y) {
const svg = await this.generateSVG();
const image = await this.svgToImage(svg);
ctx.drawImage(image, x, y);
}
async generateSVG() {
const content = this.component.render();
return `
<svg xmlns="http://www.w3.org/2000/svg">
<foreignObject width="1000" height="800">
${content}
</foreignObject>
</svg>
`;
}
}
HTMLからSVG、SVGから画像へ変換。
Canvasに描画。
動画レンダラー
class VideoRenderer {
constructor(videoUrl) {
this.video = document.createElement('video');
this.video.src = videoUrl;
this.video.loop = true;
this.video.muted = true;
}
draw(ctx, x, y) {
if (this.video.readyState >= 2) {
ctx.drawImage(this.video, x, y);
}
}
play() {
this.video.play();
}
pause() {
this.video.pause();
}
}
動画要素を直接描画。
再生・停止を制御。
カプセル化の実装
コンポーネント定義
Web Componentsで、HTMLコンテンツを定義。
class InfoComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<style>
.info { padding: 20px; background: white; }
h2 { color: #2c3e50; }
</style>
<div class="info">
<h2>動画の説明</h2>
<p>この動画では...</p>
</div>
`;
}
}
customElements.define('info-panel', InfoComponent);
カスタムタグとして定義。
スタイルも内包。
レイアウト指定
レイヤー情報で、配置を指定。
const layout = {
width: 1000,
height: 800,
layers: [
{
type: 'component',
component: 'info-panel',
y: 0,
height: 150
},
{
type: 'video',
src: 'demo.mp4',
y: 150,
height: 450
},
{
type: 'component',
component: 'caption-panel',
y: 600,
height: 200
}
]
};
宣言的に配置できる。
見通しが良い。
描画の最適化
選択的更新
変更があったレイヤーだけ再描画。
render() {
this.layers.forEach(layer => {
if (layer.needsUpdate) {
layer.render(this.ctx);
layer.needsUpdate = false;
}
});
this.updateTexture();
}
静的なHTMLは、1回だけ描画。
動画は、毎フレーム更新。
CPU使用率を抑えられた。
Canvas領域の制限
各レイヤーは、指定された領域だけ使う。
draw(ctx, x, y, width, height) {
ctx.save();
ctx.rect(x, y, width, height);
ctx.clip();
// 描画処理
this.renderContent(ctx, x, y);
ctx.restore();
}
はみ出さない。
他のレイヤーと干渉しない。
ハマったポイント
Canvas座標の計算
各レイヤーの座標を、正しく計算する必要があった。
最初、y座標を間違えた。
レイヤー2が、レイヤー1に重なって表示される。
累積計算を追加。
let currentY = 0;
this.layers.forEach(layer => {
layer.y = currentY;
currentY += layer.height;
});
これで、正しく配置された。
動画の描画タイミング
動画の準備ができていない状態で、描画しようとした。
真っ黒な画面が表示される。
readyStateをチェック。
if (this.video.readyState >= 2) {
ctx.drawImage(this.video, x, y, width, height);
} else {
// プレースホルダーを表示
ctx.fillStyle = '#cccccc';
ctx.fillRect(x, y, width, height);
}
準備完了まで、プレースホルダーを表示。
ユーザーに状態が伝わる。
SVG変換の非同期処理
HTMLからSVGへの変換は、非同期。
でも、描画は同期的に呼ばれる。
タイミングのズレで、何も表示されない。
Promise.all()で待機。
async initialize() {
const promises = this.layers
.filter(layer => layer.type === 'html')
.map(layer => layer.prepare());
await Promise.all(promises);
this.ready = true;
}
全てのHTMLレイヤーの準備を待ってから、描画開始。
正しく表示された。
パフォーマンス測定
レイヤー構成別に、フレームレートを測定した。
HTML 1層
30fps。軽い。
HTML 1層 + 動画 1層
25fps。許容範囲。
動画のデコードが、CPU負荷を上げる。
HTML 2層 + 動画 1層
20fps。やや負荷が高い。
HTMLレイヤーが増えると、SVG変換のコストが増える。
でも、実用レベル。
試してみた結果
チュートリアル
上部: 説明文
中央: デモ動画
下部: 次のステップへのボタン
分かりやすい。
見る人が迷わない。
比較デモ
左: 旧バージョンの動画
右: 新バージョンの動画
下部: 違いの説明
視覚的に差が分かる。
効果的だった。
ナレーション付きギャラリー
写真が切り替わるたびに、説明文も更新。
動画(写真のスライドショー)+ HTML(説明)の組み合わせ。
没入感がある。
使ってみて
実際に動画とHTMLを統合してみて、可能性を感じた。
単なる表示ではなく、インタラクティブな説明ができる。
ポイントは以下の3つ:
- 同一Canvas上での統合描画(レイヤー構造で柔軟に配置)
- コンポーネント指向でカプセル化(独立性、再利用性)
- 選択的更新で最適化(静的HTMLは1回、動画は毎フレーム)
次のmidori255で、さらに画質を改善した。
同じような動画+HTML統合を試している方の参考になれば嬉しいです。
まとめ
今回は、動画とHTMLを同一領域に統合表示しました。
ポイントは以下の4つ:
- レイヤー構造での統合(HTMLと動画を同一Canvas上に配置)
- コンポーネント分離(HtmlRenderer、VideoRenderer独立)
- 宣言的レイアウト指定(JSONで配置を定義)
- 選択的更新最適化(変更レイヤーのみ再描画)
動画とHTMLを、自由に組み合わせられるようになった。
これが、midori255以降の基盤になった。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。