画像と動画を統一的に3D空間に表示した
BASE64圧縮の画像では画質が悪かった。
Canvasを使った表示に変更。動画と同じ方式で、高画質な画像を表示できるようにした。
なぜ作ったか
midori254まで、HTMLを3D空間に表示する実装を進めてきた。
でも、画像の扱いに問題があった。
SVGに画像を埋め込むとき、BASE64でエンコードする。
const base64 = await imageToBase64(imageUrl);
const svgImage = `<image href="${base64}" x="0" y="0" width="100" height="100"/>`;
シンプル。でも、画質が悪い。
BASE64エンコードで、データサイズが約1.3倍に膨れる。圧縮率も下がる。
結果、低画質な画像になった。
写真展示では、画質が重要。妥協できない。
別の方法を探した。
動画と同じ方式
動画は、Canvasに描画して、テクスチャにしていた。
const videoElement = document.createElement('video');
videoElement.src = 'video.mp4';
canvas.getContext('2d').drawImage(videoElement, 0, 0);
これなら、高画質を維持できる。
画像も同じ方式にすることにした。
画像のCanvas描画
画像をCanvas上に描画。
const image = new Image();
image.src = imageUrl;
image.onload = () => {
const canvas = new OffscreenCanvas(image.width, image.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
// Canvasからテクスチャ生成
const texture = new THREE.CanvasTexture(canvas);
};
BASE64を経由しない。元の画像をそのまま使う。
画質が維持された。
配置位置の統一
動画と画像で、配置方式が違っていた。
動画: Canvasの指定位置に描画
画像: SVG内に直接配置
統一したかった。
画像も、Canvasの指定位置に描画するようにした。
drawImage(canvas, x, y, width, height) {
const ctx = canvas.getContext('2d');
ctx.drawImage(this.image, x, y, width, height);
}
動画と画像を、同じインターフェースで扱える。
コードがシンプルになった。
コンポーネント化
HTMLページをWeb Componentsで定義。
カスタムタグの作成
class PhotoComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div class="photo-container">
<h1>写真ギャラリー</h1>
<div class="photos"></div>
</div>
`;
}
}
customElements.define('photo-page', PhotoComponent);
<photo-page>というカスタムタグで、ページ全体を定義。
SVG内に、このタグを配置。
<svg>
<foreignObject x="0" y="0" width="1000" height="800">
<photo-page></photo-page>
</foreignObject>
</svg>
カプセル化できた。
スタイルやスクリプトが、他のページと干渉しない。
再利用性
同じコンポーネントを、複数の場所で使える。
// ギャラリー1
const gallery1 = document.createElement('photo-page');
gallery1.setAttribute('folder', 'images/gallery1');
// ギャラリー2
const gallery2 = document.createElement('photo-page');
gallery2.setAttribute('folder', 'images/gallery2');
属性で設定を変えるだけ。
コードの重複がなくなった。
静的コンテンツの最適化
ページ内容が変わらない時、SVG変換をスキップ。
変更検知
HTMLのハッシュ値を計算。
getContentHash() {
const content = this.element.innerHTML;
let hash = 0;
for (let i = 0; i < content.length; i++) {
const char = content.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash;
}
前回のハッシュと比較。
変わっていなければ、変換をスキップ。
const newHash = this.getContentHash();
if (newHash === this.lastHash) {
return; // 変換不要
}
this.lastHash = newHash;
this._regenerateTexture();
CPU使用率が、60%削減された。
動画の任意表示
動画は、必要な時だけ表示。
setVideoVisible(visible) {
if (visible) {
this.video.play();
} else {
this.video.pause();
}
this.videoVisible = visible;
}
見ていない時は、再生を停止。
メモリとCPUを節約できた。
ハマったポイント
画像の読み込みタイミング
最初、画像がまだ読み込まれていない状態で、Canvasに描画しようとした。
何も表示されない。
onloadイベントを待つようにした。
const image = new Image();
image.onload = () => {
this.drawToCanvas();
};
image.onerror = () => {
console.error('画像の読み込み失敗:', image.src);
};
image.src = imageUrl;
これで、確実に表示されるようになった。
Canvas座標系の違い
動画と画像で、座標系の考え方が違った。
動画: 左上が(0, 0)
画像: 中心が(0, 0)
補正が必要だった。
// 画像の場合、中心座標に補正
const offsetX = x - image.width / 2;
const offsetY = y - image.height / 2;
ctx.drawImage(image, offsetX, offsetY, width, height);
これで、動画と画像の位置が揃った。
コンポーネントのライフサイクル
Web Componentsの初期化タイミングが難しかった。
connectedCallback()が呼ばれる前に、SVG変換が実行される。
内容が空のまま、変換されてしまう。
Promise で待機。
async waitForComponent() {
return new Promise((resolve) => {
if (this.element.isConnected) {
resolve();
} else {
setTimeout(() => {
this.waitForComponent().then(resolve);
}, 100);
}
});
}
コンポーネントの準備が整ってから、SVG変換。
正しく表示された。
パフォーマンス測定
画質と速度を比較した。
BASE64方式
画質: 低(圧縮劣化あり)
初期化: 2秒(エンコードに時間がかかる)
FPS: 25fps
Canvas方式
画質: 高(元画像を維持)
初期化: 0.5秒(エンコード不要)
FPS: 30fps
Canvas方式の圧勝。
画質も速度も、両方改善した。
試してみた結果
写真ギャラリー
20枚の高解像度写真を表示。
BASE64では、ぼやけて見えた。
Canvas方式に変更したら、鮮明になった。
拡大しても、綺麗。
動画+画像の混在
動画3本、画像10枚を同時表示。
統一されたインターフェースで、扱いやすい。
平均20fps。実用的。
コンポーネントの再利用
3種類のギャラリーコンポーネント(風景、人物、建築)を作成。
それぞれ、別のフォルダの画像を表示。
コードの重複なし。管理しやすい。
使ってみて
実際に使ってみて、BASE64とCanvas方式の違いを実感した。
画質が全然違う。拡大すると、差が顕著。
ポイントは以下の3つ:
- BASE64を避けてCanvas描画(画質維持、速度向上)
- 動画と画像を統一インターフェース化(コードがシンプル)
- Web Componentsでカプセル化(再利用性向上、干渉なし)
次のmidori256で、さらにアニメーション機能を追加した。
同じようなメディア表示システムを作っている方の参考になれば嬉しいです。
まとめ
今回は、画像と動画を統一的に3D空間に表示しました。
ポイントは以下の4つ:
- Canvas方式で高画質維持(BASE64の圧縮劣化を回避)
- 動画と画像の統一インターフェース(同じ描画方式で扱う)
- Web Componentsでカプセル化(独立性、再利用性向上)
- 静的コンテンツの最適化(ハッシュ検知でCPU60%削減)
BASE64の限界を超えた。
これが、midori256以降の基盤になった。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。