@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,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-scaffolded by ReelStack.
|
|
3
|
+
*
|
|
4
|
+
* Family: Warm Signature
|
|
5
|
+
* Preset: __PRESET_NAME__ (source: __SOURCE_REEL__)
|
|
6
|
+
* Duration: __DURATION_FRAMES__ frames @ __FPS__ fps
|
|
7
|
+
*
|
|
8
|
+
* Generated rules baked in:
|
|
9
|
+
* - IG safe zones reserved (top 290px / bottom 422px)
|
|
10
|
+
* - Motion floor primer (FilmGrain + AccentGlow ambient on every scene)
|
|
11
|
+
* - GSAP-vocabulary easings via reelstack/utils/easing
|
|
12
|
+
* - Audio-lock skeleton (BEATS const ready for /reelstack-beats output)
|
|
13
|
+
*
|
|
14
|
+
* STRICT RULE: amber primary, emerald payoff, red wall. No other accents.
|
|
15
|
+
* The lint command flags any other color used in this reel.
|
|
16
|
+
*/
|
|
17
|
+
import React from "react";
|
|
18
|
+
import { Sequence, getInputProps, useCurrentFrame } from "remotion";
|
|
19
|
+
|
|
20
|
+
import { palette } from "@devinilabs/reelstack/families/warm";
|
|
21
|
+
import {
|
|
22
|
+
AccentGlow,
|
|
23
|
+
FilmGrain,
|
|
24
|
+
BentoGrid,
|
|
25
|
+
BentoCell,
|
|
26
|
+
WarmSurface,
|
|
27
|
+
ScaleBlurCounter,
|
|
28
|
+
} from "@devinilabs/reelstack/families/warm/components";
|
|
29
|
+
import { ease } from "@devinilabs/reelstack/utils/easing";
|
|
30
|
+
import { SafeZones, TOP_SAFE, BOTTOM_SAFE } from "@devinilabs/reelstack/utils/safe-zones";
|
|
31
|
+
|
|
32
|
+
// ─── BEATS ────────────────────────────────────────────────────────────────
|
|
33
|
+
// Run /reelstack-beats <vo.wav> to populate this from whisper-cli SRT.
|
|
34
|
+
// Hand-eyeballing beats causes 6+ second drift on 90s reels — don't.
|
|
35
|
+
const BEATS = {
|
|
36
|
+
hook: 0,
|
|
37
|
+
name: 180,
|
|
38
|
+
body: 600,
|
|
39
|
+
anchor: 1200,
|
|
40
|
+
cta: 1800,
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
/** Read inputProps for reduceMotion at module load.
|
|
44
|
+
* Buyers can render a low-motion variant via:
|
|
45
|
+
* npx remotion render <id> out/x.mp4 --props='{"reduceMotion":true}'
|
|
46
|
+
*/
|
|
47
|
+
const _inputProps = (getInputProps() as { reduceMotion?: boolean }) || {};
|
|
48
|
+
const REDUCE_MOTION = _inputProps.reduceMotion === true;
|
|
49
|
+
|
|
50
|
+
export const __REEL_NAME__Reel: React.FC = () => {
|
|
51
|
+
return (
|
|
52
|
+
<WarmSurface>
|
|
53
|
+
{/* Background motion layers (the family's signature ambient) */}
|
|
54
|
+
<FilmGrain reduceMotion={REDUCE_MOTION} />
|
|
55
|
+
<AccentGlow reduceMotion={REDUCE_MOTION} />
|
|
56
|
+
|
|
57
|
+
{/* Hook — between safe zones */}
|
|
58
|
+
<Sequence from={BEATS.hook}>
|
|
59
|
+
<HookScene />
|
|
60
|
+
</Sequence>
|
|
61
|
+
|
|
62
|
+
{/* Name drop — preset-specific */}
|
|
63
|
+
<Sequence from={BEATS.name}>
|
|
64
|
+
<NameScene />
|
|
65
|
+
</Sequence>
|
|
66
|
+
|
|
67
|
+
{/* Body — bento matrix */}
|
|
68
|
+
<Sequence from={BEATS.body}>
|
|
69
|
+
<BodyScene />
|
|
70
|
+
</Sequence>
|
|
71
|
+
|
|
72
|
+
{/* Anchor — second motion floor anchor */}
|
|
73
|
+
<Sequence from={BEATS.anchor}>
|
|
74
|
+
<AnchorScene />
|
|
75
|
+
</Sequence>
|
|
76
|
+
|
|
77
|
+
{/* CTA */}
|
|
78
|
+
<Sequence from={BEATS.cta}>
|
|
79
|
+
<CtaScene />
|
|
80
|
+
</Sequence>
|
|
81
|
+
|
|
82
|
+
{/* Dev-only safe-zone overlay. Set visible={false} for production. */}
|
|
83
|
+
<SafeZones visible={false} />
|
|
84
|
+
</WarmSurface>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ─── Scenes ──────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const HookScene: React.FC = () => {
|
|
91
|
+
const frame = useCurrentFrame();
|
|
92
|
+
const t = ease.power4Out(Math.min(1, frame / 30));
|
|
93
|
+
return (
|
|
94
|
+
<WarmSurface
|
|
95
|
+
style={{
|
|
96
|
+
background: "transparent",
|
|
97
|
+
paddingTop: TOP_SAFE + 100,
|
|
98
|
+
paddingInline: 64,
|
|
99
|
+
paddingBottom: BOTTOM_SAFE + 100,
|
|
100
|
+
display: "flex",
|
|
101
|
+
flexDirection: "column",
|
|
102
|
+
gap: 20,
|
|
103
|
+
justifyContent: "center",
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<span
|
|
107
|
+
style={{
|
|
108
|
+
fontSize: 90,
|
|
109
|
+
fontWeight: 700,
|
|
110
|
+
letterSpacing: -2,
|
|
111
|
+
lineHeight: 1.05,
|
|
112
|
+
opacity: t,
|
|
113
|
+
transform: `translateY(${(1 - t) * 32}px)`,
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
__HOOK__
|
|
117
|
+
</span>
|
|
118
|
+
</WarmSurface>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const NameScene: React.FC = () => (
|
|
123
|
+
<WarmSurface
|
|
124
|
+
style={{
|
|
125
|
+
background: "transparent",
|
|
126
|
+
paddingInline: 64,
|
|
127
|
+
display: "flex",
|
|
128
|
+
justifyContent: "center",
|
|
129
|
+
alignItems: "center",
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<span
|
|
133
|
+
style={{
|
|
134
|
+
fontSize: 168,
|
|
135
|
+
fontWeight: 700,
|
|
136
|
+
color: palette.amber,
|
|
137
|
+
letterSpacing: -3,
|
|
138
|
+
textShadow: `0 0 60px ${palette.amberGlow}`,
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
__SUB__
|
|
142
|
+
</span>
|
|
143
|
+
</WarmSurface>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const BodyScene: React.FC = () => (
|
|
147
|
+
<WarmSurface
|
|
148
|
+
style={{
|
|
149
|
+
background: "transparent",
|
|
150
|
+
paddingTop: TOP_SAFE + 80,
|
|
151
|
+
paddingInline: 64,
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<BentoGrid cols={2} gap={18}>
|
|
155
|
+
{[1, 2, 3, 4].map((i) => (
|
|
156
|
+
<BentoCell key={i} startFrame={i * 6} idx={i} reduceMotion={REDUCE_MOTION} />
|
|
157
|
+
))}
|
|
158
|
+
</BentoGrid>
|
|
159
|
+
</WarmSurface>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const AnchorScene: React.FC = () => {
|
|
163
|
+
const frame = useCurrentFrame();
|
|
164
|
+
// Gentle camera pan via easing
|
|
165
|
+
const t = ease.power3Out(Math.min(1, frame / 60));
|
|
166
|
+
return (
|
|
167
|
+
<WarmSurface
|
|
168
|
+
style={{
|
|
169
|
+
background: "transparent",
|
|
170
|
+
paddingTop: TOP_SAFE + 100,
|
|
171
|
+
paddingInline: 64,
|
|
172
|
+
display: "flex",
|
|
173
|
+
flexDirection: "column",
|
|
174
|
+
gap: 24,
|
|
175
|
+
transform: `translateY(${(1 - t) * 24}px)`,
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
<ScaleBlurCounter
|
|
179
|
+
startFrame={0}
|
|
180
|
+
durationFrames={48}
|
|
181
|
+
toValue={1000}
|
|
182
|
+
suffix="+"
|
|
183
|
+
fontSize={168}
|
|
184
|
+
reduceMotion={REDUCE_MOTION}
|
|
185
|
+
/>
|
|
186
|
+
<span style={{ fontSize: 96, fontWeight: 700, color: palette.emerald, letterSpacing: -2 }}>
|
|
187
|
+
Payoff.
|
|
188
|
+
</span>
|
|
189
|
+
<span style={{ fontSize: 36, color: palette.fgSoft }}>
|
|
190
|
+
Use emerald only for "ship" / launch / success moments. Single-accent rule.
|
|
191
|
+
</span>
|
|
192
|
+
</WarmSurface>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const CtaScene: React.FC = () => (
|
|
197
|
+
<WarmSurface
|
|
198
|
+
style={{
|
|
199
|
+
background: "transparent",
|
|
200
|
+
paddingInline: 64,
|
|
201
|
+
paddingBottom: BOTTOM_SAFE + 80,
|
|
202
|
+
display: "flex",
|
|
203
|
+
flexDirection: "column",
|
|
204
|
+
gap: 20,
|
|
205
|
+
justifyContent: "flex-end",
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
<span style={{ fontSize: 64, fontWeight: 700, color: palette.amber }}>__CTA__</span>
|
|
209
|
+
</WarmSurface>
|
|
210
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** "Generic AI purple/violet" hexes commonly emitted by AI design tools.
|
|
2
|
+
* ReelStack bans them in scaffolded code; lint flags any color matching this list.
|
|
3
|
+
* Forbidden's ultraviolet (#6B5BD9) and plasma (#A87FE8) are family-specific overrides
|
|
4
|
+
* and are NOT in the blocklist (they live in the Forbidden palette explicitly).
|
|
5
|
+
*/
|
|
6
|
+
export const AI_PURPLE_HEXES = [
|
|
7
|
+
"#7c3aed", "#8b5cf6", "#6366f1", "#a78bfa", "#c084fc",
|
|
8
|
+
"#9333ea", "#7e22ce", "#a855f7",
|
|
9
|
+
] as const;
|
|
10
|
+
export const isAiPurple = (hex: string): boolean => {
|
|
11
|
+
const lc = hex.toLowerCase();
|
|
12
|
+
return AI_PURPLE_HEXES.some((b) => b.toLowerCase() === lc);
|
|
13
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Fonts banned by leonxlnx/taste-skill — generic AI defaults that signal lack of taste.
|
|
2
|
+
* Used by `reelstack lint` to flag any fontFamily containing one of these.
|
|
3
|
+
* Allowed in ReelStack: Geist, Geist Mono, system-ui (fallback). Editorial-serif options
|
|
4
|
+
* per Cream Paper / Warm Signature variant overlays: Lyon Text, Newsreader, Playfair Display.
|
|
5
|
+
*/
|
|
6
|
+
export const BANNED_FONTS = [
|
|
7
|
+
"Inter", "Roboto", "Helvetica", "Arial", "Open Sans", "Lato", "Source Sans Pro",
|
|
8
|
+
"Nunito", "Poppins", "Montserrat", "Raleway", "PT Sans", "Ubuntu",
|
|
9
|
+
] as const;
|
|
10
|
+
export const isBannedFont = (fontFamily: string): boolean =>
|
|
11
|
+
BANNED_FONTS.some((b) => fontFamily.toLowerCase().includes(b.toLowerCase()));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Soft-variant cubic-bezier easings (from leonxlnx/taste-skill-soft).
|
|
2
|
+
* Applied to Glass + Paper + Warm families' "premium polish" motion.
|
|
3
|
+
* Each is a CSS cubic-bezier string; for Remotion `interpolate({easing})`,
|
|
4
|
+
* use the matching JS-callback variant via `bezierEase(...)` helper.
|
|
5
|
+
*/
|
|
6
|
+
export const SOFT_BEZIERS = {
|
|
7
|
+
softInOut: "cubic-bezier(0.32, 0.72, 0, 1)",
|
|
8
|
+
softElastic: "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
9
|
+
softSnappy: "cubic-bezier(0.22, 1, 0.36, 1)",
|
|
10
|
+
} as const;
|
|
11
|
+
/** Bezier as a Remotion-friendly easing function. */
|
|
12
|
+
export const bezierEase = (mX1: number, mY1: number, mX2: number, mY2: number) => {
|
|
13
|
+
// Cubic bezier solver. Input t in [0,1], output curve value in [0,1].
|
|
14
|
+
// Standard implementation — handles the 4-control-point bezier curve.
|
|
15
|
+
const A = (a1: number, a2: number) => 1 - 3 * a2 + 3 * a1;
|
|
16
|
+
const B = (a1: number, a2: number) => 3 * a2 - 6 * a1;
|
|
17
|
+
const C = (a1: number) => 3 * a1;
|
|
18
|
+
const calcBezier = (t: number, a1: number, a2: number) =>
|
|
19
|
+
((A(a1, a2) * t + B(a1, a2)) * t + C(a1)) * t;
|
|
20
|
+
const slope = (t: number, a1: number, a2: number) =>
|
|
21
|
+
3 * A(a1, a2) * t * t + 2 * B(a1, a2) * t + C(a1);
|
|
22
|
+
const getTForX = (x: number): number => {
|
|
23
|
+
let t = x;
|
|
24
|
+
for (let i = 0; i < 8; i++) {
|
|
25
|
+
const cs = slope(t, mX1, mX2);
|
|
26
|
+
if (cs === 0) return t;
|
|
27
|
+
const cx = calcBezier(t, mX1, mX2) - x;
|
|
28
|
+
t -= cx / cs;
|
|
29
|
+
}
|
|
30
|
+
return t;
|
|
31
|
+
};
|
|
32
|
+
return (x: number) => calcBezier(getTForX(x), mY1, mY2);
|
|
33
|
+
};
|
|
34
|
+
export const softInOut = bezierEase(0.32, 0.72, 0, 1);
|
|
35
|
+
export const softElastic = bezierEase(0.16, 1, 0.3, 1);
|
|
36
|
+
export const softSnappy = bezierEase(0.22, 1, 0.36, 1);
|
package/utils/easing.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReelStack — GSAP→Remotion easing dictionary.
|
|
3
|
+
*
|
|
4
|
+
* Translates GSAP's easing vocabulary into Remotion-compatible callback
|
|
5
|
+
* functions. Use as the `easing` argument to `interpolate()`.
|
|
6
|
+
*
|
|
7
|
+
* Example:
|
|
8
|
+
* interpolate(frame, [0, 30], [0, 1], { easing: ease.power4Out })
|
|
9
|
+
*
|
|
10
|
+
* The matching spring configs match design-system.md:
|
|
11
|
+
* spring({ frame, fps, config: spring.snappy })
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type Easing = (t: number) => number;
|
|
15
|
+
|
|
16
|
+
const clamp = (n: number) => Math.max(0, Math.min(1, n));
|
|
17
|
+
|
|
18
|
+
// Power eases (1=linear, 2=quad, 3=cubic, 4=quart, 5=quint)
|
|
19
|
+
const power = (p: number) => ({
|
|
20
|
+
In: (t: number) => Math.pow(clamp(t), p),
|
|
21
|
+
Out: (t: number) => 1 - Math.pow(1 - clamp(t), p),
|
|
22
|
+
InOut: (t: number) =>
|
|
23
|
+
clamp(t) < 0.5
|
|
24
|
+
? Math.pow(2 * clamp(t), p) / 2
|
|
25
|
+
: 1 - Math.pow(-2 * clamp(t) + 2, p) / 2,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const expo = {
|
|
29
|
+
In: (t: number) => (t === 0 ? 0 : Math.pow(2, 10 * clamp(t) - 10)),
|
|
30
|
+
Out: (t: number) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * clamp(t))),
|
|
31
|
+
InOut: (t: number) => {
|
|
32
|
+
const x = clamp(t);
|
|
33
|
+
if (x === 0) return 0;
|
|
34
|
+
if (x === 1) return 1;
|
|
35
|
+
return x < 0.5
|
|
36
|
+
? Math.pow(2, 20 * x - 10) / 2
|
|
37
|
+
: (2 - Math.pow(2, -20 * x + 10)) / 2;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const back = {
|
|
42
|
+
In: (t: number) => {
|
|
43
|
+
const x = clamp(t);
|
|
44
|
+
const c1 = 1.70158;
|
|
45
|
+
const c3 = c1 + 1;
|
|
46
|
+
return c3 * x * x * x - c1 * x * x;
|
|
47
|
+
},
|
|
48
|
+
Out: (t: number) => {
|
|
49
|
+
const x = clamp(t);
|
|
50
|
+
const c1 = 1.70158;
|
|
51
|
+
const c3 = c1 + 1;
|
|
52
|
+
return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const ease = {
|
|
57
|
+
linear: (t: number) => clamp(t),
|
|
58
|
+
power2In: power(2).In,
|
|
59
|
+
power2Out: power(2).Out,
|
|
60
|
+
power3In: power(3).In,
|
|
61
|
+
power3Out: power(3).Out,
|
|
62
|
+
power4In: power(4).In,
|
|
63
|
+
power4Out: power(4).Out,
|
|
64
|
+
power5In: power(5).In,
|
|
65
|
+
power5Out: power(5).Out,
|
|
66
|
+
expoIn: expo.In,
|
|
67
|
+
expoOut: expo.Out,
|
|
68
|
+
expoInOut: expo.InOut,
|
|
69
|
+
backIn: back.In,
|
|
70
|
+
backOut: back.Out,
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Spring configs — pass directly to Remotion's `spring({ config })`.
|
|
75
|
+
*/
|
|
76
|
+
export const springs = {
|
|
77
|
+
smooth: { damping: 200, mass: 1, stiffness: 100 },
|
|
78
|
+
snappy: { damping: 20, mass: 1, stiffness: 200 },
|
|
79
|
+
gentle: { damping: 15, mass: 1, stiffness: 80 },
|
|
80
|
+
bouncy: { damping: 8, mass: 1, stiffness: 100 },
|
|
81
|
+
glass: { damping: 12, mass: 1, stiffness: 120 },
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
export type SpringName = keyof typeof springs;
|
package/utils/grid.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** ReelStack 4-px grid system. Every padding/margin/gap should be a multiple of GRID. */
|
|
2
|
+
export const GRID = 4;
|
|
3
|
+
export const GRID_2 = GRID * 2; // 8
|
|
4
|
+
export const GRID_3 = GRID * 3; // 12
|
|
5
|
+
export const GRID_4 = GRID * 4; // 16
|
|
6
|
+
export const GRID_5 = GRID * 5; // 20
|
|
7
|
+
export const GRID_6 = GRID * 6; // 24
|
|
8
|
+
export const GRID_8 = GRID * 8; // 32
|
|
9
|
+
export const GRID_12 = GRID * 12; // 48
|
|
10
|
+
export const GRID_16 = GRID * 16; // 64
|
|
11
|
+
export const GRID_20 = GRID * 20; // 80
|
|
12
|
+
export const GRID_24 = GRID * 24; // 96
|
|
13
|
+
export const isOnGrid = (n: number): boolean => n % GRID === 0;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ig": {
|
|
3
|
+
"label": "Instagram Reels",
|
|
4
|
+
"width": 1080,
|
|
5
|
+
"height": 1920,
|
|
6
|
+
"fps": 30,
|
|
7
|
+
"codec": "h264",
|
|
8
|
+
"videoBitrate": "8M",
|
|
9
|
+
"audioCodec": "aac",
|
|
10
|
+
"audioBitrate": "192k",
|
|
11
|
+
"pixelFormat": "yuv420p",
|
|
12
|
+
"remotionFlags": [
|
|
13
|
+
"--codec=h264",
|
|
14
|
+
"--video-bitrate=8M",
|
|
15
|
+
"--audio-codec=aac",
|
|
16
|
+
"--audio-bitrate=192k",
|
|
17
|
+
"--pixel-format=yuv420p"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"tiktok": {
|
|
21
|
+
"label": "TikTok",
|
|
22
|
+
"width": 1080,
|
|
23
|
+
"height": 1920,
|
|
24
|
+
"fps": 30,
|
|
25
|
+
"codec": "h264",
|
|
26
|
+
"videoBitrate": "8M",
|
|
27
|
+
"audioCodec": "aac",
|
|
28
|
+
"audioBitrate": "192k",
|
|
29
|
+
"pixelFormat": "yuv420p",
|
|
30
|
+
"remotionFlags": [
|
|
31
|
+
"--codec=h264",
|
|
32
|
+
"--video-bitrate=8M",
|
|
33
|
+
"--audio-codec=aac",
|
|
34
|
+
"--audio-bitrate=192k",
|
|
35
|
+
"--pixel-format=yuv420p"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"shorts": {
|
|
39
|
+
"label": "YouTube Shorts",
|
|
40
|
+
"width": 1080,
|
|
41
|
+
"height": 1920,
|
|
42
|
+
"fps": 30,
|
|
43
|
+
"codec": "h264",
|
|
44
|
+
"videoBitrate": "9M",
|
|
45
|
+
"audioCodec": "aac",
|
|
46
|
+
"audioBitrate": "192k",
|
|
47
|
+
"pixelFormat": "yuv420p",
|
|
48
|
+
"remotionFlags": [
|
|
49
|
+
"--codec=h264",
|
|
50
|
+
"--video-bitrate=9M",
|
|
51
|
+
"--audio-codec=aac",
|
|
52
|
+
"--audio-bitrate=192k",
|
|
53
|
+
"--pixel-format=yuv420p"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AbsoluteFill } from "remotion";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SafeZones — IG/TikTok/Shorts overlay component.
|
|
6
|
+
*
|
|
7
|
+
* Visually marks the top 290 px and bottom 422 px of the 1920 canvas as
|
|
8
|
+
* reserved (Instagram chrome, TikTok username + caption, Shorts CTA bar).
|
|
9
|
+
*
|
|
10
|
+
* Pass `visible={false}` for production renders. For Remotion Studio
|
|
11
|
+
* iteration, pass `visible={process.env.NODE_ENV !== "production"}`.
|
|
12
|
+
*/
|
|
13
|
+
export const TOP_SAFE = 290;
|
|
14
|
+
export const BOTTOM_SAFE = 422; // bottom 22% — y where reserved band starts is 1920 - 422 = 1498
|
|
15
|
+
|
|
16
|
+
export const SafeZones: React.FC<{
|
|
17
|
+
visible?: boolean;
|
|
18
|
+
tint?: string;
|
|
19
|
+
}> = ({ visible = true, tint = "rgba(255, 0, 100, 0.10)" }) => {
|
|
20
|
+
if (!visible) return null;
|
|
21
|
+
return (
|
|
22
|
+
<AbsoluteFill style={{ pointerEvents: "none" }}>
|
|
23
|
+
<div
|
|
24
|
+
style={{
|
|
25
|
+
position: "absolute",
|
|
26
|
+
top: 0,
|
|
27
|
+
left: 0,
|
|
28
|
+
right: 0,
|
|
29
|
+
height: TOP_SAFE,
|
|
30
|
+
background: tint,
|
|
31
|
+
borderBottom: "2px dashed rgba(255, 0, 100, 0.4)",
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
<div
|
|
35
|
+
style={{
|
|
36
|
+
position: "absolute",
|
|
37
|
+
bottom: 0,
|
|
38
|
+
left: 0,
|
|
39
|
+
right: 0,
|
|
40
|
+
height: BOTTOM_SAFE,
|
|
41
|
+
background: tint,
|
|
42
|
+
borderTop: "2px dashed rgba(255, 0, 100, 0.4)",
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
</AbsoluteFill>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Helper that returns true if a y-coordinate (and optional height) overlaps
|
|
51
|
+
* a reserved band. Used by `reelstack lint`.
|
|
52
|
+
*/
|
|
53
|
+
export function overlapsSafeZone(y: number, height = 0): "top" | "bottom" | null {
|
|
54
|
+
if (y < TOP_SAFE) return "top";
|
|
55
|
+
if (y + height > 1920 - BOTTOM_SAFE) return "bottom";
|
|
56
|
+
return null;
|
|
57
|
+
}
|