From 4530ed98c5a25186ef0f48f89a31f7f57cb6a29a Mon Sep 17 00:00:00 2001
From: "factory-droid[bot]"
<138933559+factory-droid[bot]@users.noreply.github.com>
Date: Mon, 20 Apr 2026 04:27:02 +0000
Subject: [PATCH 1/3] feat(droid-control): code annotations + transition styles
Add an opt-in `codeAnnotations` layer and a `transitionStyle` resolver to
the Remotion Showcase composition. Defaults are unchanged so existing props
render identically; the new behavior only activates when those props are set.
- Schema: `codeAnnotationSchema` (t, dur, code, language, title, highlight,
focus, position) and `transitionStyleSchema` (motion-blur, flash, whip-pan,
light-leak, glitch-lite).
- CodeAnnotationOverlay: syntax-highlighted code cards via prism-react-renderer
with palette-aware theming, line highlights, focus dimming, and enter/exit
animation. Anchored top-right by default.
- ShowcaseTransition: five Remotion-native transition presentations wired via
`getTransitionPresentation(style, palette)`. Motion-blur preserves the
existing look; the others are palette-aware (warm on Factory presets).
- Docs: compose/SKILL.md gains authoring guidance and a full props reference
for both features; showcase/SKILL.md adds preset-level style guidance.
README and ARCHITECTURE.md updated to reflect the new composition surface.
Validated: `tsc --noEmit` clean; three fixture renders (default, whip-pan +
code annotation, flash) produce 9.5s 1920x1080 H.264 mp4s with expected
content.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
---
plugins/droid-control/ARCHITECTURE.md | 16 +
plugins/droid-control/README.md | 2 +-
.../droid-control/remotion/package-lock.json | 29 ++
plugins/droid-control/remotion/package.json | 1 +
.../src/components/CodeAnnotationOverlay.tsx | 243 +++++++++++++
.../src/components/MotionBlurTransition.tsx | 75 ----
.../src/components/ShowcaseTransition.tsx | 329 ++++++++++++++++++
.../remotion/src/compositions/Showcase.tsx | 34 +-
.../droid-control/remotion/src/lib/schema.ts | 29 ++
plugins/droid-control/skills/compose/SKILL.md | 31 ++
.../droid-control/skills/showcase/SKILL.md | 14 +-
11 files changed, 722 insertions(+), 81 deletions(-)
create mode 100644 plugins/droid-control/remotion/src/components/CodeAnnotationOverlay.tsx
delete mode 100644 plugins/droid-control/remotion/src/components/MotionBlurTransition.tsx
create mode 100644 plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx
diff --git a/plugins/droid-control/ARCHITECTURE.md b/plugins/droid-control/ARCHITECTURE.md
index 3452ea2..ddc85e3 100644
--- a/plugins/droid-control/ARCHITECTURE.md
+++ b/plugins/droid-control/ARCHITECTURE.md
@@ -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:
diff --git a/plugins/droid-control/README.md b/plugins/droid-control/README.md
index e9ed556..c33cbb9 100644
--- a/plugins/droid-control/README.md
+++ b/plugins/droid-control/README.md
@@ -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.
diff --git a/plugins/droid-control/remotion/package-lock.json b/plugins/droid-control/remotion/package-lock.json
index 1d15e15..acb1704 100644
--- a/plugins/droid-control/remotion/package-lock.json
+++ b/plugins/droid-control/remotion/package-lock.json
@@ -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",
@@ -2151,6 +2152,12 @@
"undici-types": "~7.18.0"
}
},
+ "node_modules/@types/prismjs": {
+ "version": "1.26.6",
+ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
+ "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -2654,6 +2661,15 @@
"node": ">=6.0"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -4684,6 +4700,19 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/prism-react-renderer": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz",
+ "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prismjs": "^1.26.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.0.0"
+ }
+ },
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
diff --git a/plugins/droid-control/remotion/package.json b/plugins/droid-control/remotion/package.json
index a9d1d39..30732ec 100644
--- a/plugins/droid-control/remotion/package.json
+++ b/plugins/droid-control/remotion/package.json
@@ -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",
diff --git a/plugins/droid-control/remotion/src/components/CodeAnnotationOverlay.tsx b/plugins/droid-control/remotion/src/components/CodeAnnotationOverlay.tsx
new file mode 100644
index 0000000..327a2a0
--- /dev/null
+++ b/plugins/droid-control/remotion/src/components/CodeAnnotationOverlay.tsx
@@ -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 (
+
+ {annotation.title && (
+
+ {annotation.title}
+
+ )}
+
+ {({ tokens, getLineProps, getTokenProps }) => (
+
+ {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 (
+
+
+ {lineNumber}
+
+
+ {line.map((token, tokenIndex) => {
+ const { key: _tokenKey, ...tokenRest } = getTokenProps({
+ token,
+ key: tokenIndex,
+ });
+ return (
+
+ );
+ })}
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+};
+
+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 (
+
+ );
+ })}
+ >
+ );
+};
diff --git a/plugins/droid-control/remotion/src/components/MotionBlurTransition.tsx b/plugins/droid-control/remotion/src/components/MotionBlurTransition.tsx
deleted file mode 100644
index 8b89b22..0000000
--- a/plugins/droid-control/remotion/src/components/MotionBlurTransition.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import React from 'react';
-import { AbsoluteFill, interpolate } from 'remotion';
-import type {
- TransitionPresentation,
- TransitionPresentationComponentProps,
-} from '@remotion/transitions';
-
-export type MotionBlurProps = {
- /** Peak blur radius in pixels (default 6). */
- maxBlur?: number;
- /** Initial scale of the entering scene (default 1.03). */
- enterScale?: number;
-};
-
-/**
- * Custom presentation component for @remotion/transitions.
- *
- * Combines three effects during the crossfade:
- * (a) CSS filter: blur() — 6 px at the edges, 0 px at the center
- * (b) scale 1.03 → 1.0 on the entering scene (camera-dolly feel)
- * (c) opacity crossfade
- */
-const MotionBlurPresentation: React.FC<
- TransitionPresentationComponentProps
-> = ({
- children,
- presentationDirection,
- presentationProgress,
- passedProps,
-}) => {
- const maxBlur = passedProps.maxBlur ?? 6;
- const enterScale = passedProps.enterScale ?? 1.03;
- const isEntering = presentationDirection === 'entering';
-
- let style: React.CSSProperties;
- if (isEntering) {
- const blur = interpolate(presentationProgress, [0, 1], [maxBlur, 0], {
- extrapolateLeft: 'clamp',
- extrapolateRight: 'clamp',
- });
- const scale = interpolate(presentationProgress, [0, 1], [enterScale, 1], {
- extrapolateLeft: 'clamp',
- extrapolateRight: 'clamp',
- });
- style = {
- opacity: presentationProgress,
- filter: `blur(${blur}px)`,
- transform: `scale(${scale})`,
- };
- } else {
- const blur = interpolate(presentationProgress, [0, 1], [0, maxBlur], {
- extrapolateLeft: 'clamp',
- extrapolateRight: 'clamp',
- });
- style = {
- opacity: 1 - presentationProgress,
- filter: `blur(${blur}px)`,
- };
- }
-
- return {children};
-};
-
-/**
- * Factory function returning a TransitionPresentation compatible with
- * ``.
- */
-export const motionBlurTransition = (
- props?: MotionBlurProps
-): TransitionPresentation => {
- return {
- component: MotionBlurPresentation,
- props: props ?? {},
- };
-};
diff --git a/plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx b/plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx
new file mode 100644
index 0000000..9da4bc7
--- /dev/null
+++ b/plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx
@@ -0,0 +1,329 @@
+import React from 'react';
+import { AbsoluteFill, interpolate, Easing, random } from 'remotion';
+import type {
+ TransitionPresentation,
+ TransitionPresentationComponentProps,
+} from '@remotion/transitions';
+import type { TransitionStyle } from '../lib/schema';
+import type { Palette } from '../lib/palettes';
+
+type BaseProps = {
+ palette: Palette;
+};
+
+const clamp01 = (v: number): number => Math.min(1, Math.max(0, v));
+
+// ---------------------------------------------------------------------------
+// Motion blur: blur() + scale + opacity crossfade. (Existing aesthetic.)
+// ---------------------------------------------------------------------------
+const MotionBlurPresentation: React.FC<
+ TransitionPresentationComponentProps
+> = ({ children, presentationDirection, presentationProgress }) => {
+ const maxBlur = 6;
+ const enterScale = 1.03;
+ const isEntering = presentationDirection === 'entering';
+
+ if (isEntering) {
+ const blur = interpolate(presentationProgress, [0, 1], [maxBlur, 0], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ const scale = interpolate(presentationProgress, [0, 1], [enterScale, 1], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ return (
+
+ {children}
+
+ );
+ }
+
+ const blur = interpolate(presentationProgress, [0, 1], [0, maxBlur], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ return (
+
+ {children}
+
+ );
+};
+
+// ---------------------------------------------------------------------------
+// Flash: hard cut emphasised by a warm/white flash overlay at the midpoint.
+// ---------------------------------------------------------------------------
+const FlashPresentation: React.FC<
+ TransitionPresentationComponentProps
+> = ({ children, presentationDirection, presentationProgress, passedProps }) => {
+ const isEntering = presentationDirection === 'entering';
+ const palette = passedProps.palette;
+ const isWarm = palette.accent === '#EE6018';
+
+ const sceneOpacity = isEntering
+ ? clamp01((presentationProgress - 0.45) / 0.35)
+ : clamp01(1 - (presentationProgress - 0.2) / 0.35);
+
+ // Flash is rendered only on the entering side so the peak is single-source.
+ const flashOpacity = isEntering
+ ? interpolate(
+ presentationProgress,
+ [0, 0.35, 0.55, 0.85, 1],
+ [0, 0.25, 1, 0.2, 0],
+ { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' },
+ )
+ : 0;
+
+ const flashColor = isWarm
+ ? `rgba(255, 220, 180, ${flashOpacity})`
+ : `rgba(235, 240, 255, ${flashOpacity})`;
+
+ return (
+
+ {children}
+ {flashOpacity > 0 && (
+
+ )}
+
+ );
+};
+
+// ---------------------------------------------------------------------------
+// Whip-pan: horizontal pan + motion blur for energetic scene changes.
+// ---------------------------------------------------------------------------
+const WhipPanPresentation: React.FC<
+ TransitionPresentationComponentProps
+> = ({ children, presentationDirection, presentationProgress }) => {
+ const isEntering = presentationDirection === 'entering';
+ const easing = Easing.bezier(0.6, 0, 0.1, 1);
+ const eased = easing(presentationProgress);
+ const maxBlur = 14;
+
+ if (isEntering) {
+ const translateX = interpolate(eased, [0, 1], [30, 0]);
+ const blur = interpolate(presentationProgress, [0, 0.6, 1], [maxBlur, 4, 0], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ const opacity = interpolate(presentationProgress, [0, 0.2, 1], [0, 1, 1], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ return (
+
+ {children}
+
+ );
+ }
+
+ const translateX = interpolate(eased, [0, 1], [0, -30]);
+ const blur = interpolate(presentationProgress, [0, 0.4, 1], [0, 10, maxBlur], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ const opacity = interpolate(presentationProgress, [0, 0.8, 1], [1, 1, 0], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ return (
+
+ {children}
+
+ );
+};
+
+// ---------------------------------------------------------------------------
+// Light leak: warm moving gradient sweeps across while scenes crossfade.
+// ---------------------------------------------------------------------------
+const LightLeakPresentation: React.FC<
+ TransitionPresentationComponentProps
+> = ({ children, presentationDirection, presentationProgress, passedProps }) => {
+ const isEntering = presentationDirection === 'entering';
+ const palette = passedProps.palette;
+ const isWarm = palette.accent === '#EE6018';
+
+ const sceneOpacity = isEntering
+ ? interpolate(presentationProgress, [0, 0.5, 1], [0, 0.4, 1], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ })
+ : interpolate(presentationProgress, [0, 0.5, 1], [1, 0.6, 0], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+
+ const sweepOpacity = isEntering
+ ? interpolate(
+ presentationProgress,
+ [0, 0.3, 0.6, 1],
+ [0, 1, 0.6, 0],
+ { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' },
+ )
+ : 0;
+
+ const sweepPosition = interpolate(presentationProgress, [0, 1], [-40, 140]);
+
+ const leakColor = isWarm ? '238, 96, 24' : '137, 180, 250';
+
+ return (
+
+ {children}
+ {sweepOpacity > 0 && (
+
+ )}
+
+ );
+};
+
+// ---------------------------------------------------------------------------
+// Glitch-lite: brief RGB offset + clipped horizontal bands near midpoint.
+// ---------------------------------------------------------------------------
+const GlitchLitePresentation: React.FC<
+ TransitionPresentationComponentProps
+> = ({ children, presentationDirection, presentationProgress }) => {
+ const isEntering = presentationDirection === 'entering';
+
+ const sceneOpacity = isEntering
+ ? interpolate(presentationProgress, [0, 0.4, 1], [0, 1, 1], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ })
+ : interpolate(presentationProgress, [0, 0.6, 1], [1, 1, 0], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+
+ const glitchIntensity = interpolate(
+ presentationProgress,
+ [0, 0.35, 0.55, 0.75, 1],
+ [0, 1, 1, 0.3, 0],
+ { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' },
+ );
+
+ const seedBase = isEntering ? 1000 : 2000;
+ const jitterX = (random(`glitch-x-${Math.round(presentationProgress * 60) + seedBase}`) - 0.5) * 16 * glitchIntensity;
+ const rgbSplit = 6 * glitchIntensity;
+
+ const bandTop = 20 + 40 * random(`glitch-band-top-${Math.round(presentationProgress * 30) + seedBase}`);
+ const bandHeight = 6 + 14 * random(`glitch-band-h-${Math.round(presentationProgress * 30) + seedBase}`);
+ const showBand = glitchIntensity > 0.6;
+
+ return (
+
+ {/* Red ghost */}
+ 0 ? 0.45 : 0,
+ }}
+ >
+
+ {children}
+
+
+ {/* Blue ghost */}
+ 0 ? 0.45 : 0,
+ }}
+ >
+
+ {children}
+
+
+ {/* Main scene */}
+
+ {children}
+
+ {/* Displaced band */}
+ {showBand && (
+
+ {children}
+
+ )}
+
+ );
+};
+
+const presentationMap: Record<
+ TransitionStyle,
+ React.FC>
+> = {
+ 'motion-blur': MotionBlurPresentation,
+ flash: FlashPresentation,
+ 'whip-pan': WhipPanPresentation,
+ 'light-leak': LightLeakPresentation,
+ 'glitch-lite': GlitchLitePresentation,
+};
+
+export const getTransitionPresentation = (
+ style: TransitionStyle,
+ palette: Palette,
+): TransitionPresentation => {
+ const component = presentationMap[style];
+ return {
+ component,
+ props: { palette },
+ };
+};
diff --git a/plugins/droid-control/remotion/src/compositions/Showcase.tsx b/plugins/droid-control/remotion/src/compositions/Showcase.tsx
index a142759..ec88b08 100644
--- a/plugins/droid-control/remotion/src/compositions/Showcase.tsx
+++ b/plugins/droid-control/remotion/src/compositions/Showcase.tsx
@@ -3,7 +3,7 @@ import { z } from 'zod';
import { AbsoluteFill, staticFile, useVideoConfig } from 'remotion';
import { Video } from '@remotion/media';
import { TransitionSeries, linearTiming } from '@remotion/transitions';
-import { motionBlurTransition } from '../components/MotionBlurTransition';
+import { getTransitionPresentation } from '../components/ShowcaseTransition';
import {
fidelitySchema,
presetSchema,
@@ -11,6 +11,8 @@ import {
keystrokeSchema,
effectSchema,
sectionSchema,
+ codeAnnotationSchema,
+ transitionStyleSchema,
} from '../lib/schema';
import { getPalette } from '../lib/palettes';
import { getPresetConfig } from '../lib/presets';
@@ -28,6 +30,7 @@ import { Watermark } from '../components/Watermark';
import { ZoomEffect } from '../components/ZoomEffect';
import { SectionTransitionOverlay } from '../components/SectionTransition';
import { FanningRotorOutro } from '../components/FanningRotorOutro';
+import { CodeAnnotationOverlay } from '../components/CodeAnnotationOverlay';
export const showcaseSchema = z.object({
clips: z.array(z.string()),
@@ -53,11 +56,17 @@ export const showcaseSchema = z.object({
// and you'd rather crop edges than see giant black bars.
// - "fill": stretch to fill the panel (distorts aspect). Rarely what you want.
objectFit: z.enum(['contain', 'cover', 'fill']).optional(),
+ // Timed syntax-highlighted code overlays rendered during the main content
+ // sequence (above content, below noise/color grade). Timings are relative
+ // to the start of the main content clip.
+ codeAnnotations: z.array(codeAnnotationSchema).optional(),
+ // Presentation used for title->content and content->outro transitions.
+ // Defaults to 'motion-blur', which preserves existing aesthetic.
+ transitionStyle: transitionStyleSchema.optional(),
});
const TITLE_DURATION_S = 4;
const TRANSITION_FRAMES = 15;
-const MOTION_BLUR = motionBlurTransition();
const resolveFidelity = (
props: z.infer
@@ -177,6 +186,14 @@ export const ShowcaseComposition: React.FC> = (
const titleFrames = TITLE_DURATION_S * fps;
const clipFrames = Math.ceil((props.clipDuration ?? 60) * fps);
const objectFit = props.objectFit ?? 'contain';
+ const transition = useMemo(
+ () =>
+ getTransitionPresentation(
+ props.transitionStyle ?? 'motion-blur',
+ palette
+ ),
+ [props.transitionStyle, palette]
+ );
const spotlights = useMemo(
() =>
@@ -212,7 +229,7 @@ export const ShowcaseComposition: React.FC> = (
{/* Crossfade from title to content */}
@@ -294,12 +311,21 @@ export const ShowcaseComposition: React.FC> = (
config={config}
/>
)}
+
+ {/* Code annotations: timed syntax-highlighted overlays */}
+ {props.codeAnnotations && props.codeAnnotations.length > 0 && (
+
+ )}
{/* Crossfade to outro */}
diff --git a/plugins/droid-control/remotion/src/lib/schema.ts b/plugins/droid-control/remotion/src/lib/schema.ts
index 3301038..9115919 100644
--- a/plugins/droid-control/remotion/src/lib/schema.ts
+++ b/plugins/droid-control/remotion/src/lib/schema.ts
@@ -73,3 +73,32 @@ export const effectSchema = z.discriminatedUnion('fx', [
}),
]);
export type Effect = z.infer;
+
+export const codeRangeSchema = z.object({
+ start: z.number().int().positive(),
+ end: z.number().int().positive(),
+});
+export type CodeRange = z.infer;
+
+export const codeAnnotationSchema = z.object({
+ t: z.number(),
+ dur: z.number(),
+ code: z.string(),
+ language: z.string().default('tsx'),
+ title: z.string().optional(),
+ highlight: z.array(codeRangeSchema).default([]),
+ focus: z.array(codeRangeSchema).default([]),
+ position: z
+ .enum(['center', 'top-right', 'bottom-left'])
+ .default('top-right'),
+});
+export type CodeAnnotation = z.infer;
+
+export const transitionStyleSchema = z.enum([
+ 'motion-blur',
+ 'flash',
+ 'whip-pan',
+ 'light-leak',
+ 'glitch-lite',
+]);
+export type TransitionStyle = z.infer;
diff --git a/plugins/droid-control/skills/compose/SKILL.md b/plugins/droid-control/skills/compose/SKILL.md
index 9187d11..eb58743 100644
--- a/plugins/droid-control/skills/compose/SKILL.md
+++ b/plugins/droid-control/skills/compose/SKILL.md
@@ -203,6 +203,8 @@ Use a run-scoped props path like `$PROPS`; do not reuse a global `/tmp/showcase-
| `width` | `number` | no | Output width (default: 2560 for inspect, else 1920) |
| `height` | `number` | no | Output height (default: 1440 for inspect, else 1080) |
| `objectFit` | `"contain" \| "cover" \| "fill"` | no | How each clip fits its panel. Default `"contain"` (letterbox to preserve aspect). Use `"cover"` when clip aspect doesn't match panel aspect and you'd rather crop than see black bars. See "Clip aspect ratio" below. |
+| `codeAnnotations` | `CodeAnnotation[]` | no | Timed syntax-highlighted code overlays shown during the main content sequence. See "Code annotations" below. |
+| `transitionStyle` | `"motion-blur" \| "flash" \| "whip-pan" \| "light-leak" \| "glitch-lite"` | no | Presentation used for title→content and content→outro transitions. Default `"motion-blur"` preserves existing aesthetic. See "Transition styles" below. |
### Preset quick reference
@@ -257,6 +259,35 @@ Regions use percentage strings (e.g., `"25%"`) relative to the video dimensions.
**Less is more.** One well-timed spotlight has more impact than five overlapping effects.
+### Code annotations
+
+Timed syntax-highlighted code card laid over the captured video. Use for PR demos where the decisive source change needs to sit next to the runtime proof.
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `t` | `number` | yes | Start time in seconds, relative to clip start. Adjust for `speed` factor (same rule as `keys[].t`). |
+| `dur` | `number` | yes | How long the card stays visible, in seconds. |
+| `code` | `string` | yes | Source text; `\n` for multiline; no trailing newline. |
+| `language` | `string` | no | Prism language id (`tsx`, `ts`, `py`, `rust`, `bash`, ...). Default `tsx`. |
+| `title` | `string` | no | Small caption above the code (usually a file path). |
+| `highlight` | `[{start,end}]` | no | 1-based inclusive line ranges with accent background + left border. |
+| `focus` | `[{start,end}]` | no | 1-based inclusive line ranges kept at full opacity; others are dimmed/blurred. |
+| `position` | `"top-right" \| "center" \| "bottom-left"` | no | Default `"top-right"`. Move to `"bottom-left"` if the captured top-right is load-bearing. |
+
+Keep it short — aim for ≤ 15 lines per card, hold for 3–6 seconds.
+
+### Transition styles
+
+`transitionStyle` selects the title→content and content→outro crossfade presentation. Both transitions in one render share the same style. `flash` and `light-leak` derive their tint from the preset palette. Default `motion-blur` is always safe; preset-tier guidance lives in `showcase/SKILL.md`.
+
+| Style | Feel | Use when… |
+|---|---|---|
+| `motion-blur` | Subtle dolly, blur + opacity crossfade | Default for PR demos, Factory content, most showcase work |
+| `flash` | Quick palette-tinted flash at midpoint | Bug-fix proofs where the "after" state should feel sudden |
+| `whip-pan` | Horizontal pan + motion blur | Energetic showcase / marketing when pacing is fast |
+| `light-leak` | Warm gradient sweep | Factory-branded landing/social clips |
+| `glitch-lite` | RGB channel offset + horizontal band | Security/vulnerability proof, terminal aesthetic; never default, never twice |
+
## Step 3: Render
Use the render script — it handles clip staging, duration detection, rendering, and cleanup:
diff --git a/plugins/droid-control/skills/showcase/SKILL.md b/plugins/droid-control/skills/showcase/SKILL.md
index d454ecb..a13e6f4 100644
--- a/plugins/droid-control/skills/showcase/SKILL.md
+++ b/plugins/droid-control/skills/showcase/SKILL.md
@@ -39,7 +39,7 @@ Each preset configures window chrome, spacing, background style, and palette sel
- Subtle cool color grade overlay
- Floating particles in accent blue
-**All presets** include: floating particles, noise texture overlay, color grade, motion blur title→content transition, animated window entrance, staggered panel entrance (side-by-side).
+**All presets** include: floating particles, noise texture overlay, color grade, motion blur title→content transition (configurable via `transitionStyle`), animated window entrance, staggered panel entrance (side-by-side), and optional `codeAnnotations` syntax-highlighted overlays during the main content sequence.
## Visual palettes
@@ -65,6 +65,18 @@ Palette is auto-selected based on preset. Factory/factory-hero use the warm pale
| text | `#cdd6f4` | Cool white |
| muted | `#6c7086` | De-emphasized text |
+## Transition styles
+
+`transitionStyle` selects the crossfade presentation. Schema lives in `compose/SKILL.md`; preset-tier matching:
+
+| Preset | Recommended (default first) | Avoid |
+|---|---|---|
+| `factory`, `factory-hero` | `motion-blur`, `light-leak`, `whip-pan`, `flash` | `glitch-lite` (clashes with warm tone) |
+| `hero`, `presentation` | `motion-blur`, `whip-pan`, `flash` | `light-leak` (warm sweep clashes with cool palette) |
+| `macos`, `minimal` | `motion-blur` | `glitch-lite`, `light-leak` (too much personality for utilitarian frames) |
+
+`codeAnnotations` is preset-agnostic — palette and font stack are auto-derived. See `compose/SKILL.md` for schema and authoring rules.
+
## Operational notes
**Render time**: ~1-3 minutes for a 30-60s video at 1920x1080. Set worker timeouts to 5 minutes.
From 845a113bbb8689698c7399b98406e547b81337f8 Mon Sep 17 00:00:00 2001
From: "factory-droid[bot]"
<138933559+factory-droid[bot]@users.noreply.github.com>
Date: Mon, 20 Apr 2026 04:59:12 +0000
Subject: [PATCH 2/3] chore(droid-control): credit hyperframes for transition
naming + prism-react-renderer
The transitionStyle taxonomy ('whip-pan', 'light-leak', 'flash', 'glitch-lite')
and the 'selectable enum of named transitions' API shape are inspired by
@hyperframes/shader-transitions (Apache-2.0). Implementations are still
original Remotion-native CSS/SVG overlays.
Also adds prism-react-renderer (MIT) to the runtime dependency list since it
ships with the new CodeAnnotationOverlay.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
---
plugins/droid-control/NOTICES.md | 5 +++++
.../remotion/src/components/ShowcaseTransition.tsx | 6 ++++++
2 files changed, 11 insertions(+)
diff --git a/plugins/droid-control/NOTICES.md b/plugins/droid-control/NOTICES.md
index a99d593..5db6f8b 100644
--- a/plugins/droid-control/NOTICES.md
+++ b/plugins/droid-control/NOTICES.md
@@ -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
@@ -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.
diff --git a/plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx b/plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx
index 9da4bc7..303091e 100644
--- a/plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx
+++ b/plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx
@@ -1,3 +1,9 @@
+// Transition style names (`whip-pan`, `light-leak`, `flash`, `glitch-lite`) and
+// the "selectable enum of named transitions" API shape are inspired by
+// @hyperframes/shader-transitions (Apache-2.0). Implementations below are
+// original Remotion-native CSS/SVG overlays — no GLSL/WebGL involved.
+// https://github.com/heygen-com/hyperframes/tree/main/packages/shader-transitions
+
import React from 'react';
import { AbsoluteFill, interpolate, Easing, random } from 'remotion';
import type {
From c9e09fbaef5bf5bd0100a6384357e0694887e30b Mon Sep 17 00:00:00 2001
From: "factory-droid[bot]"
<138933559+factory-droid[bot]@users.noreply.github.com>
Date: Mon, 20 Apr 2026 08:05:34 +0000
Subject: [PATCH 3/3] docs(droid-control): make single the default layout; gate
side-by-side to real comparisons
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The skill guidance was tipping droids toward before/after side-by-side as the
canonical video shape — primary JSON example used side-by-side with BEFORE/AFTER
labels, Demo-mode goal was phrased as 'before/after comparison', delegation
table led with 'Capture before branch / Capture after branch'. Result: droids
were reaching for side-by-side even on brand-new features where there is no
meaningful 'before', sometimes synthesizing a fake baseline.
Fix (guidance only, no schema change — layoutSchema already supports both):
- droid-control/SKILL.md: new 'Layout default' table under Workflow shape
calling out single as the default with the specific cases that warrant
side-by-side. Explicit 'do not synthesize a before' rule.
- droid-control/SKILL.md: delegation table collapses the two pre-framed
before/after rows into 'Capture clip' (single) + 'Capture both clips'
(comparison). Parallel capture section heading/gate updated to read
'comparison flows only'.
- compose/SKILL.md: primary JSON example now renders a single clip with
labels: []. One-line note shows how to flip to side-by-side when a real
comparison exists. Showcase-vs-Demo 'Goal' cell no longer pre-supposes
before/after.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
---
plugins/droid-control/skills/compose/SKILL.md | 21 ++++++++++-------
.../skills/droid-control/SKILL.md | 23 ++++++++++++++++---
2 files changed, 33 insertions(+), 11 deletions(-)
diff --git a/plugins/droid-control/skills/compose/SKILL.md b/plugins/droid-control/skills/compose/SKILL.md
index eb58743..2460c8e 100644
--- a/plugins/droid-control/skills/compose/SKILL.md
+++ b/plugins/droid-control/skills/compose/SKILL.md
@@ -55,7 +55,7 @@ Both use the same Remotion pipeline but target different visual registers.
| | Showcase | Demo |
|---|---|---|
-| **Goal** | Cinematic, high-polish marketing material | Clear, utilitarian before/after comparison |
+| **Goal** | Cinematic, high-polish marketing material | Clear, utilitarian demonstration — single or comparison, whichever the story calls for |
| **Preset** | `factory`, `factory-hero`, or `hero` | `macos`, `minimal`, or `presentation` |
| **Effects tier** | **Full** -- spotlight, zoom, callout, keystroke overlay. Go all out. | **Utilitarian** -- zoom for readability, keystroke overlay for user actions |
| **Audience** | External — landing pages, social, marketing | Internal — PR reviews, docs, QA |
@@ -146,6 +146,12 @@ This checkpoint is not optional. A video that lands outside the target range fai
## Step 2: Build props
+### Choose layout
+
+**Default: `single`.** One clip of the target/final state. New features, bug-fix proofs, walkthroughs, and README heroes all belong here.
+
+Use `side-by-side` only when the story is fundamentally a comparison: regression (broken vs fixed), behavior-preserving refactor, or an explicit user request. Never fabricate a "before" clip to justify the side-by-side shape.
+
Save the `showcaseSchema` JSON to a temp file:
```bash
@@ -154,10 +160,10 @@ PROPS="${DEMO_TMP}/showcase-props.json"
cat > "$PROPS" << 'EOF'
{
- "clips": ["before.cast", "after.cast"],
- "layout": "side-by-side",
+ "clips": ["demo.cast"],
+ "layout": "single",
"fidelity": "auto",
- "labels": ["BEFORE (dev)", "AFTER (PR #11621)"],
+ "labels": [],
"speed": 3,
"title": "PR #11621 — Prevent session freezes",
"subtitle": "Bash Mode blocks interactive commands and supports ESC cancellation",
@@ -167,10 +173,7 @@ cat > "$PROPS" << 'EOF'
{"t": 5.5, "label": "sleep 100"},
{"t": 8.0, "label": "Esc"}
],
- "sections": [
- {"t": 2.0, "title": "Testing basic echo"},
- {"t": 10.0, "title": "Testing long loop"}
- ],
+ "sections": [],
"effects": [],
"speedNote": "3x speed",
"windowTitle": "droid demo"
@@ -178,6 +181,8 @@ cat > "$PROPS" << 'EOF'
EOF
```
+For a comparison flow, swap `"clips"` to two paths, `"layout"` to `"side-by-side"`, and populate `"labels"` (e.g., `["BEFORE (main)", "AFTER (PR #11621)"]`).
+
Use a run-scoped props path like `$PROPS`; do not reuse a global `/tmp/showcase-props.json` across rerenders or concurrent demos.
**CRITICAL: `clipDuration` handling.** The render script auto-detects clip duration via ffprobe when `clipDuration` is omitted from the props. If you set it manually, it **must** match the actual clip duration or you get blank frames (too long) or truncation (too short). When in doubt, omit it and let the script auto-detect.
diff --git a/plugins/droid-control/skills/droid-control/SKILL.md b/plugins/droid-control/skills/droid-control/SKILL.md
index cb0be05..dabc9e1 100644
--- a/plugins/droid-control/skills/droid-control/SKILL.md
+++ b/plugins/droid-control/skills/droid-control/SKILL.md
@@ -71,6 +71,21 @@ Command (intent + commitments)
Commands declare **what** to produce. Atoms own **how**.
+### Layout default
+
+**Default: `single`.** One clip showing the target/final state. Pick this unless the deliverable is fundamentally a comparison.
+
+| Case | Layout |
+|---|---|
+| Brand-new feature (no meaningful prior state) | `single` |
+| Bug fix, single-clip proof of the working path | `single` |
+| Walkthrough / tutorial / readme hero | `single` |
+| Regression proof (broken vs fixed) | `side-by-side` |
+| Behavior-preserving refactor (visual parity is the point) | `side-by-side` |
+| User explicitly asks for a comparison | `side-by-side` |
+
+Do not synthesize a "before" state to justify `side-by-side`. If there is no real baseline, use `single`.
+
## Delegation
The parent agent plans and orchestrates. Mechanical work runs in **worker subagents** via the Task tool. This keeps the parent's context clean and enables parallelism.
@@ -79,8 +94,8 @@ The parent agent plans and orchestrates. Mechanical work runs in **worker subage
| Task | Delegate? | Why |
|---|---|---|
-| **Capture before branch** | YES — `run_in_background=true` | Independent from after branch; run both in parallel |
-| **Capture after branch** | YES — `run_in_background=true` | Independent from before branch |
+| **Capture clip** (single layout) | YES | Worker runs the interaction script end-to-end and returns the `.cast` path |
+| **Capture both clips** (comparison layout) | YES — `run_in_background=true` for each | Branches are independent; run in parallel |
| **Remotion render** | YES | Needs only props JSON, clip paths, output path. Runs `render-showcase.sh` (handles .cast conversion, fidelity profiles, duration detection, cleanup) |
| Planning, interaction scripting | NO — parent | Requires PR context and editorial judgment |
| Layout and prop construction | NO — parent | Requires editorial decisions about effects, timing, labels |
@@ -125,7 +140,9 @@ Task prompt for a Remotion render worker:
/tmp/droid-run-1712345678-42-xxxx/before.cast /tmp/droid-run-1712345678-42-xxxx/after.cast"
```
-### Parallel capture pattern
+### Parallel capture pattern (comparison flows only)
+
+Only applicable when the Layout default table above selects `side-by-side`. For a `single` layout, launch one capture worker and skip this section.
For before/after comparison demos, launch both capture workers simultaneously: