キネティックタイポグラフィ
単語そのものが絵になる動画を作成します。フレーズが現れ、フレームを埋め尽くし、打ち付けられ、次のフレーズに取って代わられます。リズミカルに。まるで動くポスターのように、大胆で速く、音楽的であるべきです。
オプション
| オプション | デフォルト | 備考 |
|---|---|---|
| フォーマット | 縦長 1080×1920 | または横長 1920×1080、正方形 1080×1080 |
| 長さ | 10–20 秒 | ナレーションまたは楽曲のセクションに合わせる |
| トーン | パンチ | calm(0.4秒のスライドとフェード、反転少なめ)、punchy(デフォルト)、chaotic(オールキャップス、激しい打ち付け、シェイク、フレーズごとの反転) |
| リズム | 固定テンポ | またはナレーションの単語タイミング、または音楽のビート |
準備(回答前に)
- 視聴者が覚えておくべき一文は何ですか?
- スクリプトを1~4単語のフレーズに分割し、フレーズごとに1つのヒーローワードをマークします。
- エネルギーのピーク(カラーフリップが発生するフレーズ)はどこですか?
- エンディングのロックアップ(タイトル、コールトゥアクション、ハンドル)は何ですか?
読みやすさシート(ビートリストと一緒に書く)
ビートごとに、視聴者が理解すべきこと(「ロボットは飛べない」「キャッシュミス」)と、それが読み取れる時間を1つだけ書きます。一度に1つの読み物。テキストの表示時間:ラベルは最低0.8秒、文章は完全に表示された後、単語あたり約0.3秒。時間内に収まらない場合は、速度を上げるのではなく単語を削ります。
モーションの原則
- モーションには原因があります。すべての動きには画面上での理由(クリック、落下、パケットの到着)があり、次の動きはそれに続きます。
- 動画全体で1つの主要な移動方向を選び、それを守ります。他の方向は意味を持たせて使用します:上は成功や結論、「全体像はここ」というズームアウト。
- シーンの出口は次のエントリーを設定します:同じ軸、同じ方向、同じスピード。クロスフェードではなく、1つのオブジェクトを変化にわたって持続させます。
- 図、チャート、テキストに無駄な動き(浮遊、呼吸、ふらつき)は入れません。何も動かす必要がなければ静止させます。意図的なカメラのドリフトが1つあれば十分です。
- イージング:エントリーはスムーズなイーズアウト(
power2.outからpower4.out)、エグジットはより素早いイーズイン(エントリー時間の約75%)。エントリーは最大0.8秒。グループのスタッガーは最大0.5秒。オーバーシュート(back.out)は小さなポップのみ。弾性やバウンスは使用しません。 - 重要な場面では一度に1つのものだけを動かします。目は1つしか追えません。
- 見せ場の直前に0.3~0.75秒の静寂のビートを追加します。
スタイルルール(常に)
これらがこのスタイルを作ります。色や言葉が変わってもこれらは守ってください。
- タイプはフレーム幅の50~80%を占めます。小さなテキストはキャプションであり、ビジュアルではないからです。
- 一度に画面に表示するのは1つのアイデア:1~4単語、最大3行。他はすべて消えます。
- すべての単語は動き(スラム、スライド、ストレッチ、マスクリビール、フリップ)とともに現れます。単純なフェードインはなし。
- 変化はリズムに乗って起こります:0.25~0.7秒ごとに新しい単語または動き、キーワードはビートまたはアクセントのある音節に合わせます。
- コントラストは極端に:ほぼ黒からオフホワイト、またはその逆。キーワードのみに1つのアクセントカラー。
- 各フレーズには1つのヒーローワード:最大、アクセントカラー、または他の単語と異なる動き。
デフォルト(自由に変更可)
フォント、色、単語、タイミング、背景。ユーザーがブランドフォントや色を指定した場合は、そちらを優先します。優先順位:ユーザーのブランドまたは指定 > 明示的なリクエスト > これらのデフォルト。
- 背景
#111111、テキスト#f4f1ea、アクセント#ff4d2e。フレーズごとに明暗を反転してエネルギーを出します。 - ディスプレイフォント:太いコンデンスドフェイス(Anton、Bebas Neue、Archivo Black)。ヒーローワードはオールキャップス。サポートフォント:小さな接続詞用の太いサンセリフ(Inter 800)。
- サイズ:9:16でヒーローワードは180~320 px。小さな単語は60~90 px。
セットアップ(一度だけ)
node -v # 22+が必要
ffmpeg -version
npx hyperframes init my-video --non-interactive --resolution portrait && cd my-video # 16:9の場合はlandscape
既存の空のフォルダ内では npx hyperframes init . --non-interactive --resolution portrait を使用し、生成された index.html を以下のテンプレートで置き換えます。
構築前に決定すること
コードを書く前に、これらを1行でユーザーに伝えてください。
- アスペクト比: 9:16 (1080×1920) または 16:9 (1920×1080)。デフォルトは9:16。
- 単語とタイミング: スクリプトを1~4単語のフレーズに分割します。ナレーションがある場合は、その単語のタイムスタンプから時間を取得します(
npx hyperframes transcribe voice.wav)。ない場合は、各フレーズに0.6~1.4秒を与え、ヒーローワードはより長くします。 - リズムソース: 音声(アクセントのある音節に合わせる)、音楽(ビートに合わせる;
npx hyperframes beats music.mp3)、または固定テンポ(例:120 BPM = 0.5秒ごとにビート)。 - フレーズごとのヒーローワード と、それが使用する動き。
| ニーズ | HyperFrames + GSAP (デフォルト) | Remotion |
|---|---|---|
| 単語と文字の動き、マスク、ストレッチ | はい | はい |
| 音声またはビートデータからのタイミング | はい(ラベル) | はい(フレーム) |
| Node と FFmpeg のみで動作 | はい | はい |
| ライセンス | 無料 | 3人までは無料、それ以上は有料 |
動きの語彙
| 動き | 方法(GSAP) | 用途 |
|---|---|---|
| スラム | from {scale: 2.6, opacity: 0}、expo.out、0.22 秒、さらに 6 px の画面シェイク 0.1 秒 | ヒーローワード |
| スライドスタック | 行を from {yPercent: 110} で overflow:hidden マスク内に、スタッガー 0.06 秒、power4.out 0.35 秒 | リスト、2行のフレーズ |
| ストレッチ | scaleY: 1.6 → 1、transformOrigin: bottom、back.out(3) 0.3 秒 | 強調、動詞 |
| レターカスケード | 文字を from {y: -120, rotation: -20, opacity: 0}、スタッガー 0.025 秒、back.out(2) | 名前、タイトル |
| フリップスワップ | 古い単語 rotationX: 90 でアウト(0.12 秒)、新しい単語 rotationX: -90 → 0 でイン(0.18 秒) | X が Y になる |
| スプリット | 単語の半分を x: ±140 でスライドさせて分離、間に小さな単語を表示 | コントラスト |
| エグジット | yPercent: -110 または scale: 0.7, opacity: 0、0.15 秒、power2.in | すべてのフレーズの離脱 |
エグジットは速く(0.12~0.18 秒)、エントランスは思っているよりキビキビと(0.18~0.35 秒)。各フレーズは着地後、少なくとも 0.35 秒は保持し、読めるようにします。
目を引くようにするために
- フレームを埋め尽くす。 フォントサイズではなく、フレーム幅に合わせて単語のサイズを決めます。250+ px のコンデンスドフォントが見た目です。
- カラーフリップ: 背景をダークとライト(またはアクセントカラー)の間で、2~3 のキーフレーズで、ビートに合わせてハードカットで切り替えます。
- テクスチャと奥行き: 微妙なグレインオーバーレイ、ソフトシャドウまたはヒーローワードの後ろにオフセットした複製(アクセントカラー、オフセット 10~14 px)、そして時折背景に巨大な淡色の単語。
- カメラのエネルギー: ステージ全体のゆっくりとした 2~4% のスケールドリフト、ヒーローワードのヒット時にはパンチイン(スケール 1.06 で 0.1 秒)、スラム時には 6~10 px のシェイク。
- 単語と共に使うシェイプ: 描画される下線バー、単語の背後にポップする円、矢印。アクセントカラー、フラット。
- ロックアップで終了: タイトルまたはコールトゥアクションを完全に構成し、1~1.5 秒保持。開始フレームと一致させてループできるようにします。
- 品質ゲート: スナップショットに中央揃えの小さなテキストが無地の背景に表示されている場合、それは完成ではありません。
構築: テンプレート
<!doctype html><html><head><meta charset="UTF-8">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Anton&family=Inter:wght@800&display=swap">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<style>
html,body{margin:0;width:1080px;height:1920px;overflow:hidden;background:#111}
#root{width:100%;height:100%;position:relative;overflow:hidden}
#stage{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
.phrase{position:absolute;left:60px;right:120px;top:270px;bottom:670px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:0}
.line{overflow:hidden;line-height:.92;padding:0 8px}
.w{display:inline-block;font-family:Anton,sans-serif;text-transform:uppercase;color:#f4f1ea;white-space:nowrap}
.hero{color:#ff4d2e}
.flip .w{color:#fff}.flip .hero{color:#111}
.small{font-family:Inter,system-ui,sans-serif;font-weight:800;text-transform:none}
#bg{position:absolute;inset:0;background:#111}
</style></head>
<body><div id="root" data-composition-id="main" data-start="0" data-duration="8" data-width="1080" data-height="1920">
<div id="bg"></div><div id="stage"></div></div>
<script>
// [time, lines, moveName, heroWordIndex, flipBackground?] -- このリストを編集; 時間は秒
const PHRASES = [
[0.2, ["WORDS"], "slam", 0],
[1.0, ["can", "MOVE"], "stack", 1],
[2.0, ["LIKE", "MUSIC"], "stretch", 1, true],
[3.2, ["one", "idea"], "stack", 1],
[4.2, ["AT A", "TIME"], "slam", 1],
[5.4, ["KINETIC", "TYPE"], "cascade", 1],
];
const END = 8, W = 1080 - 180;
// テキストはWebフォントのロード後にのみ測定し、タイムラインを構築して登録する
document.fonts.ready.then(() => {
const tl = gsap.timeline({ paused: true });
const stage = document.getElementById("stage");
PHRASES.forEach(([t, lines, move, hero, flip], i) => {
const p = document.createElement("div"); p.className = "phrase" + (flip ? " flip" : ""); stage.appendChild(p);
const words = lines.map((txt, k) => {
const line = document.createElement("div"); line.className = "line";
const w = document.createElement("span"); w.className = "w" + (k === hero ? " hero" : "") + (txt === txt.toLowerCase() && k !== hero ? " small" : "");
w.textContent = txt; line.appendChild(w); p.appendChild(line);
w.style.fontSize = "100px"; const fit = Math.min(420, (100 * W) / w.getBoundingClientRect().width); // 幅を埋める
w.style.fontSize = (w.classList.contains("small") ? Math.min(fit, 110) : fit) + "px";
return w;
});
gsap.set(p, { autoAlpha: 0 });
tl.set(p, { autoAlpha: 1 }, t);
if (move === "slam") tl.from(words, { scale: 2.6, opacity: 0, duration: 0.22, ease: "expo.out", stagger: 0.08 }, t)
.fromTo("#stage", { x: -10 }, { x: 10, duration: 0.04, yoyo: true, repeat: 3, ease: "none" }, t + 0.18).set("#stage", { x: 0 }, t + 0.34);
if (move === "stack") tl.from(words, { yPercent: 110, duration: 0.35, ease: "power4.out", stagger: 0.07 }, t);
if (move === "stretch") tl.from(words, { scaleY: 1.7, opacity: 0, transformOrigin: "50% 100%", duration: 0.3, ease: "back.out(3)", stagger: 0.08 }, t);
if (move === "cascade") words.forEach((w, k) => {
const letters = Array.from(w.textContent); w.textContent = "";
letters.forEach((c) => { const l = document.createElement("span"); l.style.display = "inline-block"; l.textContent = c; w.appendChild(l); });
tl.from(w.children, { y: -140, rotation: -20, opacity: 0, duration: 0.4, ease: "back.out(2)", stagger: 0.03 }, t + k * 0.15); });
const next = PHRASES[i + 1] ? PHRASES[i + 1][0] : null;
if (flip) tl.set("#bg", { background: "#ff4d2e" }, t).set("#bg", { background: "#111" }, next || END); // キーフレーズでカラーフリップ
if (next) tl.to(words, { yPercent: -110, opacity: 0, duration: 0.15, ease: "power2.in", stagger: 0.03 }, next - 0.17)
.set(p, { autoAlpha: 0 }, next);
});
tl.fromTo("#root", { scale: 1 }, { scale: 1.04, duration: END, ease: "none" }, 0); // ゆっくりとしたドリフト
tl.set({}, {}, END);
window.__timelines["main"] = tl;
});
</script></body></html>
PHRASES を編集して単語と時間を変更します。動きの語彙から if (move === ...) 分岐を追加して動きを追加します。ナレーションがある場合は、<audio> 要素をルートに配置し、フレーズの時間をそのトランスクリプトから取得します。
ユーザーに公開するパラメータ
aspect · phrases (time, lines, move, hero) · fontDisplay · fontSupport · colors (bg, text, accent) · tempo or audio · flipPhrases (背景が反転する場所) · endLockup。
避けるべきこと
- メインのモーションとしてのフェードインとゆっくりとしたドリフト。スライドショーのように見えます。
- 同時に画面に表示する単語が4つ以上、または行が3行以上。
- 複数のアクセントカラー。1つのアクセントを、ヒーローワードにのみ使用します。
- ヒーローワードに、細いまたはライトなフォントウェイト、またはスクリプトフォント。
- 9:16フレームの下部670 pxまたは上部270 pxのテキスト(プラットフォームのUIで隠れます)。
- ランダムまたはレンダリングごとのバリエーション。すべての値はタイムラインから取得する必要があります。
失敗モード
| 見た目 | 原因 | 修正 |
|---|---|---|
| スライドショー | フェード、長いホールド、毎回同じ動き | 語彙から動きを変える。エントランスは0.35秒未満、エグジットは0.15秒 |
| キャプションでありデザインではない | 単語が小さすぎる、空白の中央に配置 | 単語のサイズをフレーム幅に合わせる。コンデンスドフォント。250+ px |
| 乱雑で読みにくい | 一度に単語や動きが多すぎる | 画面には1フレーズ。1つのヒーローワード。着地後0.35秒保持 |
| ビートがずれている | フレーズ時間を推測した | トランスクリプトまたはビート時間を使用。ヒットを2~3フレーム早める |
| マスクで単語がクリップされる | フォントに対してline-heightがきつすぎる | line-height 0.92~1.0、.line にパディング。スナップショットで確認 |
| レンダリングでフォントが違う | フォントがロードされていない | head内にGoogle Fontsの <link>。スナップショットでコンデンスドフェイスが表示されているか確認 |
最終レンダリング前に検証
npx hyperframes check
npx hyperframes snapshot --at <各フレーズの着地後0.3秒の時点、およびエグジット途中の時点を1つ>
npx hyperframes render -o renders/<name>.mp4
ffprobe -v error -show_entries stream=width,height,r_frame_rate:format=duration -of csv=p=0 renders/<name>.mp4
すべてのスナップショットと snapshots/contact-sheet.jpg を確認し、以下を確認します。
- すべてのフレーズが幅の50~80%を占め、電話サイズで読めること
- フレーズごとに1つのヒーローワードがあり、アクセントカラーまたは異なる動きであること
- どのフレームでも2つのフレーズが重なっていないこと
- すべてのテキストがアスペクト比のセーフエリア内にあること
- エンディングのロックアップが少なくとも1秒間保持され、開始と一致していること
- サイズと長さがリクエストと一致していること
最終レンダリング前の自己批評
スナップショットシートを1~10でスコアリングします:フック(最初の1秒)、電話幅での読みやすさ、モーションの質、構図、正確性、「ミュートテスト」(音声なしで視聴者が一文で要約できるか)。7以下のものだけを修正し、再スナップショットし、すべてが8以上になるまで繰り返します。可能であれば、自分で採点するのではなく、独立した読み取り専用パスでスコアリングを行います。
| しないこと | 代わりにすること |
|---|---|
| 2つの混雑したレイアウト間でクロスフェード | 古いコンテンツをエグジットし、新しいコンテンツをエントリーするか、1つのオブジェクトを持ち越す |
| 複数のものを同時に動かす | スタッガー、1つの焦点 |
…
[truncated for translation]