@devinilabs/reelstack 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +128 -0
- package/README.md +125 -0
- package/cli/beats.js +124 -0
- package/cli/bootstrap.js +124 -0
- package/cli/capture.js +34 -0
- package/cli/direction.js +114 -0
- package/cli/icons.js +49 -0
- package/cli/index.js +101 -0
- package/cli/init.js +253 -0
- package/cli/license.js +168 -0
- package/cli/lint.js +865 -0
- package/cli/preview.js +59 -0
- package/cli/render.js +404 -0
- package/cli/scaffold.js +239 -0
- package/cli/smoke.js +76 -0
- package/cli/update.js +26 -0
- package/cli/utils.js +184 -0
- package/docs/buyers-guide.md +220 -0
- package/docs/design-discipline.md +130 -0
- package/docs/family-galleries/dark.md +95 -0
- package/docs/family-galleries/forbidden.md +78 -0
- package/docs/family-galleries/glass.md +98 -0
- package/docs/family-galleries/paper.md +82 -0
- package/docs/family-galleries/warm.md +86 -0
- package/docs/superpowers/plans/2026-05-09-reelstack-init-readiness-gate.md +1166 -0
- package/docs/superpowers/specs/2026-05-09-reelstack-init-readiness-gate-design.md +233 -0
- package/families/dark/components/DriftingSpotlights.tsx +59 -0
- package/families/dark/components/FilmGrain.tsx +44 -0
- package/families/dark/components/ForestCard.tsx +43 -0
- package/families/dark/components/GridBackground.tsx +29 -0
- package/families/dark/components/RadialVignette.tsx +21 -0
- package/families/dark/components/Scanlines.tsx +35 -0
- package/families/dark/components/SegmentOpacity.ts +37 -0
- package/families/dark/components/index.ts +13 -0
- package/families/dark/index.ts +31 -0
- package/families/dark/palette.ts +98 -0
- package/families/dark/presets/claudedispatch.ts +46 -0
- package/families/dark/presets/codedrop.ts +37 -0
- package/families/dark/presets/gpt55.ts +54 -0
- package/families/dark/presets/notebooklm.ts +50 -0
- package/families/dark/presets/resourcescta.ts +35 -0
- package/families/dark/presets/skills.ts +40 -0
- package/families/dark/presets/stitch.ts +46 -0
- package/families/dark/presets/stitch2.ts +43 -0
- package/families/dark/typography.ts +16 -0
- package/families/forbidden/components/ForbiddenCausticBlobs.tsx +52 -0
- package/families/forbidden/components/NewsprintTexture.tsx +28 -0
- package/families/forbidden/components/TintedShadow.tsx +36 -0
- package/families/forbidden/components/index.ts +38 -0
- package/families/forbidden/index.ts +17 -0
- package/families/forbidden/palette.ts +88 -0
- package/families/forbidden/presets/heretic.ts +44 -0
- package/families/forbidden/typography.ts +18 -0
- package/families/glass/components/BreakdownCard.tsx +158 -0
- package/families/glass/components/CausticBlobs.tsx +49 -0
- package/families/glass/components/Counter.tsx +72 -0
- package/families/glass/components/EyebrowPill.tsx +59 -0
- package/families/glass/components/FilmStrip.tsx +202 -0
- package/families/glass/components/FloatingGlyphs.tsx +78 -0
- package/families/glass/components/GlassCard.tsx +58 -0
- package/families/glass/components/GlassCardBezel.tsx +45 -0
- package/families/glass/components/HairlineGrid.tsx +30 -0
- package/families/glass/components/IridescentRing.tsx +114 -0
- package/families/glass/components/IridescentText.tsx +98 -0
- package/families/glass/components/LightBeam.tsx +46 -0
- package/families/glass/components/ParticleBurst.tsx +62 -0
- package/families/glass/components/SonarRings.tsx +81 -0
- package/families/glass/components/StaggeredWords.tsx +74 -0
- package/families/glass/components/index.ts +20 -0
- package/families/glass/index.ts +31 -0
- package/families/glass/palette.ts +93 -0
- package/families/glass/presets/claudewatch.ts +64 -0
- package/families/glass/presets/claudewatchcta.ts +43 -0
- package/families/glass/presets/graphify.ts +45 -0
- package/families/glass/presets/gstack.ts +48 -0
- package/families/glass/presets/jcode.ts +50 -0
- package/families/glass/presets/lilagents.ts +52 -0
- package/families/glass/presets/paperclip.ts +43 -0
- package/families/glass/typography.ts +15 -0
- package/families/index.ts +49 -0
- package/families/paper/components/CardSpring.tsx +42 -0
- package/families/paper/components/CreamGrid.tsx +26 -0
- package/families/paper/components/EditorialSerifText.tsx +51 -0
- package/families/paper/components/GreenAccentCard.tsx +10 -0
- package/families/paper/components/PaperShadow.tsx +30 -0
- package/families/paper/components/ScaleBlurText.tsx +40 -0
- package/families/paper/components/index.ts +11 -0
- package/families/paper/index.ts +23 -0
- package/families/paper/palette.ts +102 -0
- package/families/paper/presets/designreel.ts +32 -0
- package/families/paper/presets/devini3d.ts +45 -0
- package/families/paper/presets/justdrop.ts +39 -0
- package/families/paper/presets/opus.ts +48 -0
- package/families/paper/typography.ts +17 -0
- package/families/warm/components/AccentGlow.tsx +60 -0
- package/families/warm/components/BentoCell.tsx +56 -0
- package/families/warm/components/BentoGrid.tsx +30 -0
- package/families/warm/components/FilmGrain.tsx +36 -0
- package/families/warm/components/ScaleBlurCounter.tsx +71 -0
- package/families/warm/components/WarmSurface.tsx +35 -0
- package/families/warm/components/index.ts +11 -0
- package/families/warm/index.ts +19 -0
- package/families/warm/palette.ts +81 -0
- package/families/warm/presets/huashu.ts +49 -0
- package/families/warm/presets/mempalace.ts +51 -0
- package/families/warm/typography.ts +17 -0
- package/package.json +85 -0
- package/reference/dark/claudedispatch.tsx +2441 -0
- package/reference/dark/notebooklm.tsx +2316 -0
- package/reference/dark/stitch.tsx +3040 -0
- package/reference/forbidden/heretic.tsx +2636 -0
- package/reference/glass/claudewatch.tsx +3827 -0
- package/reference/glass/graphify.tsx +2418 -0
- package/reference/glass/paperclip.tsx +2218 -0
- package/reference/paper/designreel.tsx +883 -0
- package/reference/paper/justdrop.tsx +1898 -0
- package/reference/paper/opus.tsx +1770 -0
- package/reference/warm/huashu.tsx +3413 -0
- package/reference/warm/mempalace.tsx +2909 -0
- package/skill/SKILL.md +229 -0
- package/skill/commands/reelstack-beats.md +20 -0
- package/skill/commands/reelstack-capture.md +24 -0
- package/skill/commands/reelstack-critique.md +15 -0
- package/skill/commands/reelstack-dark.md +40 -0
- package/skill/commands/reelstack-direction.md +17 -0
- package/skill/commands/reelstack-forbidden.md +25 -0
- package/skill/commands/reelstack-glass.md +39 -0
- package/skill/commands/reelstack-icons.md +22 -0
- package/skill/commands/reelstack-init.md +17 -0
- package/skill/commands/reelstack-lint.md +22 -0
- package/skill/commands/reelstack-paper.md +36 -0
- package/skill/commands/reelstack-render.md +20 -0
- package/skill/commands/reelstack-warm.md +36 -0
- package/templates/dark/template.tsx +115 -0
- package/templates/forbidden/template.tsx +111 -0
- package/templates/glass/template.tsx +201 -0
- package/templates/paper/template.tsx +133 -0
- package/templates/warm/template.tsx +210 -0
- package/utils/ai-purple-blocklist.ts +13 -0
- package/utils/banned-fonts.ts +11 -0
- package/utils/cubic-bezier.ts +36 -0
- package/utils/easing.ts +84 -0
- package/utils/grid.ts +13 -0
- package/utils/render-presets.json +56 -0
- package/utils/safe-zones.tsx +57 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { GlassCard } from "./GlassCard";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Soft Double-Bezel glass card. Outer shell + inner core, 1px white border
|
|
6
|
+
* between them — the "nested glass" pattern from leonxlnx/taste-skill-soft.
|
|
7
|
+
* Use for premium product reveals where a single GlassCard feels too flat.
|
|
8
|
+
*/
|
|
9
|
+
export const GlassCardBezel: React.FC<{
|
|
10
|
+
variant?: "default" | "strong" | "heavy";
|
|
11
|
+
outerPadding?: number;
|
|
12
|
+
innerPadding?: number;
|
|
13
|
+
radius?: number;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
reduceMotion?: boolean;
|
|
17
|
+
}> = ({
|
|
18
|
+
variant = "strong",
|
|
19
|
+
outerPadding = 8,
|
|
20
|
+
innerPadding = 32,
|
|
21
|
+
radius = 28,
|
|
22
|
+
style,
|
|
23
|
+
children,
|
|
24
|
+
reduceMotion = false,
|
|
25
|
+
}) => {
|
|
26
|
+
return (
|
|
27
|
+
<GlassCard
|
|
28
|
+
variant={variant}
|
|
29
|
+
padding={outerPadding}
|
|
30
|
+
radius={radius}
|
|
31
|
+
style={style}
|
|
32
|
+
>
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
padding: innerPadding,
|
|
36
|
+
borderRadius: radius - outerPadding,
|
|
37
|
+
background: "rgba(255,255,255,0.18)",
|
|
38
|
+
boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.55)",
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
</GlassCard>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AbsoluteFill } from "remotion";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HairlineGrid — subtle 72×72 px grid with radial mask fade from center.
|
|
6
|
+
*
|
|
7
|
+
* Provides "environment" — the page-frame the buyer feels but doesn't see.
|
|
8
|
+
* Always sits between CausticBlobs and the foreground content.
|
|
9
|
+
*
|
|
10
|
+
* Pure CSS (background-image with two linear gradients). Zero per-frame cost.
|
|
11
|
+
*/
|
|
12
|
+
export const HairlineGrid: React.FC<{
|
|
13
|
+
cellPx?: number;
|
|
14
|
+
strokeOpacity?: number;
|
|
15
|
+
}> = ({ cellPx = 72, strokeOpacity = 0.035 }) => {
|
|
16
|
+
return (
|
|
17
|
+
<AbsoluteFill
|
|
18
|
+
style={{
|
|
19
|
+
backgroundImage: `
|
|
20
|
+
linear-gradient(to right, rgba(14,14,18,${strokeOpacity}) 1px, transparent 1px),
|
|
21
|
+
linear-gradient(to bottom, rgba(14,14,18,${strokeOpacity}) 1px, transparent 1px)
|
|
22
|
+
`,
|
|
23
|
+
backgroundSize: `${cellPx}px ${cellPx}px`,
|
|
24
|
+
WebkitMaskImage: "radial-gradient(ellipse at center, rgba(0,0,0,1) 35%, rgba(0,0,0,0) 80%)",
|
|
25
|
+
maskImage: "radial-gradient(ellipse at center, rgba(0,0,0,1) 35%, rgba(0,0,0,0) 80%)",
|
|
26
|
+
pointerEvents: "none",
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { interpolate, useCurrentFrame } from "remotion";
|
|
3
|
+
import { palette } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* IridescentRing — concentric iridescent halo rings for hero-focal-point ambience.
|
|
7
|
+
*
|
|
8
|
+
* Source: ClaudeWatchReel "IridescentRing" helper (around line 138). The
|
|
9
|
+
* conic/iridescent-rotating ring used as a heavier alternative to SonarRings
|
|
10
|
+
* when a scene needs a chromatic shimmer rather than mono-color pulses.
|
|
11
|
+
*
|
|
12
|
+
* Use when: a hero subject needs a saturated, multi-stop halo that reads as
|
|
13
|
+
* "premium glass" (vs. SonarRings' single-color sonar feel). Stack 3–5 rings,
|
|
14
|
+
* each delayed by `period / count`. Each ring's stroke fades 1.0→0 as it
|
|
15
|
+
* expands minRadius→maxRadius, and the gradient orientation rotates so the
|
|
16
|
+
* iridescence appears to flow.
|
|
17
|
+
*/
|
|
18
|
+
export const IridescentRing: React.FC<{
|
|
19
|
+
cx?: number; // canvas-x (1080 viewBox)
|
|
20
|
+
cy?: number; // canvas-y (1920 viewBox)
|
|
21
|
+
count?: number;
|
|
22
|
+
period?: number; // frames per cycle
|
|
23
|
+
minRadius?: number;
|
|
24
|
+
maxRadius?: number;
|
|
25
|
+
colors?: string[];
|
|
26
|
+
startFrame?: number;
|
|
27
|
+
strokeWidth?: number;
|
|
28
|
+
}> = ({
|
|
29
|
+
cx = 540,
|
|
30
|
+
cy = 960,
|
|
31
|
+
count = 4,
|
|
32
|
+
period = 240,
|
|
33
|
+
minRadius = 80,
|
|
34
|
+
maxRadius = 600,
|
|
35
|
+
colors = [
|
|
36
|
+
palette.iriCyan,
|
|
37
|
+
palette.iriViolet,
|
|
38
|
+
palette.iriRose,
|
|
39
|
+
palette.iriGold,
|
|
40
|
+
],
|
|
41
|
+
startFrame = 0,
|
|
42
|
+
strokeWidth = 3,
|
|
43
|
+
}) => {
|
|
44
|
+
const frame = useCurrentFrame();
|
|
45
|
+
const local = Math.max(0, frame - startFrame);
|
|
46
|
+
|
|
47
|
+
// Build gradient stops once
|
|
48
|
+
const stopOffsets = colors.map((_, i) => (i / (colors.length - 1)) * 100);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<svg
|
|
52
|
+
style={{
|
|
53
|
+
position: "absolute",
|
|
54
|
+
left: 0,
|
|
55
|
+
top: 0,
|
|
56
|
+
width: "100%",
|
|
57
|
+
height: "100%",
|
|
58
|
+
pointerEvents: "none",
|
|
59
|
+
}}
|
|
60
|
+
viewBox="0 0 1080 1920"
|
|
61
|
+
>
|
|
62
|
+
<defs>
|
|
63
|
+
{Array.from({ length: count }).map((_, i) => {
|
|
64
|
+
const id = `iri-ring-grad-${i}`;
|
|
65
|
+
// Each ring rotates its gradient at a slightly different rate so
|
|
66
|
+
// the iridescence shimmers rather than scrolling uniformly.
|
|
67
|
+
const angle = (local * 1.2 + i * 22) % 360;
|
|
68
|
+
// x1/y1/x2/y2 in objectBoundingBox space, rotated.
|
|
69
|
+
const rad = (angle * Math.PI) / 180;
|
|
70
|
+
const x1 = 0.5 - Math.cos(rad) * 0.5;
|
|
71
|
+
const y1 = 0.5 - Math.sin(rad) * 0.5;
|
|
72
|
+
const x2 = 0.5 + Math.cos(rad) * 0.5;
|
|
73
|
+
const y2 = 0.5 + Math.sin(rad) * 0.5;
|
|
74
|
+
return (
|
|
75
|
+
<linearGradient
|
|
76
|
+
key={id}
|
|
77
|
+
id={id}
|
|
78
|
+
x1={x1}
|
|
79
|
+
y1={y1}
|
|
80
|
+
x2={x2}
|
|
81
|
+
y2={y2}
|
|
82
|
+
>
|
|
83
|
+
{colors.map((color, ci) => (
|
|
84
|
+
<stop
|
|
85
|
+
key={ci}
|
|
86
|
+
offset={`${stopOffsets[ci]}%`}
|
|
87
|
+
stopColor={color}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
</linearGradient>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</defs>
|
|
94
|
+
|
|
95
|
+
{Array.from({ length: count }).map((_, i) => {
|
|
96
|
+
const phase = ((local + (i * period) / count) % period) / period;
|
|
97
|
+
const r = interpolate(phase, [0, 1], [minRadius, maxRadius]);
|
|
98
|
+
const opacity = interpolate(phase, [0, 0.15, 1], [0, 1, 0]);
|
|
99
|
+
return (
|
|
100
|
+
<circle
|
|
101
|
+
key={i}
|
|
102
|
+
cx={cx}
|
|
103
|
+
cy={cy}
|
|
104
|
+
r={r}
|
|
105
|
+
fill="none"
|
|
106
|
+
stroke={`url(#iri-ring-grad-${i})`}
|
|
107
|
+
strokeWidth={strokeWidth}
|
|
108
|
+
opacity={opacity}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</svg>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { CSSProperties } from "react";
|
|
2
|
+
import { useCurrentFrame } from "remotion";
|
|
3
|
+
import { ease } from "../../../utils/easing";
|
|
4
|
+
import { palette } from "../palette";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* IridescentText — the multi-stop iridescent gradient treatment for hero words.
|
|
8
|
+
*
|
|
9
|
+
* Source: ClaudeWatchReel hero headline "Watch." (Type. Paste. Watch. scene)
|
|
10
|
+
* and the new ReelStackReel opener "Ship.". Both apply a 5-stop iridescent
|
|
11
|
+
* `WebkitBackgroundClip: "text"` gradient and a slowly-rotating angle so the
|
|
12
|
+
* shimmer reads as alive rather than baked.
|
|
13
|
+
*
|
|
14
|
+
* Use when: one hero word in a headline needs to pop chromatically against
|
|
15
|
+
* neutral ink siblings. Pair with a StaggeredWords block where the iridescent
|
|
16
|
+
* word is rendered separately so the gradient angle can rotate without
|
|
17
|
+
* affecting other words.
|
|
18
|
+
*
|
|
19
|
+
* Behavior: scale 0.94→1.0 + blur 8px→0 + opacity 0→1 over 18 frames after
|
|
20
|
+
* `startFrame`, eased with `ease.power4Out`. If `animated`, the gradient angle
|
|
21
|
+
* rotates 0→360deg over 5 seconds (sine-eased so it slows at the extremes).
|
|
22
|
+
*/
|
|
23
|
+
export const IridescentText: React.FC<{
|
|
24
|
+
text: string;
|
|
25
|
+
startFrame?: number;
|
|
26
|
+
fontSize?: number;
|
|
27
|
+
fontWeight?: number;
|
|
28
|
+
letterSpacing?: number;
|
|
29
|
+
colors?: string[];
|
|
30
|
+
animated?: boolean;
|
|
31
|
+
reduceMotion?: boolean;
|
|
32
|
+
style?: CSSProperties;
|
|
33
|
+
}> = ({
|
|
34
|
+
text,
|
|
35
|
+
startFrame = 0,
|
|
36
|
+
fontSize = 96,
|
|
37
|
+
fontWeight = 700,
|
|
38
|
+
letterSpacing = -2,
|
|
39
|
+
colors = [
|
|
40
|
+
palette.iriCyan,
|
|
41
|
+
palette.iriViolet,
|
|
42
|
+
palette.iriRose,
|
|
43
|
+
palette.iriGold,
|
|
44
|
+
palette.iriCyan,
|
|
45
|
+
],
|
|
46
|
+
animated = true,
|
|
47
|
+
reduceMotion = false,
|
|
48
|
+
style,
|
|
49
|
+
}) => {
|
|
50
|
+
const frame = useCurrentFrame();
|
|
51
|
+
const local = Math.max(0, frame - startFrame);
|
|
52
|
+
|
|
53
|
+
// Reveal entrance — power4Out, 18 frames
|
|
54
|
+
const tRaw = Math.min(1, Math.max(0, local / 18));
|
|
55
|
+
const t = reduceMotion ? 1 : ease.power4Out(tRaw);
|
|
56
|
+
const scale = reduceMotion ? 1 : 0.94 + t * 0.06;
|
|
57
|
+
const blur = reduceMotion ? 0 : (1 - t) * 8;
|
|
58
|
+
const opacity = reduceMotion ? 1 : t;
|
|
59
|
+
|
|
60
|
+
// Animated gradient angle — rotates 0→360deg over 5 seconds (150 frames @ 30fps),
|
|
61
|
+
// sine-eased so the rotation slows near its visual extremes.
|
|
62
|
+
const period = 150;
|
|
63
|
+
const isAnimated = animated && !reduceMotion;
|
|
64
|
+
const phase = isAnimated ? (local % period) / period : 0;
|
|
65
|
+
const eased = isAnimated ? (Math.sin(phase * Math.PI * 2 - Math.PI / 2) + 1) / 2 : 0;
|
|
66
|
+
const angle = isAnimated ? eased * 360 : 135;
|
|
67
|
+
|
|
68
|
+
const stops = colors
|
|
69
|
+
.map((c, i) => `${c} ${(i / (colors.length - 1)) * 100}%`)
|
|
70
|
+
.join(", ");
|
|
71
|
+
const gradient = `linear-gradient(${angle}deg, ${stops})`;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<span
|
|
75
|
+
style={{
|
|
76
|
+
display: "inline-block",
|
|
77
|
+
fontFamily: "Geist, system-ui",
|
|
78
|
+
fontSize,
|
|
79
|
+
fontWeight,
|
|
80
|
+
letterSpacing,
|
|
81
|
+
lineHeight: 1.02,
|
|
82
|
+
background: gradient,
|
|
83
|
+
WebkitBackgroundClip: "text",
|
|
84
|
+
backgroundClip: "text",
|
|
85
|
+
WebkitTextFillColor: "transparent",
|
|
86
|
+
color: "transparent",
|
|
87
|
+
opacity,
|
|
88
|
+
filter: `blur(${blur}px)`,
|
|
89
|
+
transform: `scale(${scale})`,
|
|
90
|
+
transformOrigin: "left center",
|
|
91
|
+
willChange: "transform, opacity, filter",
|
|
92
|
+
...style,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{text}
|
|
96
|
+
</span>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useCurrentFrame } from "remotion";
|
|
3
|
+
import { palette } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LightBeam — rotating radial gradient with `screen` blend mode.
|
|
7
|
+
*
|
|
8
|
+
* Hot spot at 40% opacity. Used for hero-moment emphasis (counter reveal,
|
|
9
|
+
* name drop). Rotates slowly — full revolution takes ~12 seconds.
|
|
10
|
+
*/
|
|
11
|
+
export const LightBeam: React.FC<{
|
|
12
|
+
cx?: string;
|
|
13
|
+
cy?: string;
|
|
14
|
+
size?: number;
|
|
15
|
+
rotationSpeed?: number; // degrees per frame
|
|
16
|
+
color?: string;
|
|
17
|
+
reduceMotion?: boolean;
|
|
18
|
+
}> = ({
|
|
19
|
+
cx = "50%",
|
|
20
|
+
cy = "30%",
|
|
21
|
+
size = 1600,
|
|
22
|
+
rotationSpeed = 1,
|
|
23
|
+
color = palette.iriGold,
|
|
24
|
+
reduceMotion = false,
|
|
25
|
+
}) => {
|
|
26
|
+
const frame = useCurrentFrame();
|
|
27
|
+
const angle = reduceMotion ? 0 : (frame * rotationSpeed) % 360;
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
style={{
|
|
31
|
+
position: "absolute",
|
|
32
|
+
left: cx,
|
|
33
|
+
top: cy,
|
|
34
|
+
width: size,
|
|
35
|
+
height: size,
|
|
36
|
+
marginLeft: -size / 2,
|
|
37
|
+
marginTop: -size / 2,
|
|
38
|
+
background: `radial-gradient(ellipse at center, ${color}66 0%, ${color}22 30%, transparent 70%)`,
|
|
39
|
+
transform: `rotate(${angle}deg)`,
|
|
40
|
+
transformOrigin: "center",
|
|
41
|
+
mixBlendMode: "screen",
|
|
42
|
+
pointerEvents: "none",
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { interpolate, useCurrentFrame } from "remotion";
|
|
3
|
+
import { palette } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ParticleBurst — 12–20 small circles radiating from center with velocity
|
|
7
|
+
* + gravity. Lifetime 60–120 frames. Fired on a beat reveal.
|
|
8
|
+
*
|
|
9
|
+
* Particle positions are deterministic (seeded by index) so the burst is
|
|
10
|
+
* stable across re-renders.
|
|
11
|
+
*/
|
|
12
|
+
export const ParticleBurst: React.FC<{
|
|
13
|
+
cx: number;
|
|
14
|
+
cy: number;
|
|
15
|
+
startFrame: number;
|
|
16
|
+
count?: number;
|
|
17
|
+
lifespan?: number;
|
|
18
|
+
spread?: number; // px
|
|
19
|
+
colors?: string[];
|
|
20
|
+
reduceMotion?: boolean;
|
|
21
|
+
}> = ({
|
|
22
|
+
cx,
|
|
23
|
+
cy,
|
|
24
|
+
startFrame,
|
|
25
|
+
count = 16,
|
|
26
|
+
lifespan = 80,
|
|
27
|
+
spread = 360,
|
|
28
|
+
colors = [palette.iriCyan, palette.iriViolet, palette.iriRose, palette.iriGold],
|
|
29
|
+
reduceMotion = false,
|
|
30
|
+
}) => {
|
|
31
|
+
const frame = useCurrentFrame();
|
|
32
|
+
const local = Math.max(0, frame - startFrame);
|
|
33
|
+
const particles = useMemo(() => {
|
|
34
|
+
return Array.from({ length: count }).map((_, i) => {
|
|
35
|
+
const angle = (i / count) * Math.PI * 2 + (i % 3) * 0.13;
|
|
36
|
+
const velocity = 0.6 + ((i * 37) % 100) / 250; // 0.6..1.0
|
|
37
|
+
const radius = 4 + ((i * 13) % 6); // 4..9 px
|
|
38
|
+
const color = colors[i % colors.length];
|
|
39
|
+
return { angle, velocity, radius, color };
|
|
40
|
+
});
|
|
41
|
+
}, [count, colors]);
|
|
42
|
+
|
|
43
|
+
if (reduceMotion) return null;
|
|
44
|
+
if (local <= 0 || local > lifespan) return null;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<svg
|
|
48
|
+
style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%", pointerEvents: "none" }}
|
|
49
|
+
viewBox="0 0 1080 1920"
|
|
50
|
+
>
|
|
51
|
+
{particles.map((p, i) => {
|
|
52
|
+
const t = local / lifespan;
|
|
53
|
+
const dist = spread * p.velocity * (1 - Math.pow(1 - t, 3));
|
|
54
|
+
const gravity = t * t * 80;
|
|
55
|
+
const x = cx + Math.cos(p.angle) * dist;
|
|
56
|
+
const y = cy + Math.sin(p.angle) * dist + gravity;
|
|
57
|
+
const opacity = interpolate(t, [0, 0.1, 1], [0, 1, 0]);
|
|
58
|
+
return <circle key={i} cx={x} cy={y} r={p.radius} fill={p.color} opacity={opacity} />;
|
|
59
|
+
})}
|
|
60
|
+
</svg>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { interpolate, useCurrentFrame } from "remotion";
|
|
3
|
+
import { palette } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SonarRings — concentric expanding circles. Period ~300 frames (10s @ 30fps).
|
|
7
|
+
*
|
|
8
|
+
* Used as anchor-scene focus effect. Stack 3–5 rings, each delayed by
|
|
9
|
+
* `period / count`. Stroke fades from 1.0 → 0 as ring expands from
|
|
10
|
+
* minRadius → maxRadius.
|
|
11
|
+
*/
|
|
12
|
+
export const SonarRings: React.FC<{
|
|
13
|
+
cx: number; // center x in canvas coords
|
|
14
|
+
cy: number;
|
|
15
|
+
count?: number;
|
|
16
|
+
period?: number; // frames per ring lifecycle
|
|
17
|
+
minRadius?: number;
|
|
18
|
+
maxRadius?: number;
|
|
19
|
+
stroke?: string;
|
|
20
|
+
strokeWidth?: number;
|
|
21
|
+
startFrame?: number;
|
|
22
|
+
reduceMotion?: boolean;
|
|
23
|
+
}> = ({
|
|
24
|
+
cx,
|
|
25
|
+
cy,
|
|
26
|
+
count = 4,
|
|
27
|
+
period = 300,
|
|
28
|
+
minRadius = 60,
|
|
29
|
+
maxRadius = 520,
|
|
30
|
+
stroke = palette.iriViolet,
|
|
31
|
+
strokeWidth = 2.5,
|
|
32
|
+
startFrame = 0,
|
|
33
|
+
reduceMotion = false,
|
|
34
|
+
}) => {
|
|
35
|
+
const frame = useCurrentFrame();
|
|
36
|
+
const local = Math.max(0, frame - startFrame);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<svg
|
|
40
|
+
style={{
|
|
41
|
+
position: "absolute",
|
|
42
|
+
left: 0,
|
|
43
|
+
top: 0,
|
|
44
|
+
width: "100%",
|
|
45
|
+
height: "100%",
|
|
46
|
+
pointerEvents: "none",
|
|
47
|
+
}}
|
|
48
|
+
viewBox="0 0 1080 1920"
|
|
49
|
+
>
|
|
50
|
+
{reduceMotion ? (
|
|
51
|
+
<circle
|
|
52
|
+
cx={cx}
|
|
53
|
+
cy={cy}
|
|
54
|
+
r={maxRadius}
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke={stroke}
|
|
57
|
+
strokeWidth={strokeWidth}
|
|
58
|
+
opacity={0.3}
|
|
59
|
+
/>
|
|
60
|
+
) : (
|
|
61
|
+
Array.from({ length: count }).map((_, i) => {
|
|
62
|
+
const phase = ((local + (i * period) / count) % period) / period;
|
|
63
|
+
const r = interpolate(phase, [0, 1], [minRadius, maxRadius]);
|
|
64
|
+
const opacity = interpolate(phase, [0, 0.15, 1], [0, 1, 0]);
|
|
65
|
+
return (
|
|
66
|
+
<circle
|
|
67
|
+
key={i}
|
|
68
|
+
cx={cx}
|
|
69
|
+
cy={cy}
|
|
70
|
+
r={r}
|
|
71
|
+
fill="none"
|
|
72
|
+
stroke={stroke}
|
|
73
|
+
strokeWidth={strokeWidth}
|
|
74
|
+
opacity={opacity}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
})
|
|
78
|
+
)}
|
|
79
|
+
</svg>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { CSSProperties } from "react";
|
|
2
|
+
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
|
|
3
|
+
import { palette } from "../palette";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* StaggeredWords — text revealed word-by-word with spring easing.
|
|
7
|
+
*
|
|
8
|
+
* Default per-word offset is 4 frames (~133ms at 30fps). Spring config is
|
|
9
|
+
* gentle (damping 15, stiffness 80). Each word translates up from 16px below
|
|
10
|
+
* baseline as it scales from 0.96 to 1.0.
|
|
11
|
+
*/
|
|
12
|
+
export const StaggeredWords: React.FC<{
|
|
13
|
+
startFrame: number;
|
|
14
|
+
text: string;
|
|
15
|
+
perWordOffset?: number;
|
|
16
|
+
fontSize?: number;
|
|
17
|
+
fontWeight?: number;
|
|
18
|
+
color?: string;
|
|
19
|
+
letterSpacing?: number;
|
|
20
|
+
reduceMotion?: boolean;
|
|
21
|
+
style?: CSSProperties;
|
|
22
|
+
}> = ({
|
|
23
|
+
startFrame,
|
|
24
|
+
text,
|
|
25
|
+
perWordOffset = 4,
|
|
26
|
+
fontSize = 64,
|
|
27
|
+
fontWeight = 700,
|
|
28
|
+
color = palette.ink,
|
|
29
|
+
letterSpacing = -1.5,
|
|
30
|
+
reduceMotion = false,
|
|
31
|
+
style,
|
|
32
|
+
}) => {
|
|
33
|
+
const frame = useCurrentFrame();
|
|
34
|
+
const { fps } = useVideoConfig();
|
|
35
|
+
const words = text.split(/\s+/);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
style={{
|
|
40
|
+
display: "flex",
|
|
41
|
+
flexWrap: "wrap",
|
|
42
|
+
gap: "0.28em",
|
|
43
|
+
fontFamily: "Geist, system-ui",
|
|
44
|
+
fontSize,
|
|
45
|
+
fontWeight,
|
|
46
|
+
color,
|
|
47
|
+
letterSpacing,
|
|
48
|
+
lineHeight: 1.1,
|
|
49
|
+
...style,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{words.map((word, i) => {
|
|
53
|
+
const local = Math.max(0, frame - (startFrame + i * perWordOffset));
|
|
54
|
+
const t = reduceMotion
|
|
55
|
+
? 1
|
|
56
|
+
: spring({ frame: local, fps, config: { damping: 15, stiffness: 80 } });
|
|
57
|
+
return (
|
|
58
|
+
<span
|
|
59
|
+
key={`${word}-${i}`}
|
|
60
|
+
style={{
|
|
61
|
+
display: "inline-block",
|
|
62
|
+
opacity: t,
|
|
63
|
+
transform: reduceMotion
|
|
64
|
+
? "none"
|
|
65
|
+
: `translateY(${(1 - t) * 16}px) scale(${0.96 + t * 0.04})`,
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{word}
|
|
69
|
+
</span>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glass Iridescent — primitive components index.
|
|
3
|
+
* Scaffold templates import from here:
|
|
4
|
+
* import { CausticBlobs, GlassCard, ... } from "@reelstack/families/glass/components";
|
|
5
|
+
*/
|
|
6
|
+
export { CausticBlobs } from "./CausticBlobs";
|
|
7
|
+
export { HairlineGrid } from "./HairlineGrid";
|
|
8
|
+
export { GlassCard } from "./GlassCard";
|
|
9
|
+
export { EyebrowPill } from "./EyebrowPill";
|
|
10
|
+
export { StaggeredWords } from "./StaggeredWords";
|
|
11
|
+
export { Counter } from "./Counter";
|
|
12
|
+
export { SonarRings } from "./SonarRings";
|
|
13
|
+
export { ParticleBurst } from "./ParticleBurst";
|
|
14
|
+
export { LightBeam } from "./LightBeam";
|
|
15
|
+
export { FloatingGlyphs } from "./FloatingGlyphs";
|
|
16
|
+
export { FilmStrip } from "./FilmStrip";
|
|
17
|
+
export { IridescentRing } from "./IridescentRing";
|
|
18
|
+
export { IridescentText } from "./IridescentText";
|
|
19
|
+
export { BreakdownCard } from "./BreakdownCard";
|
|
20
|
+
export { GlassCardBezel } from "./GlassCardBezel";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glass Iridescent — family index.
|
|
3
|
+
* Re-exports palette + all 7 presets so the CLI scaffolder can import via
|
|
4
|
+
* `import { palette, presets } from "../../families/glass"`.
|
|
5
|
+
*/
|
|
6
|
+
export { palette, safeZones, gradients } from "./palette";
|
|
7
|
+
export type { GlassPalette } from "./palette";
|
|
8
|
+
|
|
9
|
+
import { preset as graphify } from "./presets/graphify";
|
|
10
|
+
import { preset as paperclip } from "./presets/paperclip";
|
|
11
|
+
import { preset as gstack } from "./presets/gstack";
|
|
12
|
+
import { preset as lilagents } from "./presets/lilagents";
|
|
13
|
+
import { preset as jcode } from "./presets/jcode";
|
|
14
|
+
import { preset as claudewatch } from "./presets/claudewatch";
|
|
15
|
+
import { preset as claudewatchcta } from "./presets/claudewatchcta";
|
|
16
|
+
|
|
17
|
+
export const presets = {
|
|
18
|
+
graphify,
|
|
19
|
+
paperclip,
|
|
20
|
+
gstack,
|
|
21
|
+
lilagents,
|
|
22
|
+
jcode,
|
|
23
|
+
claudewatch,
|
|
24
|
+
claudewatchcta,
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
export const family = {
|
|
28
|
+
name: "glass",
|
|
29
|
+
label: "Glass Iridescent",
|
|
30
|
+
presets: Object.keys(presets) as Array<keyof typeof presets>,
|
|
31
|
+
} as const;
|