引人入勝的解說影片
一個一鏡到底的視覺故事。一個主角元素(在參考風格中,是一個從切換開關旋鈕開始的白色球體)穿過一系列小型介面「站點」,每個站點都會對它做出反應:它沿著滑桿滾動並將其推至 100%,從末端掉落並在一疊通知上彈跳,通知在它經過時彈出,落在鍵盤上並在按鍵間彈跳打出一個單詞,撞擊回車鍵後被拋回空中。攝影機從不中斷:它讓主角保持在畫面中央附近,針對每個互動放大特寫,在高潮時大幅拉遠,讓觀眾一次看到整個路線,然後再潛回起點。
對於一個產品來說,這些站點就是其工作流程的步驟,在一個畫布上按空間佈局,而主角就是穿過該工作流程的事物。拉遠的鏡頭就是「現在你明白它如何運作了」的時刻。
你將其編寫為一個 HTML 頁面,根據時間值渲染任何幀,然後渲染成帶有運動模糊和配樂的 MP4。
步驟 1:獲取簡報(一條訊息,然後開始)
在一個訊息中詢問所有事項,然後獨立工作。只需要產品名稱;對於任何留空的項目,請自行決定並說明你的選擇。
- 產品:名稱,以及用幾句話描述其功能。網站 URL 更好。
- 觀眾必須記住的一件事(例如「輸入一個想法,獲得一個即時應用」)。
- 品牌:顏色、字體、Logo 檔案,或「使用網站」。如果沒有,請使用下面的預設調色板。
- 格式:16:9(1920×1080)或 9:16(1080×1920)。預設為 16:9。
- 長度:此風格的預設值如下所示。
- 音樂:使用者自己的曲目(檔案路徑),或讓你合成一個。預設:合成。
- 方向(可選):他們想要的場景、標語、號召性用語,或任何需要避免的內容。
如果他們提供了 URL,請在撰寫任何內容之前先讀取(網路爬取)產品實際的用詞、功能、顏色和字體名稱。對 UI 標籤和標語使用他們的真實詞彙。
切勿虛構事實。 不得編造客戶數量、收入、評級、推薦或引用。除非使用者或網站明確說明,否則只能使用相對條形圖、進度和佔位符骨架線。UI 內部的範例內容(提示、檔案名稱、專案名稱、收據上的價格)是可以的,並且應該是可信的;但關於產品本身的聲明(統計數據、用戶數量、評級、如「免費」等優惠)則不行。號召性用語僅使用使用者或網站提供的 URL 或優惠;如果沒有,則以產品名稱和標語結束。
Logo。 僅在使用者提供檔案或這是他們自己的產品且他們要求的情況下,才使用 Logo。否則,以品牌字體將名稱寫為文字。第三方 Logo(整合)僅在提供時使用;否則使用帶有名稱的中性圓角圖塊。
同時詢問(可選):3–6 步的工作流程(使用者做什麼 → 產品做什麼 → 結果)。如果他們提供了 URL,則從網站推導出來。
此風格的預設長度:12 秒,無縫循環。範圍為 10–30 秒(更長 = 更多站點,而不是更慢的站點)。
步驟 2:設計路線,然後建構
1. 選擇主角
主角是一個簡單、可讀的物件,產品的故事圍繞它展開,並且它永遠不會消失或被取代(它可能暫時變形,然後恢復):一個球/旋鈕(預設,適用於任何情況)、文字插入點或游標、文件或文檔圖塊、訊息氣泡、硬幣或訂單、數據點。它應自然地適合站點的介面(旋鈕、拇指、鍵帽、圖釘)。
2. 將工作流程映射到站點
每個步驟變成一個站點:一個緊湊的實際介面元素,主角與其進行物理互動,並且該元素會因為主角而明顯改變。從這些機制中汲取靈感:
| 機制 | 主角... | 站點反應 |
|---|---|---|
| 切換 / 按鈕 | 是旋鈕,或按下它 | 切換開啟,顏色填滿 |
| 滑桿 / 進度條 | 作為拇指沿其滾動 | 工具提示中的數值增加,軌道填滿 |
| 通知堆疊 | 掉落在每張卡片上並彈跳 | 每張卡片在被擊中時彈入,帶有敘述故事的訊息 |
| 鍵盤 / 輸入框 | 在按鍵間彈跳 | 每個按鍵按下,字母出現在上方的輸入框中 |
| 列表 / 表格 | 沿行滑下 | 每行高亮、打勾或排序 |
| 圖表 | 成為線條的前導點 | 線條在它後面繪製,長條圖在它經過時升起 |
| 聊天 | 作為氣泡進入 | 回覆訊息正在打字 |
| 部署 / 發送按鈕 | 被它發射出去 | 出現一個 Toast 或 URL |
站點標籤和訊息用幾個詞講述故事,帶有些許機智(參考範例使用 Slider · Reached 100、Gravity · Something is falling、Messages · What now?,然後打出 retry)。對於產品,根據其實際工作流程編寫(例如一個程式設計工具:輸入提示 → Agent · Writing 12 files → 進度 → Preview · Ready → 按下部署 → Live at myapp.app)。
3. 佈置世界
將站點放置在一個大畫布上(大約是畫面的 3 倍),按照主角訪問的順序,路徑呈鋸齒狀(沿頂部向右,穿過中間向下,沿底部向右)。重力應該合理:下降向下,發射向上。保留每個站點的位置在整個影片中固定。世界是一個帶有微弱點狀網格的深色畫布,該網格隨攝影機移動(網格使攝影機移動可讀)。
4. 編寫主角的路徑
主角的位置是時間的純函數,由接觸點之間的片段構成(每個接觸點在一個節拍上):
- 沿站點滾動/滑動:沿軌道以
ease或彈簧方式定位;站點的值從主角的位置推導出來。 - 接觸點之間的跳躍和掉落:拋物線(
x隨時間線性變化,y = y0 + v·t + ½·g·t²,求解v使其在節拍上著陸於下個接觸點)。重力在整個影片中保持不變。 - 撞擊:接觸時擠壓(比例約 1.25 × 0.8,持續約 60 毫秒,然後彈回),飛行時沿速度方向拉伸(最多 1.15 × 0.9),以及對被撞擊物體的小幅下壓。
- 發射:最後一個站點將主角向上並向後發射到起點,形成一個長弧線,同時每個站點在它後面反向重置(數值減少,通知關閉,輸入框清除),從而使循環閉合。以慢動作運行發射弧線:首先選擇你想要的最高點(路線的頂部),這確定了在給定重力下的實際飛行時間,然後扭曲時間,使飛行填滿揭示視窗,在最高點時最慢(通常慢 5–10 倍;例如在弧線上速度為
1 − 0.9·sin(πu)^0.6)。在揭示停留期間安排站點重置,在觀眾看到完整路線之後,而不是在發射的瞬間。 - 無縫:將循環點設在主角著陸於第一個站點的位置。
seek(duration)必須看起來與seek(0)完全相同;著陸擠壓和按鍵按壓也在時間 0 處顯示。對於音效,在buildAudio中將配樂渲染到你自己稍長的OfflineAudioContext中,將duration之後的尾部折返到開始處,然後將該緩衝區播放到給定的上下文中。 - 重力是側向的。 佈置站點時,使掉落從側面看合理:通知卡片形成一個樓梯(每張卡片橫向偏移超過其寬度,使主角永遠不會穿過它們),鍵盤正面朝向觀眾,主角以小的弧線在按鍵之間跳躍。
- 空槽位: 當主角曾是站點的一部分(旋鈕、拇指)並離開時,站點保留一個它離開的暗淡輪廓槽位,當主角返回時再次填滿。
5. 攝影機
攝影機是困難的部分;在完善其他任何東西之前先把它做對。
- 跟隨: 攝影機目標 = 主角位置的混合(加上約 0.15 秒其速度的前導量)和當前站點的固定錨點,通過臨界阻尼彈簧(
w≈ 6)平滑處理,使其在撞擊時永不抖動。在互動期間,權重朝向錨點約 0.7(站點保持在畫面內),在掉落和跳躍期間約 0.15(主角領先),在揭示停留期間約 0.85。平滑處理需要歷史記錄,因此在載入時烘焙它(參見頁面契約),從時間 0 的第一個站點固定畫面開始,並在前 0.8 秒內混合到跟隨模式。主角永遠不會離開畫面的中間一半。 - 縮放: 在每次互動期間緊湊:站點填滿約一半畫面寬度,主角在螢幕上的大小為 50–70 像素(在 1080p 下)。在掉落和跳躍期間稍寬,使下一個站點已經可見。構圖圍繞站點,而不是空畫布。
- 揭示停留: 在發射之前,停留在最終結果(Toast、即時 URL)上 0.4–0.6 秒,完全在畫面內。
- 揭示: 在發射時,攝影機停止跟隨並緩慢移動到一個固定的構圖,容納整個路線(約 0.3 倍),帶有輕微的 3D 傾斜(
perspective+rotateX15–25°)以增加深度感。停留約 1 秒,在此期間主角劃過畫面;將主角放大並為其添加柔和的光暈,使其保持最亮、最可見(在螢幕上至少 24 像素)。然後潛入到第一個站點的固定構圖,時機設定為主角在攝影機到達時著陸,然後混合回跟隨模式。 - 真實運動模糊: 主角和世界在快速移動時產生拖影;這是外觀的一部分。
外觀
- 預設調色板(深色 iOS 風格):畫布
#0C0C0E,點狀網格#1E1E22,表面#1C1C1F/#2C2C2E,文字#F2F2F7,次要#8E8E93,強調色#0A84FF,主角白色#FFFFFF。品牌顏色取代強調色;主角保持螢幕上最亮的東西。如果品牌是淺色,淺色變體(畫布#F2F2F4,主角 = 強調色)也可以。 - 真實感系統 UI:帶有刻度標記的切換開關和滑桿、帶有應用程式圖示方形、標題、正文行和
now的通知卡片;帶有鍵帽和回車鍵的鍵盤。字體:類似 SF(Inter 或 Geist)。 - 游標可以啟動鏈條(點擊第一個站點),然後離開。
- 禁止: 中斷、淡入黑色、解釋正在發生什麼的文字疊加(站點自身的標籤是唯一的文字)、多個主角、不反應的站點、看起來漂浮的物理效果。
音效
一個緊湊、極簡的節奏,速度為 110–120 BPM(柔和的底鼓、清脆的 hi-hat、柔和的撥奏貝斯),而主角的接觸是主要樂器:每次撞擊播放一個短促的帶音高的滴答聲或木琴音符(隨著故事進展音高升高),按鍵按下發出咔噠聲,滑桿移動播放隨數值加快的細微滴答聲,通知發出柔和的啵聲,發射發出向上呼嘯聲,潛入發出向下呼嘯聲。每個音效恰好落在其視覺接觸點上。
頁面必須如何建構
一個自包含的 HTML 檔案(內嵌 CSS 和 JS;僅在使用者提供時才包含圖片)。使用 DOM + CSS 變換、SVG 或一個全螢幕畫布繪製,任何合適的方式都可以。
頁面契約
window.VIDEO = {w, h, duration}和window.seek(t),一個時間的純函數:它為時間t(秒)設定每個元素。沒有 CSS 過渡或動畫,沒有setTimeout,沒有從前一幀繼承的狀態。任何幀都必須能夠獨立、以任意順序正確渲染。在seek內部測量佈局(例如offsetLeft、measureText)是可以的;這是確定性的。- 隨機性(顆粒、粒子、音頻中的噪聲)來自於一個基於種子的生成器,絕不使用
Math.random。 - 任何需要歷史記錄的東西(落後於移動物體的攝影機、模擬)在載入時烘焙一次:以 1/240 秒的步長從 0 步進到
duration,儲存值,並讓seek透過插值讀回它們。對於循環,從循環開始的狀態開始模擬(參見風格的無縫規則)。 - 循環:
seek(t)使用t % duration;跨越無縫邊界的效果(著陸擠壓、音效尾部)在兩端都繪製或混合。 - 如果 URL 中包含
?render:沒有控制項,沒有預覽縮放;舞台精確地位於左上角,大小為w × h。如果不包含:舞台通過 CSS 縮放以適合視窗,並顯示預覽控制項。 - 字體:嵌入它們。 下載 woff2 檔案(Google Fonts 或品牌的)並將其作為 base64
@font-face內嵌,使渲染永遠不會無聲地回退。對於字重變形,使用帶有範圍的變數字體([email protected])。 - 圖層: 為場景圖層提供透明背景,並讓一個舞台背景在它們後面提供基礎顏色,這樣一個後面的圖層就不會意外地隱藏前面的圖層。絕對定位的文字使用
white-space: nowrap。 - 速度: 大的 CSS
filter: blur()和大模糊漸層在螢幕截圖時非常慢。在載入時一次性在畫布上繪製漸層和光暈(或作為圖像),然後移動它們。
輔助函數:貼上這些並在其上構建所有內容
// 阻尼彈簧階躍響應 0 -> 1。w = 速度(8 慢,14 正常,22 快速),z = 阻尼(1 = 無超調)
function spring(t, w = 14, z = 0.82) {
if (t <= 0) return 0;
if (z >= 1) return 1 - Math.exp(-w * t) * (1 + w * t);
const wd = w * Math.sqrt(1 - z * z);
return 1 - Math.exp(-z * w * t) * (Math.cos(wd * t) + (z * w / wd) * Math.sin(wd * t));
}
// 一個多次改變目標的值,每次彈簧到位:track(start, [[time, value, w?, z?], ...]) -> f(t)
const track = (v0, keys) => t => { let v = v0, p = v0; for (const [tk, val, w, z] of keys) { v += (val - p) * spring(t - tk, w, z); p = val; } return v; };
// 精確緩動關鍵幀(最適合攝影機移動):kf([[t0, v0], [t1, v1, easeFn?], ...]) -> f(t)
const kf = keys => t => { if (t <= keys[0][0]) return keys[0][1]; for (let i = 1; i < keys.length; i++) { const [a, va] = keys[i - 1], [b, vb, e = ease] = keys[i]; if (t <= b) return lerp(va, vb, e((t - a) / (b - a))); } return keys[keys.length - 1][1]; };
const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
const ease = x => (x = clamp(x), x < .5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2); // easeInOutCubic
const lerp = (a, b, k) => a + (b - a) * k;
const seg = (t, a, b) => clamp((t - a) / (b - a)); // 0..1 progress of t through [a, b]
const rng = s => () => (s = (s +
...
[截斷以進行翻譯]