算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
product-demo-director【设计与创意】Direct the craft of putting a real software UI on screen - cursor choreography, zoom/pan/callout language, screen-recording vs recreated UI, and making state changes legible with highlights, focus pulls, and slow-downs. Use when filming or recreating an app for a product demo, feature walkthrough, onboarding clip, or release reel and asking "how do I…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
You direct the single hardest shot in software marketing: a real UI doing a real thing, in a way a stranger can follow on the first watch. The default failure is a raw screen recording at human speed - the cursor teleports, three things change at once, and the viewer never knows where to look. Your job is to choreograph attention: one change at a time, the camera pointed at it, held long enough to read.
This is the taste layer. The grammar of how things move (easing curves, anticipation, the 12 principles) lives in motion-design-principles; the beat list lives in video-storyboard; word animation lives in kinetic-typography. You decide what the camera and cursor do over the UI and hand the result down to those skills and to the build pack (remotion-compose, remotion-render).
Work one example throughout: a 12-second clip showing a user turn on a feature flag in a settings dashboard and watch a metric tile light up.
Run these in order. Do not start choreographing the cursor before you have decided record-vs-rebuild - the decision changes everything downstream.
This is the fork. Pick before any other work.
Write the decision down with its reason. See references/record-vs-recreate for the full tradeoff table.
A demo is legible only if exactly one thing changes at a time and the viewer is looking at it when it does. List every state change in the flow (focus moves, a field fills, a toggle flips, a tile updates, a toast appears). If two happen on the same frame, you must either separate them in time or subordinate one (dim it, push it out of the zoom). "Turn on the flag" is not one beat - it is: cursor arrives at toggle → toggle flips → metric tile recomputes → confirmation toast. Four beats, four held reads.
The cursor is the narrator. Treat it like an actor, not a mouse log.
The frame is a camera over the UI. Use a consistent move vocabulary so the viewer learns your language:
Rule: the camera should already be looking at the element before it changes. If the toggle flips and then you zoom to it, you missed the moment.
The whole demo exists for the state changes. Each one gets deliberate treatment:
Run the linter (below) on your beat list to catch the classic legibility failures - overlapping changes, missing holds, cursor teleports, a camera that arrives late. Fix every flag. Then hand the directed shot list down: timing/easing specifics to motion-design-principles, the beat ordering to video-storyboard, and the actual implementation (cursor layer, zoom transforms, component screens) to remotion-compose for build and remotion-render for the iterate loop.
The demo is A+ only when all hold:
Self-contained Node script. Save as demo_lint.js and run with node demo_lint.js. No dependencies. Describe your demo as an ordered list of beats; the linter flags the classic legibility failures from the workflow. Edit the beats array for your demo.
// Product-demo shot-list linter. Run: node demo_lint.js
// Each beat is one moment on the timeline. Fields:
// t start time in seconds (ordered, ascending)
// dur how long the beat is held, in seconds
// changes UI state changes that happen on this beat (0, 1, or more)
// camera 'push' | 'pan' | 'pull' | 'hold' (camera move into this beat)
// framedOn the element the camera is pointed at when the beat starts
// cursorTo where the cursor moves this beat (null = stays / not the subject)
// click true if a click happens this beat
// clickAffordance true if the click is made visible (ripple/press/scale)
// legibility 'slowdown' | 'highlight' | 'focuspull' | 'countup' | null
const beats = [
{ t: 0.0, dur: 1.2, changes: [], camera: 'push', framedOn: 'settings-panel', cursorTo: 'toggle', click: false, clickAffordance: false, legibility: null },
{ t: 1.2, dur: 0.5, changes: [], camera: 'hold', framedOn: 'toggle', cursorTo: null, click: true, clickAffordance: true, legibility: null },
{ t: 1.7, dur: 1.0, changes: ['toggle-on'], camera: 'hold', framedOn: 'toggle', cursorTo: null, click: false, clickAffordance: false, legibility: 'slowdown' },
{ t: 2.7, dur: 1.4, changes: ['tile-update'], camera: 'pan', framedOn: 'metric-tile', cursorTo: null, click: false, clickAffordance: false, legibility: 'countup' },
{ t: 4.1, dur: 0.8, changes: ['toast','badge'],camera: 'hold', framedOn: 'metric-tile', cursorTo: 'export-btn', click: false, clickAffordance: false, legibility: null },
]
const HOLD_MIN = 0.4 // a state change needs at least this long to read
const flags = []
for (let i = 0; i < beats.length; i++) {
const b = beats[i]
const id = `beat ${i + 1} (t=${b.t}s)`
// 1. One change per beat.
if (b.changes.length > 1)
flags.push(`${id}: ${b.changes.length} changes at once (${b.changes.join(', ')}) - split or subordinate all but one`)
// 2. Every change gets a held read.
if (b.changes.length === 1 && b.dur < HOLD_MIN)
flags.push(`${id}: change "${b.changes[0]}" held only ${b.dur}s - hold >= ${HOLD_MIN}s so it reads`)
// 3. Every change gets a legibility treatment.
if (b.changes.length >= 1 && !b.legibility)
flags.push(`${id}: change "${b.changes[0]}" has no legibility treatment (slowdown/highlight/focuspull/countup)`)
// 4. Clicks must be visible.
if (b.click && !b.clickAffordance)
flags.push(`${id}: click has no visible affordance - add a ripple/press/scale`)
// 5. Camera frames the element BEFORE it changes, not on the change beat.
if (b.changes.length >= 1 && (b.camera === 'push' || b.camera === 'pan'))
flags.push(`${id}: camera moves (${b.camera}) on the same beat the change happens - arrive first, change second`)
// 6. No cursor teleport: a click beat whose cursor just arrived has no settle.
if (b.click && b.cursorTo)
flags.push(`${id}: cursor moves AND clicks on the same beat - settle (~6-10 frames) before clicking`)
}
if (flags.length === 0) console.log('PASS - shot list is legible.')
else {
console.log(`${flags.length} flag(s):`)
for (const f of flags) console.log(' - ' + f)
}
With the beats above the script prints:
3 flag(s):
- beat 4 (t=2.7s): camera moves (pan) on the same beat the change happens - arrive first, change second
- beat 5 (t=4.1s): 2 changes at once (toast, badge) - split or subordinate all but one
- beat 5 (t=4.1s): change "toast" has no legibility treatment (slowdown/highlight/focuspull/countup)
Read it: the toggle click (beats 2-3) is clean - the cursor settles, the click has a ripple, the flip is slowed down and held. But beat 4 pans to the metric tile while it updates, so the camera arrives late on the payoff; fix it by panning during beat 3's hold so the tile is framed before it recomputes. And beat 5 is doing two wrong things at once: it fires a toast and a badge on the same frame, and the toast has no legibility treatment. Subordinate the badge (dim it, or push it outside the zoom) so the toast is the single subject, and give the toast a highlight or hold. Fix all three and the linter passes.
Copy this, fill the FILL fields, and feed the result to the linter, then down to the sibling skills.
PRODUCT DEMO SHOT CARD. [FILL: feature name] - [FILL: length]s
PRODUCTION DECISION
Record / Recreate / Hybrid: [FILL]
Reason: [FILL: why this path]
Capture resolution: [FILL: 2x final if recording]
SUBJECT OF THE CLIP (one sentence the viewer should take away)
[FILL]
BEAT SHEET (one change per row; camera arrives before the change)
# TIME CAMERA FRAMED ON CURSOR CHANGE LEGIBILITY HOLD
1 [FILL] [push] [FILL] [moves to FILL] - - [FILL]s
2 [FILL] [hold] [FILL] [settles+click] - - [FILL]s
3 [FILL] [hold] [FILL] [parked/fade] [FILL: the flip] [slowdown] [FILL]s
4 [FILL] [pan] [FILL] [off-frame] [FILL: the effect][countup/glow] [FILL]s
(add rows; never two changes in one row)
CALLOUT LANGUAGE (kept consistent across the clip)
Highlight style: [FILL: ring / spotlight-dim / connector]
Click affordance: [FILL: ripple / press / cursor-scale]
Focus-pull treatment: [FILL: dim others / blur others]
HANDOFF
Easing + timing detail -> motion-design-principles
Beat order / story arc -> video-storyboard
On-screen words -> kinetic-typography
Color + light of highlights ->motion-color-and-light
Sound design / hit on click ->sound-and-music-sync
Reframe for 9:16 / 1:1 -> social-video-formatter
Build (screens, cursor, zoom)->remotion-compose, then remotion-render
| Dimension | Screen-record | Recreate the UI |
|---|---|---|
| Setup cost | Low - capture the real app | High - rebuild screens as components |
| Cursor control | None (real mouse path) | Total (animated cursor layer) |
| Timing control | None (real latency, spinners) | Deterministic per-frame |
| Data | Real (may need redaction) | Fake-but-honest, fully chosen |
| Zoom quality | Limited by capture res | Pixel-perfect at any zoom |
| Credibility | "This is the real product" | Needs to look real to be honest |
| States you don't have yet | Impossible | Easy (mock the future state) |
| Best for | Stable, shippable, credibility-critical | Pre-launch, messy UI, control-critical |
The honesty line: a recreated UI must represent behavior the product actually has (or will have at ship). Never recreate a state the product cannot reach - that is a fabricated demo, not a directed one. When in doubt, hybrid: record the real texture, author the choreography on top.
The five moves that make a state change readable, fastest to reach for first:
Layer at most two of these on any single change. More than two competes with itself and the change stops reading - the exact problem you were solving.