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,662 @@
|
|
|
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>Maison Solenne — Quiet Luxury · cinematic-scroll example</title>
|
|
7
|
+
<meta name="description" content="A restrained, quiet-luxury cinematic-scroll page — warm ivory and sand, refined thin serif display, barely-there parallax, and a letter-spacing title scrub. A worked example for the cinematic-scroll Agent Skill." />
|
|
8
|
+
|
|
9
|
+
<!-- Open Graph / preview -->
|
|
10
|
+
<meta property="og:title" content="Maison Solenne — Quiet Luxury" />
|
|
11
|
+
<meta property="og:description" content="Four chapters in warm ivory and cognac — long slow pins, letter-spacing title reveals, museum-restraint motion. Built with the cinematic-scroll Agent Skill, no build step." />
|
|
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
|
+
<!-- Cormorant Garamond (thin serif display), EB Garamond (body serif), Jost (mono-ish UI labels) -->
|
|
17
|
+
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,300;1,400&family=EB+Garamond:wght@400;500&family=Jost:wght@300;400&display=swap" rel="stylesheet" />
|
|
18
|
+
|
|
19
|
+
<!-- GSAP + ScrollTrigger (now 100% free) — powers the one dedicated "The Object"
|
|
20
|
+
showcase beat: a PUSH-IN (slow scale 1 → 1.08) + a MATCH-CUT (two captions
|
|
21
|
+
crossfade by opacity over a perfectly-still composition). Deferred + feature-
|
|
22
|
+
detected: if the CDN fails, the beat simply stays at its complete static state
|
|
23
|
+
and the hand-rolled rAF quiet-luxury engine below still runs untouched. -->
|
|
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
|
+
/* ───────────────────────────────────────────────────────────────
|
|
29
|
+
cinematic-scroll · Quiet-Luxury example (MAISON SOLENNE)
|
|
30
|
+
Single-file, no build, no external JS. GitHub-Pages friendly.
|
|
31
|
+
|
|
32
|
+
Visual system: Atmospheric Sublime restraint + Symmetric Monument stillness.
|
|
33
|
+
Vast negative space · glacial 220vh pins · barely-there parallax
|
|
34
|
+
(depth multipliers ≤ 0.50; background drifts ~3% over the whole pin).
|
|
35
|
+
Title treatment: LETTER-SPACING SCRUB — 0.45em → 0em over the first
|
|
36
|
+
~30% of each pin (the "refined / luxury" reveal from taste-guardrails).
|
|
37
|
+
Motion contract: only `transform` and `opacity` mutate per frame; one
|
|
38
|
+
rAF batch; passive scroll listener; only in-view sections are driven.
|
|
39
|
+
No `filter`, no glow, no 3D tilt (calm, and avoids motion-sickness).
|
|
40
|
+
Degrade: reduced-motion AND mobile (≤680px) snap to a clean static
|
|
41
|
+
mid-state — full opacity, letter-spacing settled, no scrolljack.
|
|
42
|
+
─────────────────────────────────────────────────────────────── */
|
|
43
|
+
|
|
44
|
+
:root{
|
|
45
|
+
--ivory: #F6F1E7; /* warm ivory ground */
|
|
46
|
+
--sand: #ECE3D4; /* secondary sand ground */
|
|
47
|
+
--ecru: #E4D8C4; /* deepest light ground */
|
|
48
|
+
--bark: #36302A; /* primary text — strong contrast on ivory */
|
|
49
|
+
--body: #4E463C; /* body copy — legible, warm */
|
|
50
|
+
--muted: #8A7F6D; /* labels / captions */
|
|
51
|
+
--cognac: #9A6A3C; /* the single restrained accent */
|
|
52
|
+
--cognac-2:#7E5530;
|
|
53
|
+
--line: rgba(54,48,42,.14);
|
|
54
|
+
|
|
55
|
+
--display: "Cormorant Garamond", Georgia, "Times New Roman", serif;
|
|
56
|
+
--serif: "EB Garamond", Georgia, serif;
|
|
57
|
+
--ui: "Jost", system-ui, -apple-system, sans-serif;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
*{ margin:0; padding:0; box-sizing:border-box; }
|
|
61
|
+
html{ scroll-behavior:smooth; }
|
|
62
|
+
body{
|
|
63
|
+
background:var(--ivory);
|
|
64
|
+
color:var(--bark);
|
|
65
|
+
font-family:var(--serif);
|
|
66
|
+
line-height:1.6;
|
|
67
|
+
-webkit-font-smoothing:antialiased;
|
|
68
|
+
overflow-x:hidden;
|
|
69
|
+
transition:background 1.2s cubic-bezier(.22,1,.36,1);
|
|
70
|
+
}
|
|
71
|
+
a{ color:inherit; }
|
|
72
|
+
::selection{ background:rgba(154,106,60,.22); color:var(--bark); }
|
|
73
|
+
|
|
74
|
+
/* ── Fixed atmosphere + grain ────────────────────────────────── */
|
|
75
|
+
.atmos{
|
|
76
|
+
position:fixed; inset:0; z-index:-2; pointer-events:none;
|
|
77
|
+
background:
|
|
78
|
+
radial-gradient(130% 90% at 22% 8%, rgba(154,106,60,.06), rgba(154,106,60,0) 58%),
|
|
79
|
+
radial-gradient(110% 80% at 88% 96%, rgba(54,48,42,.05), rgba(54,48,42,0) 60%),
|
|
80
|
+
var(--ivory);
|
|
81
|
+
transition:background 1.2s cubic-bezier(.22,1,.36,1);
|
|
82
|
+
}
|
|
83
|
+
.grain{
|
|
84
|
+
position:fixed; inset:0; z-index:-1; pointer-events:none; opacity:.4;
|
|
85
|
+
mix-blend-mode:multiply;
|
|
86
|
+
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.45'/%3E%3C/svg%3E");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── Fixed masthead ──────────────────────────────────────────── */
|
|
90
|
+
.masthead{
|
|
91
|
+
position:fixed; top:0; left:0; right:0; z-index:50;
|
|
92
|
+
display:flex; align-items:center; justify-content:space-between;
|
|
93
|
+
padding:clamp(18px,2.6vw,30px) clamp(20px,5vw,64px);
|
|
94
|
+
padding-top:max(clamp(18px,2.6vw,30px), env(safe-area-inset-top));
|
|
95
|
+
background:linear-gradient(to bottom, var(--ivory) 18%, rgba(246,241,231,.72) 64%, rgba(246,241,231,0));
|
|
96
|
+
}
|
|
97
|
+
.masthead .mark{ display:flex; align-items:baseline; gap:14px; }
|
|
98
|
+
.masthead .mark b{
|
|
99
|
+
font-family:var(--display); font-weight:400; font-size:clamp(17px,2vw,22px);
|
|
100
|
+
letter-spacing:.30em; text-transform:uppercase; color:var(--bark);
|
|
101
|
+
}
|
|
102
|
+
.masthead .mark span{
|
|
103
|
+
font-family:var(--ui); font-weight:300; font-size:9px; letter-spacing:.36em;
|
|
104
|
+
text-transform:uppercase; color:var(--cognac);
|
|
105
|
+
}
|
|
106
|
+
.masthead nav{ display:flex; gap:clamp(16px,2.6vw,34px); }
|
|
107
|
+
.masthead nav a{
|
|
108
|
+
font-family:var(--ui); font-weight:300; font-size:11px; letter-spacing:.26em;
|
|
109
|
+
text-transform:uppercase; color:var(--muted); text-decoration:none; transition:color .4s;
|
|
110
|
+
}
|
|
111
|
+
.masthead nav a:hover{ color:var(--bark); }
|
|
112
|
+
@media (max-width:680px){ .masthead nav{ display:none; } }
|
|
113
|
+
|
|
114
|
+
/* ── Quiet progress rail (chapter index) ─────────────────────── */
|
|
115
|
+
.rail{
|
|
116
|
+
position:fixed; top:50%; left:clamp(18px,3vw,42px); transform:translateY(-50%);
|
|
117
|
+
z-index:40; display:flex; flex-direction:column; gap:18px;
|
|
118
|
+
padding-left:env(safe-area-inset-left);
|
|
119
|
+
}
|
|
120
|
+
.rail button{
|
|
121
|
+
display:flex; align-items:center; gap:12px;
|
|
122
|
+
background:none; border:none; cursor:pointer; color:var(--muted);
|
|
123
|
+
font-family:var(--ui); font-weight:300; font-size:10px; letter-spacing:.22em;
|
|
124
|
+
text-transform:uppercase; transition:color .4s; padding:2px 0;
|
|
125
|
+
}
|
|
126
|
+
.rail button .tick{ width:18px; height:1px; background:currentColor; opacity:.45; transition:width .5s, opacity .5s; flex:none; }
|
|
127
|
+
.rail button .nm{ opacity:0; transform:translateX(-4px); transition:opacity .4s, transform .4s; max-width:0; overflow:hidden; white-space:nowrap; }
|
|
128
|
+
.rail:hover button .nm{ opacity:.7; transform:translateX(0); max-width:160px; }
|
|
129
|
+
.rail button[aria-current="true"]{ color:var(--bark); }
|
|
130
|
+
.rail button[aria-current="true"] .tick{ width:40px; opacity:1; background:var(--cognac); }
|
|
131
|
+
.rail button[aria-current="true"] .nm{ opacity:1; transform:translateX(0); max-width:160px; }
|
|
132
|
+
@media (max-width:880px){ .rail{ display:none; } }
|
|
133
|
+
|
|
134
|
+
/* ── Chapter / pin ───────────────────────────────────────────── */
|
|
135
|
+
.chapter{ position:relative; height:220vh; } /* long, slow pin room */
|
|
136
|
+
.chapter .stage{
|
|
137
|
+
position:sticky; top:0; height:100vh; overflow:hidden;
|
|
138
|
+
display:grid; align-content:center;
|
|
139
|
+
}
|
|
140
|
+
.stage .inner{
|
|
141
|
+
width:min(1200px,90vw); margin:0 auto;
|
|
142
|
+
display:grid; grid-template-columns:1.05fr .95fr; gap:clamp(36px,6vw,88px);
|
|
143
|
+
align-items:center; padding:0 clamp(10px,2vw,28px);
|
|
144
|
+
}
|
|
145
|
+
.chapter[data-side="left"] .copy{ order:2; }
|
|
146
|
+
.chapter[data-side="left"] .figure{ order:1; }
|
|
147
|
+
|
|
148
|
+
.layer{ will-change:transform, opacity; }
|
|
149
|
+
|
|
150
|
+
/* copy column — generous negative space, left-aligned body */
|
|
151
|
+
.copy .eyebrow{
|
|
152
|
+
font-family:var(--ui); font-weight:300; font-size:10px; letter-spacing:.40em;
|
|
153
|
+
text-transform:uppercase; color:var(--cognac); margin-bottom:clamp(22px,3vw,34px); display:block;
|
|
154
|
+
}
|
|
155
|
+
.copy h2{
|
|
156
|
+
font-family:var(--display); font-weight:300; font-size:clamp(2.6rem,6.4vw,5.6rem);
|
|
157
|
+
line-height:1.04; letter-spacing:0; color:var(--bark);
|
|
158
|
+
margin-bottom:clamp(24px,3vw,36px);
|
|
159
|
+
}
|
|
160
|
+
.copy h2 .scrub{ display:inline-block; will-change:letter-spacing; }
|
|
161
|
+
.copy h2 em{ font-style:italic; font-weight:300; color:var(--cognac-2); }
|
|
162
|
+
.copy .rule{ height:1px; width:120px; background:linear-gradient(to right, var(--cognac), rgba(154,106,60,0)); margin:2px 0 26px; }
|
|
163
|
+
.copy p.lede{
|
|
164
|
+
font-family:var(--serif); font-weight:400; font-size:clamp(1.05rem,1.5vw,1.26rem);
|
|
165
|
+
line-height:1.72; color:var(--body); max-width:44ch;
|
|
166
|
+
}
|
|
167
|
+
.copy .meta{
|
|
168
|
+
margin-top:24px; font-family:var(--ui); font-weight:300; font-size:10px; letter-spacing:.24em;
|
|
169
|
+
text-transform:uppercase; color:var(--muted);
|
|
170
|
+
}
|
|
171
|
+
.copy .cta{
|
|
172
|
+
margin-top:clamp(28px,3.4vw,42px); display:inline-flex; align-items:center; gap:14px;
|
|
173
|
+
padding:15px 30px; border-radius:999px; border:1px solid var(--line);
|
|
174
|
+
background:transparent; color:var(--bark);
|
|
175
|
+
font-family:var(--ui); font-weight:300; font-size:11px; letter-spacing:.24em; text-transform:uppercase;
|
|
176
|
+
text-decoration:none; transition:background .5s, color .5s, border-color .5s;
|
|
177
|
+
}
|
|
178
|
+
.copy .cta:hover{ background:var(--bark); color:var(--ivory); border-color:var(--bark); }
|
|
179
|
+
|
|
180
|
+
/* figure column — the single framed "product moment" */
|
|
181
|
+
.figure{ position:relative; }
|
|
182
|
+
.frame{
|
|
183
|
+
position:relative; width:100%; max-width:480px; margin-inline:auto;
|
|
184
|
+
aspect-ratio:4/5; border-radius:18px; overflow:hidden;
|
|
185
|
+
box-shadow:0 40px 90px -50px rgba(54,48,42,.55);
|
|
186
|
+
}
|
|
187
|
+
.chapter[data-side="left"] .frame{ margin-left:auto; }
|
|
188
|
+
.frame .pic{
|
|
189
|
+
position:absolute; inset:0; background-size:cover; background-position:center;
|
|
190
|
+
background-color:var(--ecru); border-radius:18px;
|
|
191
|
+
}
|
|
192
|
+
/* CSS-only placeholder visual (when no real image is present) — soft warm
|
|
193
|
+
gradients + subtle grain, so the page reads intentional with zero files. */
|
|
194
|
+
.frame .pic.placeholder{
|
|
195
|
+
background-image:
|
|
196
|
+
radial-gradient(120% 90% at 34% 24%, rgba(255,252,246,.9) 0%, rgba(228,216,196,.6) 46%, rgba(154,106,60,.30) 100%),
|
|
197
|
+
linear-gradient(155deg, #F3ECDF 0%, #E7DAC6 52%, #C9A878 100%);
|
|
198
|
+
}
|
|
199
|
+
.frame .pic.placeholder::before{
|
|
200
|
+
/* a faint vertical "object" highlight — suggests a framed still life */
|
|
201
|
+
content:""; position:absolute; left:50%; top:14%; width:34%; height:72%;
|
|
202
|
+
transform:translateX(-50%); border-radius:120px;
|
|
203
|
+
background:linear-gradient(180deg, rgba(255,253,248,.55), rgba(154,106,60,.10) 70%, rgba(126,85,48,.18));
|
|
204
|
+
box-shadow:0 30px 60px -30px rgba(126,85,48,.5);
|
|
205
|
+
}
|
|
206
|
+
.frame .pic::after{ /* fine grain over the still */
|
|
207
|
+
content:""; position:absolute; inset:0; opacity:.35; mix-blend-mode:multiply;
|
|
208
|
+
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%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(%23g)' opacity='0.4'/%3E%3C/svg%3E");
|
|
209
|
+
}
|
|
210
|
+
.frame .edge{ position:absolute; inset:0; border-radius:18px; pointer-events:none;
|
|
211
|
+
box-shadow:inset 0 0 0 1px rgba(54,48,42,.10); }
|
|
212
|
+
|
|
213
|
+
.figcap{
|
|
214
|
+
margin-top:18px; display:flex; align-items:baseline; gap:14px;
|
|
215
|
+
font-family:var(--ui); font-weight:300; font-size:10px; letter-spacing:.24em;
|
|
216
|
+
text-transform:uppercase; color:var(--muted);
|
|
217
|
+
}
|
|
218
|
+
.figcap .n{ color:var(--cognac); }
|
|
219
|
+
.figcap .ln{ height:1px; flex:1; background:currentColor; opacity:.25; }
|
|
220
|
+
|
|
221
|
+
/* scroll cue (opening chapter only) */
|
|
222
|
+
.cue{
|
|
223
|
+
position:absolute; left:50%; bottom:26px; transform:translateX(-50%); z-index:30;
|
|
224
|
+
display:flex; flex-direction:column; align-items:center; gap:10px; color:var(--muted);
|
|
225
|
+
background:none; border:none; cursor:pointer;
|
|
226
|
+
font-family:var(--ui); font-weight:300; font-size:9px; letter-spacing:.30em; text-transform:uppercase;
|
|
227
|
+
}
|
|
228
|
+
.cue .bar{ width:1px; height:44px; background:currentColor; opacity:.4; transform-origin:top; animation:cuebar 3.2s infinite ease-in-out; }
|
|
229
|
+
@keyframes cuebar{ 0%,100%{ transform:scaleY(1); } 50%{ transform:scaleY(.35); } }
|
|
230
|
+
|
|
231
|
+
/* colophon / footer */
|
|
232
|
+
.colophon{ position:relative; padding:clamp(80px,16vh,180px) 24px; text-align:center; }
|
|
233
|
+
.colophon .vine{ width:120px; height:1px; margin:0 auto 30px; background:linear-gradient(to right, transparent, var(--cognac), transparent); }
|
|
234
|
+
.colophon h3{ font-family:var(--display); font-weight:300; font-style:italic; font-size:clamp(1.7rem,3.6vw,2.8rem); color:var(--bark); margin-bottom:16px; letter-spacing:.01em; }
|
|
235
|
+
.colophon p{ font-family:var(--serif); color:var(--body); max-width:52ch; margin:0 auto 10px; }
|
|
236
|
+
.colophon .src{ font-family:var(--ui); font-weight:300; font-size:10px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted); margin-top:24px; }
|
|
237
|
+
.colophon .src a{ color:var(--cognac-2); text-decoration:none; border-bottom:1px solid rgba(126,85,48,.3); }
|
|
238
|
+
|
|
239
|
+
/* ── Mobile (≤680px): no pin, stacked, clean fade-up ─────────── */
|
|
240
|
+
@media (max-width:680px){
|
|
241
|
+
.chapter{ height:auto; }
|
|
242
|
+
.chapter .stage{ position:relative; height:auto; min-height:auto; padding:clamp(72px,13vh,120px) 0; }
|
|
243
|
+
.stage .inner{ grid-template-columns:1fr; gap:32px; width:88vw; }
|
|
244
|
+
.chapter[data-side="left"] .copy,
|
|
245
|
+
.chapter[data-side="left"] .figure{ order:initial; }
|
|
246
|
+
.figure{ order:2 !important; } .copy{ order:1 !important; }
|
|
247
|
+
.layer{ transform:none !important; opacity:1 !important; }
|
|
248
|
+
.copy h2 .scrub{ letter-spacing:0 !important; }
|
|
249
|
+
.frame{ max-width:100%; aspect-ratio:5/4; }
|
|
250
|
+
.reveal{ opacity:0; transform:translateY(22px); transition:opacity .9s ease, transform .9s cubic-bezier(.16,1,.3,1); }
|
|
251
|
+
.reveal.in{ opacity:1; transform:none; }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ── Reduced motion: snap to a readable static mid-state ─────── */
|
|
255
|
+
@media (prefers-reduced-motion:reduce){
|
|
256
|
+
html{ scroll-behavior:auto; }
|
|
257
|
+
.chapter{ height:auto; }
|
|
258
|
+
.chapter .stage{ position:relative; height:auto; min-height:auto; padding:96px 0; }
|
|
259
|
+
.stage .inner{ grid-template-columns:1fr; }
|
|
260
|
+
.layer{ transform:none !important; opacity:1 !important; }
|
|
261
|
+
.copy h2 .scrub{ letter-spacing:0 !important; }
|
|
262
|
+
.cue .bar{ animation:none; }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* ── GSAP SHOWCASE — "THE OBJECT, HELD" ──────────────────────────
|
|
266
|
+
One dedicated beat demonstrating two restrained techniques the
|
|
267
|
+
hand-rolled engine doesn't (taste-guardrails §2):
|
|
268
|
+
• PUSH-IN — a very slow scale(1 → 1.08) on the framed object over
|
|
269
|
+
the pin (intensifying focus; no other foreground motion).
|
|
270
|
+
• MATCH-CUT — two captions occupy the SAME position; content swaps by
|
|
271
|
+
opacity crossfade while the layout holds perfectly still.
|
|
272
|
+
No 3D tilt, no snap, no velocity, no glow (museum restraint, §1.9).
|
|
273
|
+
STATIC DEFAULTS below keep the beat whole when GSAP is absent / reduced-
|
|
274
|
+
motion / mobile: object at rest scale 1, first caption visible, second
|
|
275
|
+
hidden, all content present. GSAP owns the pin scroll-room at runtime. */
|
|
276
|
+
.chapter.showcase{ height:auto; } /* GSAP pin grants the scroll-room */
|
|
277
|
+
.show-stage{
|
|
278
|
+
position:relative; height:100vh; overflow:hidden;
|
|
279
|
+
display:grid; place-items:center; padding:0 clamp(20px,5vw,64px);
|
|
280
|
+
}
|
|
281
|
+
.show-inner{ width:min(720px,90vw); text-align:center; }
|
|
282
|
+
.show-eyebrow{
|
|
283
|
+
font-family:var(--ui); font-weight:300; font-size:10px; letter-spacing:.40em;
|
|
284
|
+
text-transform:uppercase; color:var(--cognac); display:block;
|
|
285
|
+
margin-bottom:clamp(26px,4vw,44px);
|
|
286
|
+
}
|
|
287
|
+
/* the still the PUSH-IN scales (own layer; not driven by the rAF engine) */
|
|
288
|
+
.show-object{
|
|
289
|
+
position:relative; width:min(360px,64vw); aspect-ratio:4/5; margin:0 auto;
|
|
290
|
+
border-radius:18px; overflow:hidden; transform-origin:50% 46%;
|
|
291
|
+
box-shadow:0 40px 90px -50px rgba(54,48,42,.55);
|
|
292
|
+
will-change:transform; /* one element only */
|
|
293
|
+
background-image:
|
|
294
|
+
radial-gradient(120% 90% at 38% 22%, rgba(255,252,246,.92) 0%, rgba(228,216,196,.6) 48%, rgba(154,106,60,.32) 100%),
|
|
295
|
+
linear-gradient(155deg, #F3ECDF 0%, #E7DAC6 52%, #C9A878 100%);
|
|
296
|
+
background-color:var(--ecru);
|
|
297
|
+
}
|
|
298
|
+
.show-object::before{
|
|
299
|
+
content:""; position:absolute; left:50%; top:14%; width:34%; height:72%;
|
|
300
|
+
transform:translateX(-50%); border-radius:120px;
|
|
301
|
+
background:linear-gradient(180deg, rgba(255,253,248,.55), rgba(154,106,60,.10) 70%, rgba(126,85,48,.18));
|
|
302
|
+
}
|
|
303
|
+
.show-object::after{ /* static grain locked to the still */
|
|
304
|
+
content:""; position:absolute; inset:0; opacity:.32; mix-blend-mode:multiply; pointer-events:none;
|
|
305
|
+
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='go'%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(%23go)' opacity='0.4'/%3E%3C/svg%3E");
|
|
306
|
+
}
|
|
307
|
+
/* MATCH-CUT: both captions share ONE grid cell → identical position; only
|
|
308
|
+
opacity differs. Static default: first shown, second hidden. */
|
|
309
|
+
.show-caps{ display:grid; margin-top:clamp(30px,4vw,48px); }
|
|
310
|
+
.show-cap{
|
|
311
|
+
grid-area:1 / 1; align-self:start;
|
|
312
|
+
font-family:var(--display); font-weight:300; font-style:italic;
|
|
313
|
+
font-size:clamp(1.4rem,3.4vw,2.4rem); line-height:1.2; color:var(--bark);
|
|
314
|
+
}
|
|
315
|
+
.show-cap small{
|
|
316
|
+
display:block; margin-top:14px; font-family:var(--ui); font-weight:300; font-style:normal;
|
|
317
|
+
font-size:10px; letter-spacing:.24em; text-transform:uppercase; color:var(--muted);
|
|
318
|
+
}
|
|
319
|
+
.show-cap.b{ opacity:0; } /* second state hidden at rest */
|
|
320
|
+
.show-cap em{ color:var(--cognac-2); }
|
|
321
|
+
|
|
322
|
+
@media (max-width:680px){
|
|
323
|
+
/* static + stacked: no pin, show both captions, object at rest */
|
|
324
|
+
.chapter.showcase .show-stage{ height:auto; padding:clamp(72px,13vh,120px) 22px; }
|
|
325
|
+
.show-object{ transform:none !important; width:min(320px,74vw); }
|
|
326
|
+
.show-caps{ display:block; }
|
|
327
|
+
.show-cap{ display:block; opacity:1 !important; }
|
|
328
|
+
.show-cap.b{ margin-top:24px; }
|
|
329
|
+
}
|
|
330
|
+
@media (prefers-reduced-motion:reduce){
|
|
331
|
+
.show-stage{ height:auto; padding:96px 22px; }
|
|
332
|
+
.show-object{ transform:none !important; }
|
|
333
|
+
.show-caps{ display:block; }
|
|
334
|
+
.show-cap{ opacity:1 !important; }
|
|
335
|
+
.show-cap.b{ margin-top:24px; }
|
|
336
|
+
}
|
|
337
|
+
</style>
|
|
338
|
+
</head>
|
|
339
|
+
<body>
|
|
340
|
+
<div class="atmos" id="atmos" aria-hidden="true"></div>
|
|
341
|
+
<div class="grain" aria-hidden="true"></div>
|
|
342
|
+
|
|
343
|
+
<header class="masthead">
|
|
344
|
+
<div class="mark">
|
|
345
|
+
<b>Maison Solenne</b>
|
|
346
|
+
<span>Paris · MCMXXIV</span>
|
|
347
|
+
</div>
|
|
348
|
+
<nav aria-label="Primary">
|
|
349
|
+
<a href="#provenance">Provenance</a>
|
|
350
|
+
<a href="#object">The Object</a>
|
|
351
|
+
<a href="#atelier">Atelier</a>
|
|
352
|
+
<a href="#audience">Enquire</a>
|
|
353
|
+
</nav>
|
|
354
|
+
</header>
|
|
355
|
+
|
|
356
|
+
<nav class="rail" id="rail" aria-label="Chapters"></nav>
|
|
357
|
+
|
|
358
|
+
<main id="book"></main>
|
|
359
|
+
|
|
360
|
+
<footer class="colophon">
|
|
361
|
+
<div class="vine"></div>
|
|
362
|
+
<h3>What is made slowly is kept for a long time.</h3>
|
|
363
|
+
<p>Four chapters for a maison that does not exist — rendered as a single, build-free HTML page in warm ivory and cognac.</p>
|
|
364
|
+
<p class="src">
|
|
365
|
+
Built with the <a href="https://github.com/MustBeSimo/cinematic-scroll-skill">cinematic-scroll</a> Agent Skill · fictional maison, no real brands ·
|
|
366
|
+
one of many possible aesthetics.
|
|
367
|
+
</p>
|
|
368
|
+
</footer>
|
|
369
|
+
|
|
370
|
+
<script>
|
|
371
|
+
/* ───────────────────────────────────────────────────────────────
|
|
372
|
+
Manifest — the page is generated from this array. Edit to retheme.
|
|
373
|
+
Fictional maison; no real brands or people.
|
|
374
|
+
title: array of [text, italic?] fragments. The first fragment of each
|
|
375
|
+
title carries the letter-spacing scrub class (the reveal).
|
|
376
|
+
─────────────────────────────────────────────────────────────── */
|
|
377
|
+
const CHAPTERS = [
|
|
378
|
+
{ id:'overture', num:'I', nm:'Overture',
|
|
379
|
+
eyebrow:'Maison Solenne · Volume I',
|
|
380
|
+
title:[['The art of', 0],['keeping', 1],['still.', 0]],
|
|
381
|
+
lede:'A maison founded on a single, unfashionable belief: that the finest objects ask nothing of you. They wait. They weather. They are made once, and made completely — and then they are simply present, for the length of a life.',
|
|
382
|
+
fig:'I', figLabel:'Frontispiece · warm ivory',
|
|
383
|
+
meta:'Provenance · est. MCMXXIV',
|
|
384
|
+
side:'right',
|
|
385
|
+
morph:'radial-gradient(130% 90% at 22% 8%, rgba(154,106,60,.06), rgba(154,106,60,0) 58%), radial-gradient(110% 80% at 88% 96%, rgba(54,48,42,.05), rgba(54,48,42,0) 60%), #F6F1E7',
|
|
386
|
+
cue:true,
|
|
387
|
+
prompt:'overture · still life'
|
|
388
|
+
},
|
|
389
|
+
{ id:'provenance', num:'II', nm:'Provenance',
|
|
390
|
+
eyebrow:'Chapter II · Provenance',
|
|
391
|
+
title:[['A hundred', 0],['unhurried', 1],['years.', 0]],
|
|
392
|
+
lede:'Four generations in the same three rooms above the rue. No collections, no seasons, no haste — only the slow accumulation of judgement that cannot be bought, briefed, or rushed. The house keeps its own time.',
|
|
393
|
+
fig:'II', figLabel:'The archive · sand light',
|
|
394
|
+
meta:'Atelier · rue de Solenne',
|
|
395
|
+
side:'left',
|
|
396
|
+
morph:'radial-gradient(130% 90% at 20% 10%, rgba(154,106,60,.07), rgba(154,106,60,0) 56%), radial-gradient(100% 80% at 86% 92%, rgba(54,48,42,.04), rgba(54,48,42,0) 60%), #ECE3D4',
|
|
397
|
+
prompt:'provenance · archive'
|
|
398
|
+
},
|
|
399
|
+
{ id:'object', num:'III', nm:'The Object',
|
|
400
|
+
eyebrow:'Chapter III · The Object',
|
|
401
|
+
title:[['One object,', 0],['considered', 1],['completely.', 0]],
|
|
402
|
+
lede:'A single form, returned to until nothing remains to remove. Hand-burnished cognac leather over a frame that will outlast its first owner. No monogram. No hardware that announces itself. The restraint is the luxury.',
|
|
403
|
+
fig:'III', figLabel:'The object · cognac still',
|
|
404
|
+
meta:'Made to be kept',
|
|
405
|
+
side:'right',
|
|
406
|
+
morph:'radial-gradient(130% 90% at 24% 12%, rgba(154,106,60,.10), rgba(154,106,60,0) 56%), radial-gradient(100% 80% at 88% 90%, rgba(126,85,48,.06), rgba(126,85,48,0) 60%), #E4D8C4',
|
|
407
|
+
prompt:'the object · product moment'
|
|
408
|
+
},
|
|
409
|
+
{ id:'audience', num:'IV', nm:'Enquire',
|
|
410
|
+
eyebrow:'Chapter IV · An Audience',
|
|
411
|
+
title:[['Request an', 0],['audience', 1],['with the house.', 0]],
|
|
412
|
+
lede:'We make a small number of pieces each year, by enquiry only. If you would like to commission one — and to wait the time it properly takes — the house keeps a quiet correspondence with those who understand the value of patience.',
|
|
413
|
+
fig:'IV', figLabel:'The correspondence',
|
|
414
|
+
meta:'By enquiry only',
|
|
415
|
+
side:'left',
|
|
416
|
+
morph:'radial-gradient(130% 90% at 22% 8%, rgba(154,106,60,.06), rgba(154,106,60,0) 58%), radial-gradient(110% 80% at 88% 96%, rgba(54,48,42,.05), rgba(54,48,42,0) 60%), #EFE7D8',
|
|
417
|
+
cta:'Begin a correspondence',
|
|
418
|
+
prompt:'audience · the correspondence'
|
|
419
|
+
},
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
/* ── helpers ─────────────────────────────────────────────────── */
|
|
423
|
+
const clamp = (n,a,b)=>Math.max(a,Math.min(b,n));
|
|
424
|
+
const smooth = (a,b,t)=>{ const x=clamp((t-a)/(b-a),0,1); return x*x*(3-2*x); };
|
|
425
|
+
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
426
|
+
const isMobile = () => window.matchMedia('(max-width: 680px)').matches;
|
|
427
|
+
|
|
428
|
+
const book = document.getElementById('book');
|
|
429
|
+
const rail = document.getElementById('rail');
|
|
430
|
+
const atmos = document.getElementById('atmos');
|
|
431
|
+
|
|
432
|
+
/* probe a real still; fall back to the CSS placeholder if it 404s */
|
|
433
|
+
function probe(src){ return new Promise(res=>{ const i=new Image(); i.onload=()=>res(true); i.onerror=()=>res(false); i.src=src; }); }
|
|
434
|
+
|
|
435
|
+
(async () => {
|
|
436
|
+
for (const ch of CHAPTERS){
|
|
437
|
+
const imgPath = `assets/${ch.id}.jpg`;
|
|
438
|
+
const hasImg = await probe(imgPath);
|
|
439
|
+
const picCls = hasImg ? 'pic' : 'pic placeholder';
|
|
440
|
+
const picStyle= hasImg ? `background-image:url('${imgPath}')` : '';
|
|
441
|
+
|
|
442
|
+
// first title fragment carries the letter-spacing scrub
|
|
443
|
+
const titleHTML = ch.title.map(([txt,it],k)=>{
|
|
444
|
+
const inner = it ? `<em>${txt}</em>` : txt;
|
|
445
|
+
return k===0 ? `<span class="scrub">${inner}</span> ` : `${inner} `;
|
|
446
|
+
}).join('');
|
|
447
|
+
|
|
448
|
+
const sec = document.createElement('section');
|
|
449
|
+
sec.className = 'chapter';
|
|
450
|
+
sec.id = ch.id;
|
|
451
|
+
sec.dataset.side = ch.side;
|
|
452
|
+
sec.innerHTML = `
|
|
453
|
+
<div class="stage">
|
|
454
|
+
<div class="inner">
|
|
455
|
+
<div class="copy">
|
|
456
|
+
<div class="layer copytext" data-depth="0.50">
|
|
457
|
+
<span class="eyebrow reveal">${ch.eyebrow}</span>
|
|
458
|
+
<h2>${titleHTML}</h2>
|
|
459
|
+
<div class="rule reveal"></div>
|
|
460
|
+
<p class="lede reveal">${ch.lede}</p>
|
|
461
|
+
${ch.cta
|
|
462
|
+
? `<a class="cta reveal" href="mailto:audience@example.com?subject=An%20audience%20with%20Maison%20Solenne">${ch.cta} →</a>`
|
|
463
|
+
: `<p class="meta reveal">${ch.meta}</p>`}
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
<div class="figure">
|
|
467
|
+
<div class="layer figwrap" data-depth="0.28">
|
|
468
|
+
<div class="frame">
|
|
469
|
+
<div class="${picCls}" style="${picStyle}"></div>
|
|
470
|
+
<div class="edge"></div>
|
|
471
|
+
</div>
|
|
472
|
+
<div class="figcap reveal">
|
|
473
|
+
<span class="n">Fig. ${ch.fig}</span><span class="ln"></span><span>${ch.figLabel}</span>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
${ch.cue ? `<button class="cue" type="button" data-goto="provenance" aria-label="Scroll to Provenance">
|
|
479
|
+
<span>Read on</span><span class="bar"></span></button>` : ''}
|
|
480
|
+
</div>`;
|
|
481
|
+
book.appendChild(sec);
|
|
482
|
+
|
|
483
|
+
const b = document.createElement('button');
|
|
484
|
+
b.type='button'; b.dataset.goto=ch.id;
|
|
485
|
+
b.innerHTML = `<span style="font-family:var(--display);font-size:12px">${ch.num}</span><span class="nm">${ch.nm}</span><span class="tick"></span>`;
|
|
486
|
+
rail.appendChild(b);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* ── INSERT THE GSAP SHOWCASE BEAT ("THE OBJECT, HELD") ──────────────
|
|
490
|
+
Placed right after Chapter III · The Object, so the object is first
|
|
491
|
+
introduced, then held and considered, before the closing enquiry —
|
|
492
|
+
and BEFORE sections/railBtns/CHAPTERS are read by the scroll-spy, so the
|
|
493
|
+
parallel arrays (sections ↔ railBtns ↔ CHAPTERS) stay index-aligned.
|
|
494
|
+
Pure markup here; motion is wired at the end (desktop + non-reduced only,
|
|
495
|
+
and only if GSAP actually loaded). Stays static otherwise. */
|
|
496
|
+
const SHOW_AFTER = 'object'; // chapter whose index we insert after
|
|
497
|
+
const anchorSec = document.getElementById(SHOW_AFTER);
|
|
498
|
+
const anchorIdx = CHAPTERS.findIndex(c=>c.id===SHOW_AFTER); // 2 → insert at 3
|
|
499
|
+
const showMorph = 'radial-gradient(130% 90% at 28% 14%, rgba(154,106,60,.12), rgba(154,106,60,0) 56%), radial-gradient(100% 80% at 84% 88%, rgba(126,85,48,.07), rgba(126,85,48,0) 60%), #E8DCC8';
|
|
500
|
+
|
|
501
|
+
const show = document.createElement('section');
|
|
502
|
+
show.className = 'chapter showcase';
|
|
503
|
+
show.id = 'held';
|
|
504
|
+
show.innerHTML = `
|
|
505
|
+
<div class="show-stage">
|
|
506
|
+
<div class="show-inner">
|
|
507
|
+
<span class="show-eyebrow">Chapter III · ½ — The Object, Held</span>
|
|
508
|
+
<div class="show-object" role="img" aria-label="The object, considered in soft light"></div>
|
|
509
|
+
<div class="show-caps">
|
|
510
|
+
<p class="show-cap a">One object, considered <em>completely</em>.<small>The form, before it is named</small></p>
|
|
511
|
+
<p class="show-cap b">Nothing left <em>to remove</em>.<small>The same form, now resolved</small></p>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
</div>`;
|
|
515
|
+
anchorSec.after(show); // DOM: directly after Chapter III
|
|
516
|
+
|
|
517
|
+
const showBtn = document.createElement('button');
|
|
518
|
+
showBtn.type = 'button'; showBtn.dataset.goto = 'held';
|
|
519
|
+
showBtn.innerHTML = `<span style="font-family:var(--display);font-size:12px">·</span><span class="nm">Held</span><span class="tick"></span>`;
|
|
520
|
+
rail.children[anchorIdx].after(showBtn); // rail: matching entry, same offset
|
|
521
|
+
// keep the CHAPTERS lookup aligned with the new section list (spy reads CHAPTERS[idx].morph)
|
|
522
|
+
CHAPTERS.splice(anchorIdx + 1, 0, { id:'held', nm:'Held', morph:showMorph });
|
|
523
|
+
|
|
524
|
+
const sections = [...document.querySelectorAll('.chapter')];
|
|
525
|
+
const railBtns = [...rail.children];
|
|
526
|
+
|
|
527
|
+
/* navigation clicks (rail + scroll cue) */
|
|
528
|
+
document.querySelectorAll('[data-goto]').forEach(el=>{
|
|
529
|
+
el.addEventListener('click', ()=>{
|
|
530
|
+
document.getElementById(el.dataset.goto)?.scrollIntoView({ behavior: reduce ? 'auto':'smooth' });
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
/* scroll-spy → active rail + background morph.
|
|
535
|
+
Sections are 220vh — taller than the viewport — so collapse the observer
|
|
536
|
+
root to a 1px band at the vertical centre via rootMargin: the section
|
|
537
|
+
straddling screen-centre is the active one (reliable at any height). */
|
|
538
|
+
let activeIdx = -1;
|
|
539
|
+
const spy = new IntersectionObserver((entries)=>{
|
|
540
|
+
entries.forEach(e=>{
|
|
541
|
+
if (!e.isIntersecting) return;
|
|
542
|
+
const idx = sections.indexOf(e.target);
|
|
543
|
+
if (idx===activeIdx) return;
|
|
544
|
+
activeIdx = idx;
|
|
545
|
+
railBtns.forEach((b,j)=> b.setAttribute('aria-current', j===idx ? 'true':'false'));
|
|
546
|
+
const m = CHAPTERS[idx].morph;
|
|
547
|
+
atmos.style.background = m;
|
|
548
|
+
document.body.style.background = m;
|
|
549
|
+
});
|
|
550
|
+
}, { threshold:0, rootMargin:'-50% 0px -50% 0px' });
|
|
551
|
+
sections.forEach(s=> spy.observe(s));
|
|
552
|
+
// paint initial state immediately
|
|
553
|
+
railBtns[0].setAttribute('aria-current','true'); activeIdx = 0;
|
|
554
|
+
|
|
555
|
+
/* mobile / reduced-motion: clean fade-up only, skip the rAF engine */
|
|
556
|
+
if (reduce || isMobile()){
|
|
557
|
+
const up = new IntersectionObserver((es)=>{
|
|
558
|
+
es.forEach(e=>{ if (e.isIntersecting){ e.target.classList.add('in'); up.unobserve(e.target); } });
|
|
559
|
+
}, { threshold:.16 });
|
|
560
|
+
document.querySelectorAll('.reveal').forEach(el=> up.observe(el));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/* ── Desktop parallax — quiet-luxury engine ────────────────────
|
|
565
|
+
Unified progress T ∈ [0,1] over each section's whole pass
|
|
566
|
+
(approach → pin → exit), so content is fully composed when the
|
|
567
|
+
chapter is centred (including chapter I at load). Never starts
|
|
568
|
+
invisible. Only transform + opacity mutate per frame.
|
|
569
|
+
|
|
570
|
+
• figure : BARELY-there drift — depth 0.28, ~3% of viewport over
|
|
571
|
+
the whole pin (perceptual depth, no obvious movement)
|
|
572
|
+
+ a hair of scale settle. No 3D tilt, no filter.
|
|
573
|
+
• copy : gentle counter-drift, full block.
|
|
574
|
+
• title : LETTER-SPACING SCRUB 0.45em → 0em over the first ~30%.
|
|
575
|
+
• reveals: eyebrow / rule / lede / caption ease in and hold. */
|
|
576
|
+
let ticking = false;
|
|
577
|
+
function parallax(){
|
|
578
|
+
const vh = window.innerHeight;
|
|
579
|
+
for (const sec of sections){
|
|
580
|
+
const rect = sec.getBoundingClientRect();
|
|
581
|
+
// only drive sections that are actually on/near screen (perf)
|
|
582
|
+
if (rect.bottom < -vh*0.2 || rect.top > vh*1.2) continue;
|
|
583
|
+
|
|
584
|
+
const total = sec.offsetHeight + vh;
|
|
585
|
+
const T = clamp((vh - rect.top) / total, 0, 1); // 0 below → 1 past
|
|
586
|
+
const enter = smooth(0.10, 0.34, T); // composed by pin
|
|
587
|
+
const exit = 1 - smooth(0.70, 0.90, T); // fades away late
|
|
588
|
+
const live = enter * exit;
|
|
589
|
+
const drift = 0.46 - T; // signed, ~0 at hold
|
|
590
|
+
|
|
591
|
+
// figure — barely-there: depth 0.28 × 24px ≈ ~3% of a 760px viewport
|
|
592
|
+
const fig = sec.querySelector('.figwrap');
|
|
593
|
+
if (fig){
|
|
594
|
+
const depth = +fig.dataset.depth; // 0.28
|
|
595
|
+
const y = drift * 24 * (depth / 0.28); // ~±5–6px across the pin
|
|
596
|
+
const sc = 1.012 - 0.012 * enter; // imperceptible settle
|
|
597
|
+
fig.style.transform = `translate3d(0, ${y.toFixed(2)}px, 0) scale(${sc.toFixed(4)})`;
|
|
598
|
+
fig.style.opacity = (0.18 + 0.82 * live).toFixed(3);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// copy block — gentle counter-drift
|
|
602
|
+
const copy = sec.querySelector('.copytext');
|
|
603
|
+
if (copy){
|
|
604
|
+
copy.style.transform = `translate3d(0, ${(drift * 30).toFixed(2)}px, 0)`;
|
|
605
|
+
copy.style.opacity = (0.16 + 0.84 * live).toFixed(3);
|
|
606
|
+
|
|
607
|
+
// title letter-spacing scrub: 0.45em → 0em over the first ~30%
|
|
608
|
+
const sp = (1 - smooth(0.06, 0.32, T)) * 0.45;
|
|
609
|
+
copy.querySelectorAll('h2 .scrub').forEach(s=>{ s.style.letterSpacing = `${sp.toFixed(3)}em`; });
|
|
610
|
+
|
|
611
|
+
// supporting reveals ease in, then hold (and fade with the exit)
|
|
612
|
+
const rv = smooth(0.16, 0.40, T) * exit;
|
|
613
|
+
copy.querySelectorAll('.reveal').forEach(r=>{ r.style.opacity = rv.toFixed(3); });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// caption — rides with the figure, a touch later
|
|
617
|
+
const cap = sec.querySelector('.figcap');
|
|
618
|
+
if (cap){ cap.style.opacity = (smooth(0.22, 0.46, T) * exit).toFixed(3); }
|
|
619
|
+
}
|
|
620
|
+
ticking = false;
|
|
621
|
+
}
|
|
622
|
+
function onScroll(){ if (!ticking){ ticking = true; requestAnimationFrame(parallax); } }
|
|
623
|
+
window.addEventListener('scroll', onScroll, { passive:true });
|
|
624
|
+
window.addEventListener('resize', ()=>{ ticking = true; requestAnimationFrame(parallax); }, { passive:true });
|
|
625
|
+
parallax(); // initial paint so nothing starts invisible if scroll never fires
|
|
626
|
+
|
|
627
|
+
/* ── GSAP SHOWCASE WIRING — "The Object, Held" ─────────────────────
|
|
628
|
+
Polls for GSAP (the CDN is deferred); guarded again by matchMedia so it
|
|
629
|
+
only arms on desktop + no-reduced-motion and auto-reverts on resize into a
|
|
630
|
+
small / reduced context. Two restrained techniques (taste-guardrails §2):
|
|
631
|
+
• PUSH-IN — the framed object scales 1 → 1.08 over the pin (slow zoom
|
|
632
|
+
toward the subject; nothing else moves in the foreground).
|
|
633
|
+
• MATCH-CUT — caption A → caption B by opacity over a perfectly-still
|
|
634
|
+
composition (identical position; only the words change).
|
|
635
|
+
Only transform + opacity mutate. If GSAP never loads, #held stays static. */
|
|
636
|
+
(function wireShowcase(tries){
|
|
637
|
+
if (!window.gsap || !window.ScrollTrigger){
|
|
638
|
+
if (tries > 0) requestAnimationFrame(()=> wireShowcase(tries-1));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
642
|
+
ScrollTrigger.defaults({ scrub:0.5, fastScrollEnd:true, invalidateOnRefresh:true });
|
|
643
|
+
const mm = gsap.matchMedia();
|
|
644
|
+
mm.add('(min-width:769px) and (prefers-reduced-motion:no-preference)', () => {
|
|
645
|
+
const stage = document.querySelector('#held .show-stage');
|
|
646
|
+
if (!stage) return;
|
|
647
|
+
const obj = stage.querySelector('.show-object');
|
|
648
|
+
const capA = stage.querySelector('.show-cap.a');
|
|
649
|
+
const capB = stage.querySelector('.show-cap.b');
|
|
650
|
+
const els = [obj, capA, capB];
|
|
651
|
+
const tl = gsap.timeline({ scrollTrigger:{
|
|
652
|
+
trigger:'#held', start:'top top', end:'+=220%', pin:stage, anticipatePin:1 } });
|
|
653
|
+
tl.fromTo(obj, { scale:1 }, { scale:1.08, ease:'none' }, 0) // PUSH-IN (§2)
|
|
654
|
+
.to(capA, { opacity:0, ease:'none', duration:0.4 }, 0.45) // MATCH-CUT: A out
|
|
655
|
+
.fromTo(capB, { opacity:0 }, { opacity:1, ease:'none', duration:0.4 }, 0.45); // MATCH-CUT: B in (same cell)
|
|
656
|
+
return () => gsap.set(els, { clearProps:'all' }); // revert to static defaults
|
|
657
|
+
});
|
|
658
|
+
})(120);
|
|
659
|
+
})();
|
|
660
|
+
</script>
|
|
661
|
+
</body>
|
|
662
|
+
</html>
|