design-to-motion について
design-to-motionは、静的なデザインを忠実なアニメーションにするスキルです。ショーケースやウェブサイト向けに、デザイン再現、モーション制作、記録と書き出しを段階的に進めます。
主な機能
- 01元デザインの忠実な再現
- 02ショーケースとウェブサイトに対応
- 03ロード演出とビジュアルのストーリー演出
- 04動画とGIFの書き出し
- 05書き出し前の再現度チェック
活用シーン
- 01Figmaフレームのショーケース化
- 02ランディングページの機能カードに動きを追加
- 03製品モックアップにスクロール演出を追加
- 04GIFやMP4のデザインプレビューを作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ elayadesign/ ai-design-skills --skill design-to-motionインストール後、Claude Code で/design-to-motionを入力して読み込まれたか確認します。
詳細
デザインをモーション化
ショーケース用のアニメーションは、デザインのリデザインではありません。見る人には、元のデザインそのものと、それが生き生きと動く様子の両方が伝わる必要があります。
正確なフレーム → 空の状態 → スムーズな読み込み → すべてのビジュアルがそれぞれのストーリーを表現 → シームレスに繰り返し。
このスキルは3つのパートで構成されています。パートAでは、完成したフレームが元のデザインと見分けがつかないように再現します。パートBはモーションシステムです。パートCでは記録と書き出しを行います。Bに取りかかる前にAを完了し、A7とB9のチェックに合格するまで、決してエンコードしないでください。パートAとBは以下の両方のコンテキストに適用されます。パートCはショーケース用、または書き出しを求められた場合に限りウェブサイト用です。
対象範囲
静的なデザインをアニメーション化する依頼全般に適用します。Figmaのフレーム、UIカード、ダッシュボード、アプリ画面、マーケティング用ヒーロー、写真背景に配置されたデザインなどが対象です。ここに記載されたルールがフレームワークのデフォルトと競合する場合は、このファイルを優先してください。ユーザーの明示的な指示がルールと競合する場合は、ユーザーの指示を優先してください。
**対象外:**リリース済み製品内のインタラクションモーション(ホバー、メニュー、トーストなど)。その場合は、emilkowalski/skillsなど、製品向けのモーションスキルを使用してください。**関連スキル:**アニメーションをランディングページに組み込む場合は、このリポジトリのlanding-page-designを使用してください。
2つのコンテキスト
何よりも先にコンテキストを判断してください。アニメーションの対象、モーションの開始と終了、納品物が変わります。
| ショーケース | ウェブサイト | |
|---|---|---|
| 典型的な依頼 | 「このデザインをアニメーション化して」、またはFigmaリンクだけがある | ランディングページやサイトの構築・編集、または依頼でサイトが指定されている |
| アニメーション化するもの | フレーム全体 | 条件を満たすセクションのみ(下表) |
| 開始 | ページ読み込み時、空の状態から | セクションがスクロールで表示されたとき、一度だけ |
| イントロの後 | ストーリーを再生し、フレームをフェードアウトしてサイクルを繰り返す(B5) | ストーリーを無限に再生。フェードアウトやサイクルはなし |
| テキスト | フレームをピクセル単位で正確にするため、レイヤーとして書き出す | SEOとアクセシビリティのため、サイトのフォントを使ったHTMLテキストにする。ビジュアル部分のみ画像またはSVGとして書き出す |
| 納品物 | ページに加え、X用MP4、ウェブサイト用GIF、800×500 GIF(パートC) | サイトのコードに組み込まれたアニメーションセクション。書き出しは依頼された場合のみ |
どちらか指定されていない場合は、作業する場所を確認してください。サイトのコードベース内であればウェブサイトとみなします。デザインだけがあり、サイトがなければショーケースとみなします。それでも不明な場合は、一度だけ質問してください。
ウェブサイト:セクションごとのモーション
ページをセクションごとに確認してください。ビジュアル部分があるセクションにのみ、このスキルを適用します。テキストだけのセクションはアニメーション化しないでください。
| セクション | 適用内容 |
|---|---|
| Bentoや機能紹介のセクション:テキストとビジュアル(チャート、リスト、フロー、カレンダー、プロンプト、プレーヤーなど)を含むカード | スクロール時にカードのイントロを再生(B2、card種別、テキストを含む)。続いて各カードのビジュアルでストーリーループを再生(B4)。ランディングページでの主な用途です |
| 製品またはダッシュボードのモックアップ(アプリのヒーロー画像、製品のスクリーンショット) | スクロール時に読み込みのイントロを再生:背景面はそのままにして、コンテンツを読み込む(A5)。その後、読み込み後にもストーリーを追加するかユーザーに確認してください。求められていないストーリーは追加しないでください |
| ロゴ列、インテグレーション | マーキー、または穏やかに明るくなるストーリー |
| テキストのみのセクション:見出しブロック、お客様の声、FAQ、料金、CTA、フッター | このスキルの対象外です。ページ標準のスクロール表示(例:landing-page-design B7)を使用してください |
ウェブサイトのルール:
- **
IntersectionObserverでトリガーします。**セクションが約30%表示された時点でイントロを開始します。一度だけ再生してください。スクロールリスナーは使用しないでください。 - **画面外ではストーリーを一時停止します。**セクションがビューポートから外れたら時計を止め、戻ってきたら再開します。
- **セクション内の読み順に沿ってスタッガーを設定します。**ページ全体をまたいで設定してはいけません。
- **サイトのシステムを尊重します。**サイトのコンポーネント、トークン、角丸、フォントを使用してください。セクションごとに比較し、デザインの忠実な再現も引き続き適用します(A7)。
prefers-reduced-motion: reduceの場合は、各セクションを最終状態で表示し、ストーリーは再生しません。
パートA — 忠実な再現
A1. 受付
作成を始める前に、以下を確認します。不明な項目だけを、まとめて一度に質問してください。
- コンテキスト。 ショーケースかウェブサイトかを確認します(「2つのコンテキスト」を参照)。まずこれを決めます。
- ソース。 ノード ID を含む Figma のリンクが最適です。1枚の画像でも構いませんが、レイヤーを完全に分離することはできません(A2 を参照)。
- レイアウト。 ツリー(A5)から判断します。結合されたサーフェス(サイドバーや本文など)で構成されるダッシュボードやアプリ画面の アプリフレーム、またはベントーグリッド、カードセット、ウィジェットなどの 独立したカード のどちらかを選びます。それぞれ読み込み方が異なります。
- 背景。 UI の背後に写真やテクスチャはありますか?デザインどおりに維持する必要があるものとして扱います。
- ビジュアル。 読み込み後にストーリーを演出させるビジュアルはどの部分ですか(B4)?すべてのチャート、リスト、積み重ね、カレンダー、フロー、プロンプト、写真、メディアコントロール、ステータスインジケーターが該当するものとして扱います。
- 納品物。 ショーケースの場合は、デフォルトで3種類すべて(X 用の MP4、ウェブサイト用の GIF、800×500 の GIF)を用意します。ウェブサイトの場合は、サイトのコード内のアニメーションセクションを対象とし、書き出しは依頼があった場合にのみ行います。
- カメラ。 ズームもパンもなしとします(B7)。
ユーザーが回答できない場合は、妥当な仮定を置き、その内容を1行で述べてから続行します。セッションで最初にデザインする際は、書き出しを録画する前にライブページを表示し、方向性を低コストで修正できるようにします。
A2. 信頼できる情報源
Figma。 パーソナルアクセストークンを使って REST API を使用します。
- トークンは安全な保管場所(環境変数または OS のキーチェーン)から読み取ります。表示、エコー、チャットへの貼り付け、コミットは絶対にしないでください。
GET /v1/files/:key/nodes?ids=:node&geometry=pathsで、ツリー、境界、塗り、エフェクト、角丸を取得します。GET /v1/images/:key?ids=:node&format=png&scale=2で 参照フレーム をレンダリングします。完成した静止画はこの画像と一致している必要があります(A7)。- フレームの絶対原点を記録します。各レイヤーの位置は
絶対境界 − 原点です。 - API が返す画像 URL は、接続がリセットされることのある CDN を参照しています。ダウンロードは毎回再試行し、使用前に各 PNG のサイズを確認してください。
1枚の画像のみの場合。 その画像を参照元とします。切り分け可能な部分を再構築し、アニメーション化する前に、分離できない部分をユーザーにはっきり伝えてください。スクリーンショット全体を1つのブロックとしてアニメーション化し、忠実さをごまかしてはいけません。
A3. レイヤーの書き出し
動かす要素はすべて、/v1/images を使って 3x で個別の PNG として書き出します。1回の呼び出しにつき、ID を20〜25個まとめて処理してください。
| ケース | 配置 |
|---|---|
PNG のサイズが absoluteRenderBounds と等しい場合(テキストレイヤーは常に該当します) | レンダリング境界に配置します |
| PNG がレンダリング境界より大きい場合(親によってクリップされている)、効果が均等にはみ出している場合 | x = bbox.x − (pngW − bbox.w) / 2。上端がクリップされていない場合は renderBounds.y を使用し、クリップされている場合は効果の半径とオフセットから算出します |
| はみ出し方が均等でない場合(矢印の付いた線、片側が四角い端のストローク) | 数式ではなく、geometry=paths のレスポンスから strokeGeometry または fillGeometry の境界を取得します |
| 大きなレイヤーブラーがかかった回転ベクター(柔らかな色の塊) | use_absolute_bounds=true でエクスポートし、バウンディングボックスに配置します。その領域のスコアが約 45 dB を下回る場合に限り、参照画像に対して PSNR オフセット検索を実行します。色の塊は 1x で保存できます。劣化は目視できません |
| 高さまたは幅がゼロの、ストロークが設定されたベクター | use_absolute_bounds=true を指定すると null が返されます。指定せずにエクスポートします |
| ぼかしたコンテンツを含むマスクグループ | クリップされていない端(通常はマスクの下端とそのストローク)を基準に配置します |
| 円弧、ドーナツ形、部分楕円 | use_absolute_bounds=true でエクスポートします |
| 表面より大幅に大きいパターンやオーバーレイ | use_absolute_bounds=true でエクスポートし、表面の矩形に合わせてクロップします |
インスタンスの子(I12:34;56:78 のような ID) | null が返されることがよくあります。親インスタンスをエクスポートします |
| 線を描画するアニメーションにする場合(破線の有無を問わず) | Figma はストロークを塗りつぶされた図形にアウトライン化するため、アニメーションに使える中心線がありません。中心線を自分で作成し、エクスポートした PNG のマスクとして使用します(以下の例を参照) |
| 大きくエクスポートしたレイヤー内に焼き込まれた写真 | 写真ノードも個別にエクスポートします。焼き込まれた写真をスケルトンで覆い、個別の写真レイヤーを photo の解決用に最前面に配置します。静止時には参照画像と一致している必要があります |
フレームを正確に保つためのルール:
- ショーケースでは、テキストはエクスポートしたレイヤーのままにします。 HTML でテキストを打ち直してはいけません。フォント、カーニング、行のメトリクスが一致しないためです。ウェブサイトではテキストをライブ HTML にします(「2 つのコンテキスト」を参照)。
- Figma の z 順序を維持します。 静的レイヤーは、ツリー内と同じ順序でアニメーションレイヤーの間に配置できます。
- フレームのストロークは通常、子要素より上に描画されます。 カードのストロークは、クリップされたコンテンツの上に独立したオーバーレイとして描画し、参照画像と照合して確認します。カード内側のグローが、Figma のレンダリングでストロークに色を付ける場合があります。
- 子要素が個別に移動するコンテナー。 API は常にフレームとその子要素をまとめてエクスポートするため、カードやパネルをエクスポートすると内部のすべての要素が焼き込まれます。子要素を個別に読み込んだり動作させたりする必要がある場合は、ノードデータを使って、塗り、角丸、内側のストローク(
individualStrokeWeightsを含む)、効果からコンテナーを CSS で再構築します。その後、各子要素を個別にエクスポートします。 - シャドウの対応付け。 ぼかし
Rの Figma ドロップシャドウは、単一要素のぼかしRの CSSbox-shadow、またはグループのぼかし約R / 2のfilter: drop-shadow()に相当します。どちらも同じガウスぼかしになります。参照画像と照合して確認します。 - グループのシャドウは、結合した図形全体にかかるシャドウです。 ドロップシャドウが結合した表面(アプリのフレームなど)のグループにかかっている場合は、すべての表面図形を 1 つの静的コンテナーにまとめ、
filter: drop-shadow()を適用します。要素ごとにbox-shadowを設定すると、表面が重なった部分で隣接する要素が暗くなります。個別のカードにはそれぞれのcardユニット内に独自のシャドウを設定し、カードと一緒にシャドウが移動するようにします。 - 不透明な図形の下に埋もれた非表示レイヤー(
visible: false)や無関係なレイヤーは省略します。
線を描画するアニメーション。 strokeGeometry のアウトラインから角の点と円弧の半径を読み取ります。中心線はストローク幅の半分だけ内側にあり、角丸の中心線の半径は、アウトラインの内側の半径にストロークの半分を加えた値です。ほとんどの UI コネクターは直線と円弧で構成されるため、パスは短くなります:
<svg class="ly" viewBox="0 0 1440 990" style="left:0;top:0;width:1440px;height:990px">
<mask id="draw1">
<path d="M193 832 V700 A45 45 0 0 1 238 655 H380" fill="none" stroke="#fff" stroke-width="8"
pathLength="1" stroke-dasharray="1" stroke-dashoffset="1"/>
</mask>
<image href="data:image/png;base64,..." x="192" y="653" width="190" height="181" mask="url(#draw1)"/>
</svg>
JS クロックで stroke-dashoffset を 1 から 0 へアニメーションさせます。pathLength="1" により長さを測定する必要がなくなり、太いマスクのストロークが線のアンチエイリアスを覆います。線に沿って移動するパルスの場合は、同じパス上で getPointAtLength を使って位置を読み取ります。
A4. 背景(最優先)
デザインが写真やテクスチャの上に配置される場合、背景は装飾ではなくデザインの一部です。多くの場合、アプリのフレームの外側にあるキャンバス全体が背景です。
- 背景は決して変わりません。 イントロでも、ループでも、ディゾルブでも、ズームでも変わりません。移動、ぼかし、拡大縮小、フェード、色の変更は一切しません。
- すべてのバージョンで背景を維持します。 「ローディングのみ」や「ズームなし」のバリエーションも、同じ背景上の同じデザインです。バリエーションを簡単にするために、写真を取り除いてはいけません。
- レンダリング画像ではなく、元の画像をエクスポートします。
GET /v1/files/:key/imagesは生の画像の塗りつぶしを返します。塗りつぶしのスケールモードとimageTransformを CSS として適用します。変形を伴うストレッチ塗りつぶしの場合、background-sizeはフレームサイズを変形のスケールで割った値にし、background-positionはオフセットの負の値にします。 - 必ず検証します。 静止画の背景領域を参照画像と比較します。PSNR は 45 dB を超えることを目指します。切り抜きやオフセットの誤りはすぐにわかります。
- すりガラスパネルには写真が透けて見える必要があります。
backdrop-filter: blur()を使用するサーフェスは独立した要素とし、背景の上に兄弟要素として配置します。filterまたは 1 未満のopacityが設定された祖先要素の内側には配置しません。Headless Chrome でこれをレンダリングするには、その要素にtransform: translateZ(0)とz-indexを設定する必要があります。 - 再現できない場合は、作成を始める前に伝えます。 画像にアクセスできない場合、生成されたテクスチャを使う場合、塗りつぶしが欠けている場合は、ユーザーに伝えて確認を求めます。何も言わずに単色へ置き換えてはいけません。
- 写真は PNG ではなく、高品質の JPEG または WebP で保存します。
A5. レイアウト:アプリフレームまたは独立したカード
デザインが次の2種類のどちらに該当するかを判断します。空の状態に何を含めるか、何を動かすかが変わります。
アプリフレーム(結合されたサーフェス)。 サイドバーと本文など、2つ以上のサーフェスを結合して1つのユニットにしたダッシュボードやアプリ画面です。数ピクセル重なっていることもあります。このレイアウトでのみ、静的なサーフェスを使用します。
- サーフェスは空の状態から表示します。 サイドバーと本文は所定の位置にあり、完全に不透明な状態で、最初のフレームから静止しています。フェード、スライド、拡大縮小、ぼかしによって表示させることはありません。読み込まれるのは内部の要素だけです。
- 接合部では Figma の重なり順を維持します。 本文はサイドバーのコンテンツクリップより手前に配置します。そのため、サイドバーのコンテンツが本文の上に描画されることはなく、本文がサイドバーの上をスライドすることもありません。
- 本文内のカードはコンテンツです。他の要素と同じように読み込まれます。
独立したカード(ベントー、カードセット、ウィジェット)。 カード同士が隙間を空けて独立しているレイアウトです。これらは結合されたサーフェスではありません。
- カード自体を読み込みます。 読み取り順にずらしながら、それぞれのカードをぼかしを伴って浮かび上がらせ(B2 の
card種別)、内部のコンテンツはカードの後に表示します。 - 空の状態には背景だけを表示します。
両方のレイアウトに共通するルール:
- 各コンテナーは、角ごとの半径も含め、正確な形状で内部のコンテンツをクリップします。 隣接する要素や背景にはみ出してはいけません。
- 画像形状のサーフェス。 コンテナーのシルエットが写真の場合(カバー画像のヘッダーや、全面画像のカードなど)、画像の平均色を使ったプレースホルダーを設定します。その後、写真を読み込みます。
- 入れ子になったコンテンツもコンテンツです。 トレイ、ツールバー、内側のパネルも、他の要素と同じように読み込みます。
ステージの座標をクリップ内で有効に保つクリップのパターンです。独立したカードでは、カードとそのコンテンツが一緒に動くよう、カードのユニットでクリップを囲みます。
<div class="clip" style="left:113px;top:138px;width:305px;height:907px;border-radius:16px 0 0 16px">
<div class="cin" style="left:-113px;top:-138px"></div>
</div>
.clip { position: absolute; overflow: hidden; isolation: isolate; }
.cin { position: absolute; width: var(--stage-w); height: var(--stage-h); }
A6. ページの構成(ショーケース)
ウェブサイトでは、「2つのコンテキスト」のウェブサイト向けルールに従い、サイトのコード内に構築します。以下のクロック、単位、モードは、各セクションに引き続き適用されます。
- 自己完結型のHTMLファイルを1つ用意します。フレームと正確に同じサイズの固定サイズのステージを作り、ビューポートに収まるように拡大縮小します。画像はbase64でインライン化し、同じ画像が繰り返される場合は重複を排除します。
<img>要素のみデコード完了を待ちます。CSSグラデーションは画像ではありません。 - モード:
?stillでは動きを付けず、Figmaフレームを正確にレンダリングします(モーションの軽減設定でも使用します)。?loopでは、エクスポートと同様にサイクルを繰り返します。?captureではrequestAnimationFrameを無効にし、window.__start()、window.__tick(dt)、ループ時間window.__ltを公開することで、レコーダーがクロックを制御します(C1)。 - 単位。 アニメーションする各要素を、遅延
--dを持つステージ全体のレイヤー.uでラップします。イントロは.stage.inをキーにしたCSSトランジションです。単位はステージ全体を覆うため、その上のclip-pathやtransform-originのパーセンテージは要素ではなくステージを基準にします。要素の矩形から算出したピクセル値を使用します。 - 静的レイヤーは合成状態を保ちます。 クリップとストロークのオーバーレイに
transform: translateZ(0)を指定し、近くのトランジションが始まってもアンチエイリアスが変わらないようにします。そうしないと、サイクル開始のたびに1pxの縁がちらつきます。 - JavaScriptクロックは1つだけ(
requestAnimationFrame)にし、マスク、スイープ、ストーリー、アクセントなど、時間ベースのすべてを制御します。実時間ではなくループ時間を基準に制御し、キャプチャを決定論的にします。?captureでは、クロックは__tickによってのみ進みます。 - ページのデフォルト動作:画像のデコード完了を待ち、イントロを1回再生し(長いページの一部である場合は、フレームがビューポート内にスクロールされてから再生)、その後はストーリーを再生し続けます。
A7. 静止画を検証する
動きの作業に入る前に、?stillを2倍の解像度でレンダリングし、参照画像と比較します。
- 軽いぼかし(
gblur sigma≈1.2)を適用し、サブピクセルのアンチエイリアスが支配的にならないようにしたうえで、フレーム全体と各コンテナのPSNRを測定します。全体で35 dB超、各コンテナで30 dB超を目指します。テキストが多いコンテナは、3倍レイヤーを2倍にリサンプリングするため30 dB前後になります。これを下回る場合は、sigma≈2.5で再テストします。この場合、テキストの縁は評価対象から外れ、構造だけが残ります。 - ぼかして増幅した差分画像を確認します。高すぎる位置に配置されたグロー、誤った重なり順、欠落したストローク、ずれたシャドウなど、構造上の誤りがわかります。
- サブピクセルのずれを追いかけないでください。 2倍の解像度での測定では、0.5ピクセル未満の誤差を判別できず、「補正」によってずれが大きくなります。アンチエイリアスではなく、構造を修正してください。
PART B — モーションシステム
以下のタイミング、距離、カーブはすべて、Make it yoursの値に基づきます。その場しのぎの値を作らないでください。
B1. 空の状態から始める
すべてのページ読み込みとエクスポートの最初のフレームは、空の状態です。空の状態から読み込み済みの状態に変化します。完成したフレームから始めてはいけません。
- アプリフレーム: 背景と、静的に結合されたサーフェスのみを表示します。
- 独立したカード: 背景のみを表示します。
B2. 読み込みの演出
表示アニメーションは滑らかで、ゆっくりと柔らかくします。要素は下から浮かび上がり、ぼやけた状態から鮮明な状態へと変化し、順番に表示されます。
| 種類 | 用途 | 開始状態 |
|---|---|---|
card | 個別のカード(ベントー、カードセット)全体と、その影 | translateY(24px)、scale(0.985)、blur(6px)、不透明度 0 |
up | テキスト、ヘッダー、数値、行 | translateY(10px)、blur(4px)、不透明度 0 |
down | 上部ツールバー、トップバー | translateY(-8px)、blur(4px) |
left | サイドバー項目、リスト行、ラベル | translateX(-10px)、blur(4px) |
pop | アイコン、バッジ、アバター、ボタン、ドット | 要素自身の中心を基準にした scale(0.82–0.88)、blur(4px) |
photo | 画像 | まずスケルトンのシマーを表示し、その後 blur(14px) saturate(0.6) から鮮明な状態へ。大きなレイヤーに写真が埋め込まれている場合は、A3 を参照してください |
wipe | バー、塗りつぶし、ハイライト | ステージ全体の単位(A6)で、左から右へピクセル単位の clip-path リビールを行います。右側のインセットは stageW − x(幅ゼロのため、細い部分も表示されません)から始まり、stageW − (x + w)(要素全体が表示された状態)で終わります。上端と下端のインセットはそれぞれ y と stageH − (y + h) のままです |
line / grow | 罫線、目盛り、タイムライン | 開始端を基準に scaleX(0) / scaleY(0) |
donut | リング、ゲージ | 12 時の位置から時計回りに広がる円錐マスク |
tray / rise | シート、トレイ、フォルダー | コンテナーの下端の内側から 40px 上へスライドし、コンテナーによってクリップされます |
fade | 別のレイヤーの下にあるレイヤー | 不透明度のみ |
- 順序。 コンテナーは読み順に沿って、80〜90ms 間隔で読み込まれます。アプリのフレームでは、静的な面の内側にある領域が対象です。個別のカードではカード自体が対象となり、それぞれの内容より約 0.15 秒早く開始します。コンテナー内では、ヘッダー、ラベル、数値、ビジュアルの順に表示します。グループ内の項目は 30〜80ms 間隔で順番に表示します。
- オブジェクトに
scale(0)を適用しないでください。 描画される線とバーのみ、ゼロから伸ばします。 - 覆われているレイヤーは最後に現れ、最初に消えます。 最終フレームで別のレイヤーの下に隠れるもの(会議トレイの下にあるカレンダーの週など)は、覆いが表示された後に現れ、ディゾルブ時にはより速く消えるため、透けて見えることはありません。
- スケルトンは実際の形状と一致させ、内側に 1px 入れて配置し、画像の読み込み後に周囲にハローが見えないようにします。
- イントロ全体の長さは、フレーム全体で約 2〜2.5 秒です。
B3. バーは進捗のように塗りつぶす
- データバー、メーター、ハイライトは、進捗バーのように塗りつぶします。左から右への
clip-pathワイプを、インアウトカーブで 0.7〜1.1 秒かけて行います。 - 読み込み用の線は追加しないでください。 カードや面の上端に沿った進捗線や、デザインにないバーや罫線は追加しません。アニメーションさせるのは、デザインに存在する形状だけです。
B4. ストーリーループ(メインの演出)
イントロは始まりにすぎません。メインの演出では、あらゆるビジュアルが何をするのかを動きで表現し、ループします。リードリストには新しいリードが追加され、チャートは読み取られ、カレンダーは空き時間を見つけ、プロンプトは入力されます。ユーザーが「ビジュアルをアニメーション化して」と言うとき、意図しているのはこれです。読み込まれた後、数回脈打つだけで静止するフレームでは完成とは言えません。
まずストーリーを書いてください。 作成を始める前に、それぞれのビジュアルのストーリーを一文で説明します。たとえば「新しいリードが一番上に加わり、古いリードは後ろへ下がって落ち着く」のようにします。この表から選ぶか、同じ考え方で新たに作ってください。
| ビジュアル | ストーリー |
|---|---|
| アイテム(リード、通知、ファイル)のスタック | 新しいアイテムがぼかしとともに一番上に現れ、ほかのアイテムは1つ分後ろに下がり、最後のアイテムが消えます |
| 折れ線グラフまたは面グラフ | スクラバーが滑らかに移動し、ドットが曲線に沿って動き、ツールチップの値がカウントアップします。または、線が描き直され、マーカーが現れ、数値がそれに合わせてカウントアップします |
| 棒グラフ、イコライザー、セルのグリッド | 棒がライブフィードのように呼吸するように動き、デザインで指定された高さに戻ります。ドットとツールチップがアクティブな棒に沿って動きます |
| ドーナツ、ゲージ | セグメントが1つずつ滑らかに外側へ動き、元に戻ります。または、リングがもう一度時計回りに一周します |
| テーブル、リスト | カーソルが行を1つずつ調べ、行がハイライトされ、その行のプレビューが読み込まれます。または、柔らかなハイライトが行を順に下がります |
| カレンダー、スケジューラー | ハイライトが空き枠を探し、イベントがそこへ滑らかに移動してから、元に戻ります |
| フロー、コネクター、パイプライン | パルスがフローの方向にコネクターを伝わり、パルスが通過するとノードやアバターが点灯します。破線が進みます |
| AI、プロンプト、フォーム | プロンプトが削除されて再入力され、チップが1つずつ評価され、その後、結果がぼかしとともに表示されます |
| カウンター、KPI | 数値が、それを動かすグラフとともにカウントアップします |
| プロフィール、長いコンテンツ | コンテンツがクリップ内をゆっくりスクロールし、その後、元に戻ります |
| ロゴの列 | 1ループでセット全体をちょうど1回分移動する連続マーキーにより、シームレスに繰り返します |
| メディアプレーヤー | 再生ボタンが脈打つ間、再生ハンドルと進行状況が進みます |
ストーリーのルール
- 開始時と終了時は、デザインと完全に一致させます。 各ストーリーはFigmaフレームから離れて動作し、元に戻ります。停止時のフレームは
?stillと一致します。 - デザイン独自の要素を使います。 デザインにあるカード、行、棒、ドット、チップ、ツールチップを使います。ストーリーに必要な補助要素(カーソル、複製したスタックアイテム、追加のパルス)はデザインのスタイルに合わせ、
?stillでは非表示にします。 - テキストを変えるのは、それ自体が見せ場の場合だけです。 カウンター、タイピング、ツールチップの値が対象です。それ以外のテキストは動かしません。
- タイピングでは、エクスポートされたテキストレイヤーをグリフごとに表示し、滑らかなワイプで表示してはいけません。テキストPNGのアルファが空の列からグリフの境界を特定し、45~70msごとに1グリフずつ表示します。カーソルは境界に表示し、入力待ちの間は1秒ごとに点滅させます。削除も同じ手順を逆向きに行い、少し速くします。
- カウンターには、デザインと同じフォント、正確なサイズとウェイトを使ったライブテキストが必要です。停止時にはデザインのテキストと完全に一致させます。
- 明確に、ただし動かしすぎないようにします。 ストーリーのある各カードは、1~2秒ごとに分かりやすい動きを見せます。ストーリーは拍(パルスが進む、ノードが点灯する、矢印が走る)で構成し、その拍によってリズムを生み出します。2秒以内にフレームがアニメーションしていると視聴者に分からない場合は、ストーリーの動きが控えめすぎます(「動きを増やす」)。ドキュメントダッシュボードのような落ち着いた画面は、ユーザーが希望する場合、読み込み後に静止してもかまいません。希望を一度だけ尋ねます。
- テキストだけのカードには、ショーケースでストーリーを付けません。 カードとともに浮かび上がり、そのまま静止します。すべてのカードを動かす必要はありませんが、すべてのビジュアルには動きが必要です。
- 最小サイズでエクスポートしても読み取れるようにします。 ストーリーは800×500のGIFでも見える必要があります。動く要素がそのサイズで約4px未満(パルスドットや細い線など)の場合は、デザインのスタイルを保ちながら大きくするか、ハローを加えるか、より大きなストーリーを選びます。
- カードのタイミングをずらします。 カードを一斉に動かしてはいけません。フレーム内の注目が読み順に沿って移動するよう、各カードのストーリーをずらします。
- カードごとにストーリーは1つ、長さは3~8秒です。短いストーリーは1サイクル内で繰り返してもかまいません(8.6秒のストーリーフェーズ内で2回繰り返すのが一般的です)。2回目の実行では、拍を変えてもかまいません。どの実行も、ディゾルブの前に停止状態で終わる必要があります。
- ストーリーは、JSクロック(A6)上でイージング付きのキーフレームトラックを使って構築するため、キャプチャーは決定的に保たれます。
// [[time, value], ...] eased per segment; values may be numbers or arrays
function track(keys, t, ease = easeInOut) {
if (t <= keys[0][0]) return keys[0][1];
for (let i = 1; i < keys.length; i++) {
const [t1, v1] = keys[i], [t0, v0] = keys[i - 1];
if (t <= t1) {
const k = ease(Math.min(1, (t - t0) / (t1 - t0 || 1)));
return Array.isArray(v0) ? v0.map((v, j) => v + (v1[j] - v) * k) : v0 + (v1 - v0) * k;
}
}
return keys[keys.length - 1][1];
}
// a slot that scrubs along a curve and returns to its design position
slot.style.transform = `translate(${track([[0, [0, 0]], [1.2, [46, -18]], [2.6, [46, -18]], [3.8, [0, 0]]], t).join('px,')}px)`;
アクセントはストーリーに重ねるものであり、ストーリーの代わりにはなりません。
| アクセント | 用途 | 値 |
|---|---|---|
| パルスリング | ステータスドット、ライブインジケーター、再生ボタン | 周期 1.8–2.2s、リングの拡大率 1 → 2.6、不透明度 0.55 → 0 |
| ハートビート | ハート、いいね、きらめき | 0.35s で +20%、3s ごと |
| ゆるやかな揺れ | 浮遊するオブジェクト(フォルダー、デバイス、カード内のカード) | ±2.6px、2.8s の正弦波 |
| 光沢のスイープ | 写真、バー、ボタン | 要素の幅の 38% の帯、白さのピークは 34%、3.4s ごとに 1.5s かけて通過します。薄いグレーの形状では見えなくなるため、代わりに補充の動きを使います |
| スケルトンのきらめき | デザイン内のプレースホルダーの線 | 2.4s のきらめき |
- ストーリーとアクセントは、イントロの表示完了後(約 2.4s)に開始します。ストーリーはデザインそのものから始まるため、フェードは不要です。アクセントと追加の補助要素は 0.6s かけてフェードインします。
B5. ループサイクル(ショーケース)
Webサイトではサイクルはありません。スクロール時にイントロが一度だけ再生され、ストーリーはセクションがビューポートから外れるまで続きます。
| フェーズ | デフォルトのタイミング |
|---|---|
| 表示 | 0 → 2.4s |
| ストーリー | 2.4 → 11.0s |
| ディゾルブ | 11.0 → 11.6s(不透明度 0.5s) |
| 空白の間 | 11.6 → 12.0s |
- ストーリーフェーズでは、すべてのカードで少なくとも 1 回、ストーリー全体を表示します。 ストーリーを途中で切り上げるのではなく、サイクルを長くしてください。ディゾルブの開始時にすべてのカードが静止状態に戻るよう、ストーリーのタイミングを調整します。
- ライブページでは、イントロの後もストーリーがずっと続きます。 ディゾルブして再開するのは
?loopとエクスポートのみです。 - 空の状態(B1)に戻るようディゾルブします。 アプリフレームでは、内容のみをディゾルブし、つながった面はそのまま残します。個別のカードでは、カードもディゾルブします。背景は決してディゾルブしません。
- すべてをリセットします。 1 フレームの間トランジションを無効にするクラスで CSS はリセットできますが、JS で制御するすべての値(マスク、トラック、補助要素、入力済みテキスト、パルス)も空の状態に戻す必要があります。そうしないと、空白の間に残像が現れます。サイクルの終わりのフレームは、始まりのフレームと一致しなければなりません。
B6. イージングと時間
表示時には、デフォルトの ease や linear を決して使わないでください。
| トークン | カーブ | 用途 |
|---|---|---|
--e | cubic-bezier(0.16, 1, 0.3, 1) | 表示時の変形(1.05s) |
--e2 | cubic-bezier(0.33, 1, 0.68, 1) | 不透明度とぼかし(0.7–0.8s) |
--io | cubic-bezier(0.65, 0, 0.35, 1) | ワイプ、塗りつぶし、スイープ、カメラ移動 |
| linear | linear | 一定の動きのみ(きらめき、再生) |
ショーケースのタイミングがプロダクト UI より遅いのは意図的です。「300ms 未満」などのプロダクト向けルールはここには当てはまりません。ゆっくりとなめらかに動かすことが求められます。
B7. カメラ
- デフォルトではズームもパンもしません。 ユーザーからの要望が必要です。
- 要望された場合は、アプリフレームのみをアイテムに向かって拡大し、背景は動かしません(A4)。移動には
--ioを使い、ディゾルブの前にフレームを静止位置に戻します。
B8. パフォーマンスとアクセシビリティ
transform、opacity、filter、clip-pathのみをアニメーションさせます。top、left、width、heightは決して使わないでください。- アニメーションするぼかしは 14px 以下にします。静的な
backdrop-filterは問題ありません。 prefers-reduced-motion: reduceでは静止フレームを表示します。- イントロには CSS トランジションを使い、時間に基づくエフェクトにのみ JS のクロックを使います。
B9. モーションチェック
エンコードする前に、記録したフレーム(C1)で以下を実行します。
- はみ出しチェック。 矩形ではなく、角丸を含むコンテナーの実際の形状でテストします。ページ自体からマスクをレンダリングします(すべてのコンテナーを、正確な角丸を適用した白一色の形状として黒地に配置し、シャドウがある場合はその領域も含めます)。アンチエイリアスを除外するためにマスクを2px拡張し、黒いピクセルだけを比較します。ここで比較に使用する
?stillは、A7の2xではなく、キャプチャのデバイススケールファクターでレンダリングします。- アプリフレーム: 結合したサーフェスの外側にあるすべてのピクセルは、サイクル内のすべてのフレームで、空の最初のフレームと完全に一致する必要があります(PSNR
inf)。 - 個別のカード: ストーリーのフェーズでは、カード(およびシャドウがある場合はそのシャドウ)の外側にあるすべてのピクセルが、
?stillフレームと完全に一致する必要があります。これにより、ストーリー、ヘルパー、コンテンツがカードからはみ出していないことを確認できます。カード自体の上昇によってピクセルが移動するため、イントロフレームは対象外です。コンタクトシートでは、コンテンツがカードより先に動いたり、カードの外に出たりせず、カードと一緒に動くことを確認します。
- アプリフレーム: 結合したサーフェスの外側にあるすべてのピクセルは、サイクル内のすべてのフレームで、空の最初のフレームと完全に一致する必要があります(PSNR
- 継ぎ目チェック。 書き出しウィンドウの最初のフレームは、1サイクル後のフレームと一致します。
- 空のビートのチェック。 空のビートのすべてのフレームは、
__startの前のフレームと一致します。継ぎ目チェックでは、どちらのフレームもループ時間が0であるため、リセットされなかったJSの状態を検出できません。このチェックなら検出できます。 - 復帰チェック。 ディゾルブ前の最後のフレームは、キャプチャ時のスケールでの
?stillと一致します。これにより、すべてのストーリーがデザインの状態に戻ったことを確認できます。 - コンタクトシート。 重要な時点(空の状態、イントロの途中、配置完了時、ストーリー中の複数の時点、ディゾルブの途中、終了時)のフレームを並べて、はみ出し、ゴースト、急な変化、目立たないストーリーがないか確認します。
パート C — 記録と書き出し
C1. 決定論的なキャプチャ
リアルタイムで画面を録画してはいけません。疑似クロックを進め、各フレームのスクリーンショットを撮ります。ループが滑らかで、継ぎ目がきれいになるかどうかを左右する重要な点は3つあり、すべてスニペットに含まれています。
await page.clock.install({ time: 0 });
await page.goto(url + '?capture', { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.clock.pauseAt(10000);
const loopTime = [];
for (let i = 0; i < frames; i++) {
// 1. whole milliseconds that average exactly 1000/60 (17, 17, 16, ...); runFor(1000 / 60) drifts
const dt = Math.round((i + 1) * 1000 / 60) - Math.round(i * 1000 / 60);
await page.clock.runFor(dt);
loopTime.push(await page.evaluate(([i, PRE, dt]) => {
if (i === PRE) window.__start(); // 2. start inside the same evaluate as the seek
if (i >= PRE) window.__tick(dt / 1000); // 3. the page clock advances here, never through rAF
const now = performance.now();
for (const a of document.getAnimations()) { // CSS transitions follow the fake clock
a.__m ??= window.__mark; // each one keeps the mark it was created under
a.pause(); a.currentTime = Math.max(0, now - a.__m);
}
return i >= PRE ? window.__lt : 0;
}, [i, PRE, dt]));
await page.screenshot({ path: `${out}/${String(i).padStart(5, '0')}.png` });
}
ページ側の ?capture では:
__start()はinクラスを追加します。ステージの状態クラス(in、out、リセット)が変わるたびにwindow.__mark = performance.now()が設定されるため、各トランジションは作成された時点を基準にシークされます。トランジションの遅延(--d)はcurrentTimeに含まれます。__tick(dt)はループ時間を進め、サイクルのフェーズ(B5)を実行し、すべてのストーリーとアクセントを描画して、window.__ltを設定します。- 終了しないループ(シマー、マーキー)は、無限のCSSキーフレームではなくJSクロックで駆動します。CSSキーフレームはシーク時に再開されます。
PRE は開始前に記録する数フレーム(約15フレーム)で、ページの状態が落ち着くようにします。各フレームとともに loopTime を保存します。
- ビューポートはフレームサイズです。長辺がX制限に達するように、
deviceScaleFactor = min(1920 / W, 1200 / H)とします。スクリーンショットはピクセル単位に丸められるため、1ピクセルずれることがあります(1440×990のフレームに対して1745×1200など)。MP4の偶数サイズへのクロップで処理されるため、1744×1200になることを想定してください。 - 書き出しウィンドウはフレーム
PRE − 1(ループ時間0、空の状態)から始まり、ちょうどcycle × 60フレーム続きます。継ぎ目チェック用に、ウィンドウの後も少なくとも10フレーム記録します。
C2. フォーマット
| ファイル | 仕様 |
|---|---|
<name>-x.mp4 | H.264、yuv420p、60fps、CRF 14–15、preset slow、+faststart、最大1920×1200、偶数サイズ |
<name>-loop.gif | 幅960px、15–20fps、約2~3MB、ウェブサイト用 |
<name>-800x500.gif | 必ず納品します。フレームに広い空白の余白がある場合(多くのカードやベントーフレーム)、コンテンツを約40pxの余白付きで、正確に1.6:1の比率にクロップします。コンテンツがフレームいっぱいに広がる場合は、ページの背景色でパディングして収めます。写真背景では、写真の横に単色の帯ができないよう、必ずクロップします。通常は20fps、負荷が高い場合は12fps |
ffmpeg -start_number $A -framerate 60 -i frames/%05d.png -frames:v $N \
-vf "crop=trunc(iw/2)*2:trunc(ih/2)*2" -c:v libx264 -preset slow -crf 15 \
-pix_fmt yuv420p -movflags +faststart name-x.mp4
ffmpeg -start_number $A -framerate 60 -i frames/%05d.png -filter_complex \
"trim=end_frame=$N,fps=20,scale=960:-2:flags=lanczos,split[a][b];\
[a]palettegen=max_colors=192:stats_mode=diff[p];\
[b][p]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle" -loop 0 name-loop.gif
800×500のGIFでは、split の前に scale=800:500:force_original_aspect_ratio=decrease,pad=800:500:(ow-iw)/2:(oh-ih)/2:color=<page bg> を追加します。
- 写真がフレーム内にある場合は、常にバイエルディザリングを使用します。使用しないと写真にバンディングが発生します。
- GIFが重すぎる場合は、サイズを小さくする前に、フレームレートまたは色数を減らします。
出力形式
作成を始める前に、数行で以下を記載します。
- コンテキスト — ショーケースまたはウェブサイト。ウェブサイトの場合は、各セクションと、そのセクションに何を表示するか(イントロとストーリー、イントロのみ、または何も表示しない)を列挙します
- レイアウト — アプリフレーム(結合されたどのサーフェスを固定表示するか)、または個別のカード(どのカードがせり上がるか)、およびクリップ形状
- 背景 — 完全にそのまま使用するか、再現できない部分を記載します
- 振り付け — 読み込み順と、要素グループごとの動きの種類
- ストーリー — 各ビジュアルが何を演じるかを一文で説明し、アクセントがあれば記載します
- サイクル — 全体の長さと各フェーズ
作成後、以下を報告します。
- 静止画の忠実度(全体および最も低いサーフェス PSNR)
- はみ出しチェックと継ぎ目チェックの結果
- 3 つのファイルのサイズ、寸法、フレームレート、再生時間
- 完全には再現できなかったものを明記
自分らしくカスタマイズする
すべてのモーションと書き出しの値はここで設定します。このセクションを編集すると、スキルの残りもそれに従います。
| 値 | デフォルト |
|---|---|
| せり上がり距離 | 10px(up)、8px(down)、10px(left) |
| 登場時のぼかし | 4px、写真は 14px |
| ポップ開始時のスケール | 0.82–0.88 |
| トランスフォーム時間 | --e を使って 1.05s |
| 不透明度とぼかしの時間 | --e2 を使って 0.7–0.8s |
| ワイプ時間 | --io を使って 0.7s、低速バーは 1.1s |
| カードのせり上がり | 24px、scale(0.985)、blur(6px)、--e を使った 0.9s のトランスフォーム、0.6s の不透明度 |
| コンテナーのスタッガー | 80–90ms |
| トレイとフォルダーのせり上がり | 40px |
| タイピング | 45–70ms ごとに 1 グリフ、キャレットの点滅は 1s |
| 項目のスタッガー | 30–80ms |
| ストーリーの開始 | 2.4s |
| ストーリーの長さ | カードごとに 3–8s、カード間でオフセット |
| サイクル | 12s(11.0s でディゾルブ、11.6s でリセット)、ストーリーに必要なら延長 |
| MP4 | 60fps、CRF 15、最大 1920×1200 |
| ウェブサイト GIF | 幅 960px、20fps |
| 小さい GIF | 800×500、20fps |
クイックチェックリスト
忠実度
- ソースから参照フレームをレンダリングした
- テキストを含め、動く要素をすべて個別のレイヤーとして書き出した
- Figma の Z 順を維持し、ストロークは子要素の上、グループのシャドウは結合領域に適用した
- 背景には元の画像を使い、そのトランスフォームに従って配置し、PSNR で検証した
- すべてのバリアントで背景を維持し、フロストガラスのパネルでは背後の写真をぼかした
- 静止フレームが参照と一致している(構造上の違いがない)
コンテキストとレイアウト
- コンテキストを決定した:ショーケース、または各セクションを割り当てたウェブサイト(テキストのみのセクションは変更しない)
- ウェブサイトのモックアップ:読み込みイントロの後にストーリーを追加する前にユーザーに確認した
- レイアウトを特定した:結合されたサーフェスを持つアプリフレーム、または個別のカード
- アプリフレーム:結合されたサーフェスが表示され、不透明で、空の状態を保っている。接合部では Figma の重なり順を維持した
- 個別のカード:カード自体がせり上がり、空の状態では背景のみを表示する
- 各コンテナーの内容を、正確な角丸で個別にクリップする
- レイアウト種別に応じたはみ出しチェックに合格した
モーション
- 書き出しとページ読み込みは空の状態から始まる
- せり上がりとぼかしから鮮明になるイントロを、読み順に沿ってずらして表示する
- バーはプログレスバーのように伸び、デザインにない読み込み線や図形を追加していない
- 読み込み後は、すべてのビジュアルがストーリーを演じ、デザインと完全に一致する状態で始まり、終わる。テキストのみのカードは静止したままにする
- 800×500 の GIF でもストーリーが見える
- ストーリーが明確で、カード間でオフセットされ、テキストが変化するのはカウンターまたはタイピングのみ
- 隠れるレイヤーは最後に表示され、最初に消える
- 指示がない限りズームやパンはせず、背景は動かさない
- 空の状態にディゾルブして戻り、背景には手を加えない
- 継ぎ目、空白時間、静止状態のチェックに合格した
書き出し
- 決定論的なフレームキャプチャ:1000/60 に平均化した整数ミリ秒間隔を使い、シーク内で開始とティックを行い、実時間録画はしない
-
<name>-x.mp4、<name>-loop.gif、<name>-800x500.gifを納品した - トークンを出力もコミットもしていない
クレジット
Elaya による ai-design-skills の一部で、MIT ライセンスの下で提供されています。
いくつかの一般的なモーションルールは、以下の MIT ライセンスのスキルコレクションからコピーしたのではなく、参考にして取り入れました。
- emilkowalski/skills — 負荷の低いプロパティのみをアニメーション化する、ぼかしを小さく保つ、オブジェクトをゼロから拡大しない、モーション低減への対応
- Owl-Listener/designer-skills — 実際のコンテンツに似せたスケルトン、段階的な登場
- dylantarre/animation-principles — 兄弟要素のループ開始タイミングをずらす、最後のフレームから最初のフレームへ自然につながるループ