← Catalog
No preview yet
Source
563 lines// Wireflow Canvas — the self-demo hero. A miniature Wireflow graph builds
// itself: a chat prompt types in, nodes spring onto a dotted canvas, edges
// draw port-to-port with a travelling data pulse, a render bar fills on the
// Compose node, and the output tile pops. It literally depicts the product
// making the video you're watching. Native Remotion, deterministic, SVG in a
// 1080x1920 viewBox so it's resolution-independent.
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { loadFont as loadGrotesk } from '@remotion/google-fonts/SpaceGrotesk';
import { loadFont as loadMono } from '@remotion/google-fonts/JetBrainsMono';
const { fontFamily: GROTESK } = loadGrotesk();
const { fontFamily: MONO } = loadMono();
export type WireflowCanvasProps = {
promptText: string;
outputImage: string;
accent: string;
accentSoft: string;
backgroundColor: string;
nodeColor: string;
textColor: string;
};
type NodeDef = {
id: string;
x: number;
y: number;
w: number;
h: number;
title: string;
sub: string;
glyph: string;
appear: number; // 0..1 fraction of duration when it springs in
};
// A clean top-to-bottom DAG that mirrors the real "Wireflow Promo Generator"
// graph: prompt -> Script -> (Image, Voice) -> Compose -> Output.
const NODES: NodeDef[] = [
{
id: 'script',
x: 360,
y: 560,
w: 360,
h: 156,
title: 'Script',
sub: 'Opus 4.8',
glyph: '✦',
appear: 0.16,
},
{
id: 'image',
x: 132,
y: 884,
w: 352,
h: 156,
title: 'Image',
sub: 'Nano Banana',
glyph: '◐',
appear: 0.3,
},
{
id: 'voice',
x: 596,
y: 884,
w: 352,
h: 156,
title: 'Voice',
sub: 'ElevenLabs',
glyph: '♪',
appear: 0.34,
},
{
id: 'compose',
x: 360,
y: 1208,
w: 360,
h: 176,
title: 'Compose',
sub: 'Remotion',
glyph: '▦',
appear: 0.5,
},
];
const cx = (n: NodeDef) => n.x + n.w / 2;
const topY = (n: NodeDef) => n.y;
const botY = (n: NodeDef) => n.y + n.h;
const byId = (id: string) => NODES.find((n) => n.id === id)!;
// Prompt bar + output tile geometry
const PROMPT = { x: 120, y: 250, w: 840, h: 150 };
const OUTPUT = { x: 392, y: 1536, w: 296, h: 300 };
type Edge = {
from: [number, number];
to: [number, number];
start: number;
end: number;
};
export default function WireflowCanvas({
promptText = 'make a promo reel for my product',
outputImage = '',
accent = '#8B7CFF',
accentSoft = '#5B4FCF',
backgroundColor = '#0A0A12',
nodeColor = '#14141F',
textColor = '#F4F4FA',
}: Partial<WireflowCanvasProps>) {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
const t = durationInFrames > 0 ? frame / durationInFrames : 0; // 0..1 progress
const exit = interpolate(
frame,
[durationInFrames - 9, durationInFrames - 1],
[1, 0],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
);
// Edges with draw windows (fractions of duration)
const promptAnchor: [number, number] = [
PROMPT.x + PROMPT.w / 2,
PROMPT.y + PROMPT.h,
];
const outAnchor: [number, number] = [OUTPUT.x + OUTPUT.w / 2, OUTPUT.y];
const edges: Edge[] = [
{
from: promptAnchor,
to: [cx(byId('script')), topY(byId('script'))],
start: 0.14,
end: 0.26,
},
{
from: [cx(byId('script')), botY(byId('script'))],
to: [cx(byId('image')), topY(byId('image'))],
start: 0.28,
end: 0.42,
},
{
from: [cx(byId('script')), botY(byId('script'))],
to: [cx(byId('voice')), topY(byId('voice'))],
start: 0.32,
end: 0.46,
},
{
from: [cx(byId('image')), botY(byId('image'))],
to: [cx(byId('compose')), topY(byId('compose'))],
start: 0.48,
end: 0.62,
},
{
from: [cx(byId('voice')), botY(byId('voice'))],
to: [cx(byId('compose')), topY(byId('compose'))],
start: 0.5,
end: 0.64,
},
{
from: [cx(byId('compose')), botY(byId('compose'))],
to: outAnchor,
start: 0.8,
end: 0.92,
},
];
const renderFill = interpolate(t, [0.62, 0.82], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// Output tile pop
const outPop = spring({
frame: frame - durationInFrames * 0.82,
fps,
config: { damping: 12, stiffness: 130 },
durationInFrames: 22,
});
// Prompt typing
const promptIn = spring({
frame: frame - 2,
fps,
config: { damping: 20, stiffness: 120 },
durationInFrames: 16,
});
const typed = Math.round(
interpolate(t, [0.03, 0.16], [0, promptText.length], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
})
);
const shownPrompt = promptText.slice(0, typed);
const caretOn = Math.floor(frame / 8) % 2 === 0;
return (
<AbsoluteFill
style={{ backgroundColor, opacity: exit, fontFamily: GROTESK }}
>
<svg
viewBox='0 0 1080 1920'
width='100%'
height='100%'
preserveAspectRatio='xMidYMid meet'
>
<defs>
<radialGradient id='wfGlow' cx='50%' cy='42%' r='60%'>
<stop offset='0%' stopColor={accent} stopOpacity='0.16' />
<stop offset='60%' stopColor={accent} stopOpacity='0.04' />
<stop offset='100%' stopColor={accent} stopOpacity='0' />
</radialGradient>
<linearGradient id='wfEdge' x1='0' y1='0' x2='0' y2='1'>
<stop offset='0%' stopColor={accentSoft} />
<stop offset='100%' stopColor={accent} />
</linearGradient>
<filter id='wfSoft' x='-30%' y='-30%' width='160%' height='160%'>
<feGaussianBlur stdDeviation='6' />
</filter>
</defs>
{/* ambient glow */}
<rect x='0' y='0' width='1080' height='1920' fill='url(#wfGlow)' />
{/* dotted-grid canvas */}
<DotGrid color={textColor} />
{/* edges (under nodes) */}
{edges.map((e, i) => (
<EdgePath key={i} edge={e} t={t} accent={accent} />
))}
{/* prompt bar */}
<g opacity={promptIn}>
<rect
x={PROMPT.x}
y={PROMPT.y}
width={PROMPT.w}
height={PROMPT.h}
rx={22}
fill={nodeColor}
stroke={accent}
strokeOpacity={0.5}
strokeWidth={2}
/>
<circle
cx={PROMPT.x + 42}
cy={PROMPT.y + PROMPT.h / 2}
r={10}
fill={accent}
/>
<text
x={PROMPT.x + 78}
y={PROMPT.y + PROMPT.h / 2 + 12}
fill={textColor}
fontSize={38}
fontFamily={MONO}
>
{shownPrompt}
{caretOn && typed < promptText.length ? (
<tspan fill={accent}>▍</tspan>
) : null}
</text>
</g>
{/* nodes */}
{NODES.map((n) => (
<Node
key={n.id}
n={n}
frame={frame}
fps={fps}
durationInFrames={durationInFrames}
accent={accent}
nodeColor={nodeColor}
textColor={textColor}
renderFill={n.id === 'compose' ? renderFill : -1}
/>
))}
{/* output tile */}
<g
transform={`translate(${OUTPUT.x + OUTPUT.w / 2} ${
OUTPUT.y + OUTPUT.h / 2
}) scale(${outPop}) translate(${-(OUTPUT.x + OUTPUT.w / 2)} ${-(
OUTPUT.y +
OUTPUT.h / 2
)})`}
opacity={Math.min(1, outPop)}
>
<rect
x={OUTPUT.x - 8}
y={OUTPUT.y - 8}
width={OUTPUT.w + 16}
height={OUTPUT.h + 16}
rx={26}
fill={accent}
opacity={0.18}
filter='url(#wfSoft)'
/>
{/* output thumbnail: SVG-native <image> (Remotion <Img> is an HTML
<img>, invalid inside <svg> — it calls .decode() on an SVG node
and crashes). href resolves at render; empty = clean play tile. */}
<clipPath id='outClip'>
<rect
x={OUTPUT.x}
y={OUTPUT.y}
width={OUTPUT.w}
height={OUTPUT.h}
rx={20}
/>
</clipPath>
<rect
x={OUTPUT.x}
y={OUTPUT.y}
width={OUTPUT.w}
height={OUTPUT.h}
rx={20}
fill={nodeColor}
stroke={accent}
strokeWidth={2.5}
/>
{outputImage ? (
<image
href={outputImage}
clipPath='url(#outClip)'
x={OUTPUT.x}
y={OUTPUT.y}
width={OUTPUT.w}
height={OUTPUT.h}
preserveAspectRatio='xMidYMid slice'
/>
) : null}
{/* play triangle */}
<g opacity={0.95}>
<circle
cx={OUTPUT.x + OUTPUT.w / 2}
cy={OUTPUT.y + OUTPUT.h / 2}
r={42}
fill={accent}
/>
<path
d={`M ${OUTPUT.x + OUTPUT.w / 2 - 13} ${
OUTPUT.y + OUTPUT.h / 2 - 20
} L ${OUTPUT.x + OUTPUT.w / 2 + 22} ${
OUTPUT.y + OUTPUT.h / 2
} L ${OUTPUT.x + OUTPUT.w / 2 - 13} ${
OUTPUT.y + OUTPUT.h / 2 + 20
} Z`}
fill='#0A0A12'
/>
</g>
</g>
</svg>
</AbsoluteFill>
);
}
function DotGrid({ color }: { color: string }) {
const dots = [];
for (let gx = 60; gx < 1080; gx += 84) {
for (let gy = 180; gy < 1900; gy += 84) {
dots.push(
<circle
key={`${gx}-${gy}`}
cx={gx}
cy={gy}
r={2.2}
fill={color}
opacity={0.06}
/>
);
}
}
return <g>{dots}</g>;
}
function bezier(from: [number, number], to: [number, number]) {
const [x1, y1] = from;
const [x2, y2] = to;
const dy = (y2 - y1) * 0.5;
return `M ${x1} ${y1} C ${x1} ${y1 + dy}, ${x2} ${y2 - dy}, ${x2} ${y2}`;
}
function EdgePath({
edge,
t,
accent,
}: {
edge: Edge;
t: number;
accent: string;
}) {
const draw = interpolate(t, [edge.start, edge.end], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
if (draw <= 0) return null;
const d = bezier(edge.from, edge.to);
// travelling pulse once the edge is mostly drawn
const pulseT = interpolate(t, [edge.end - 0.02, edge.end + 0.18], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
<g>
<path
d={d}
fill='none'
stroke='url(#wfEdge)'
strokeWidth={4}
strokeLinecap='round'
pathLength={100}
strokeDasharray={100}
strokeDashoffset={100 * (1 - draw)}
opacity={0.85}
/>
{draw >= 1 && pulseT < 1 ? (
<path
d={d}
fill='none'
stroke={accent}
strokeWidth={7}
strokeLinecap='round'
pathLength={100}
strokeDasharray='5 95'
strokeDashoffset={100 * (1 - pulseT)}
/>
) : null}
</g>
);
}
function Node({
n,
frame,
fps,
durationInFrames,
accent,
nodeColor,
textColor,
renderFill,
}: {
n: NodeDef;
frame: number;
fps: number;
durationInFrames: number;
accent: string;
nodeColor: string;
textColor: string;
renderFill: number;
}) {
const pop = spring({
frame: frame - durationInFrames * n.appear,
fps,
config: { damping: 13, stiffness: 130 },
durationInFrames: 20,
});
if (pop <= 0.001) return null;
const ccx = n.x + n.w / 2;
const ccy = n.y + n.h / 2;
return (
<g
transform={`translate(${ccx} ${ccy}) scale(${pop}) translate(${-ccx} ${-ccy})`}
opacity={Math.min(1, pop)}
>
{/* glow */}
<rect
x={n.x - 4}
y={n.y - 4}
width={n.w + 8}
height={n.h + 8}
rx={22}
fill={accent}
opacity={0.1}
filter='url(#wfSoft)'
/>
<rect
x={n.x}
y={n.y}
width={n.w}
height={n.h}
rx={18}
fill={nodeColor}
stroke={accent}
strokeOpacity={0.55}
strokeWidth={2}
/>
{/* port dots */}
<circle cx={n.x + n.w / 2} cy={n.y} r={7} fill={accent} />
<circle cx={n.x + n.w / 2} cy={n.y + n.h} r={7} fill={accent} />
{/* glyph chip */}
<rect
x={n.x + 22}
y={n.y + 26}
width={56}
height={56}
rx={14}
fill={accent}
opacity={0.16}
/>
<text
x={n.x + 50}
y={n.y + 62}
fill={accent}
fontSize={34}
textAnchor='middle'
fontFamily={GROTESK}
>
{n.glyph}
</text>
{/* title + sub */}
<text
x={n.x + 96}
y={n.y + 56}
fill={textColor}
fontSize={38}
fontWeight={700}
fontFamily={GROTESK}
>
{n.title}
</text>
<text
x={n.x + 96}
y={n.y + 96}
fill={textColor}
opacity={0.5}
fontSize={26}
fontFamily={MONO}
>
{n.sub}
</text>
{/* render bar on compose */}
{renderFill >= 0 ? (
<g>
<rect
x={n.x + 24}
y={n.y + n.h - 38}
width={n.w - 48}
height={12}
rx={6}
fill={textColor}
opacity={0.12}
/>
<rect
x={n.x + 24}
y={n.y + n.h - 38}
width={(n.w - 48) * renderFill}
height={12}
rx={6}
fill={accent}
/>
</g>
) : null}
</g>
);
}
Schema (Zod)
import { z } from 'zod';
export const Schema = z.object({
// PORTS — wired from upstream nodes
promptText: z
.string()
.default('make a promo reel for my product')
.describe('$port:text The chat prompt typed into the canvas'),
outputImage: z
.string()
.default('')
.describe('$port:image Optional thumbnail shown in the output tile'),
// STYLE
accent: z.string().default('#8B7CFF').describe('$style:color Accent color'),
accentSoft: z
.string()
.default('#5B4FCF')
.describe('$style:color Secondary accent (edge gradient)'),
backgroundColor: z
.string()
.default('#0A0A12')
.describe('$style:color Background color'),
nodeColor: z
.string()
.default('#14141F')
.describe('$style:color Node fill color'),
textColor: z.string().default('#F4F4FA').describe('$style:color Text color'),
});
Wireflow Canvas
wireflow_canvasThe self-demo hero — a miniature Wireflow graph builds itself: a chat prompt types in, nodes spring onto a dotted canvas, edges draw port-to-port with a travelling data pulse, the Compose node renders, and the output tile pops. Use it as the opening "this is what the product does" beat.
Workflow Inputs (2)
- The chat prompt typed into the canvas
promptTextTEXT - Optional thumbnail shown in the output tile
outputImageIMAGE
Config Fields (5)
accentnodeColortextColoraccentSoftbackgroundColor
Meta
- Updated
- 7/10/2026