Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions plugins/droid-control/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,22 @@ The compose stage uses the Remotion project in `remotion/` as a single video eng

This keeps droids out of the common failure modes: stale files in `public/`, mismatched `clipDuration`, wrong `agg` theme, invalid pixel formats, and hand-written Remotion commands with missing encode flags.

### Composition surface

The `Showcase` composition in `remotion/src/compositions/Showcase.tsx` is the only video entry point. Everything else lives in `remotion/src/components/` and is composed by props:

| Layer | Purpose | Controlled by |
|---|---|---|
| Background + FloatingParticles | Preset-driven warmth or coolness | `preset` |
| TitleCard / FanningRotorOutro | Opening and closing cards | `title`, `subtitle`, `speedNote` |
| Window chrome + layouts | `SingleLayout` or `SideBySideLayout` | `layout`, `labels`, `objectFit` |
| ZoomEffect / SpotlightOverlay / KeystrokeOverlay / SectionHeader | Timed in-scene overlays | `effects`, `keys`, `sections` |
| CodeAnnotationOverlay | Timed syntax-highlighted code cards | `codeAnnotations` |
| Transition presentation | Title→content and content→outro crossfade | `transitionStyle` (default `motion-blur`) |
| NoiseOverlay + ColorGradeOverlay + Watermark | Topmost polish pass | `fidelity`, `preset` |

The key property is that the main composition is data-driven: the droid never writes Remotion JSX. Adding a new overlay or transition style is a new component plus a schema field, not a new composition.

## Platform isolation

Platform-specific mechanics live below the atom that needs them:
Expand Down
5 changes: 5 additions & 0 deletions plugins/droid-control/NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This plugin depends on several third-party tools and libraries. They are not bun
- **[Remotion](https://www.remotion.dev/)** -- React-based video renderer used by the compose/showcase pipeline. Remotion is free for individuals, small teams (<=3 employees), and non-profits. Larger companies require a [company license](https://www.remotion.pro/). See the [full license terms](https://github.com/remotion-dev/remotion/blob/main/LICENSE.md).
- **[React](https://react.dev/)** -- MIT License
- **[Zod](https://zod.dev/)** -- MIT License
- **[prism-react-renderer](https://github.com/FormidableLabs/prism-react-renderer)** -- MIT License. Powers the syntax highlighting in the `CodeAnnotationOverlay` component.

## Terminal automation

Expand All @@ -23,3 +24,7 @@ This plugin depends on several third-party tools and libraries. They are not bun
- **[ffmpeg](https://ffmpeg.org/)** -- multimedia framework (LGPL-2.1+ / GPL-2.0+, depending on build configuration)
- **[cage](https://github.com/cage-kiosk/cage)** -- Wayland kiosk compositor (MIT)
- **[wtype](https://github.com/atx/wtype)** -- Wayland keystroke injection (MIT)

## Design influences

- **[@hyperframes/shader-transitions](https://github.com/heygen-com/hyperframes/tree/main/packages/shader-transitions)** (Apache-2.0) -- the `transitionStyle` prop's naming and taxonomy (`whip-pan`, `light-leak`, `flash`, `glitch-lite`) was shaped by Hyperframes' shader-transitions catalog. Implementations in `ShowcaseTransition.tsx` are original Remotion-native CSS/SVG overlays, not GLSL ports.
2 changes: 1 addition & 1 deletion plugins/droid-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ For the full rationale and runtime pipeline, see [`ARCHITECTURE.md`](ARCHITECTUR

## Video rendering

The compose stage uses [Remotion](https://www.remotion.dev/) for video compositing. Presets provide window chrome, spacing, palettes, backgrounds, particles, noise, color grading, motion-blur transitions, zooms, spotlights, keystroke overlays, and section headers.
The compose stage uses [Remotion](https://www.remotion.dev/) for video compositing. Presets provide window chrome, spacing, palettes, backgrounds, particles, noise, color grading, configurable transitions (`motion-blur`, `flash`, `whip-pan`, `light-leak`, `glitch-lite`), zooms, spotlights, keystroke overlays, section headers, and syntax-highlighted code annotations.

The `render-showcase.sh` helper owns the full pipeline: `.cast` conversion via `agg`, clip staging, duration detection, Remotion rendering, and cleanup.

Expand Down
29 changes: 29 additions & 0 deletions plugins/droid-control/remotion/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions plugins/droid-control/remotion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@remotion/media": "4.0.445",
"@remotion/tailwind": "4.0.445",
"@remotion/transitions": "4.0.445",
"prism-react-renderer": "^2.4.1",
"react": "^19",
"react-dom": "^19",
"remotion": "4.0.445",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import React, { useMemo } from 'react';
import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion';
import { Highlight, themes, type PrismTheme } from 'prism-react-renderer';
import type { CodeAnnotation, CodeRange } from '../lib/schema';
import type { Palette } from '../lib/palettes';
import type { PresetConfig } from '../lib/presets';

const MONO =
"'Geist Mono', 'Berkeley Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";

const inRange = (line: number, ranges: CodeRange[]): boolean =>
ranges.some((r) => line >= r.start && line <= r.end);

const buildPrismTheme = (palette: Palette): PrismTheme => {
const base = themes.vsDark;
return {
...base,
plain: {
...base.plain,
color: palette.text,
backgroundColor: 'transparent',
},
};
};

const positionToAnchor = (
position: CodeAnnotation['position'],
margin: number,
): React.CSSProperties => {
const edge = Math.max(40, margin + 24);
switch (position) {
case 'center':
return {
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
};
case 'bottom-left':
return { left: edge, bottom: edge };
case 'top-right':
default:
return { right: edge, top: edge };
}
};

const CodeCard: React.FC<{
annotation: CodeAnnotation;
palette: Palette;
config: PresetConfig;
enterFrame: number;
exitFrame: number;
}> = ({ annotation, palette, config, enterFrame, exitFrame }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const lines = useMemo(
() => annotation.code.replace(/\n$/, '').split('\n'),
[annotation.code],
);
const prismTheme = useMemo(() => buildPrismTheme(palette), [palette]);

if (frame < enterFrame || frame >= exitFrame) return null;

const localFrame = frame - enterFrame;
const totalFrames = exitFrame - enterFrame;

const enterProgress = interpolate(localFrame, [0, 0.35 * fps], [0, 1], {
easing: Easing.bezier(0.16, 1, 0.3, 1),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});

const exitStart = Math.max(0, totalFrames - 0.35 * fps);
const exitProgress = interpolate(
localFrame,
[exitStart, totalFrames],
[0, 1],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' },
);

const opacity = enterProgress * (1 - exitProgress);
const scale = interpolate(enterProgress, [0, 1], [0.96, 1]);
const blur = interpolate(enterProgress, [0, 1], [6, 0]);

const highlightRanges = annotation.highlight ?? [];
const focusRanges = annotation.focus ?? [];
const hasFocus = focusRanges.length > 0;
const anchor = positionToAnchor(
annotation.position ?? 'top-right',
config.margin,
);
const maxLine = lines.length;
const gutterWidth = `${String(maxLine).length + 1}ch`;

return (
<div
style={{
position: 'absolute',
...anchor,
opacity,
transform: `${anchor.transform ?? ''} scale(${scale})`.trim(),
filter: `blur(${blur}px)`,
zIndex: 90,
maxWidth: '42%',
minWidth: 320,
backgroundColor: `${palette.surface}F2`,
border: `1px solid ${palette.border}`,
borderBottom: `2px solid ${palette.accent}AA`,
borderRadius: 12,
padding: 16,
boxShadow: `0 18px 48px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.04)`,
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
fontFamily: MONO,
fontSize: 14,
lineHeight: 1.5,
color: palette.text,
}}
>
{annotation.title && (
<div
style={{
color: palette.muted,
fontSize: 11,
textTransform: 'uppercase',
letterSpacing: '0.1em',
marginBottom: 10,
fontFamily: "'Geist', system-ui, sans-serif",
fontWeight: 500,
}}
>
{annotation.title}
</div>
)}
<Highlight
code={annotation.code.replace(/\n$/, '')}
language={annotation.language ?? 'tsx'}
theme={prismTheme}
>
{({ tokens, getLineProps, getTokenProps }) => (
<pre
style={{
margin: 0,
padding: 0,
backgroundColor: 'transparent',
fontFamily: MONO,
overflow: 'hidden',
}}
>
{tokens.map((line, i) => {
const lineNumber = i + 1;
const isHighlighted = inRange(lineNumber, highlightRanges);
const isFocused = !hasFocus || inRange(lineNumber, focusRanges);
const lineOpacity = isFocused ? 1 : 0.3;
const lineFilter = isFocused ? 'none' : 'blur(1.2px)';
const bg = isHighlighted ? `${palette.accent}22` : 'transparent';
const lineBorder = isHighlighted
? `2px solid ${palette.accent}`
: `2px solid transparent`;
const { key: _lineKey, ...lineRest } = getLineProps({
line,
key: i,
});

return (
<div
key={`line-${i}`}
{...lineRest}
style={{
display: 'flex',
opacity: lineOpacity,
filter: lineFilter,
backgroundColor: bg,
borderLeft: lineBorder,
paddingLeft: 8,
marginLeft: -8,
marginRight: -8,
paddingRight: 8,
}}
>
<span
style={{
width: gutterWidth,
flex: '0 0 auto',
color: palette.muted,
userSelect: 'none',
textAlign: 'right',
paddingRight: 12,
}}
>
{lineNumber}
</span>
<span style={{ flex: 1 }}>
{line.map((token, tokenIndex) => {
const { key: _tokenKey, ...tokenRest } = getTokenProps({
token,
key: tokenIndex,
});
return (
<span
key={`tok-${i}-${tokenIndex}`}
{...tokenRest}
/>
);
})}
</span>
</div>
);
})}
</pre>
)}
</Highlight>
</div>
);
};

export const CodeAnnotationOverlay: React.FC<{
annotations: CodeAnnotation[];
palette: Palette;
config: PresetConfig;
}> = ({ annotations, palette, config }) => {
const { fps } = useVideoConfig();

if (!annotations.length) return null;

return (
<>
{annotations.map((annotation, i) => {
const enterFrame = Math.round(annotation.t * fps);
const exitFrame = Math.round((annotation.t + annotation.dur) * fps);
return (
<CodeCard
key={`code-${i}-${annotation.t}`}
annotation={annotation}
palette={palette}
config={config}
enterFrame={enterFrame}
exitFrame={exitFrame}
/>
);
})}
</>
);
};
Loading
Loading