ストレッチモーフィングUI
このスキルは、すべての状態変化がモーフィングするインターフェースを生成します。タブインジケーターが次のタブに向かって虫のように伸び、ボタンがローディングサークルに押しつぶされてから膨らみ戻り、商品画像がカードから浮き上がり、チップに縮んでカートに落ちます。このスキルは、24コンポーネントのショーケース(assets/showcase.html)の構築と、その過程で見つかったバグやデザイン修正から抽出されました。
その見た目は、ひとつのアイデアに基づいています:ストレッチには原因がある。要素は、その部品が一致しない(一方の端が他方より先に到着する)、高速で移動している(経路に沿って伸びる)、押しつぶされている(体積を保つために別の方向に膨らむ)、または衝突された(振動する)ために変形します。ループでランダムに振動させるのは安っぽく見えますが、原因のあるストレッチはマテリアルのように見えます。
ステップ0、毎回:ユーザーのコードを監査して確認
何かを構築または編集する前に、ユーザーのインターフェースのどこがストレッチ可能になるかを調べ、ユーザーに選択させます。詳細、判断基準、メッセージテンプレートはreferences/audit.mdにあります。簡略版は以下の通りです。
- コードを見つける:ペーストされたコード、添付ファイル(claude.aiの場合、
/mnt/user-data/uploadsをリスト)、作業ディレクトリまたはリポジトリ、アーティファクトリンク(アーティファクトツールで読む)、またはユーザーが指定したソース。コードがない場合は、何を探したかを伝え、コードを共有するか、ゼロから新しく構築するかの2つの選択肢を提示します。 - スキャンする:
python scripts/audit_ui.py <paths>は、スタック、既存のモーション、欠落している動きの軽減処理、およびUIカテゴリごとの候補をfile:line形式で報告します。各候補は、ショーケースコンポーネントとテクニックにマッピングされます。 - ヒットしたすべてをコンテキストで読み、実際の候補のみを残します。それぞれについて、その場所、現在の動作、それがなるべきモーフィング文、労力(小、中、大)、およびリスク(コンポーネントライブラリのマークアップ、既存のアニメーションライブラリ、モバイルピッカーが失われるネイティブコントロール、非常に長いリストなど)を記録します。
- 可視性×頻度×適合度で優先順位を付け、重複をグループ化し(すべてのボタンが共有の
Buttonを使用している場合は1項目とする)、リストを約10項目に制限し、推奨しないものは理由とともにリストします。 - ユーザーの言語で確認を求める:番号付きの提案、次に番号を選択、すべての推奨を受け入れる、またはリストにないストレッチにしたい追加の部分を入力するように促す質問。環境にあればタップ可能な選択ツールを使用し、そうでなければプレーンテキストを使用します。そのターンはそこで終了。返信があるまで編集は行わない。
- ユーザーが選択したものと追加されたものを構築し、以下のワークフローに従って項目ごとに報告します。
ユーザーがすでにストレッチしたい部分を指定している場合でも、監査は行います。その部分を最初に配置し、残りをオプションとして提供し、質問は短く保ちます。
ファイル
すべてのパスは、このスキルのフォルダ(このSKILL.mdを含むフォルダ)からの相対パスであり、ユーザーのプロジェクトからの相対パスではありません。読み取りまたは実行時に、スキルフォルダを基準に解決します。
| パス | 内容 | 開くタイミング |
|---|---|---|
assets/stretchy-core.js | スプリングエンジンとヘルパー(エッジペア、セグメント化インジケーター、ボリュームスケール、膨張するSVGエッジ、FLIP並べ替え、投げられたオブジェクト) | 構築するすべてのページに貼り付けます。ヘルパーを把握するために一度読んでください。 |
assets/starter.html | ページスケルトン:ライト/ダークトークン、タイ語対応可変フォント、動きの軽減処理、インライン化されたコア、1つのサンプルコンポーネント | 新しいページを開始する場合 |
assets/showcase.html | 元のビルドからの24の動作コンポーネントすべて。各JSセクションは/* ---------- tabs ---------- */のようなバナーで始まります。 | リクエストがカタログコンポーネントと一致する場合、grep -n "/\* ---------- " assets/showcase.htmlを実行し、そのセクションとそのCSSだけを読みます。 |
references/audit.md | ユーザーのコードを見つけ、スキャンし、判断し、優先順位を付ける方法、および確認メッセージテンプレート(タイ語と英語) | ステップ0で、毎回 |
references/techniques.md | 11のストレッチテクニックの詳細、コードと調整数値付き | まだ構築したことのないものを構築する前に |
references/components.md | カタログ:各コンポーネントのレシピ、スプリング値、落とし穴 | リクエストがカタログコンポーネントを指定または類似している場合 |
references/new-components.md | 任意のコンポーネントのストレッチバージョンを設計する方法、およびショーケースにない14の設計済みサンプル | リクエストがカタログ外の場合 |
scripts/audit_ui.py | ユーザーコードのスキャナー:スタック、既存のモーション、およびfile:line形式のストレッチ候補 | ステップ0で、毎回 |
scripts/check_page.py | ヘッドレスブラウザチェック:JSエラー、電話とデスクトップでの横方向オーバーフロー、クリックを奪う非表示要素、アニメーションフレームのコンタクトシート | ビルド後、引き渡す前に毎回 |
ビジュアルデザインスキル(例:frontend-designやui-ux-pro-max)が利用可能な場合は、それも読んでください。そのスキルがページの見た目(パレット、タイポグラフィ、レイアウト)を決定し、このスキルが動きを決定します。動きの洗練だけでは、凡庸なデザインのページを救うことはできません。
ワークフロー(ユーザーの確認後)
- 各コンポーネントのモーフィング文を書きます。「トリガーが起こると、形状Aが形状Bになり、原因のために伸びる。」例:「タブがクリックされると、インジケーターがそのタブに移動し、先行エッジが後続エッジよりも硬いために伸びる。」原因が埋められない場合は、以下の表から選択します。この文がデザインであり、残りは実装です。
- **テーブルからテクニックを選択します。**ほとんどのコンポーネントは、1つの主要なストレッチと1つの小さなフィードバック振動を使用します。
- 静止状態を数値として定義します。:幅、高さ、半径、位置、進行度(0から1)。スプリングは数値をアニメーション化し、
onUpdateでstyleに書き込みます。CSSトランジションは、色と不透明度(および短いコンテンツのクロスフェード)のみを処理します。 - **コンテンツを計画します。**テキストとアイコンは、コンテナがモーフィングしている間、リフローや押しつぶされてはいけません。各コンテンツ状態に、最終的なサイズで独自のレイヤーを与え、アンカーし(通常は中央)、クロスフェードします。出口は速く(約150ms)、約100~150msの遅延後にわずかなスケールやぼかしを伴ってエントリーします。コンテナの
overflow:hiddenは、コンテンツが成長するにつれて表示します。 - **
assets/starter.htmlからコアをインライン化して構築します。**スタイルはスプリングコールバックに直接書き込みます。フレームごとにフレームワークの再レンダリングをトリガーしないでください。 - アクセシビリティと割り込み処理パス(以下のチェックリスト)。
- テストして確認します。
scripts/check_page.pyを実行し、重要なインタラクションのフレームコンタクトシートをキャプチャして実際に確認します。元のビルドの「カートに追加」アニメーションは「機能」していましたが、フレームが検査されるまで安っぽく見えました。修正は「見た目を良くする」にあります。
ストレッチの源泉
| 状況 | テクニック(詳細はreferences/techniques.md) | 使用例 |
|---|---|---|
| 何かがスロット間を移動する | エッジペア:先行エッジと後続エッジに2つのスプリング、先行は硬く、方向によって入れ替わる | タブ、カルーセルドット、トグル、選択チップ、ラジオドット、ステッパー、ドロップダウンハイライト、評価カプセル、サイドバーナビ |
| コンテナのサイズや形状が変わる | 軸分割:幅、高さ、半径に異なる剛性で個別のスプリング、一方の軸がリードする | ダイナミックアイランド、FABからフォームへ、ボタンからのモーダル、ドロップダウン、カートに追加ボタン |
| 何かが速く狭くまたは広くなる | 体積保存:速度で他方の軸をスケール、scaleY = clamp(1 − v/k) | 送信ボタン、検索フィールド、ドロップダウンの幅 |
| 何かが速く移動する | 速度ストレッチ:移動方向に沿って伸ばし、垂直方向を細くする | スライダーのつまみ、投げられたカートアイテム、カルーセルカード(スキュー)、ツールチップ |
| 大きなサーフェスが移動する | 膨張エッジ:サーフェスをSVGパスで描画し、そのエッジの制御点を-速度でオフセットする | サイドバー、ボトムシート |
| パーツが動いている部分にぶら下がる | 遅延フォロワー:メインのスプリングをより柔らかいスプリングが追いかける。隙間を傾きやオフセットに使用する | スライダー値バブル、アコーディオンの内部テキスト |
| タップや到着に対するフィードバック | キック:0で静止しているスプリングに速度を追加すると、振動して落ち着く | チェックボックス、チップ、カート、星、バッジ、数量制限シェイク |
| ブロブが結合または分離する | グーイフィルター:ぼかしとアルファしきい値、上部に合成されたシャープなソース | グーイメニュー |
| 要素が別の要素になる | コンテナ変換:トリガーの矩形から始まり、ターゲットの矩形にスプリングで移動し、コンテンツをクロスフェードする | モーダル、FAB、カートスロー |
| 変更に途中で一時停止が必要 | 中間形状を通したシーケンシャルモーフィング(意味を持つ:スピナー、チェック) | 送信ボタン、カートに追加ボタン |
| 制限を超えてドラッグする、またはリストが並べ替えられる | ラバーバンド(オーバースクロールを約3で割る)および速度を維持するスプリングによるFLIP | カルーセル、シート、ソート済み商品リスト |
エンジンの概要(1パラグラフ)
Springは、減調和振動子であり、フレームあたり8つのサブステップで半陰的オイラー積分されます(フレームデルタは1/30秒に制限され、バックグラウンドタブで爆発するのを防ぎます)。to(target)は、速度をリセットせずに再ターゲットします。これにより、高速クリックが再起動ではなく連続的に感じられます。set(x)はアニメーションなしでジャンプし、drag(x, v)は速度を記録しながらポインタに追従し、kick(v)はインパルスを注入します。減衰はすべてのティックでグローバルなbounce係数が乗算されるため、1つのページレベルの制御(ソフト1.8、バウンシー1、ストレッチ0.5)ですべてのコンポーネントをライブで再調整できます。prefers-reduced-motion: reduceの下では、to()はすぐにターゲットにスナップしてonRestを起動するため、アニメーションを待つロジックは引き続き実行されます。
減衰比 ζ = c / (2√k)。UI状態の変化は、ζ 0.6~0.8程度で適切に感じられます。遊び心のあるフィードバックは0.3~0.5。1以上ではオーバーシュートしません。ショーケース全体で使用されるプリセットは次のとおりです。
| 役割 | k | c | 注 |
|---|---|---|---|
| エッジペアの先行エッジ | 420~520 | 30~34 | 最初に到着 |
| 後続エッジ | 140~170 | 17~19 | 遅れる、これがストレッチを生み出す |
| コンテナ幅(モーフ) | 240~320 | 18~24 | |
| コンテナ高さ(モーフ) | 150~170 | 15~17 | 幅より遅いため、形状は最初に横に伸びる |
| ポインタフォロワー(スライダー) | 650 | 42 | タイトだが、読み取る速度はまだある |
| 遅延フォロワー(バブル) | 150 | 11 | 意図的にルーズ |
| キックフィードバック | 380~420 | 12~16 | キックは6~10 |
| オーバーシュートが見える高さ(アコーディオン) | 210 | 14 |
経験則
- すべての変形に上限を設定する。 制御:体積保存は±7~15%以内。小さく速いもの(投げられたチップ、スライダーのつまみ):最大35~70%のストレッチ。不足減衰スプリングはゼロ以下にアンダーシュートする可能性があるため、常にサイズを正の最小値にクランプします。
- 方向によって剛性を切り替える。 エッジペアでは、進行方向を向いているエッジが硬いスプリングを取得します。切り替えがないと、インジケーターは正しい方向に伸びますが、反対方向では圧縮されます。
- 戻り時は逆のタイミングにする。 開くときに幅が高さをリードする場合、閉じるときは高さが幅をリードするようにします。これにより、形状は異なる経路をたどり、巻き戻しではなく弾性的に読み取られます。
- 状態変化にはスプリング、経路の形状が重要なパスには時間ベースの曲線を使用する。 投げられたオブジェクトには予測可能な弧が必要です(二次ベジェ、イーズインイーズアウト進行、約800ms)。独立したx/yスプリングは迷走パスを与えます。どちらの場合も、フレーム間の速度からストレッチを導出します。
- コンテンツは直立に保つ。 画像やテキストを回転させずに任意の方向にストレッチするには、
rotate(a) scale(1+s, 1−s・0.45) rotate(−a)を使用します。 - コンポーネントごとに1つの大胆なモーション。 コンポーネントの残りの部分は静かに保ちます:色の変更、短いフェード。
- アイドル時は静かに。 何も勝手に振動しません。ページ読み込み時に最大1つの調整された瞬間(ショーケースは900ms後にアイランドを1回開く)。
見た目を良くする(ビルドからの教訓)
最初のカートに追加バージョンは、2つの独立したスプリングに沿って無地の色の付いたボールを飛ばし、ボタンを空のバイオレットの楕円形に縮め、カートをつぶしました。ユーザーはそれが美しくないと言いました。それを修正したものは一般化できます。
- 実際のものを動かす。 商品画像をクローンし、影をつけて少し持ち上げ、飛んでいる間も画像を表示する丸いチップに縮めます。抽象的なドットはデバッグ出力のように読み取られます。
- 中間形状には意味を持たせる。 縮んだボタンは、スピナー、自分自身を描画するチェック、カウントなど、何かを表示しない限り、空のブロブです。追加ボタンは現在、緑色の丸にチェックマークになり、次に - n + ステッパーに成長します。
- コントロールには微妙なボリュームを。 ボタンの±18%の垂直方向の膨らみは、錠剤型の卵のように見えました。±7%は柔らかい素材として読み取られます。
- 到着には受け手が必要。 カートアイコンが沈み込み傾き、リングが脈動して広がり、バッジがポップします。投げられたものを目に見えてキャッチするものがなければなりません。
- ステータスマークをクリップしない。 モーフィングする
overflow:hiddenの錠剤内のバッジは切り取られます。クリップの外側のラッパーに配置します。 - 情報を繰り返さない。 カートの錠剤は、3と表示されたバッジの横に「3 ชิ้น 195 บาท」と表示していました。現在は合計のみを表示しています。
- イラストは固定アスペクトで描画する。 タイル内のパーセント配置されたCSSシェイプは、タイルが広がるときに伸びます。タイルの中央に配置された、viewBoxを持つインラインSVGを使用します。
アクセシビリティと堅牢性のチェックリスト
- 動きの軽減を尊重する(コアは行います。CSSキーフレームとトランジションも
prefers-reduced-motionメディアクエリでカットされていることを確認してください)。 button、input、role="switch"、role="tab"、role="slider"などの実際の要素を、適切なaria-*状態とともに使用し、アニメーションを開始する同じ呼び出しで更新します。- モーフィングによって非表示になるコンテンツには
inertを設定します(また、クリックをインターセプトしてはなりません。非表示のレイヤーにはpointer-events:noneを与え、表示状態でそれを再度オンに切り替えます)。 - フォーカスされたコントロールがモーフィングに消えるときは、意図的にフォーカスを移動します。置き換えコントロールへ、ダイアログが閉じた後にトリガーへ戻すなど、要素が再び表示された後にのみ行います(
visibility:hidden要素はフォーカスを受け取ることができません)。 - すべてのインタラクションがアニメーション中に中断された場合に備えて、クリーンな状態になることをテストします。
Spring.cancel()または新しいto()呼び出しは、中間状態をクリーンアップします。 - アニメーションの時間は短く保ちます(変形は300~500ms、フィードバック振動は150~300ms)。