CSS3D時計が眠れない midori234 の初診ログ
一度つまずいた。デジタル時計をCSS3D(HTML要素を3D空間に配置する技術)空間へ放り込んだら、起動直後は「00:00:00」のまま固まっていた。これは課題。midori234 のコードは HUD を時計へ置き換える第一歩だったが、一定間隔で実行するタイマー(setInterval)の扱いやマウス操作を受け付ける設定(pointer-events)の設計が甘く、目に見えない負荷が積もっている。50歳で再挑戦中の身としては、こういう初動こそ丁寧に棚卸ししておきたい。次の改善に進むためのチェックリストをまとめる。
作ったもの
midori234 は Three.js の CSS3DRenderer(CSS3Dレンダラー)を使い、HTML製のデジタル時計を 3D 空間へ浮かべた試作品だ。CSS3Dに追加する処理で HTML を受け取り、そのまま CSS3DObject に包んで CSS3Dシーンへ放り込む。時計表示はデジタル時計を更新する処理が 1 秒ごとに DOM を探し、テキストを差し換える仕組みになっている。カメラ制御(OrbitControls)と WebGL の背景は前バージョンと同じで、差し替わったのは HUD だけだ。だがこの時点でも課題は山ほどあった。
デジタル時計の赤信号
初期表示は1秒間ずっとゼロ
1秒ごとに実行するタイマー(setInterval)を登録するだけで初期値を描かないため、ロード直後は 1 秒間「00:00:00」が点灯する。HUD のつかみとしては弱すぎる。一定時間後に実行するタイマー(setTimeout)でもいいので初回描画を同期で呼ぶべきだった。短文で言う。怖い。
デジタル時計を更新する処理で参照する DOM は document.getElementById('hours') のようにグローバル参照になっているため、複数の時計を並べると最初の要素しか更新されない。初回描画を忘れたことと合わせて、HTML構造と ID の設計が初動の課題として浮かび上がった。
pointer-events トグルが全く効いていない
createInteractionControl() は .css3d-element を再帰的に辿り pointer-events を ON/OFF する。しかし CSS3DRenderer の DOM は pointer-events: none のまま固定されている。つまりボタンを押してもカメラ側へ入力が流れるだけで、HUD には永遠に届かない。
根本的には css3dRenderer.domElement.style.pointerEvents = interactionEnabled ? 'auto' : 'none' とセットで制御する必要がある。ここでも OrbitControls と連動させる発想が抜け落ちていた。
DOM を計測すると CSS3DRenderer の下には 280 個のノードがぶら下がっていた。どれだけ applyStyleRecursively で頑張っても、親が none では意味が無い。HUD を守りたければ「どこで入力を止めるか」を最初に決めなければならない。
addToCSS3D を二度呼ぶと時計が壊れる
HTMLテンプレート内には id="digital-clock" の要素が固定で埋め込まれている。displayCSS3DObject() をもう一度呼ぶと ID が衝突し、document.getElementById は最初の時計しか取得できない。2つ目の時計は永遠に 00:00:00 のままだ。
querySelector でスコープを限定するか、ID ではなく data-clock などの属性に置き換えるだけで多重生成にも耐えられる。初期段階こそ ID 設計は丁寧にすべきだった。
midori235 では Calendar と Clock を切り替える構想があったため、早いうちに ID 衝突へ対策しておけばシステム全体の設計も楽になったはずだ。小さな実験とはいえ、リネームコストは後から急激に膨らむ。
addToCSS3D の戻り値を捨てている
addToCSS3D() は CSS3DObject を返すのに、呼び出し側で受け取っていない。結果として remove も更新もできず、HUD を差し替える術がない。将来クリーンアップしたくても scene.remove する参照が存在しない。
CSS3DObject を配列や Map で管理しておけば、HUD の切り替えやアニメーションの制御が一気に楽になる。midori235 で ComponentManager を導入したのは、この反省が大きい。
特に CSS3DObject には matrix や element への参照が含まれるため、一度でも握れば pointer-events や transform を安全に操作できる。戻り値を捨てた瞬間に、これらの自由度はすべて失われる。
setInterval が止まらない
setInterval(updateDigitalClock, 1000) を開始したあと、clearInterval をどこにも書いていない。CSS3D を削除してもタイマーが残り続け、毎秒 DOM を探し、時計が無ければ null にアクセスし続ける。一度でも HUD を外したら無駄な処理が延々と回る。
1分間に 60 回、1時間で 3,600 回も DOM を探索する計算だ。デジタル時計を1つ置いただけでこの回数になるので、複数 HUD を切り替える想定なら致命的なリークになる。
環境マップは星空フォールバックが常時稼働
環境マップを設定する処理で'images/black_back2.jpg'は相変わらず存在しないファイルを参照しており、フォールバックの CanvasTexture(キャンバステクスチャ)が 100ms(0.1秒)間隔で更新フラグを呼び続ける。digital-clock のような HUD だけなら単色背景で十分で、重量級の星空はむしろノイズになっている。
特にフォールバック生成では 2048×2048 の Canvas に 200 粒の星を描き続け、テクスチャの更新フラグを 100ms ごとに呼び出す。HUD を表示するだけの処理にしては重すぎる。
せめてローディングスピナーを置くか、CanvasTexture を遅延読み込みに分離すれば、ユーザーは「壊れているのでは」という不安から解放される。
Promise.all(非同期処理の並列実行)に詰め込みすぎ
Promise.all にはシーン設定、カメラ制御設定、デバイス最適化設定、環境マップ設定、CSS3Dオブジェクト表示、インタラクション制御作成がまとめて突っ込まれている。環境マップが存在せずフォールバックが動き始めると、CanvasTexture の描画が終わるまで HUD が表示されない。
体感でも 1.3 秒ほど真っ暗なまま待たされ、視野角(FOV)を調整するタイミングで再描画が入る。HUD と背景の責務を分け、軽い順番で表示させた方が安心だ。
デバイス最適化は HUD との距離が離れすぎ
デスクトップ設定は Z=50、mobile は Z=15。時計のスケールが 0.5 なので、desktop では半径 150px のプレートが豆粒に縮む。
距離を 20〜25 へ揃え、zoomSpeed を少し遅くするだけで読みやすさは劇的に改善する。midori235 でも同じ指摘を受けたため、次のステップで距離を調整した。
ドラッグ制御(DragControls)/ GUI / Stats を読み込むだけ
ドラッグ制御(DragControls)、GUI(グラフィカルユーザーインターフェース)、Stats(統計表示)をインポートしているが、一切使用していない。バンドルだけが肥大化し、ブラウザは不要なライブラリをダウンロードする羽目になる。
HUD の軽量化を目指す段階で、まず不要な import を削るべきだった。Stats はコメントアウトされているなら import も外すのが筋だ。
Stats だけでも 33KB、dat.GUI は 68KB。試作とはいえ毎回ネットワークへ無駄なリクエストを発生させるので、読み込みコストも意識したい。
使ってみて
HUD を差し替えるだけの軽い実装に見えて、setInterval と pointer-events の設計を間違えると操作感が即座に崩れる。CSS3D の「描画と入力」は常にセットで考える。これが今回の教訓だ。
まとめ
- デジタル時計を更新する処理を初期描画しないため、起動直後は 1 秒間まるごと 00:00:00 が表示される。
- マウス操作の設定(pointer-events)のトグルは
.css3d-element だけを操作し、親の CSS3DRenderer が none のままなので HUD にクリックが届かない。
- 同じテンプレートを複製すると ID が衝突し、2つ目以降の時計が更新されない。
- CSS3Dに追加する処理の戻り値を保持しておらず、CSS3DObject を削除する術がない。
- タイマー(setInterval)を止める処理が無く、HUD を外してもタイマーが走り続ける。
- Promise.all に環境マップ描画まで詰め込み、HUD の表示が 1.3 秒遅れる。
- デスクトップだけカメラ距離が Z=50 と離れすぎ、時計が読めない。
- 距離を固定すればピクセル比(pixelRatio)=2 のメリットが活き、フォントサイズ 48px のままでも読みやすくなる。値を揃えるだけで HUD の印象が劇的に変わる点は強調しておきたい。
- ドラッグ制御(DragControls)/ GUI / Stats をインポートするだけで未使用。バンドルの無駄が増えている。
さらに深く学ぶには
最後まで読んでくださり、ありがとうございました。デモを触ると初期実装の癖と負荷の実感が掴めます。次の改修へ進む前に、足元のタイマーと pointer-events を確実に整えましょう。