cinematic-scroll-skill 2.1.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/COMPATIBILITY.md +244 -0
- package/LICENSE +21 -0
- package/MODELS.md +92 -0
- package/README.md +250 -0
- package/SKILL.md +1003 -0
- package/audit-mode.md +497 -0
- package/bin/install.mjs +91 -0
- package/compile-choreography.mjs +296 -0
- package/decision-log.md +241 -0
- package/examples/GETTING_STARTED.md +279 -0
- package/examples/KNOWN_ISSUES.md +50 -0
- package/examples/PROMPTS.md +166 -0
- package/examples/luxe/README.md +88 -0
- package/examples/luxe/index.html +662 -0
- package/examples/noir/README.md +72 -0
- package/examples/noir/index.html +634 -0
- package/examples/pop/README.md +81 -0
- package/examples/pop/index.html +711 -0
- package/examples/renaissance/README.md +39 -0
- package/examples/renaissance/index.html +648 -0
- package/examples/studio/README.md +77 -0
- package/examples/studio/chapters.js +105 -0
- package/examples/studio/index.html +520 -0
- package/manifest.json +92 -0
- package/manifest.md +136 -0
- package/package.json +56 -0
- package/references/film-archetypes.md +211 -0
- package/references/performance-budget.md +499 -0
- package/references/scroll-patterns.md +693 -0
- package/scroll-choreography-compilation.md +543 -0
- package/scroll-choreography.json +1512 -0
- package/taste-guardrails.md +164 -0
- package/templates/nextjs/.env.example +41 -0
- package/templates/nextjs/app/api/fal/proxy/route.ts +33 -0
- package/templates/nextjs/app/api/fal/webhook/route.ts +132 -0
- package/templates/nextjs/app/api/generate-edition-asset/route.ts +66 -0
- package/templates/nextjs/app/globals.css +80 -0
- package/templates/nextjs/app/layout.tsx +21 -0
- package/templates/nextjs/app/page.tsx +10 -0
- package/templates/nextjs/components/ChapterDemoVisual.tsx +212 -0
- package/templates/nextjs/components/ChapterScene.tsx +373 -0
- package/templates/nextjs/components/EditionsPage.tsx +116 -0
- package/templates/nextjs/components/SmoothScrollProvider.tsx +8 -0
- package/templates/nextjs/lib/api-guard.ts +110 -0
- package/templates/nextjs/lib/editions-manifest.ts +224 -0
- package/templates/nextjs/lib/fal-client.ts +12 -0
- package/templates/nextjs/lib/fal-generate.ts +86 -0
- package/templates/nextjs/lib/fal-models.ts +213 -0
- package/templates/nextjs/lib/prompt-contract.ts +97 -0
- package/templates/nextjs/lib/use-device.ts +42 -0
- package/templates/nextjs/lib/use-lenis.ts +35 -0
- package/templates/nextjs/next.config.ts +29 -0
- package/templates/nextjs/package-lock.json +6455 -0
- package/templates/nextjs/package.json +41 -0
- package/templates/nextjs/package.patch.json +28 -0
- package/templates/nextjs/postcss.config.js +6 -0
- package/templates/nextjs/scripts/generate-chapter-assets.mjs +243 -0
- package/templates/nextjs/scripts/setup.mjs +170 -0
- package/templates/nextjs/tailwind.config.ts +37 -0
- package/templates/nextjs/tsconfig.json +23 -0
- package/troubleshooting.md +1284 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<title>Maya Torres — Creative Director · cinematic-scroll example</title>
|
|
7
|
+
<meta name="description" content="A brutalist Swiss-editorial cinematic-scroll page — giant grotesk type, monochrome + electric accent, parallax stills. The second flagship example for the cinematic-scroll Agent Skill." />
|
|
8
|
+
|
|
9
|
+
<!-- Open Graph -->
|
|
10
|
+
<meta property="og:title" content="Maya Torres — Creative Director" />
|
|
11
|
+
<meta property="og:description" content="Brutalist cinematic scroll — giant type, monochrome, parallax. Built with the cinematic-scroll Agent Skill." />
|
|
12
|
+
<meta property="og:type" content="website" />
|
|
13
|
+
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16
|
+
<!-- Archivo (grotesk display + UI), JetBrains Mono (labels) -->
|
|
17
|
+
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
18
|
+
|
|
19
|
+
<!-- GSAP + ScrollTrigger (now 100% free) — powers ONE dedicated showcase beat:
|
|
20
|
+
a MONTAGE SNAP contact-sheet (taste-guardrails §2 "Montage") + a
|
|
21
|
+
VELOCITY-REACTIVE heading (scroll-patterns.md #3). Deferred + feature-
|
|
22
|
+
detected: if the CDN fails, the hand-rolled rAF engine below still runs and
|
|
23
|
+
the montage stays at its complete static state. -->
|
|
24
|
+
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js"></script>
|
|
25
|
+
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/ScrollTrigger.min.js"></script>
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
:root{
|
|
29
|
+
--ink:#111113; --grey:#E6E6E6; --grey2:#D4D4D6; --white:#FFFFFF;
|
|
30
|
+
--muted:#6B6B70; --accent:#0F4CFF;
|
|
31
|
+
--bg:#E6E6E6; --fg:#111113; --fg-muted:#6B6B70; --line:rgba(17,17,19,.16);
|
|
32
|
+
}
|
|
33
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
34
|
+
html{scroll-behavior:smooth}
|
|
35
|
+
body{
|
|
36
|
+
background:var(--bg); color:var(--fg);
|
|
37
|
+
font-family:'Archivo',system-ui,sans-serif; font-weight:500;
|
|
38
|
+
-webkit-font-smoothing:antialiased; overflow-x:hidden;
|
|
39
|
+
transition:background .6s linear, color .6s linear;
|
|
40
|
+
}
|
|
41
|
+
::selection{ background:var(--accent); color:#fff; }
|
|
42
|
+
|
|
43
|
+
/* ── fixed masthead ───────────────────────────────────────── */
|
|
44
|
+
.masthead{
|
|
45
|
+
position:fixed; inset:0 0 auto 0; z-index:50;
|
|
46
|
+
display:flex; align-items:center; justify-content:space-between;
|
|
47
|
+
padding:22px clamp(18px,4vw,56px);
|
|
48
|
+
mix-blend-mode:difference; color:#fff; /* legible over any morph */
|
|
49
|
+
pointer-events:none;
|
|
50
|
+
}
|
|
51
|
+
.masthead .mark{ font-weight:800; letter-spacing:-.02em; font-size:16px; pointer-events:auto;}
|
|
52
|
+
.masthead nav{ display:flex; gap:clamp(14px,2.4vw,34px); font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.16em; text-transform:uppercase; pointer-events:auto;}
|
|
53
|
+
.masthead nav a{ color:inherit; text-decoration:none; opacity:.85;}
|
|
54
|
+
@media (max-width:680px){ .masthead nav{ display:none; } }
|
|
55
|
+
|
|
56
|
+
/* ── progress + index rail ────────────────────────────────── */
|
|
57
|
+
.rail{
|
|
58
|
+
position:fixed; right:clamp(12px,2vw,26px); top:50%; transform:translateY(-50%);
|
|
59
|
+
z-index:40; display:flex; flex-direction:column; gap:13px;
|
|
60
|
+
font-family:'JetBrains Mono',monospace; font-size:11px; mix-blend-mode:difference; color:#fff;
|
|
61
|
+
}
|
|
62
|
+
.rail .r{ display:flex; align-items:center; gap:10px; justify-content:flex-end; opacity:.5; transition:opacity .3s; }
|
|
63
|
+
.rail .r .ln{ width:22px; height:1px; background:currentColor; transition:width .3s; }
|
|
64
|
+
.rail .r.on{ opacity:1; }
|
|
65
|
+
.rail .r.on .ln{ width:46px; background:var(--accent); height:2px; }
|
|
66
|
+
@media (max-width:680px){ .rail{ display:none; } }
|
|
67
|
+
|
|
68
|
+
/* ── chapter section ──────────────────────────────────────── */
|
|
69
|
+
section{
|
|
70
|
+
position:relative; min-height:235vh; /* pin room */
|
|
71
|
+
}
|
|
72
|
+
.stage{
|
|
73
|
+
position:sticky; top:0; height:100vh; overflow:hidden;
|
|
74
|
+
display:grid; grid-template-columns:1fr; align-content:center;
|
|
75
|
+
padding:0 clamp(18px,5vw,72px);
|
|
76
|
+
perspective:1400px; /* scroll-driven 3D camera depth */
|
|
77
|
+
perspective-origin:60% 50%;
|
|
78
|
+
}
|
|
79
|
+
.figwrap, .copytext, .aicard{ transform-style:preserve-3d; }
|
|
80
|
+
.figwrap .frame{ transform-style:preserve-3d; }
|
|
81
|
+
|
|
82
|
+
/* giant grotesk type — the hero element */
|
|
83
|
+
.copytext{ position:relative; z-index:6; max-width:min(92vw,1400px); will-change:transform,opacity; }
|
|
84
|
+
.eyebrow{ font-family:'JetBrains Mono',monospace; font-size:clamp(10px,1vw,12px); letter-spacing:.34em; text-transform:uppercase; color:var(--accent); margin-bottom:clamp(14px,2vw,26px); }
|
|
85
|
+
h2.giant{
|
|
86
|
+
font-weight:900; line-height:.86; letter-spacing:-.03em; text-transform:uppercase;
|
|
87
|
+
font-size:clamp(2.8rem,11.5vw,11rem);
|
|
88
|
+
}
|
|
89
|
+
h2.giant .w{ display:inline-block; margin-right:.12em; will-change:transform,opacity; }
|
|
90
|
+
h2.giant .w.accent{ color:var(--accent); }
|
|
91
|
+
.lede{ margin-top:clamp(20px,2.6vw,34px); max-width:46ch; font-size:clamp(1rem,1.5vw,1.32rem); line-height:1.5; color:var(--fg-muted); font-weight:500; }
|
|
92
|
+
|
|
93
|
+
/* ghost watermark word sliding across */
|
|
94
|
+
.ghost{
|
|
95
|
+
position:absolute; z-index:1; font-weight:900; text-transform:uppercase;
|
|
96
|
+
font-size:clamp(8rem,30vw,28rem); line-height:.8; letter-spacing:-.04em;
|
|
97
|
+
color:var(--fg); opacity:.04; white-space:nowrap; pointer-events:none;
|
|
98
|
+
top:50%; left:50%; transform:translate(-50%,-50%); will-change:transform;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* framed parallax still (under the type) */
|
|
102
|
+
.figwrap{ position:absolute; z-index:3; right:clamp(18px,5vw,72px); top:50%;
|
|
103
|
+
width:clamp(280px,38vw,560px); aspect-ratio:4/5; transform:translateY(-50%);
|
|
104
|
+
will-change:transform,opacity; perspective:1200px; }
|
|
105
|
+
.frame{ position:relative; width:100%; height:100%; }
|
|
106
|
+
.pic{
|
|
107
|
+
position:absolute; inset:0; background-size:cover; background-position:center;
|
|
108
|
+
background-color:var(--grey2);
|
|
109
|
+
filter:grayscale(1) contrast(1.06);
|
|
110
|
+
transition:transform .25s ease-out;
|
|
111
|
+
box-shadow:0 30px 80px rgba(0,0,0,.28);
|
|
112
|
+
}
|
|
113
|
+
/* CSS-only placeholder visual (when no real image present) */
|
|
114
|
+
.pic.placeholder{
|
|
115
|
+
background-image:
|
|
116
|
+
repeating-linear-gradient(135deg, rgba(0,0,0,.05) 0 2px, transparent 2px 7px),
|
|
117
|
+
radial-gradient(120% 90% at 30% 20%, #fafafa 0%, #cfcfd2 55%, #2a2a2e 100%);
|
|
118
|
+
}
|
|
119
|
+
.pic::after{ /* grain */
|
|
120
|
+
content:""; position:absolute; inset:0; opacity:.5; mix-blend-mode:multiply;
|
|
121
|
+
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.35'/%3E%3C/svg%3E");
|
|
122
|
+
}
|
|
123
|
+
.figcap{ position:absolute; left:0; bottom:-26px; display:flex; justify-content:space-between; width:100%;
|
|
124
|
+
font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.18em; color:var(--fg-muted); }
|
|
125
|
+
/* corner brackets */
|
|
126
|
+
.frame .br{ position:absolute; width:26px; height:26px; border:2px solid var(--accent); }
|
|
127
|
+
.frame .br.tl{ top:-1px; left:-1px; border-right:0; border-bottom:0; }
|
|
128
|
+
.frame .br.tr{ top:-1px; right:-1px; border-left:0; border-bottom:0; }
|
|
129
|
+
.frame .br.bl{ bottom:-1px; left:-1px; border-right:0; border-top:0; }
|
|
130
|
+
.frame .br.brc{ bottom:-1px; right:-1px; border-left:0; border-top:0; }
|
|
131
|
+
|
|
132
|
+
/* floating mono UI card (foreground) */
|
|
133
|
+
.aicard{ position:absolute; z-index:7; left:clamp(18px,5vw,72px); bottom:clamp(60px,12vh,120px);
|
|
134
|
+
width:min(300px,72vw); padding:14px 16px; background:rgba(255,255,255,.86);
|
|
135
|
+
border:1px solid var(--line); backdrop-filter:blur(6px);
|
|
136
|
+
font-family:'JetBrains Mono',monospace; will-change:transform,opacity; }
|
|
137
|
+
.aicard .t{ font-size:11px; color:var(--accent); margin-bottom:6px; letter-spacing:.04em; }
|
|
138
|
+
.aicard .b{ font-size:11px; line-height:1.5; color:var(--ink); }
|
|
139
|
+
section.dark .aicard{ background:rgba(20,20,22,.82); }
|
|
140
|
+
section.dark .aicard .b{ color:#e8e8ea; }
|
|
141
|
+
|
|
142
|
+
/* mobile / reduced-motion fallback */
|
|
143
|
+
@media (max-width:680px){
|
|
144
|
+
section{ min-height:auto; padding:14vh 0; }
|
|
145
|
+
.stage{ position:static; height:auto; display:block; }
|
|
146
|
+
.figwrap{ position:static; width:100%; transform:none; margin:24px 0 40px; aspect-ratio:16/10; opacity:1!important; }
|
|
147
|
+
.ghost{ display:none; }
|
|
148
|
+
.aicard{ position:static; margin-top:18px; }
|
|
149
|
+
h2.giant{ font-size:18vw; }
|
|
150
|
+
.copytext .w,.copytext .reveal{ opacity:1!important; transform:none!important; letter-spacing:normal!important; }
|
|
151
|
+
}
|
|
152
|
+
@media (prefers-reduced-motion:reduce){
|
|
153
|
+
html{ scroll-behavior:auto; }
|
|
154
|
+
.copytext .w,.copytext .reveal,.figwrap{ opacity:1!important; transform:none!important; letter-spacing:normal!important; }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
footer{ padding:14vh clamp(18px,5vw,72px) 16vh; }
|
|
158
|
+
footer .big{ font-weight:900; text-transform:uppercase; font-size:clamp(2rem,8vw,6rem); line-height:.9; letter-spacing:-.03em; }
|
|
159
|
+
footer .big a{ color:var(--accent); text-decoration:none; }
|
|
160
|
+
footer .meta{ margin-top:28px; font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.14em; color:var(--fg-muted); text-transform:uppercase; }
|
|
161
|
+
footer .meta a{ color:inherit; }
|
|
162
|
+
|
|
163
|
+
/* ── GSAP SHOWCASE — "SELECTED WORK" CONTACT SHEET ────────────────────
|
|
164
|
+
The ONE GSAP-pinned beat (studio's normal chapters use CSS sticky, NOT
|
|
165
|
+
ScrollTrigger pins — so this is the only pin and never stacks >3 deep).
|
|
166
|
+
Technique 1 · MONTAGE SNAP (taste-guardrails §2 "Montage" / scroll-
|
|
167
|
+
patterns.md "Landing Sequence"): a horizontal track of N work-cards
|
|
168
|
+
advanced by transform:translate on the scrubbed pin timeline, snapping
|
|
169
|
+
card→card.
|
|
170
|
+
Technique 2 · VELOCITY-REACTIVE typography (pattern #3): the big display
|
|
171
|
+
heading compresses letter-spacing + scaleY on fast scroll.
|
|
172
|
+
STATIC DEFAULTS below keep it whole when GSAP is absent / reduced-motion /
|
|
173
|
+
mobile: the section is normal flow, the track wraps, all cards visible. */
|
|
174
|
+
section.showcase{ min-height:0; } /* GSAP pin owns the scroll room when active */
|
|
175
|
+
.montage{ position:relative; padding:16vh clamp(18px,5vw,72px) 14vh; overflow:hidden; }
|
|
176
|
+
.montage .head{ margin-bottom:clamp(28px,5vh,56px); }
|
|
177
|
+
.montage .eyebrow{ font-family:'JetBrains Mono',monospace; font-size:clamp(10px,1vw,12px);
|
|
178
|
+
letter-spacing:.34em; text-transform:uppercase; color:var(--accent); margin-bottom:clamp(12px,1.6vw,20px); }
|
|
179
|
+
/* large display heading — the velocity-reactive target (≥48px, never body) */
|
|
180
|
+
.montage h2.mtitle{ font-weight:900; line-height:.86; letter-spacing:-.02em; text-transform:uppercase;
|
|
181
|
+
font-size:clamp(3rem,9vw,8rem); transform-origin:left center; }
|
|
182
|
+
.montage h2.mtitle .accent{ color:var(--accent); }
|
|
183
|
+
/* contact-sheet track — STATIC DEFAULT: flex-wrap grid (all cards on screen) */
|
|
184
|
+
.montage .track{ display:flex; flex-wrap:wrap; gap:clamp(14px,2vw,28px); }
|
|
185
|
+
.mcard{ position:relative; flex:1 1 clamp(200px,18vw,260px); aspect-ratio:4/5;
|
|
186
|
+
background:var(--white); border:1px solid var(--line); padding:16px 18px;
|
|
187
|
+
display:flex; flex-direction:column; justify-content:space-between; }
|
|
188
|
+
.mcard .mi{ font-weight:900; font-size:clamp(2.6rem,5vw,4.4rem); line-height:.9; letter-spacing:-.04em; color:var(--ink); }
|
|
189
|
+
.mcard .ml{ font-weight:700; font-size:clamp(1rem,1.5vw,1.3rem); line-height:1.12; color:var(--ink); }
|
|
190
|
+
.mcard .mm{ font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.16em;
|
|
191
|
+
text-transform:uppercase; color:var(--fg-muted); }
|
|
192
|
+
.mcard .mb{ position:absolute; top:0; left:0; height:3px; width:100%; background:var(--accent); transform-origin:left; }
|
|
193
|
+
/* accent on the last "open" card */
|
|
194
|
+
.mcard:last-child{ background:var(--ink); }
|
|
195
|
+
.mcard:last-child .mi,.mcard:last-child .ml{ color:var(--white); }
|
|
196
|
+
.mcard:last-child .mm{ color:#9A9AA0; }
|
|
197
|
+
|
|
198
|
+
@media (max-width:680px){
|
|
199
|
+
/* static stacked contact sheet — no pin, no snap, no velocity */
|
|
200
|
+
.montage{ padding:13vh 22px 11vh; }
|
|
201
|
+
.montage h2.mtitle{ font-size:14vw; transform:none!important; letter-spacing:-.02em!important; }
|
|
202
|
+
.montage .track{ flex-direction:column; flex-wrap:nowrap; gap:16px; }
|
|
203
|
+
.mcard{ flex:1 1 auto; aspect-ratio:16/9; }
|
|
204
|
+
}
|
|
205
|
+
@media (prefers-reduced-motion:reduce){
|
|
206
|
+
.montage h2.mtitle{ transform:none!important; letter-spacing:-.02em!important; }
|
|
207
|
+
}
|
|
208
|
+
</style>
|
|
209
|
+
</head>
|
|
210
|
+
<body>
|
|
211
|
+
|
|
212
|
+
<header class="masthead">
|
|
213
|
+
<div class="mark">MAYA TORRES</div>
|
|
214
|
+
<nav>
|
|
215
|
+
<a href="#work">Work</a>
|
|
216
|
+
<a href="#recognition">Recognition</a>
|
|
217
|
+
<a href="#contact">Contact</a>
|
|
218
|
+
</nav>
|
|
219
|
+
</header>
|
|
220
|
+
|
|
221
|
+
<div class="rail" id="rail"></div>
|
|
222
|
+
|
|
223
|
+
<main id="app"></main>
|
|
224
|
+
|
|
225
|
+
<footer id="contact">
|
|
226
|
+
<div class="big">Let’s make work<br>that <a href="mailto:studio@example.com">matters →</a></div>
|
|
227
|
+
<div class="meta">Built with the <a href="https://github.com/MustBeSimo/cinematic-scroll-skill">cinematic-scroll</a> Agent Skill · fictional persona, no real brands · one of many possible aesthetics</div>
|
|
228
|
+
</footer>
|
|
229
|
+
|
|
230
|
+
<script type="module">
|
|
231
|
+
import { CHAPTERS, PALETTE, MONTAGE } from './chapters.js';
|
|
232
|
+
|
|
233
|
+
const clamp=(v,a,b)=>Math.min(b,Math.max(a,v));
|
|
234
|
+
const smooth=(e0,e1,x)=>{ if(x<=e0)return 0; if(x>=e1)return 1; const t=(x-e0)/(e1-e0); return t*t*(3-2*t); };
|
|
235
|
+
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
236
|
+
const isTouch = matchMedia('(pointer: coarse)').matches;
|
|
237
|
+
const isMobile = () => innerWidth <= 680;
|
|
238
|
+
|
|
239
|
+
const app = document.getElementById('app');
|
|
240
|
+
const rail = document.getElementById('rail');
|
|
241
|
+
|
|
242
|
+
// preflight: which real images exist? (fall back to CSS placeholder)
|
|
243
|
+
function probe(src){ return new Promise(res=>{ const i=new Image(); i.onload=()=>res(true); i.onerror=()=>res(false); i.src=src; }); }
|
|
244
|
+
|
|
245
|
+
const ghostWord = t => (t.flat().filter(x=>typeof x==='string').join(' ').split(' ')[0]||'').toUpperCase();
|
|
246
|
+
|
|
247
|
+
(async () => {
|
|
248
|
+
for (const ch of CHAPTERS){
|
|
249
|
+
const sec = document.createElement('section');
|
|
250
|
+
sec.id = ch.id.replace(/^\d+-/,'');
|
|
251
|
+
if (ch.depthTextDark) sec.classList.add('dark');
|
|
252
|
+
sec.dataset.morph = ch.morph;
|
|
253
|
+
sec.dataset.dark = ch.depthTextDark ? '1':'0';
|
|
254
|
+
|
|
255
|
+
const titleHTML = ch.title.map(([chunk,acc]) =>
|
|
256
|
+
chunk.split(' ').map(w=>`<span class="w${acc?' accent':''}">${w}</span>`).join(' ')
|
|
257
|
+
).join(' ');
|
|
258
|
+
|
|
259
|
+
const imgPath = `assets/${ch.id}.jpg`;
|
|
260
|
+
const hasImg = await probe(imgPath);
|
|
261
|
+
const picStyle = hasImg ? `background-image:url('${imgPath}')` : '';
|
|
262
|
+
const picClass = hasImg ? 'pic' : 'pic placeholder';
|
|
263
|
+
|
|
264
|
+
sec.innerHTML = `
|
|
265
|
+
<div class="stage">
|
|
266
|
+
<div class="ghost" data-depth="0.3">${ghostWord(ch.title)}</div>
|
|
267
|
+
<div class="figwrap" data-depth="0.5">
|
|
268
|
+
<div class="frame">
|
|
269
|
+
<span class="br tl"></span><span class="br tr"></span><span class="br bl"></span><span class="br brc"></span>
|
|
270
|
+
<div class="${picClass}" style="${picStyle}"></div>
|
|
271
|
+
<div class="figcap"><span>${ch.fig}</span><span>${ch.figLabel}</span></div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
<div class="copytext">
|
|
275
|
+
<div class="eyebrow">${ch.eyebrow}</div>
|
|
276
|
+
<h2 class="giant">${titleHTML}</h2>
|
|
277
|
+
<p class="lede reveal">${ch.body}</p>
|
|
278
|
+
</div>
|
|
279
|
+
<div class="aicard" data-depth="1.25">
|
|
280
|
+
<div class="t">${ch.card.tag}</div>
|
|
281
|
+
<div class="b">${ch.card.body}</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>`;
|
|
284
|
+
app.appendChild(sec);
|
|
285
|
+
|
|
286
|
+
const r = document.createElement('div');
|
|
287
|
+
r.className='r'; r.dataset.id=ch.id;
|
|
288
|
+
r.innerHTML = `<span>${ch.id.split('-')[0].padStart(2,'0')}</span><span class="ln"></span>`;
|
|
289
|
+
rail.appendChild(r);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* ── APPEND THE GSAP SHOWCASE BEAT ("SELECTED WORK" contact sheet) ────
|
|
293
|
+
Inserted right after THE WORK chapter (2-work) so it reads: work →
|
|
294
|
+
contact-sheet montage → craft. Built BEFORE sections/rails are collected
|
|
295
|
+
so the centre-spy and index rail stay index-aligned (the spy uses
|
|
296
|
+
sections.indexOf against the parallel rails[] array). The montage is the
|
|
297
|
+
ONE GSAP-pinned section; studio's chapters are CSS-sticky (not pins), so
|
|
298
|
+
this never creates >3 consecutive ScrollTrigger pins (§1.3) and the
|
|
299
|
+
chapters on either side give >80vh of free-scroll release. Pure markup
|
|
300
|
+
here; motion is wired at the end (desktop-only, only if GSAP loaded). */
|
|
301
|
+
const cardsHTML = MONTAGE.map(c => `
|
|
302
|
+
<article class="mcard">
|
|
303
|
+
<span class="mb"></span>
|
|
304
|
+
<div class="mi">${c.idx}</div>
|
|
305
|
+
<div>
|
|
306
|
+
<div class="ml">${c.label}</div>
|
|
307
|
+
<div class="mm">${c.year} · ${c.meta}</div>
|
|
308
|
+
</div>
|
|
309
|
+
</article>`).join('');
|
|
310
|
+
const show = document.createElement('section');
|
|
311
|
+
show.className = 'showcase'; show.id = 'selected';
|
|
312
|
+
show.dataset.morph = '#E6E6E6'; show.dataset.dark = '0';
|
|
313
|
+
show.innerHTML = `
|
|
314
|
+
<div class="montage">
|
|
315
|
+
<div class="head">
|
|
316
|
+
<div class="eyebrow">SELECTED WORK · CONTACT SHEET</div>
|
|
317
|
+
<h2 class="mtitle">Twelve years<span class="accent">, on file.</span></h2>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="track">${cardsHTML}</div>
|
|
320
|
+
</div>`;
|
|
321
|
+
const workSec = [...app.children].find(s => s.id === 'work');
|
|
322
|
+
app.insertBefore(show, workSec ? workSec.nextSibling : null); // after 2-work
|
|
323
|
+
const sr = document.createElement('div');
|
|
324
|
+
sr.className = 'r'; sr.dataset.id = 'selected';
|
|
325
|
+
sr.innerHTML = `<span>W</span><span class="ln"></span>`;
|
|
326
|
+
const workRail = [...rail.children].find(r => r.dataset.id === '2-work');
|
|
327
|
+
rail.insertBefore(sr, workRail ? workRail.nextSibling : null);
|
|
328
|
+
|
|
329
|
+
const sections = [...document.querySelectorAll('section')];
|
|
330
|
+
const rails = [...rail.querySelectorAll('.r')];
|
|
331
|
+
|
|
332
|
+
/* background morph + active rail via whichever section crosses screen-center.
|
|
333
|
+
NOTE: sections are 235vh — taller than the viewport — so a threshold-based
|
|
334
|
+
observer (which needs N% of the element visible) can NEVER fire. Instead we
|
|
335
|
+
collapse the observer root to a 1px band at the vertical center via
|
|
336
|
+
rootMargin: the section whose body straddles the middle of the screen is the
|
|
337
|
+
active one. Reliable regardless of section height. */
|
|
338
|
+
function applyActive(sec){
|
|
339
|
+
const idx = sections.indexOf(sec);
|
|
340
|
+
document.body.style.background = sec.dataset.morph;
|
|
341
|
+
const dark = sec.dataset.dark==='1';
|
|
342
|
+
document.body.style.setProperty('--fg', dark ? '#FFFFFF':'#111113');
|
|
343
|
+
document.body.style.setProperty('--fg-muted', dark ? '#9A9AA0':'#6B6B70');
|
|
344
|
+
rails.forEach((r,i)=> r.classList.toggle('on', i===idx));
|
|
345
|
+
}
|
|
346
|
+
const spy = new IntersectionObserver((entries)=>{
|
|
347
|
+
entries.forEach(e=>{ if (e.isIntersecting) applyActive(e.target); });
|
|
348
|
+
},{ rootMargin: '-50% 0px -50% 0px', threshold: 0 });
|
|
349
|
+
sections.forEach(s=>spy.observe(s));
|
|
350
|
+
applyActive(sections[0]); // paint initial state immediately
|
|
351
|
+
|
|
352
|
+
/* mobile / reduced-motion: fade-up only */
|
|
353
|
+
if (reduce || isMobile()){
|
|
354
|
+
const up=new IntersectionObserver((es)=>{es.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');up.unobserve(e.target);}});},{threshold:.15});
|
|
355
|
+
document.querySelectorAll('.reveal').forEach(el=>up.observe(el));
|
|
356
|
+
return; // skip the rAF parallax engine entirely on mobile
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* desktop parallax — BOLD cinematic engine.
|
|
360
|
+
Unified approach→pin→exit timeline T∈[0,1]; never starts invisible.
|
|
361
|
+
Layers move on different planes with real scroll-driven 3D:
|
|
362
|
+
• figure: Y-drift + Z-dolly toward viewer + rotateY/rotateX camera swing
|
|
363
|
+
• giant type: per-word Z-stagger (words arrive at staggered depths) + rotateX settle
|
|
364
|
+
• ghost watermark: long horizontal sweep, counter-axis to the type
|
|
365
|
+
• card: faster drift + slight rotateY */
|
|
366
|
+
let mx=0,my=0; // pointer for the live 3D camera, blended with scroll
|
|
367
|
+
if(!isTouch && !reduce){
|
|
368
|
+
addEventListener('pointermove',e=>{ mx=(e.clientX/innerWidth-0.5); my=(e.clientY/innerHeight-0.5); },{passive:true});
|
|
369
|
+
}
|
|
370
|
+
let ticking=false;
|
|
371
|
+
function parallax(){
|
|
372
|
+
const vh=innerHeight;
|
|
373
|
+
for(const sec of sections){
|
|
374
|
+
const rect=sec.getBoundingClientRect();
|
|
375
|
+
const total=sec.offsetHeight+vh;
|
|
376
|
+
const T=clamp((vh-rect.top)/total,0,1);
|
|
377
|
+
const enter=smooth(0.06,0.32,T);
|
|
378
|
+
const exit=1-smooth(0.66,0.88,T);
|
|
379
|
+
const live=enter*exit;
|
|
380
|
+
const drift=0.46-T; // signed, ~0 at hold
|
|
381
|
+
const onStage = T>0.04 && T<0.96; // only drive 3D while in view (perf)
|
|
382
|
+
|
|
383
|
+
const fig=sec.querySelector('.figwrap');
|
|
384
|
+
if(fig){ const d=+fig.dataset.depth;
|
|
385
|
+
// Z-dolly: arrives from -260px toward the viewer; bigger Y travel
|
|
386
|
+
const z = -260*(1-enter) + drift*120*d;
|
|
387
|
+
const y = drift*300*d;
|
|
388
|
+
const sc = 0.9 + 0.1*enter;
|
|
389
|
+
// scroll-driven camera swing + subtle pointer blend
|
|
390
|
+
const ry = drift*22 + (onStage? mx*10 : 0);
|
|
391
|
+
const rx = -drift*12 + (onStage? -my*7 : 0);
|
|
392
|
+
fig.style.transform =
|
|
393
|
+
`translateY(calc(-50% + ${y.toFixed(1)}px)) translateZ(${z.toFixed(1)}px) `+
|
|
394
|
+
`rotateY(${ry.toFixed(2)}deg) rotateX(${rx.toFixed(2)}deg) scale(${sc.toFixed(3)})`;
|
|
395
|
+
fig.style.opacity=(0.08+0.92*live).toFixed(3);
|
|
396
|
+
}
|
|
397
|
+
const ghost=sec.querySelector('.ghost');
|
|
398
|
+
if(ghost){ const d=+ghost.dataset.depth;
|
|
399
|
+
// long horizontal sweep + slight scale breathing, deep behind everything
|
|
400
|
+
ghost.style.transform=
|
|
401
|
+
`translate(calc(-50% + ${(drift*620*d).toFixed(0)}px),-50%) translateZ(-340px) scale(${(1.06-0.06*enter).toFixed(3)})`;
|
|
402
|
+
}
|
|
403
|
+
const copy=sec.querySelector('.copytext');
|
|
404
|
+
if(copy){
|
|
405
|
+
// whole block: gentle counter-drift + rotateX settle (type tilts up into place)
|
|
406
|
+
copy.style.transform=`translateY(${(drift*90).toFixed(1)}px) rotateX(${(drift*7).toFixed(2)}deg)`;
|
|
407
|
+
copy.style.opacity=(0.05+0.95*live).toFixed(3);
|
|
408
|
+
const words=copy.querySelectorAll('h2 .w'); const n=words.length;
|
|
409
|
+
words.forEach((w,k)=>{
|
|
410
|
+
const t0=0.05+(k/n)*0.20; const wp=smooth(t0,t0+0.14,T);
|
|
411
|
+
// each word flies in from its own depth + slight Y — staggered 3D arrival
|
|
412
|
+
const wz=(1-wp)*-220; const wy=(1-wp)*44;
|
|
413
|
+
w.style.opacity=wp.toFixed(3);
|
|
414
|
+
w.style.transform=`translateY(${wy.toFixed(1)}px) translateZ(${wz.toFixed(0)}px)`;
|
|
415
|
+
});
|
|
416
|
+
const rev=copy.querySelector('.reveal');
|
|
417
|
+
if(rev) rev.style.opacity=(smooth(0.14,0.34,T)*exit).toFixed(3);
|
|
418
|
+
}
|
|
419
|
+
const card=sec.querySelector('.aicard');
|
|
420
|
+
if(card){ const d=+card.dataset.depth;
|
|
421
|
+
card.style.transform=`translateY(${(drift*150*(d-1)*2.6).toFixed(1)}px) translateZ(40px) rotateY(${(drift*-10).toFixed(2)}deg)`;
|
|
422
|
+
card.style.opacity=(0.95*smooth(0.18,0.40,T)*(1-smooth(0.62,0.84,T))).toFixed(3);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
ticking=false;
|
|
426
|
+
}
|
|
427
|
+
function onScroll(){ if(!ticking){ ticking=true; requestAnimationFrame(parallax); } }
|
|
428
|
+
addEventListener('scroll',onScroll,{passive:true});
|
|
429
|
+
addEventListener('resize',()=>{ticking=true;requestAnimationFrame(parallax);},{passive:true});
|
|
430
|
+
// pointer also re-renders so the figure's camera-swing blend stays live at rest
|
|
431
|
+
if(!isTouch && !reduce) addEventListener('pointermove',onScroll,{passive:true});
|
|
432
|
+
parallax(); // initial paint
|
|
433
|
+
|
|
434
|
+
/* second-order inner micro-tilt on the image itself (adds depth over the
|
|
435
|
+
figure's scroll camera; lives on .pic so it doesn't fight .figwrap) */
|
|
436
|
+
if(!isTouch && !reduce){
|
|
437
|
+
document.querySelectorAll('.frame').forEach(frame=>{
|
|
438
|
+
const pic=frame.querySelector('.pic');
|
|
439
|
+
frame.addEventListener('pointermove',ev=>{
|
|
440
|
+
const r=frame.getBoundingClientRect();
|
|
441
|
+
const px=(ev.clientX-r.left)/r.width-0.5, py=(ev.clientY-r.top)/r.height-0.5;
|
|
442
|
+
pic.style.transform=`translateZ(40px) rotateY(${px*7}deg) rotateX(${-py*5}deg)`;
|
|
443
|
+
});
|
|
444
|
+
frame.addEventListener('pointerleave',()=>{pic.style.transform='translateZ(0) rotateY(0) rotateX(0)';});
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* ── GSAP SHOWCASE WIRING — desktop & non-reduced only (matchMedia-gated so
|
|
449
|
+
it auto-kills on resize into a small/reduced context). Two cinematic
|
|
450
|
+
techniques on the #selected contact sheet:
|
|
451
|
+
• MONTAGE SNAP (taste-guardrails §2 "Montage" / "Landing Sequence") —
|
|
452
|
+
the card track advances via transform:translateX on the scrubbed pin
|
|
453
|
+
timeline; ScrollTrigger.snap snaps card→card. NEVER touches left/width.
|
|
454
|
+
• VELOCITY-REACTIVE typography (scroll-patterns.md #3) — the big display
|
|
455
|
+
heading compresses letter-spacing + scaleY from a lerped velocity
|
|
456
|
+
tracker, driven by gsap.quickTo. Large display type only; off on touch.
|
|
457
|
+
If GSAP never loads, #selected stays at its complete static (wrapped) state. */
|
|
458
|
+
(function wireShowcase(tries){
|
|
459
|
+
if(!window.gsap || !window.ScrollTrigger){
|
|
460
|
+
if(tries>0) requestAnimationFrame(()=>wireShowcase(tries-1));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
464
|
+
ScrollTrigger.defaults({ scrub:0.5, fastScrollEnd:true, invalidateOnRefresh:true });
|
|
465
|
+
const mm = gsap.matchMedia();
|
|
466
|
+
mm.add('(min-width:769px) and (prefers-reduced-motion:no-preference)', () => {
|
|
467
|
+
const sec = document.querySelector('#selected');
|
|
468
|
+
const wrap = sec && sec.querySelector('.montage');
|
|
469
|
+
const track = sec && sec.querySelector('.track');
|
|
470
|
+
const title = sec && sec.querySelector('.mtitle');
|
|
471
|
+
if(!sec || !track || !title) return;
|
|
472
|
+
const cards = [...track.children];
|
|
473
|
+
const n = cards.length;
|
|
474
|
+
|
|
475
|
+
/* MONTAGE SNAP — horizontal track laid out in one row for the pinned beat,
|
|
476
|
+
advanced by translateX across the scrub timeline; snaps card→card.
|
|
477
|
+
These layout writes are one-time setup (not per-frame), transform-only
|
|
478
|
+
drives the motion. */
|
|
479
|
+
gsap.set(track, { display:'flex', flexWrap:'nowrap', willChange:'transform' });
|
|
480
|
+
gsap.set(cards, { flex:'0 0 clamp(220px,24vw,340px)' });
|
|
481
|
+
|
|
482
|
+
const shift = () => -(track.scrollWidth - wrap.clientWidth); // cached on refresh
|
|
483
|
+
const tl = gsap.timeline({ scrollTrigger:{
|
|
484
|
+
trigger:'#selected', start:'top top', end:'+=' + (n*60) + '%',
|
|
485
|
+
pin:wrap, anticipatePin:1,
|
|
486
|
+
snap:{ snapTo: 1/(n-1), duration:{min:.2,max:.5}, ease:'power2.out' } } });
|
|
487
|
+
tl.fromTo(track, { x:0 }, { x:shift, ease:'none' }, 0);
|
|
488
|
+
|
|
489
|
+
/* VELOCITY-REACTIVE typography — lerped velocity tracker (deltaY/dt, lerp
|
|
490
|
+
0.15, capped) → gsap.quickTo on the heading's letter-spacing + scaleY.
|
|
491
|
+
Disabled on touch. Transform-based scaleY + letter-spacing (allowed on
|
|
492
|
+
≥48px display type only, §1.4). Lives in this section's lifecycle only. */
|
|
493
|
+
let velRAF = null;
|
|
494
|
+
if(!isTouch){
|
|
495
|
+
const setLS = gsap.quickTo(title, 'letterSpacing', { duration:.3, ease:'power2.out' });
|
|
496
|
+
const setSY = gsap.quickTo(title, 'scaleY', { duration:.3, ease:'power2.out' });
|
|
497
|
+
let lastY = scrollY, lastT = performance.now(), vel = 0;
|
|
498
|
+
const tick = (now) => {
|
|
499
|
+
const dt = Math.max(now - lastT, 1);
|
|
500
|
+
const dy = scrollY - lastY;
|
|
501
|
+
vel += (dy/dt - vel) * 0.15; // lerp toward instantaneous velocity
|
|
502
|
+
lastY = scrollY; lastT = now;
|
|
503
|
+
const v = Math.min(Math.abs(vel), 3); // cap px/ms
|
|
504
|
+
setLS((-0.02 - v*0.02) + 'em'); // -0.02em (calm) → ~-0.08em (fast)
|
|
505
|
+
setSY(1 - v*0.04); // scaleY 1 → ~0.88 on fast scroll
|
|
506
|
+
velRAF = requestAnimationFrame(tick);
|
|
507
|
+
};
|
|
508
|
+
velRAF = requestAnimationFrame(tick);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return () => {
|
|
512
|
+
if(velRAF) cancelAnimationFrame(velRAF);
|
|
513
|
+
gsap.set([track, ...cards, title], { clearProps:'all' });
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
})(120);
|
|
517
|
+
})();
|
|
518
|
+
</script>
|
|
519
|
+
</body>
|
|
520
|
+
</html>
|
package/manifest.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cinematic-scroll",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Build cinematic scroll-driven, 3D-tilt, parallax, and environment-morphing websites — pinned chapter reveals, hero parallax, depth-image figures, hover-tilt cards, release/launch pages, and editorial microsites. Single self-contained scroll sections (Mode A) or full Shopify-Editions-style Next.js release pages with AI-generated visuals (Mode B). A cinematic web production studio in an Agent Skill: an optional 5-phase pipeline (Cinematic Audit → Motion Storyboard → Technical Spec → Build → Polish) with taste guardrails, 12 scroll patterns, 7 visual systems, a scroll-choreography.json schema, and a 4-dimension audit mode. Built with React, Next.js, GSAP, Lenis, fal.ai.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Simone Leonelli",
|
|
7
|
+
"email": "simone@w230.net"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"licenseFile": "LICENSE",
|
|
11
|
+
"price": 0,
|
|
12
|
+
"currency": "USD",
|
|
13
|
+
"maintained": true,
|
|
14
|
+
"maintenanceNote": "Open-source, actively developed. Community contributions and feedback welcome.",
|
|
15
|
+
"category": "frontend",
|
|
16
|
+
"tags": [
|
|
17
|
+
"react",
|
|
18
|
+
"nextjs",
|
|
19
|
+
"gsap",
|
|
20
|
+
"framer-motion",
|
|
21
|
+
"scroll",
|
|
22
|
+
"3d",
|
|
23
|
+
"parallax",
|
|
24
|
+
"animation",
|
|
25
|
+
"cinematic",
|
|
26
|
+
"fal.ai",
|
|
27
|
+
"ai-images",
|
|
28
|
+
"shopify-editions",
|
|
29
|
+
"release-page",
|
|
30
|
+
"landing-page",
|
|
31
|
+
"tailwind"
|
|
32
|
+
],
|
|
33
|
+
"compatibility": {
|
|
34
|
+
"claude": ">=3.5",
|
|
35
|
+
"agentskills.io": "1.x"
|
|
36
|
+
},
|
|
37
|
+
"entry": "SKILL.md",
|
|
38
|
+
"templates": "templates/nextjs/",
|
|
39
|
+
"repository": "https://github.com/MustBeSimo/cinematic-scroll-skill",
|
|
40
|
+
"homepage": "https://mustbesimo.github.io/cinematic-scroll-skill/",
|
|
41
|
+
"requires": {
|
|
42
|
+
"npmPackages": [
|
|
43
|
+
"choreo-3d",
|
|
44
|
+
"framer-motion",
|
|
45
|
+
"gsap",
|
|
46
|
+
"lenis",
|
|
47
|
+
"@fal-ai/client",
|
|
48
|
+
"@fal-ai/server-proxy",
|
|
49
|
+
"next",
|
|
50
|
+
"react",
|
|
51
|
+
"react-dom",
|
|
52
|
+
"tailwindcss"
|
|
53
|
+
],
|
|
54
|
+
"envVars": [
|
|
55
|
+
"FAL_KEY",
|
|
56
|
+
"FAL_IMAGE_MODEL"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"modes": [
|
|
60
|
+
{
|
|
61
|
+
"name": "Scroll artifact",
|
|
62
|
+
"description": "Single self-contained HTML or .tsx scroll section / page. No build step, no AI keys. Fast preview."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "Full release site",
|
|
66
|
+
"description": "Tested Next.js App Router templates + fal.ai asset pipeline for a complete cinematic release website. Bring your own fal.ai key."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "Cinematic audit",
|
|
70
|
+
"description": "Analyze an existing URL's scroll experience. Scores pacing, performance, accessibility, and emotional arc. Outputs remediation-plan.md with prioritized fixes."
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"references": [
|
|
74
|
+
"taste-guardrails.md",
|
|
75
|
+
"references/scroll-patterns.md",
|
|
76
|
+
"references/film-archetypes.md",
|
|
77
|
+
"references/performance-budget.md",
|
|
78
|
+
"audit-mode.md",
|
|
79
|
+
"scroll-choreography.json"
|
|
80
|
+
],
|
|
81
|
+
"pipeline": {
|
|
82
|
+
"phases": 5,
|
|
83
|
+
"gated": true,
|
|
84
|
+
"artifacts": [
|
|
85
|
+
"cinematic-audit.md",
|
|
86
|
+
"motion-storyboard.md",
|
|
87
|
+
"technical-spec.md",
|
|
88
|
+
"code-output",
|
|
89
|
+
"polish-report.md"
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
}
|