算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
remotion-compose【设计与创意】Turns a natural-language video brief into a complete, ready-to-preview Remotion composition - extracts duration, scenes, brand colors, aspect ratio, and real copy; plans the frame budget; and writes data-driven React/TypeScript using useCurrentFrame, interpolate, spring, AbsoluteFill, and Sequence, registered in Root.tsx. Use when someone says "build…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
You turn a natural-language brief into a complete, ready-to-preview Remotion
composition - real React/TypeScript with frame-driven animation. The output is
a .tsx file in src/compositions/, registered in src/Root.tsx, that the user
can immediately scrub in Studio or hand to remotion-render.
Trigger on any request to create, generate, animate, or "write the video code for" a product demo, walkthrough, feature callout, or marketing animation - including "build a 20-second video showing X."
Do not start coding until you have these. Ask in one compact batch; fill obvious gaps with sensible defaults rather than stalling:
@remotion/google-fonts family name).Convert seconds to frames everywhere: durationInFrames = seconds * fps.
Typical total lengths: 15-60s for a social/product demo, 6-15s for an ad cut.
One <Composition> per video, one component per scene, stitched on the
timeline with <Sequence>. Lay out the frame budget before writing code:
Total: 20s @ 30fps = 600 frames
Intro (logo) frames 0-90 (Sequence from=0 durationInFrames=90)
Feature 1 frames 90-240 (Sequence from=90 durationInFrames=150)
Feature 2 frames 240-390 (Sequence from=240 durationInFrames=150)
Feature 3 frames 390-540 (Sequence from=390 durationInFrames=150)
CTA outro frames 540-600 (Sequence from=540 durationInFrames=60)
Pacing thresholds while budgeting:
<Sequence from={F}> shifts a child's local clock so inside that component
useCurrentFrame() starts at 0. Animate each scene as if it begins at frame 0.
useCurrentFrame() - the current frame (the clock). Everything animates as
a function of this.useVideoConfig() - { fps, width, height, durationInFrames }.interpolate(frame, [inFrames], [outValues], opts) - linear mapping. Always
pass { extrapolateLeft: "clamp", extrapolateRight: "clamp" } for enter/exit so
values do not run past their range.spring({ frame, fps, config }) - physics-based 0→1 (overshoots slightly).
Use for anything that should feel alive (pops, slides, scale-ins).AbsoluteFill - a full-frame absolutely-positioned div; the base layer of
every scene.Sequence - time-shifts and time-bounds children (the scene scheduler).Series / Series.Sequence - back-to-back scenes without computing from
offsets by hand.Img, staticFile - see Step 6.Use spring() when… | Use interpolate() when… |
|---|---|
| The motion should feel physical - pops, bounces, scale-ins, slide-ins | You need precise, linear control between exact frames |
| You want a natural settle/overshoot | Fades (opacity 0→1), color shifts, progress bars, anything timed-to-the-frame |
| Entrances of cards, logos, badges | Cross-fades and exits where overshoot would look wrong |
Pattern - drive a transform with a spring, an opacity with interpolate:
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
const pop = spring({ frame, fps, config: { damping: 200 } }); // 0 → 1, lively
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
const scale = interpolate(pop, [0, 1], [0.8, 1]);
damping: 200 is the workhorse config - a quick settle with barely-there
overshoot. Lower damping (~10-20) bounces visibly; use it only when bounce is
the point.
Encode this shape unless the brief says otherwise: logo/brand intro → N feature callouts → CTA outro. Make it data-driven (Step 6) so variants are free.
// src/compositions/ProductDemo.tsx
import {
AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig,
interpolate, spring,
} from "remotion";
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily } = loadFont();
export type Feature = { title: string; body: string };
export type ProductDemoProps = {
brand: string;
colors: { bg: string; fg: string; accent: string };
logoText: string;
features: Feature[];
cta: string;
};
const Intro: React.FC<{ text: string; colors: ProductDemoProps["colors"] }> = ({ text, colors }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const s = spring({ frame, fps, config: { damping: 200 } });
const scale = interpolate(s, [0, 1], [0.7, 1]);
const opacity = interpolate(frame, [0, 12], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.bg, justifyContent: "center", alignItems: "center" }}>
<h1 style={{ fontFamily, fontSize: 120, color: colors.fg, transform: `scale(${scale})`, opacity }}>
{text}
</h1>
</AbsoluteFill>
);
};
const FeatureCallout: React.FC<{ feature: Feature; colors: ProductDemoProps["colors"] }> = ({ feature, colors }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const slide = spring({ frame, fps, config: { damping: 200 } });
const x = interpolate(slide, [0, 1], [-60, 0]);
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.bg, justifyContent: "center", paddingLeft: 160 }}>
<div style={{ transform: `translateX(${x}px)`, opacity }}>
<div style={{ fontFamily, fontSize: 32, color: colors.accent, fontWeight: 700, letterSpacing: 2 }}>
{feature.title.toUpperCase()}
</div>
<div style={{ fontFamily, fontSize: 64, color: colors.fg, maxWidth: 1100, marginTop: 16 }}>
{feature.body}
</div>
</div>
</AbsoluteFill>
);
};
const Outro: React.FC<{ cta: string; colors: ProductDemoProps["colors"] }> = ({ cta, colors }) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 18], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill style={{ backgroundColor: colors.accent, justifyContent: "center", alignItems: "center" }}>
<h2 style={{ fontFamily, fontSize: 84, color: colors.bg, opacity }}>{cta}</h2>
</AbsoluteFill>
);
};
export const ProductDemo: React.FC<ProductDemoProps> = ({ colors, logoText, features, cta }) => {
const intro = 90;
const per = 150;
const outro = 60;
return (
<AbsoluteFill>
<Sequence durationInFrames={intro}>
<Intro text={logoText} colors={colors} />
</Sequence>
{features.map((f, i) => (
<Sequence key={i} from={intro + i * per} durationInFrames={per}>
<FeatureCallout feature={f} colors={colors} />
</Sequence>
))}
<Sequence from={intro + features.length * per} durationInFrames={outro}>
<Outro cta={cta} colors={colors} />
</Sequence>
</AbsoluteFill>
);
};
Register it in src/Root.tsx, computing durationInFrames from the same numbers
and supplying defaultProps:
<Composition
id="ProductDemo"
component={ProductDemo}
durationInFrames={90 + 3 * 150 + 60}
fps={30}
width={1920}
height={1080}
defaultProps={{
brand: "Acme",
colors: { bg: "#0B0B0F", fg: "#FFFFFF", accent: "#6C5CE7" },
logoText: "Acme",
features: [
{ title: "Fast", body: "Ship in seconds, not sprints." },
{ title: "Simple", body: "One prompt, one video." },
{ title: "Yours", body: "Your brand, your copy." },
],
cta: "Start free →",
}}
/>
For vertical/social, set width={1080} height={1920}; for square, 1080×1080.
Keep the components identical - only the canvas changes.
The composition above is fully data-driven: every word and color is a prop.
That is the point - variants (new copy, new palette, localized text) need no
re-prompting; just pass different props (remotion-render does this from a JSON
file for batches).
Static assets (logos, screenshots, audio) live in public/assets/ and are
referenced with staticFile() - never an import or a raw path:
import { Img, staticFile } from "remotion";
<Img src={staticFile("assets/logo.png")} />
Save the finished file to src/compositions/<VideoName>.tsx, register it in
src/Root.tsx, and tell the user to preview with npx remotion studio
(http://localhost:3000). Then hand off: "Scrub it in Studio. Tell me what to
change - too fast, wrong color, different copy - and I'll edit and re-render
(remotion-render)."
setTimeout/setInterval.
Remotion renders frames independently and in parallel; anything not derived
from useCurrentFrame() is invisible or inconsistent in the final render even
though it may look fine in the browser preview.Math.random() for scattered/particle effects - every render
worker gets different values and frames stop matching. Use Remotion's
random(seed) for deterministic pseudo-randomness.delayRender() /
continueRender() - the renderer captures the frame before the data arrives.useVideoConfig() so switching 30↔60fps or landscape↔vertical doesn't break
every timing and layout.extrapolateLeft/Right: "clamp" on enter/exit interpolations -
opacity climbs past 1 and elements drift off-canvas after their range ends.useCurrentFrame() (plus props) -
scrubbing backwards in Studio looks identical to playing forwards.durationInFrames in
Root.tsx, and no text beat holds for less than its readable minimum.defaultProps supplied - no
literals buried in scene components.staticFile(); the file registers cleanly and previews in
Studio without console errors.