「背景コマ送りアニメーション」システムを作って、無料でオープンソースにした
林政賢 ·
今 tangyi.mx のトップページを下にスクロールすると、背景が一緒に動く。止まれば、そのコマで止まる。戻れば、逆再生する。あれは動画ではない。
トップページの「スクロール制御背景」をオープンソースにした
あれは一連のフレームを、コードが一コマずつ駆動している。そのために書いたのが scroll-frame-sequence:535 行、依存ゼロ(zero-dependency)の JavaScript ライブラリだ。外部パッケージに一切頼らず、動画をフレーム列に分解してスクロール位置で精密に制御し、ブラウザの動画自動再生やスクロール同期に関する制限をすべて迂回する。
開発した理由は明快だ。複雑な映像インタラクションのページを作るとき、頻繁なスクロール下でネイティブ動画がカクつくのに耐えられなかった。ページがコマ落ちした瞬間、体験は崩壊する。このシステムは素材処理ツールを四つの Python コマンドに包み、あらゆる解像度向けのスプライトシートを直接出力する。高インタラクションなページを作っているなら、まず私たちの AI デモ で、この構成がモバイルでどう滑らかさを保っているかを見てほしい——性能のためだけではなく、どの端末でもアニメーションの見た目を揃えるためでもある。
スクロールアニメーションでスマホのメモリを落とさないには?
画像はただの画像だと思うだろうが、ブラウザにとってデコード後の画像はすべてメモリの奪い合いだ。これが開発中に踏んだ最大の穴だった。スマホの描画限界は非常に残酷で、閾値を超えた瞬間にページは落ちるか固まる。
ブラウザが画像のデコードに使うメモリは固定の式で決まる:幅 × 高さ × 4 bytes。つまりスプライトシート(spritesheet)のファイル自体が小さくても、デコード後のサイズは一瞬で端末を溢れさせうる。
実測:スプライトシート解像度 vs デコード後メモリ
だからスプライトシートは特定の規格に固定し、一枚に高すぎる解像度を使うことを禁止した。ここまでしつこくメモリ負荷を計算するのは、モバイルのインタラクティブページでは、画像が大きすぎてページが再読み込みされると離脱率が跳ね上がるからだ。スクロール途中で真っ白になるなら、まずスプライトシートの元サイズを確認すること——一枚の画像にスマホの RAM の半分を食わせてはいけない。ブラウザの問題ではなく、開発者である私たちの責任だ。
読み込みが遅い?三層ロードと二分接近法
メモリのクラッシュを解決しただけでは足りない。スクロール中にまだ長いローディングバーを待たされるなら、定着率は悲惨なままだ。そこで「三層ロード」を設計し、リソースを段階的に届ける:
- 下層(約 2MB):スプライトシートを骨格として最優先で読み込み、ページを開いた瞬間からアニメーションがスクロールに反応するようにする。
- 中層(約 5MB):続いてフル解像度のフレームを読み込み、下層のぼやけたプレビューを順次置き換える。
- 上層:停留区間のループ処理。ユーザーがそこまでスクロールしたときだけ、その区間のループ素材を動的に読み込む。
ユーザーを待たせないために、ダウンロード順は「二分接近法」を使う。1 コマ目から最後まで順に並べるのではなく、飛ばして読む——0 → 149 → 75 → 37 → 112、以下同様。利点は、ファイル量の約二割が届いた時点で、ユーザーが全編の視覚動線を一通り見られること。途中の数コマが未着でも、リズムは途切れない。
これはページを「速く見せる」ためだけではなく、高遅延のモバイル回線でインタラクションを保つためだ。AI デモ で見るスクロール効果の土台がこの階層戦略——大容量ファイルを一度に要求せず、サーバー負荷もユーザーの待つ不安も下げる。
Python でフレーム抽出を自動化する
フレームの品質と滑らかさを保証するために、手動のスクリーンショットに代わる Python ツールを四つ書いた。一つ目はフレーム抽出 extract_frames.py:動画が全 577 コマなら 150 コマのサンプルを推奨し、ツールは round(i × 576 / 149) で精密に配分する。単純な fps フィルタより、最初と最後のコマの正確さを保てる。
--weighted を付けると、画面の変化が大きい箇所により多くのコマが割り当てられ、静的なシーンは自動で減り、通信量を節約できる。完全なコマンド:
python tools/extract_frames.py 元動画.mp4 frames/ --count 150 --scale 1920:1080 --quality 72
フレームが揃ったら build_spritesheet.py で結合してスプライトシートにする。9 列(--cols 9)に設定すると、ブラウザが描画時に画像メモリを効率よく扱える:
python tools/build_spritesheet.py frames/ spritesheet.webp --cols 9
品質に自信がなければ、先に contact_sheet.py でプレビュー画像を作り、--mark で 1、75、150 などのキーフレームに印を付けて、サンプリング後の画のつながりが想定どおりか確認する。これらのスクリプトのおかげで、かつてのコマ単位の確認作業が消え、産線に入る素材はすべて規格化済みになった。
AI 素材の首尾がつながらない?Ping-pong モードと JSON 座標表
AI 生成の動く素材には硬い欠点がある。最初と最後のコマが滑らかにつながらず、そのままループすると目に見えて跳ねる。実測では、素材の首尾の差が 2.3 倍から 7.7 倍の範囲にあると、無理なループの結果は極めて悪い。首尾がつながる素材づくりにこだわらず、ping-pong モードに切り替える——フレーム列を「順再生してから逆再生」し、物理的に継ぎ目を避ける。
この種の処理は make_loop.py で行い、ループのコマ数は 24 にして滑らかさを保つ:
python tools/make_loop.py ループ素材.mp4 hold_mid/ --count 24
ループを処理しても、スマホでの表示はまた別の穴だ。縦画面で横長素材を正確に見せるために、ハードコードをやめ、JSON 形式の「座標タイムテーブル」に切り替えた。各コマに対応する frame 番号、スマホでのオフセット cx(画面幅の比率)、fit モードを記録する。フロントエンドはスクロール時にこれを元に背景のオフセットと拡縮を動的に調整し、重要な画が常に画面中央に来て、画面サイズの違いで切れないようにする。スマホで素材に余白が出るなら、まず JSON の fit が cover になっているか、cx が画面中心のオフセットに合わせて補正されているかを確認すること。
最も見つけにくい二つの穴:sticky の失効とデータベースの文字列上限
一つ目は position: sticky が予告なく効かなくなること。<html> または <body> レベルで overflow-x: hidden を設定すると、ブラウザは新しいスクロールコンテナ(scrolling container)を強制的に作り、sticky の位置決めの連鎖を直接断ち切る。解法は単純で、body { overflow-x: clip; } に変える。溢れを切りつつ、余計なスクロールコンテキストを作らない。
二つ目はバックエンドで起きた。150 個のフレーム URL をデータベースに保存しようとして、単一のテキスト欄が長さ制限に引っかかると分かった——150 個の URL を連ねた文字列は約 20KB で、無理に入れると切り捨てられる。最終的な解法は「分割保存」:
- 150 個の URL を 25 個ずつに分け、別々の欄に保存する。
- フロントエンドで読み込み時に再結合し、結果を
localStorageにキャッシュする。 - キャッシュの有効期間は一週間。頻繁な DB 読み取りを避け、ページ読み込み時のリクエストも減らす。
バックエンド統合で上書きの衝突が起きるなら、データ構造に「シーケンス識別子」を入れること。これは アプリ製品 を作る際に素材プロジェクトを区別するために加えた重要な印で、複数ページが同じアップローダーを共有しても、ID の衝突でデータがずれない。
画質と性能のトレードオフ:WebP 圧縮と重み設定
極端なファイル削減を追うと、「画質を空間と引き換える」限界効用の罠に落ちやすい。実測では、JPEG を全面的に WebP に置き換えるとファイルサイズを約 53% 削減できる。これが最も効く一手だ。だがさらに押し込むと失望する——品質を q75 から q55 に下げると画面は明らかに粗くなるのに、実際に浮くのは約 20% だけ。投資対効果は極めて低く、帯域が極端に制限された環境でない限り q72 から q75 に留めるのを勧める。見た目と容量の最良のバランス点だ。
もう一つ面白い数字がある。AI 生成素材の圧縮率は実写より良い。AI の画面は細部が滑らかなことが多く、実写より約 55% 小さくなる。ただし重み(weight)を調整するとき、デスクトップのパラメータをそのままスマホに当ててはいけない——モバイルはサンプリングのコマ数が少なく(デスクトップ 150 コマ、スマホは 48 コマ推奨)、重みの比率を合わせないとスクロールに深刻なつっかえが出る。実装上はスマホの重みをデスクトップの三分の一に保つ。勘ではなく、小さな画面で速くスクロールしたとき、画面の変化とスクロール進行の同期率を一定に保つためだ。
実装ガイドとオープンソースリソース
この解法を scroll-frame-sequence にまとめた。535 行、依存ゼロ、正式にオープンソース化済み。モバイルでの動画スクロールのカクつきや、ブラウザの自動再生制限に悩んでいるなら、そのまま開発リストに入れてほしい。
- ソースコード:GitHub リポジトリ。素材処理用の Python スクリプト四本を含む。
- パッケージ:npm でインストールし、そのままフロントエンドに取り込む。
着手前に、デモページ で実際のスクロールの滑らかさを体感することを勧める。三層ロードと二分接近法を完全に示している。データベース管理が不要なら、manifest.json を生成してスプライトシートとフレームを静的ストレージに置くだけで、このライブラリで複雑なスクロールテリングが作れる。position: sticky の失効やデータベースの文字列上限といった境界問題に当たったら、ドキュメントのバグ対処の項を参照して、本番で同じ穴を踏み直さないでほしい。
この記事の数字について:メモリ使用量(134MB/538MB)、WebP の圧縮率(53%)、q75→q55 の容量差(約 20%)、AI 素材と実写の容量差(約 55%)はすべて本サイトのトップページ素材での実測で、素材が変われば数字も変わる。
著者:林政賢(監督 · Gen AI クリエイター兼エンジニア · 瑭宜影像創意工作室 創業者)