remocn icons — Introducing remocn icons (3D gallery cut)
Install this video
Adds the full composition, its remocn dependencies, and the prompt to your Remotion project via the shadcn registry.
$ pnpm dlx shadcn@latest add kapishdima/remocn-demo/remocn-icons-3dRender it locally
Renders the MP4 on your machine with the Remotion CLI.
$ pnpm dlx remotion render remocn-icons-3d out/remocn-icons-3d.mp4 --scale=2 --crf=15 --x264-preset=slower --jpeg-quality=95 --gl=angleThe prompt
Reconstructed draftA reconstruction of the prompt this video was generated from.
The code
The exact source the AI wrote — the same files the install command puts in your project.
import React from "react";
import {
AbsoluteFill,
Easing,
Sequence,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import {
TransitionSeries,
linearTiming,
type TransitionPresentation,
type TransitionPresentationComponentProps,
} from "@remotion/transitions";
import { evolvePath, getLength, getPointAtLength } from "@remotion/paths";
import { loadFont as loadSans } from "@remotion/google-fonts/Manrope";
import { loadFont as loadMono } from "@remotion/google-fonts/GeistMono";
import { ShaderNeuroNoise } from "@/components/remocn/shader-neuro-noise";
import { SoftBlurIn } from "@/components/remocn/soft-blur-in";
import { ScaleDownFade } from "@/components/remocn/scale-down-fade";
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
import { RolodexFlip } from "@/components/remocn/rolodex-flip";
import { pushThrough } from "@/components/remocn/push-through";
import { whipPan } from "@/components/remocn/whip-pan";
import { focusPull } from "@/components/remocn/focus-pull";
import { BellIcon, BellIconStatic } from "@/components/remocn/icon-bell";
import { HeartIconStatic } from "@/components/remocn/icon-heart";
import { StarIconStatic } from "@/components/remocn/icon-star";
import { Gallery3DScene } from "./gallery-3d";
import { ROutro3D } from "./outro-r-3d";
const { fontFamily: SANS_FAMILY } = loadSans("normal", {
subsets: ["latin"],
weights: ["400", "500", "600"],
});
const { fontFamily: MONO_FAMILY } = loadMono("normal", {
subsets: ["latin"],
weights: ["400", "500"],
});
const SANS = `${SANS_FAMILY}, -apple-system, BlinkMacSystemFont, sans-serif`;
const MONO = `${MONO_FAMILY}, ui-monospace, SFMono-Regular, monospace`;
// The shipped remocn.dev brand: warm obsidian + one lime accent.
const OBSIDIAN = "#141318";
const INK = "#f2f2f2";
const MUTED = "rgba(242,242,242,0.62)";
const FAINT = "rgba(242,242,242,0.4)";
const LIME = "#C3E88D";
const clampOpts = {
extrapolateLeft: "clamp" as const,
extrapolateRight: "clamp" as const,
};
// The neuro-noise field that carries the whole video — a web of fine living
// strokes, the same material the icons are made of. Obsidian family with a
// faint lime undertone, pushed far back by opacity + the scrim.
const NOISE_PROPS = {
speed: 0.4,
colorBack: "#141318",
colorMid: "#26252f",
colorFront: "#7d8672",
brightness: 0.06,
contrast: 0.32,
};
// ---------------------------------------------------------------------------
// Scene timings (frames @ 30fps). Transitions overlap.
// ---------------------------------------------------------------------------
const S_HOOK_A = 72; // "Everything in your video moves" — the lush entrance
const S_HOOK_B = 66; // "Except the icons" — deliberately dead
const S_REVEAL = 196; // the bell draws itself, then the name lands
const S_POS = 96; // "100 Lucide icons, rewritten for video"
const S_GALLERY = 168; // 24 tube-stroke icons draw on in a 3D field, camera dollies in
const S_ACTS = 464; // "Then it acts" + one stroke morphs through ten icons
const S_INSTALL = 180; // the typed command with a rolodex of icon names
const S_OUTRO = 198; // the lockup, inherited — plus one 3D coin turn of the R
const T_CROSS_A = 16; // hookA → hookB (fade-through: out, then in)
const T_PUSH_IN = 32; // hookB → reveal
const T_CROSS = 14; // reveal → positioning
const T_WHIP = 18; // positioning → gallery
const T_FOCUS = 24; // acts → install
const T_PUSH_OUT = 32; // install → outro
// Readability scrim over the backdrop shader.
const Scrim: React.FC<{ strength?: number }> = ({ strength = 1 }) => (
<AbsoluteFill
style={{
background: `radial-gradient(120% 120% at 50% 42%, rgba(20,19,24,${
0.3 * strength
}) 0%, rgba(20,19,24,${0.78 * strength}) 100%)`,
}}
/>
);
// ===========================================================================
// Scene 1 — Hook A. The lushest text entrance remocn ships, deliberately:
// this frame is the world where everything moves.
// ===========================================================================
const HookMovesScene: React.FC = () => (
<AbsoluteFill>
<SoftBlurIn
text="Everything in your video moves"
fontSize={54}
fontWeight={400}
color={INK}
/>
</AbsoluteFill>
);
// ===========================================================================
// Scene 2 — Hook B. The line itself is dead: no entrance, no easing, no
// life — the joke is the grammar, over three frozen gray outlines. The
// crossfade in is the only motion it gets.
// ===========================================================================
const HookStillScene: React.FC = () => (
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 46,
}}
>
<span
style={{
fontFamily: SANS,
fontWeight: 400,
fontSize: 54,
color: INK,
lineHeight: 1,
}}
>
Except the icons
</span>
<div style={{ display: "flex", gap: 34 }}>
<StarIconStatic size={44} color={FAINT} strokeWidth={1.8} />
<BellIconStatic size={44} color={FAINT} strokeWidth={1.8} />
<HeartIconStatic size={44} color={FAINT} strokeWidth={1.8} />
</div>
</div>
</AbsoluteFill>
);
// ===========================================================================
// Scene 3 — The reveal. One huge bell stroke-draws itself on alone, gets its
// grand beat, rings once — then glides up and shrinks as the name resolves
// tight beneath it. The product demonstrates itself before it is named.
// ===========================================================================
const RevealScene: React.FC = () => {
const frame = useCurrentFrame();
// The bell's grand beat ends around frame 102 (draw + ring at speed 0.55);
// then it glides up into the lockup as the name lands.
const glide = interpolate(frame, [104, 130], [0, 1], {
...clampOpts,
easing: Easing.inOut(Easing.cubic),
});
const bellY = -10 - glide * 62;
const bellScale = 1 - glide * 0.52;
const subOpacity = interpolate(frame, [152, 172], [0, 1], clampOpts);
return (
<AbsoluteFill>
{/* the bell */}
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
<div
style={{
transform: `translateY(${bellY}px) scale(${bellScale})`,
}}
>
<Sequence from={18} layout="none">
<BellIcon size={170} color={INK} strokeWidth={1.6} speed={0.55} />
</Sequence>
</div>
</AbsoluteFill>
{/* the name — resolves tight beneath the glided bell */}
<Sequence from={108}>
<AbsoluteFill style={{ transform: "translateY(40px)" }}>
<SoftBlurIn
text="Remocn Icons"
fontSize={64}
fontWeight={400}
color={INK}
/>
</AbsoluteFill>
</Sequence>
{/* the category line */}
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
<span
style={{
transform: "translateY(104px)",
fontFamily: SANS,
fontWeight: 400,
fontSize: 21,
color: FAINT,
opacity: subOpacity,
}}
>
A new icon set for Remotion
</span>
</AbsoluteFill>
</AbsoluteFill>
);
};
// ===========================================================================
// Scene 4 — The positioning, assembling word by word. The plain number 100
// rides in as just another word.
// ===========================================================================
const PositioningScene: React.FC = () => (
<AbsoluteFill>
<Sequence from={8}>
<KineticCenterBuild
text="100 Lucide icons, rewritten for video"
fontSize={52}
fontWeight={400}
color={INK}
measureScale={1.08}
/>
</Sequence>
</AbsoluteFill>
);
// ===========================================================================
// Scene 5 — The gallery, in depth. The 24 icons live in ./gallery-3d.tsx:
// real 3D tube strokes on a three-layer field, drawn on by a diagonal wave
// while the camera dollies in — the one @remotion/three scene in the cut.
// ===========================================================================
// ===========================================================================
// Scene 6 — Then it acts. ONE continuous lime stroke carries the whole
// scene: it draws on as the heart, performs its little action, then morphs
// into the next icon, and the next — ten icons, one stroke, never cutting.
// Each station's mono name crossfades beneath. The finale lands the claim
// this shot just proved: the paths land in your repo, so you can write
// motion the registry never shipped.
// ===========================================================================
// Path data from the installed icon components (components/remocn/icon-*.tsx
// and the remocn-icons registry).
const HEART_PATH =
"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5";
const STAR_PATH =
"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z";
const ZAP_PATH =
"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z";
const FLAME_PATH =
"M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4";
const MOON_PATH =
"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401";
const CLOUD_PATH = "M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z";
const BUBBLE_PATH =
"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719";
const SHIELD_PATH =
"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z";
const BOOKMARK_PATH =
"M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z";
const PLAY_PATH =
"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z";
// ---------------------------------------------------------------------------
// Ring morphing. `interpolatePath` maps command-by-command from each path's
// own start point, so shapes whose parameterizations start on opposite sides
// (or wind the other way) twist through figure-eights mid-morph. Instead we
// resample every glyph into a fixed-size ring of points, normalize the
// winding, and rotate each morph target's ring to the phase that minimizes
// travel — then a plain point lerp reads as one shape flowing into the next.
// All rings and alignments are precomputed at module scope.
// ---------------------------------------------------------------------------
const RING_N = 180;
type Ring = ReadonlyArray<{ x: number; y: number }>;
const ringOf = (d: string): Ring => {
const len = getLength(d);
const pts: { x: number; y: number }[] = [];
for (let i = 0; i < RING_N; i++) {
pts.push(getPointAtLength(d, (len * i) / RING_N));
}
// normalize winding (shoelace; SVG's y grows downward)
let area = 0;
for (let i = 0; i < RING_N; i++) {
const a = pts[i];
const b = pts[(i + 1) % RING_N];
area += a.x * b.y - b.x * a.y;
}
if (area < 0) pts.reverse();
return pts;
};
// Rotate ring `b` to the phase offset that best matches ring `a`.
const alignRing = (a: Ring, b: Ring): Ring => {
let bestK = 0;
let bestCost = Infinity;
for (let k = 0; k < RING_N; k++) {
let cost = 0;
for (let i = 0; i < RING_N; i += 4) {
const p = a[i];
const q = b[(i + k) % RING_N];
cost += (p.x - q.x) ** 2 + (p.y - q.y) ** 2;
}
if (cost < bestCost) {
bestCost = cost;
bestK = k;
}
}
return a.map((_, i) => b[(i + bestK) % RING_N]);
};
const lerpRingPath = (a: Ring, b: Ring, t: number): string => {
let s = "";
for (let i = 0; i < RING_N; i++) {
const x = a[i].x + (b[i].x - a[i].x) * t;
const y = a[i].y + (b[i].y - a[i].y) * t;
s += `${i === 0 ? "M" : "L"}${x.toFixed(3)} ${y.toFixed(3)} `;
}
return `${s}Z`;
};
// keyframe helper for the little station actions.
const kf = (q: number, stops: number[], vals: number[]) =>
interpolate(q, stops, vals, {
...clampOpts,
easing: Easing.inOut(Easing.quad),
});
// scale around a pivot in the 24×24 icon space.
const pivotScale = (sx: number, sy: number, px = 12, py = 12) =>
`translate(${px} ${py}) scale(${sx} ${sy}) translate(${-px} ${-py})`;
// Each station: the icon's path, its mono name, and a hand-written action —
// motion the registry does NOT ship, performed on the raw path. The order is
// chosen for silhouette similarity (round blobs first, then the angular
// family), so every morph reads as one shape flowing into a neighbour.
// Every action starts AND ends at identity, so the hand-off from a finished
// morph into the action never jumps.
type MorphStation = {
name: string;
d: string;
act: (q: number) => string;
};
const STATIONS: MorphStation[] = [
{
name: "icon-heart",
d: HEART_PATH,
// beat, twice
act: (q) => pivotScale(kf(q, [0, 0.25, 0.5, 0.75, 1], [1, 1.12, 1, 1.07, 1]), kf(q, [0, 0.25, 0.5, 0.75, 1], [1, 1.12, 1, 1.07, 1])),
},
{
name: "icon-flame",
d: FLAME_PATH,
// flicker from the base
act: (q) => pivotScale(1, kf(q, [0, 0.25, 0.5, 0.75, 1], [1, 1.12, 0.94, 1.06, 1]), 12, 21),
},
{
name: "icon-moon",
d: MOON_PATH,
// rock to sleep
act: (q) => `rotate(${kf(q, [0, 0.3, 0.6, 0.85, 1], [0, -12, 7, -3, 0])} 12 12)`,
},
{
name: "icon-cloud",
d: CLOUD_PATH,
// drift
act: (q) => `translate(${kf(q, [0, 0.33, 0.66, 1], [0, -2.6, 2.6, 0])} 0)`,
},
{
name: "icon-message-circle",
d: BUBBLE_PATH,
// bounce off its tail
act: (q) => pivotScale(kf(q, [0, 0.3, 0.6, 1], [1, 1.1, 0.97, 1]), kf(q, [0, 0.3, 0.6, 1], [1, 1.1, 0.97, 1]), 4, 21),
},
{
name: "icon-shield",
d: SHIELD_PATH,
// a firm guard thump
act: (q) => pivotScale(kf(q, [0, 0.3, 0.6, 1], [1, 1.09, 0.97, 1]), kf(q, [0, 0.3, 0.6, 1], [1, 1.09, 0.97, 1])),
},
{
name: "icon-bookmark",
d: BOOKMARK_PATH,
// dip, like it just saved something
act: (q) => `translate(0 ${kf(q, [0, 0.35, 0.7, 1], [0, 2.5, -1.5, 0])})`,
},
{
name: "icon-star",
d: STAR_PATH,
// twinkle — a rock and a pop, easing out of restShowing the first 400 of 1182 lines. View the full file on GitHub.