@struq/skill 0.3.0 → 0.4.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/README.md +22 -36
- package/package.json +3 -2
- package/skills/struq/SKILL.md +139 -307
- package/skills/struq/blocks/CATALOG.md +426 -0
- package/skills/struq/blocks/backgrounds/background-aurora.tsx +818 -0
- package/skills/struq/blocks/backgrounds/background-facets.tsx +1283 -0
- package/skills/struq/blocks/backgrounds/background-silk.tsx +906 -0
- package/skills/struq/blocks/catalog.json +1302 -0
- package/skills/struq/blocks/footers/footer-contact.tsx +789 -0
- package/skills/struq/blocks/footers/footer-reveal.tsx +313 -0
- package/skills/struq/blocks/headers/header-bar.tsx +652 -0
- package/skills/struq/blocks/headers/menu-curtain.tsx +790 -0
- package/skills/struq/blocks/heroes/hero-framed.tsx +627 -0
- package/skills/struq/blocks/heroes/hero-index.tsx +855 -0
- package/skills/struq/blocks/heroes/hero-poster.tsx +1086 -0
- package/skills/struq/blocks/heroes/hero-split.tsx +685 -0
- package/skills/struq/blocks/heroes/hero-still.tsx +862 -0
- package/skills/struq/{recipes/components/struq → blocks/kit}/Cursor.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/HeroCanvas.tsx +2 -2
- package/skills/struq/{recipes/components/struq → blocks/kit}/HorizontalGallery.tsx +2 -2
- package/skills/struq/{recipes/components/struq → blocks/kit}/MagneticButton.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/MediaFrame.tsx +2 -2
- package/skills/struq/{recipes/components/struq → blocks/kit}/PinnedZoom.tsx +14 -4
- package/skills/struq/{recipes/components/struq → blocks/kit}/Reveal.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/SmoothScroll.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/SplitHeadline.tsx +1 -1
- package/skills/struq/blocks/kit/blocks.css +2102 -0
- package/skills/struq/blocks/kit/images.ts +17 -0
- package/skills/struq/blocks/kit/photo-read.ts +102 -0
- package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +706 -0
- package/skills/struq/blocks/page-transitions/page-transition-slices.tsx +469 -0
- package/skills/struq/blocks/page-transitions/page-transition-stack.tsx +583 -0
- package/skills/struq/blocks/preloaders/preloader-gate.tsx +358 -0
- package/skills/struq/blocks/preloaders/preloader-slices-uncovered.ts +50 -0
- package/skills/struq/blocks/preloaders/preloader-slices.tsx +289 -0
- package/skills/struq/blocks/preloaders/preloader-stack.tsx +367 -0
- package/skills/struq/blocks/preloaders/preloader-wordmark.tsx +568 -0
- package/skills/struq/blocks/sections/about-studio.tsx +436 -0
- package/skills/struq/blocks/sections/approach.tsx +661 -0
- package/skills/struq/blocks/sections/contact-form.tsx +664 -0
- package/skills/struq/blocks/sections/cta-closing.tsx +763 -0
- package/skills/struq/blocks/sections/fanned-cards.tsx +547 -0
- package/skills/struq/blocks/sections/film-strip.tsx +385 -0
- package/skills/struq/blocks/sections/grid-assembly.tsx +616 -0
- package/skills/struq/blocks/sections/hover-list.tsx +467 -0
- package/skills/struq/blocks/sections/image-trail.tsx +432 -0
- package/skills/struq/blocks/sections/manifesto.tsx +691 -0
- package/skills/struq/blocks/sections/panel-accordion.tsx +380 -0
- package/skills/struq/blocks/sections/reveals.tsx +416 -0
- package/skills/struq/blocks/sections/split-pair.tsx +670 -0
- package/skills/struq/blocks/sections/sticky-chapters.tsx +256 -0
- package/skills/struq/blocks/sections/velocity-type.tsx +269 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-fill-rows.tsx → blocks/showpieces/fill-rows.tsx} +1 -1
- package/skills/struq/{cinematic/showpieces/toonstuk-hero-opener.tsx → blocks/showpieces/hero-opener.tsx} +5 -5
- package/skills/struq/{cinematic/showpieces/toonstuk-image-to-fullbleed.tsx → blocks/showpieces/image-to-fullbleed.tsx} +16 -5
- package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +48 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-panel-wipe.tsx → blocks/showpieces/panel-wipe.tsx} +2 -2
- package/skills/struq/{cinematic/showpieces/toonstuk-pixel-resolve.tsx → blocks/showpieces/pixel-resolve.tsx} +1 -1
- package/skills/struq/{cinematic/showpieces/toonstuk-project-stack.tsx → blocks/showpieces/project-stack.tsx} +2 -2
- package/skills/struq/blocks/showpieces/scrub-connector.tsx +307 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-wordmark-finale.tsx → blocks/showpieces/wordmark-finale.tsx} +1 -1
- package/skills/struq/blocks/transitions/zoom-through.tsx +613 -0
- package/skills/struq/canon/canon.json +627 -0
- package/skills/struq/cinematic/README.md +7 -18
- package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +1 -1
- package/skills/struq/cinematic/examples/code/shared/styles.css +1 -1
- package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +1 -1
- package/skills/struq/cinematic/references/01-core-mental-model.md +1 -1
- package/skills/struq/cinematic/references/02-vocabulary.md +1 -1
- package/skills/struq/cinematic/references/07-stack-integration.md +2 -2
- package/skills/struq/recipes/app/fonts.ts +12 -22
- package/skills/struq/recipes/app/globals.css +34 -177
- package/skills/struq/recipes/app/layout.tsx +13 -11
- package/skills/struq/references/craft.md +77 -0
- package/skills/struq/references/iconography.md +28 -230
- package/skills/struq/references/stack-targets.md +23 -74
- package/skills/struq/references/tells.md +52 -0
- package/skills/struq/references/triage.md +6 -7
- package/skills/struq/scripts/blocks.mjs +156 -0
- package/skills/struq/scripts/canon.mjs +106 -281
- package/skills/struq/scripts/check.mjs +92 -126
- package/skills/struq/scripts/images.mjs +2 -2
- package/skills/struq/scripts/log.mjs +6 -23
- package/skills/struq/scripts/tokens.mjs +222 -0
- package/skills/struq/cinematic/showpieces/README.md +0 -18
- package/skills/struq/cinematic/showpieces/toonstuk-manifesto-scrub.tsx +0 -81
- package/skills/struq/cinematic/showpieces/toonstuk-scrub-connector.tsx +0 -78
- package/skills/struq/references/anti-patterns.md +0 -330
- package/skills/struq/references/baseline-prompt.md +0 -122
- package/skills/struq/references/brand.md +0 -143
- package/skills/struq/references/brief-router.md +0 -218
- package/skills/struq/references/composition.md +0 -190
- package/skills/struq/references/depth-parallax.md +0 -235
- package/skills/struq/references/direction.md +0 -188
- package/skills/struq/references/feel-layer.md +0 -107
- package/skills/struq/references/gsap-api.md +0 -293
- package/skills/struq/references/hero-scenes.md +0 -107
- package/skills/struq/references/imagery.md +0 -125
- package/skills/struq/references/loaders.md +0 -287
- package/skills/struq/references/micro-interactions.md +0 -270
- package/skills/struq/references/mobile-composition.md +0 -70
- package/skills/struq/references/nav-header.md +0 -296
- package/skills/struq/references/niche-packets.md +0 -338
- package/skills/struq/references/page-architecture.md +0 -134
- package/skills/struq/references/page-entrances.md +0 -198
- package/skills/struq/references/performance.md +0 -235
- package/skills/struq/references/pinning-scrub.md +0 -340
- package/skills/struq/references/pitfalls.md +0 -290
- package/skills/struq/references/route-transitions.md +0 -281
- package/skills/struq/references/scaling.md +0 -151
- package/skills/struq/references/scroll-reveals.md +0 -279
- package/skills/struq/references/sections.md +0 -141
- package/skills/struq/references/seo.md +0 -219
- package/skills/struq/references/setup.md +0 -397
- package/skills/struq/references/showcase.md +0 -87
- package/skills/struq/references/struq-imagery.md +0 -107
- package/skills/struq/references/svg-motion.md +0 -390
- package/skills/struq/references/transitions.md +0 -160
- package/skills/struq/references/typography.md +0 -720
- package/skills/struq/references/vibe-presets.md +0 -287
- package/skills/struq/references/webgl-r3f.md +0 -274
- package/skills/struq/references/webgl-scenes.md +0 -593
- package/skills/struq/scripts/font-names.mjs +0 -40
- package/skills/struq/scripts/recipes.mjs +0 -135
- /package/skills/struq/{recipes/lib → blocks/kit}/gsap.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/contours.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/dots.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/ripple.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/types.ts +0 -0
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
# Direction: plan everything before the first component
|
|
2
|
-
|
|
3
|
-
Award-tier sites are decided before they are built. The logo, the one idea the site is about, the two typefaces, the palette, the icon family, the arc and the showpieces are made once, together, so every later choice can be checked against them. A build that discovers its direction while coding ends up with a Tailwind default and a nice hero.
|
|
4
|
-
|
|
5
|
-
This file is the orchestrator for **Build**, **Redesign** and the planning half of **Directed**. It runs seven phases, numbered 0 to 6, with one gate between planning and building. It writes one contract file, `STRUQ.md` in the project root, and that file is the only plan artifact: there is no second plan block, no separate brief document, no parallel folder for a normal build.
|
|
6
|
-
|
|
7
|
-
## The phases
|
|
8
|
-
|
|
9
|
-
| # | Phase | Output (a section of STRUQ.md) | Procedure | Skip when |
|
|
10
|
-
|---|---|---|---|---|
|
|
11
|
-
| 0 | **Intake** | header (Mode, Stack), MATERIALS | this file §1, with [brief-router.md](brief-router.md) §1 and §1b | never |
|
|
12
|
-
| 1 | **Brand** | BRAND | [brand.md](brand.md) | a brand exists (extract it, §2) |
|
|
13
|
-
| 2 | **Concept** | CONCEPT | this file §3; the tension from [brief-router.md](brief-router.md) §1 | never |
|
|
14
|
-
| 3 | **System** | SYSTEM | this file §4 (sole owner of type, palette, icons, imagery, motion) | never |
|
|
15
|
-
| 4 | **Site plan** | PLAN | [brief-router.md](brief-router.md) §2 | never |
|
|
16
|
-
| Gate | **Approval** | one message: summary, materials request, "Akkoord?" | this file §5 | never: it is the one stop, and there is no second |
|
|
17
|
-
| 5 | **Build** | the site | [../SKILL.md](../SKILL.md) workflow steps 2 to 7 | |
|
|
18
|
-
| 6 | **Verify** | VERIFY | [../SKILL.md](../SKILL.md) workflow step 8 | never |
|
|
19
|
-
|
|
20
|
-
Phases 0 to 4 together are workflow step 1, **Direction**, in [../SKILL.md](../SKILL.md). Directed mode (`/struq direct`) runs phases 0 to 3 here, then hands over to [../cinematic/README.md](../cinematic/README.md), whose lean outputs are appended to the same STRUQ.md as a DIRECTION section.
|
|
21
|
-
|
|
22
|
-
**Scale the ceremony to the job.** A one-page site for an existing brand runs phases 0, 2, 3 and 4 in a few minutes. A new company with no name runs all of them. Never ask a question the brief already answers; never skip a phase because it feels obvious. Write the obvious answer down, because the later phases read it.
|
|
23
|
-
|
|
24
|
-
## 1. Intake
|
|
25
|
-
|
|
26
|
-
**One stop, not two.** With a usable brief, do not block here. Run the extraction in [brief-router.md](brief-router.md) §1 silently, record what exists under MATERIALS, record what is missing (and how each gap is handled) under MATERIALS and ASSUMPTIONS, and carry on to the gate. The request for materials goes inside the single gate message (§5). Block early only when the wizard trigger conditions in [brief-router.md](brief-router.md) §1b hold: no usable brief, a request to be guided, or no answer to "who buys" and "what are they afraid of".
|
|
27
|
-
|
|
28
|
-
What MATERIALS tracks:
|
|
29
|
-
|
|
30
|
-
- Name and logo (files, not descriptions)
|
|
31
|
-
- Brand colors and fonts, if any are fixed
|
|
32
|
-
- Real copy, or at least the facts: what it is, who it is for, what it costs, where it is
|
|
33
|
-
- Photos, product shots, video, 3D models
|
|
34
|
-
- Real numbers (clients, years, results) and real quotes, with permission
|
|
35
|
-
- An existing site (switch to Redesign: [triage.md](triage.md))
|
|
36
|
-
- One or two sites whose feel they envy
|
|
37
|
-
|
|
38
|
-
**Never invent proof.** Testimonials, client logos, stats and awards that do not exist are left out, not faked.
|
|
39
|
-
|
|
40
|
-
**Never write in a real person's or brand's voice.** On a site named after a real person or a real business:
|
|
41
|
-
|
|
42
|
-
- No first person attributed to them ("I make those decisions...") and no quotes in their name. Write in the studio's or the site's third-person voice.
|
|
43
|
-
- No biography, training, process or sourcing stated as fact ("trained as a production thrower", "eats at the restaurant before drawing", "clay dug in Alentejo") unless the brief says it.
|
|
44
|
-
- No catalogue facts: invented works, sizes, glazes, materials, prices, editions, sold-out states, stockists or client names.
|
|
45
|
-
|
|
46
|
-
When the layout needs that copy to exist, draft it as plain layout copy and list every drafted claim under ASSUMPTIONS ("Studio paragraph: drafted, replace with Ana's words"; "Work index: 8 entries drafted as layout copy, no prices, no client names"). A portfolio with no work list gets its entries drafted the same way, titled as layout copy, never as the record. The gate message names the drafts so the client can replace them.
|
|
47
|
-
|
|
48
|
-
**No real imagery yet.** Plan for it instead of papering over it:
|
|
49
|
-
|
|
50
|
-
- Media-led showpieces work from the first build: the beeldbank pack supplies the photos. Never stage one on empty frames.
|
|
51
|
-
- Every missing image becomes a shot-list line (subject, grade, ratio) under PLAN `Assets to produce`, so it can be shot, sourced or generated later ([imagery.md](imagery.md) §4).
|
|
52
|
-
- In the build, every image slot is filled from the Struq beeldbank pack ([struq-imagery.md](struq-imagery.md)), automatically and without asking: the images are CC0. A frame is never empty and never a placeholder.
|
|
53
|
-
|
|
54
|
-
## 2. Brand
|
|
55
|
-
|
|
56
|
-
If there is no name or no logo, run [brand.md](brand.md). It produces a name shortlist with one recommendation, a wordmark and a mark as SVG, a favicon set, and one voice line; the OG image is built later, once routes exist ([seo.md](seo.md) §3). If a brand exists, extract it instead: logo file, the exact colors, the fonts, how they write. Existing brand assets always beat the preset; the preset still supplies the token structure they pour into.
|
|
57
|
-
|
|
58
|
-
| What exists | Run |
|
|
59
|
-
|---|---|
|
|
60
|
-
| Nothing | [brand.md](brand.md) in full |
|
|
61
|
-
| A name, no logo, no brand | [brand.md](brand.md) §1 and §3 to §5: skip naming |
|
|
62
|
-
| **A named brand (colours, a font, a voice) but no logo file** | Skip [brand.md](brand.md) §1 and §2: never rename, never reposition. Draw a **provisional** wordmark from the brand's own face ([brand.md](brand.md) §3), write it to the same paths (`brand/logo.svg`, `brand/mark.svg`) so the real logo drops in, and record it under ASSUMPTIONS: "Logo: provisional wordmark from <face>, replace with the brand file" |
|
|
63
|
-
| A full brand with logo files | Extract only; no [brand.md](brand.md) |
|
|
64
|
-
|
|
65
|
-
**Person-named sites skip naming.** Portfolios, studios named after a person, practices and clinics use the person's name. If the name is unknown, write a placeholder, record it under ASSUMPTIONS, skip [brand.md](brand.md) §2 and go straight to the wordmark (§3 there).
|
|
66
|
-
|
|
67
|
-
## 3. Concept
|
|
68
|
-
|
|
69
|
-
The concept is what makes the site feel authored instead of assembled. Write three lines:
|
|
70
|
-
|
|
71
|
-
- **Idea:** one sentence the whole site is about, specific to this client. Not "modern and clean"; "every page is a quiet room with one object in it" or "the site moves like a slow pan across a construction drawing".
|
|
72
|
-
- **Tension:** what the visitor needs to feel (convince) and what the site must never signal (never). From [brief-router.md](brief-router.md) §1 question 2.
|
|
73
|
-
- **Motif:** the one recurring visual device that carries the idea through the page. A line that draws through every section, a frame that every image sits in, a numeral system, a crop direction, a mark that morphs. One motif, used everywhere, beats five effects used once.
|
|
74
|
-
|
|
75
|
-
Every later decision gets checked against these three lines. If a showpiece or effect does not serve the idea or the motif, it goes.
|
|
76
|
-
|
|
77
|
-
## 4. System
|
|
78
|
-
|
|
79
|
-
This section is the only place SYSTEM is decided. Other files point here.
|
|
80
|
-
|
|
81
|
-
**Read budget.** During planning, read only the Decide-here section at the top of [typography.md](typography.md) and of [iconography.md](iconography.md), the token contract and the one chosen preset in [vibe-presets.md](vibe-presets.md), and the grade table in [imagery.md](imagery.md) §1. Two more reads are planning reads, not build reads, when they apply: the preset's **hero entrance** recipe ([page-entrances.md](page-entrances.md) for `match-cut-hero` and `clip-line-hero`, on any site), and, when the brief asks for 3D, the **WebGL plan** ([webgl-r3f.md](webgl-r3f.md) §1 and [webgl-scenes.md](webgl-scenes.md) §5: poster, gate, touch fallback, and the "No model file yet" ladder in §4). Read the full files during the build (workflow step 2), not now.
|
|
82
|
-
|
|
83
|
-
Decide in this order:
|
|
84
|
-
|
|
85
|
-
0. **Niche.** Buyer, genre and preset from [brief-router.md](brief-router.md) §2 step 1, plus the one borrowed field (TYPE or PALETTE) if any.
|
|
86
|
-
1. **Type.** Exactly two faces: display (run light, 300 to 400, unless the concept is a declared heavy genre) and text. Take a pairing id from the brand's temperament per [typography.md](typography.md) Decide here: Signature (S1 to S7) first, Extended (E1 to E6) only when no Signature temperament fits. "Unsure" is not a temperament and never a reason to take S1. Name the faces, the role weights, the source and the license, the **one signature move** ([typography.md](typography.md) §5) and the **hero entrance** (the preset's named entrance, or a declared different one).
|
|
87
|
-
2. **Palette.** Surface ladder (four steps), ink tiers (ink, muted, faint), one accent and the line color, written as tokens. Existing brand colors pour into this structure. Add the radius. [vibe-presets.md](vibe-presets.md)
|
|
88
|
-
|
|
89
|
-
**Brand colours onto the ladder.** A brand's colour names are not token names: brand "ink" usually means the brand's dark, while `--color-ink` is the *text* colour. Map by role, never by name:
|
|
90
|
-
|
|
91
|
-
| Brand colour | On a light preset | On a dark preset |
|
|
92
|
-
|---|---|---|
|
|
93
|
-
| The brand's dark | `ink` (text), and the inverted surfaces | `surface-0` (the page); `ink` becomes an off-white |
|
|
94
|
-
| The brand's light | `surface-0` (the page) | `ink` (text) |
|
|
95
|
-
| The brand's accent | `accent` | `accent` |
|
|
96
|
-
|
|
97
|
-
Derive the rest of the ladder from the mapped pair (surfaces step toward the ink, muted and faint tiers mixed from ink). Check the accent against `surface-0`: 4.5:1 or better and it may be text (links); 3:1 or better and it may set large type and UI; below that it is fills and rules only. Write which under SYSTEM.
|
|
98
|
-
3. **Icons.** One icon family for the whole site, chosen by the order in [iconography.md](iconography.md) §2: the brand's own set, else Struq Icons. Name the package, the stroke and how its accent maps to the palette.
|
|
99
|
-
4. **Imagery.** One grade, the declared ratio set (2 to 3), grain yes or no. [imagery.md](imagery.md)
|
|
100
|
-
5. **Scroll and motion.** Lenis lerp (or native, with the reason). Motion is the baseline MOTION line in [../SKILL.md](../SKILL.md) by default: write "baseline". Any other duration band or ease pair, including the alternative pairs in [feel-layer.md](feel-layer.md) §3, is an allowed deviation only when it is written here with its reason.
|
|
101
|
-
|
|
102
|
-
## 5. The gate
|
|
103
|
-
|
|
104
|
-
Write `STRUQ.md` (template below), then send one message with: the name, the idea line, the two typefaces and pairing id, the signature move and hero entrance, the palette as hex, the icon family, the arc as one line, the showpiece ids, the open assumptions, and the materials request. End it with exactly this:
|
|
105
|
-
|
|
106
|
-
> **Akkoord? Heb je een logo, foto's of echte cijfers, stuur ze nu.**
|
|
107
|
-
|
|
108
|
-
Apply edits and new materials to the file; do not re-interview. This is the only stop between the brief and the build (the wizard in [brief-router.md](brief-router.md) §1b is the one exception, and it runs before this gate, not instead of it).
|
|
109
|
-
|
|
110
|
-
If the user said "just build it", or the run is non-interactive, write the file, state the assumptions at the top and continue.
|
|
111
|
-
|
|
112
|
-
## 6. During the build
|
|
113
|
-
|
|
114
|
-
`STRUQ.md` is the contract. Before each workflow step, re-read the section it depends on. If a decision has to change during the build (a font does not have the weight, an asset is missing), change it **in the file first**, with one line under LOG saying why, then change the code. A site that drifts from its own plan is how a directed build turns back into a template.
|
|
115
|
-
|
|
116
|
-
After verify, append the score and the open issues under VERIFY.
|
|
117
|
-
|
|
118
|
-
## STRUQ.md template
|
|
119
|
-
|
|
120
|
-
```markdown
|
|
121
|
-
# STRUQ.md: <project name>
|
|
122
|
-
|
|
123
|
-
Mode: build | redesign | directed
|
|
124
|
-
Stack: <Next.js | Vite> <installed major> · React <major> · Tailwind <major> · GSAP <x.y> · Lenis <major> · three <0.x> (if used), read from package.json (setup.md §0); Vite translations per stack-targets.md
|
|
125
|
-
|
|
126
|
-
## MATERIALS
|
|
127
|
-
Have: <logo, copy, photos, numbers, quotes>
|
|
128
|
-
Missing: <list, with how each gap is handled>
|
|
129
|
-
References: <sites the client envies, and what to take from each>
|
|
130
|
-
|
|
131
|
-
## BRAND
|
|
132
|
-
Name: <name> (shortlist and rationale in brand/NAMING.md, if generated)
|
|
133
|
-
Logo: brand/logo.svg (wordmark), brand/mark.svg (mark), app/icon.svg, app/apple-icon.png
|
|
134
|
-
OG: app/opengraph-image.tsx, plus one per shared route (seo.md §3)
|
|
135
|
-
Voice: <one line, e.g. "plain, certain, a little dry; short sentences; no exclamation marks">
|
|
136
|
-
|
|
137
|
-
## CONCEPT
|
|
138
|
-
Idea: <one sentence>
|
|
139
|
-
Tension: convince <...> / never signal <...>
|
|
140
|
-
Motif: <the one recurring device>
|
|
141
|
-
|
|
142
|
-
## SYSTEM
|
|
143
|
-
Niche: <buyer + business model> · genre <Portfolio | Studio | Product launch | Campaign>
|
|
144
|
-
· preset <Soft Modern | Warm Editorial | Dark Cinematic | Luxury Minimal | Brutalist (genre appendix, declared genre only)>
|
|
145
|
-
[+ borrowed TYPE or PALETTE from <preset>]
|
|
146
|
-
Display: <face> <weight> (<source>, <license>) · pairing <S1 to S7 | E1 to E6>
|
|
147
|
-
Text: <face> <weights> (<source>, <license>)
|
|
148
|
-
Signature move: <weight-in | soft-scrub | type-mask-media | fit-width-wordmark | cropped display>
|
|
149
|
-
Hero entrance: <the preset's named entrance, or a declared different one>
|
|
150
|
-
Palette: <light | dark> · --surface-0 #... --surface-1 #... --surface-2 #... --surface-3 #...
|
|
151
|
-
--ink #... --ink-muted #... --ink-faint #... (non-text only) --accent #... --line <...>
|
|
152
|
-
Radius: <n>px
|
|
153
|
-
Icons: <family, package, stroke> (default: Struq Icons, @struq/icons, strokeWidth 1.5, accent = --color-accent)
|
|
154
|
-
Imagery: <grade>, ratios <2 to 3>, grain <opacity | none>
|
|
155
|
-
Scroll: Lenis lerp <the preset's stated value | native, because ...> on gsap.ticker
|
|
156
|
-
Motion: baseline | deviations: <each one, with its reason>
|
|
157
|
-
|
|
158
|
-
## PLAN
|
|
159
|
-
Pages: <list with slugs>
|
|
160
|
-
Arc (home): <numbered rows: section [beat] note, spacing role>; proof form; grid-breaks
|
|
161
|
-
Reveals: headlines <...> · blocks <...> · images <...> · svg <moment | none>
|
|
162
|
-
Showpieces: <showpiece ids, arc row, mobile fallback> (1 to 2, breathers on both sides; a showpiece is its own density class, page-architecture.md §3)
|
|
163
|
-
System recipes: <system ids: line-rise-hero, route-curtain, fullscreen-menu ...>
|
|
164
|
-
Transitions: <route-curtain variant | none>, loader <yes | no>
|
|
165
|
-
WebGL: <none | the moment, its poster, its fallback>
|
|
166
|
-
Copy budget: <per section>
|
|
167
|
-
Assets to produce: <shot list: subject, grade, ratio, source>
|
|
168
|
-
|
|
169
|
-
## PRESERVED (redesign only)
|
|
170
|
-
- <what will not be touched: brand color, copy, component primitives>
|
|
171
|
-
|
|
172
|
-
## ASSUMPTIONS
|
|
173
|
-
- <every decision made without the user's input>
|
|
174
|
-
- <every drafted claim: copy in a real person's or brand's name, catalogue entries, a provisional logo, a 3D stand-in>
|
|
175
|
-
|
|
176
|
-
## LOG
|
|
177
|
-
- <date> <decision changed and why>
|
|
178
|
-
- <date> <API>: installed <pkg major> differs from the skill's code; adapted to <change> (setup.md §0)
|
|
179
|
-
|
|
180
|
-
## VERIFY
|
|
181
|
-
Server: next start on :<port>, port owner confirmed
|
|
182
|
-
Viewports: 390, 768, 1440, 1920
|
|
183
|
-
Score: <n>/100 · LCP <s> (element) CLS <n> INP <ms | field data pending> · first-load JS <n> kB gzip
|
|
184
|
-
Motion: pins engaged slow and fast <ok>, descender test <ok>, frame trace <ok>
|
|
185
|
-
Open: <issues>
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Directed mode appends `## DIRECTION` after PLAN, per [../cinematic/README.md](../cinematic/README.md).
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# The Feel Layer: Values You'd Actually Ship
|
|
2
|
-
|
|
3
|
-
Techniques are copyable; feel is in the numbers. Two builds can run the same code and one feels like Locomotive while the other feels like a tutorial. The delta is lerp values, scrub smoothing, ease pairing, and duration scale. This file is the tuning reference for everything else in the skill.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 1. Lenis: the scroll's personality
|
|
8
|
-
|
|
9
|
-
Lenis interpolates real scroll position toward the target each frame. `lerp` is the fraction covered per frame: **lower = floatier, heavier; higher = tighter, more direct.** This single number sets the personality of the entire site, because every scrub-tied effect inherits it.
|
|
10
|
-
|
|
11
|
-
| lerp | Feel | Ship it for |
|
|
12
|
-
|---|---|---|
|
|
13
|
-
| 0.05-0.07 | syrup-heavy, luxurious, slightly defiant | luxury/fashion one-pagers, short narrative scrolls. Risky: long pages feel slow to operate |
|
|
14
|
-
| **0.09-0.11** | **weighted but obedient (the default)** | **portfolios, studios, most marketing** |
|
|
15
|
-
| 0.12-0.16 | light glide, barely-smoothed | content-heavy marketing, editorial with lots of reading |
|
|
16
|
-
| (no Lenis) | native | dashboards, docs, apps: smoothing a tool insults the user |
|
|
17
|
-
|
|
18
|
-
Decision rule: the more *reading* per viewport, the higher the lerp; the more *imagery*, the more floor you can afford. Alternative: `duration: 1.2` + `easing` (Lenis's time-based mode) instead of `lerp`. Same idea, easier to reason about as "a scroll impulse takes ~1.2s to settle." Pick one mode, don't set both.
|
|
19
|
-
|
|
20
|
-
Always:
|
|
21
|
-
- `autoRaf: false` + GSAP ticker + `lagSmoothing(0)` ([setup.md](setup.md)): two clocks = micro-jitter on every scrub.
|
|
22
|
-
- `anchors: true` so in-page links still work.
|
|
23
|
-
- Under `prefers-reduced-motion`, keep Lenis mounted and gate the motion it drives (pins, parallax, scrub showpieces) in `matchMedia` branches, as [setup.md](setup.md) §2 and §4 wire it. Returning early from the provider makes native scroll look like a broken setup.
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## 2. Scrub: direct drive vs. smoothed
|
|
28
|
-
|
|
29
|
-
`scrub: true` = value locks to scroll position exactly. `scrub: <n>` = value *chases* scroll position over ~n seconds: scroll stops, animation eases into place.
|
|
30
|
-
|
|
31
|
-
| Value | Feel | Use for |
|
|
32
|
-
|---|---|---|
|
|
33
|
-
| `true` | mechanical 1:1, instant stop | things that must feel *attached*: hero departure, progress bars, curtains the user "drags," marquee drift |
|
|
34
|
-
| `0.5-1` | weighted, settles after the hand stops | **default for transforms**: parallax layers, pinned sequences, horizontal scroll |
|
|
35
|
-
| `1.5-2.5` | cinematic float, noticeable lag | large background media, ambient/atmospheric layers |
|
|
36
|
-
| `>3` | detached, reads as broken | nothing |
|
|
37
|
-
|
|
38
|
-
Two interacting smoothings: Lenis already smooths the scroll *input*; scrub smooths the *output* on top. Lenis at 0.1 + `scrub: 1` is the classic pairing. If you stack Lenis 0.06 + scrub 2.5, the page feels drunk. Rule: **lower Lenis lerp → lower scrub numbers.**
|
|
39
|
-
|
|
40
|
-
Mixed rates in one section are the pro move: hero title at `scrub: true` (attached), background media at `scrub: 1.5` (floating behind it). The differential *is* the depth.
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## 3. Ease pairing: the system, not the menu
|
|
45
|
-
|
|
46
|
-
Full curve table lives in [transitions.md](transitions.md). The shipping system:
|
|
47
|
-
|
|
48
|
-
**The default system is the baseline MOTION line in [../SKILL.md](../SKILL.md), and these are the same numbers:**
|
|
49
|
-
|
|
50
|
-
| Role | Ease | GSAP name | CSS |
|
|
51
|
-
|---|---|---|---|
|
|
52
|
-
| Entrances | expo-out | `expo.out` | `cubic-bezier(0.16, 1, 0.3, 1)` |
|
|
53
|
-
| Exits | power2-in (cubic-in) | `power2.in` | `cubic-bezier(0.32, 0, 0.67, 0)` |
|
|
54
|
-
| Position swaps (A to B moves) | quart-in-out | `power3.inOut` | `cubic-bezier(0.76, 0, 0.24, 1)` |
|
|
55
|
-
| Curtains and panels | quint-in-out | `power4.inOut` | `cubic-bezier(0.83, 0, 0.17, 1)` |
|
|
56
|
-
|
|
57
|
-
GSAP's names are offset by one from the classic curve names: `power1` = quad, `power2` = cubic, `power3` = quart, `power4` = quint.
|
|
58
|
-
|
|
59
|
-
**Allowed deviations, recorded in STRUQ.md SYSTEM with a reason.** A preset or concept may swap the pair; it is never a silent per-component choice:
|
|
60
|
-
- *Kinetic*: `power4.out` (quint-out) entrances + `back.out(1.4)` accents (crisp, springy)
|
|
61
|
-
- *Editorial-calm*: `power3.out` (quart-out) entrances + `power2.inOut` moves (nothing shouts)
|
|
62
|
-
|
|
63
|
-
Then three rules:
|
|
64
|
-
1. Entrances out-ease, exits in-ease (`power2.in`), A→B moves in-out. Never the reverse.
|
|
65
|
-
2. Scrubbed tweens: `ease: "none"` for tracking values; segment eases (power2) only *inside* scrubbed timelines for phase swaps.
|
|
66
|
-
3. `back`/`elastic` are accent spices: ≤1 element type per page (toast, badge, magnetic return). Two springy things = toy.
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
## 4. Duration & stagger scale
|
|
71
|
-
|
|
72
|
-
Durations scale with **visual mass** (size × contrast of the change), not importance. A 90vw image takes 1.2s; a 14px tag takes 0.25s; same duration on both makes one feel sluggish and the other twitchy.
|
|
73
|
-
|
|
74
|
-
| Tier | Duration | Examples |
|
|
75
|
-
|---|---|---|
|
|
76
|
-
| Micro | 0.2 to 0.3s | press, toggles, underline, tag slide |
|
|
77
|
-
| Standard UI | 0.3 to 0.6s | dropdowns, card hovers, accordion, text swap |
|
|
78
|
-
| Reveal | 0.6 to 1.0s | section entrances, split lines, masked images |
|
|
79
|
-
| Hero and cinematic | 1.0 to 1.6s | hero entrance, page curtain + content rise, full-bleed media |
|
|
80
|
-
| Over 1.6s | n/a | only multi-part timelines; a single tween this long = user waiting |
|
|
81
|
-
|
|
82
|
-
These bands are the baseline's, number for number. A different band is a deviation and goes into STRUQ.md SYSTEM with its reason.
|
|
83
|
-
|
|
84
|
-
Exits run about 60% of their entrance's duration (leaving is decisive; see [transitions.md](transitions.md) §4).
|
|
85
|
-
|
|
86
|
-
Stagger:
|
|
87
|
-
|
|
88
|
-
| each | Feel | Use |
|
|
89
|
-
|---|---|---|
|
|
90
|
-
| 0.02-0.04 | texture: perceived as one rich motion | chars, long lists (12+) |
|
|
91
|
-
| **0.06-0.1** | **readable rhythm (the default)** | cards, lines, nav items |
|
|
92
|
-
| 0.12-0.18 | deliberate, editorial | 2-4 big blocks |
|
|
93
|
-
| >0.2 | queueing: last item enters embarrassingly late | avoid; for n items keep `each × n ≤ ~0.8s` |
|
|
94
|
-
|
|
95
|
-
`stagger: { each: 0.08, from: "center" | "edges" | "random" }`: vary the origin by composition (centered logo walls from `"center"`, grids from `"start"`, particle-ish textures `"random"`).
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## 5. The 60-second feel audit
|
|
100
|
-
|
|
101
|
-
Scroll the build top to bottom once, then:
|
|
102
|
-
|
|
103
|
-
1. **Stop scrolling abruptly mid-section.** Do scrubbed layers settle softly (good) or freeze instantly (raise scrub) or keep drifting awkwardly (lower it)?
|
|
104
|
-
2. **Watch one entrance.** Can you see it *end*? If you can't perceive the deceleration tail, the ease or duration is too timid. This is the #1 "feels like a template" cause even when the technique is right.
|
|
105
|
-
3. **Count layers in a hover.** One moving layer = cheap, two or three = choreography.
|
|
106
|
-
4. **Flick-scroll the whole page.** Does the site stay composed (Lenis + scrub doing their job) or strobe with triggered animations firing everywhere? If strobing: too many `once` reveals triggered too late (move starts earlier, e.g. `top 85%`).
|
|
107
|
-
5. **Is anything moving that the user didn't cause?** Autonomous looping motion (except one deliberate marquee/ambient element) drains the premium feel. Agency motion is overwhelmingly *response*, not *activity*.
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
# GSAP API: the 15% this skill actually uses
|
|
2
|
-
|
|
3
|
-
This is not a GSAP manual. It's the exact API surface the technique files rely on, so the skill needs no external reference. Everything imports from the `@/lib/gsap` registry ([setup.md](setup.md) §1); every animating component follows the `useGSAP` rules ([setup.md](setup.md) §3).
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 1. Tweens & timelines
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
gsap.to(target, { x: 100, duration: 0.8, ease: "expo.out" }); // current → values
|
|
11
|
-
gsap.from(target, { y: 48, autoAlpha: 0 }); // values → current (SSR HTML is the final state; prefer this)
|
|
12
|
-
gsap.fromTo(target, { yPercent: -8 }, { yPercent: 8, ease: "none" }); // explicit both ends
|
|
13
|
-
gsap.set(target, { autoAlpha: 0 }); // instant, no tween
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
- `target` = element, ref `.current`, selector string (scoped inside `useGSAP`), or array.
|
|
17
|
-
- **`autoAlpha`** = opacity + `visibility`. Use it instead of `opacity` for entrances so hidden elements can't intercept clicks.
|
|
18
|
-
- **Stagger object:** `stagger: { each: 0.08, from: "start" | "center" | "edges" | "end" | "random", grid: [rows, cols] }`. Plain `stagger: 0.08` = shorthand for `each`.
|
|
19
|
-
- **`gsap.defaults({ ease, duration })`**: set once in the registry so every tween starts agency-grade.
|
|
20
|
-
|
|
21
|
-
**Timelines** sequence tweens; the *position parameter* (third argument) is the craft:
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
const tl = gsap.timeline({ defaults: { ease: "expo.out", duration: 1 } });
|
|
25
|
-
tl.from(a, {...}) // starts at 0
|
|
26
|
-
.from(b, {...}, "-=0.4") // overlaps: starts 0.4s before previous ends
|
|
27
|
-
.from(c, {...}, "<") // starts WITH the previous tween
|
|
28
|
-
.from(d, {...}, "<0.15") // 0.15s after the previous tween's START
|
|
29
|
-
.from(e, {...}, "+=0.2"); // gap: 0.2s after everything ends
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Overlapping (`-=` / `<`) reads as one composed gesture; strict sequencing reads as a PowerPoint build ([scroll-reveals.md](scroll-reveals.md)).
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## 2. useGSAP (the React hook)
|
|
37
|
-
|
|
38
|
-
```tsx
|
|
39
|
-
const { contextSafe } = useGSAP(() => {
|
|
40
|
-
gsap.from(".card", { y: 40, autoAlpha: 0, stagger: 0.08 }); // ".card" scoped to container
|
|
41
|
-
}, { scope: container });
|
|
42
|
-
|
|
43
|
-
const onHover = contextSafe(() => gsap.to(".dot", { scale: 1.4 })); // event-handler tweens MUST be wrapped
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Everything created inside is auto-killed on unmount (no leaked ScrollTriggers after route changes). Full rules (`"use client"`, scope ref, animate-`from`, no `window` at module scope): [setup.md](setup.md) §3.
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## 3. ScrollTrigger
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
gsap.to(target, {
|
|
54
|
-
...,
|
|
55
|
-
scrollTrigger: {
|
|
56
|
-
trigger: container.current,
|
|
57
|
-
start: "top 78%", // "[trigger-edge] [viewport-edge]"
|
|
58
|
-
end: "bottom top", // or "+=250%" (2.5 viewports of scroll-room), or a function
|
|
59
|
-
scrub: 1, // true = locked 1:1; number = seconds of catch-up (feel-layer.md §2)
|
|
60
|
-
pin: true, // freeze the trigger while the scroll distance plays out
|
|
61
|
-
once: true, // entrances fire once; scrub effects may repeat
|
|
62
|
-
snap: { snapTo: 1 / (n - 1), duration: 0.4, ease: "power2.inOut" }, // stepped content only
|
|
63
|
-
invalidateOnRefresh: true, // re-run function-based values on refresh/resize
|
|
64
|
-
onUpdate: (self) => { progress.current = self.progress; }, // 0 to 1; mutate refs, never setState
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
// Standalone (no tween): ScrollTrigger.create({ trigger, start, end, scrub, onUpdate })
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
**The pinning rules (each one is a real failure when skipped):**
|
|
71
|
-
|
|
72
|
-
1. **Pinning re-parents** the trigger into a generated pin-spacer, so measure and position accordingly; `pinSpacing: false` only when a following section should slide over the pinned one.
|
|
73
|
-
2. **Function-based distances** for anything measured: `x: () => -getDistance()` and `end: () => "+=" + getDistance()`, plus `invalidateOnRefresh: true`. Otherwise resize breaks the mapping.
|
|
74
|
-
3. **`ScrollTrigger.refresh()`** after layout settles (fonts/images loaded, entrance finished). Triggers measured against a mid-animation layout are wrong ([page-entrances.md](page-entrances.md)).
|
|
75
|
-
4. **Never `anticipatePin` with smooth scroll.** Lenis feeds it a smoothed velocity, the prediction overshoots, and the pin engages a frame early then corrects: a visible jerk at the handoff. Full diagnosis: [pinning-scrub.md](pinning-scrub.md) §2.
|
|
76
|
-
5. **One trigger per concern**, not per element: stagger ten cards from one trigger on the parent.
|
|
77
|
-
6. **Never pin on touch.** The matchMedia + scroll-snap fallback in [pinning-scrub.md](pinning-scrub.md) §4 is the law.
|
|
78
|
-
|
|
79
|
-
Pin symptoms (a jerk, a jump, a snap on first engage, a pin that lands in the wrong place), each with cause, fix and check: [pitfalls.md](pitfalls.md) §2.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## 4. gsap.matchMedia: responsive + reduced-motion, declaratively
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
const mm = gsap.matchMedia();
|
|
87
|
-
mm.add({
|
|
88
|
-
motionOK: "(prefers-reduced-motion: no-preference)",
|
|
89
|
-
isDesktop: "(min-width: 768px)",
|
|
90
|
-
}, (ctx) => {
|
|
91
|
-
const { motionOK, isDesktop } = ctx.conditions!;
|
|
92
|
-
if (!motionOK) return; // SSR markup is already the final state
|
|
93
|
-
if (isDesktop) { /* pin/scrub here */ }
|
|
94
|
-
});
|
|
95
|
-
// Or one combined query: mm.add("(min-width: 768px) and (prefers-reduced-motion: no-preference)", () => {...});
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Everything created inside a branch **auto-reverts** when the condition stops matching (rotate, resize, OS setting change). No stale pins, nothing to clean up by hand. Never `if (window.innerWidth < 768)` at setup time: it doesn't revert.
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## 5. quickTo: per-frame pointer effects
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
const xTo = gsap.quickTo(el, "x", { duration: 0.4, ease: "power3.out" });
|
|
106
|
-
el.addEventListener("mousemove", (e) => xTo(computeX(e))); // call the function, per frame, cheaply
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Pre-compiles one reusable tween per property. For anything updating every frame (magnetic buttons, cursor followers, hide-on-scroll headers): never `setState` per mousemove (React render per frame) and never `gsap.to` per event (allocates a tween per pixel). This is also why those effects stay on GSAP's ticker, not a Motion spring loop: the one-clock rule (see the decision table in [SKILL.md](../SKILL.md)).
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
## 6. SplitText (GSAP 3.13 or later)
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
SplitText.create(el, {
|
|
117
|
-
type: "lines", // "lines" | "words" | "chars" (chars: short display words only)
|
|
118
|
-
mask: "lines", // wraps each line in an overflow-clipped element: the whole effect
|
|
119
|
-
autoSplit: true, // re-splits on font load / resize
|
|
120
|
-
onSplit: (self) => // animation MUST live here so re-splits re-run it
|
|
121
|
-
gsap.from(self.lines, { yPercent: 110, stagger: 0.09, ease: "expo.out", duration: 1.1 }),
|
|
122
|
-
});
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Do **not** use the old `new SplitText()` + manual resize listeners. Accessibility is automatic (`aria-label` set on the parent). Craft details (leading, descenders, the scrub variant): [scroll-reveals.md](scroll-reveals.md) §3. SplitText typography choreography in depth: [typography.md](typography.md).
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## 7. Flip: expand-to-detail / shared-element morphs
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
const state = Flip.getState(".card, .card img"); // 1. measure
|
|
133
|
-
container.classList.toggle("expanded"); // 2. change the DOM/layout however you like
|
|
134
|
-
Flip.from(state, { // 3. animate FROM the old positions
|
|
135
|
-
duration: 0.6, ease: "power4.inOut",
|
|
136
|
-
absolute: true, // see §7.1: REQUIRES a positioned + height-pinned container
|
|
137
|
-
nested: true, // when parents AND children both move
|
|
138
|
-
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Reach for Flip when the morph must coordinate with scroll or a timeline (card→modal, grid↔list, thumbnail→hero); a simple React-state list→detail swap is Motion `layoutId` territory ([transitions.md](transitions.md) §3, and the decision table in [SKILL.md](../SKILL.md)).
|
|
142
|
-
|
|
143
|
-
### 7.1 The four Flip failures: read before writing any Flip
|
|
144
|
-
|
|
145
|
-
Flip has a reputation for "layout shift that nothing fixes" and for fighting the
|
|
146
|
-
page. It is almost always one of these four, and they compound, so fixing one and
|
|
147
|
-
still seeing a jump does **not** mean you were wrong; it means there were two.
|
|
148
|
-
Work the list in order.
|
|
149
|
-
|
|
150
|
-
**1. `absolute: true` collapses the container.**
|
|
151
|
-
`absolute: true` sets every target to `position: absolute` for the tween's
|
|
152
|
-
duration. If the container derives its height from those in-flow children (any
|
|
153
|
-
CSS grid with `grid-auto-rows`, any auto-height flex/block wrapper), it now has
|
|
154
|
-
zero in-flow children and its height collapses to `0`, then snaps back. Everything
|
|
155
|
-
below lurches up and slams down.
|
|
156
|
-
|
|
157
|
-
> The older note that `absolute: true` "prevents reflow fighting" is only half
|
|
158
|
-
> true: it prevents *sibling* reflow, and causes *container* collapse. It is not
|
|
159
|
-
> a free win. It is a trade that you must pay for with the two fixes below.
|
|
160
|
-
|
|
161
|
-
**2. No positioned ancestor → items escape the container.**
|
|
162
|
-
Absolutely-positioned targets resolve offsets against the nearest *positioned*
|
|
163
|
-
ancestor. If the container is not `position: relative`, tiles fly out of its
|
|
164
|
-
coordinate space and render **on top of the next section**. This looks like a
|
|
165
|
-
z-index bug and is not one. Symptom: content overlapping the section below, not
|
|
166
|
-
just shifting.
|
|
167
|
-
|
|
168
|
-
**3. The height release races the stagger.**
|
|
169
|
-
With `stagger`, the last item lands at `duration + stagger.amount`, and stays
|
|
170
|
-
`position: absolute` until then. Pin the height for only `duration` and it
|
|
171
|
-
releases while items are still out of flow → the same collapse, but *only at the
|
|
172
|
-
very end* of the animation. Symptom: the tween looks perfect, then jumps on the
|
|
173
|
-
final beat.
|
|
174
|
-
|
|
175
|
-
**4. A CSS `transition` on a property Flip tweens.**
|
|
176
|
-
`Flip.getState(el, { props: 'filter' })` makes GSAP write `filter` inline every
|
|
177
|
-
frame. A CSS `transition: filter …` on the same element re-animates every one of
|
|
178
|
-
those writes. Two systems own one property; the result is mushy, laggy, or stuck.
|
|
179
|
-
Same for `transition: all`. **Never** transition a property you pass to `props`,
|
|
180
|
-
and never use `transition: all` on a Flip target.
|
|
181
|
-
|
|
182
|
-
### 7.2 The rock-solid pattern (React)
|
|
183
|
-
|
|
184
|
-
Covers all four. Measure the old height *before* the re-render, pin, tween the
|
|
185
|
-
container in lockstep with the staggered items, release only after the last one
|
|
186
|
-
lands.
|
|
187
|
-
|
|
188
|
-
```tsx
|
|
189
|
-
const DURATION = 0.8;
|
|
190
|
-
const STAGGER = 0.18; // shared, so the height tween can never drift out of sync
|
|
191
|
-
|
|
192
|
-
const gridRef = useRef<HTMLDivElement>(null);
|
|
193
|
-
const flipState = useRef<Flip.FlipState | null>(null);
|
|
194
|
-
const prevHeight = useRef(0);
|
|
195
|
-
|
|
196
|
-
// Capture in the EVENT HANDLER, before React re-renders.
|
|
197
|
-
const handleChange = (next: Id) => {
|
|
198
|
-
const grid = gridRef.current;
|
|
199
|
-
if (grid && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
200
|
-
prevHeight.current = grid.getBoundingClientRect().height; // old height
|
|
201
|
-
flipState.current = Flip.getState(grid.querySelectorAll('[data-flip]'));
|
|
202
|
-
}
|
|
203
|
-
setActive(next);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
useLayoutEffect(() => {
|
|
207
|
-
const state = flipState.current;
|
|
208
|
-
flipState.current = null;
|
|
209
|
-
if (!state) return;
|
|
210
|
-
const grid = gridRef.current;
|
|
211
|
-
if (!grid) return;
|
|
212
|
-
|
|
213
|
-
const from = prevHeight.current;
|
|
214
|
-
const to = grid.getBoundingClientRect().height; // measure BEFORE pinning
|
|
215
|
-
gsap.set(grid, { height: from });
|
|
216
|
-
|
|
217
|
-
const flip = Flip.from(state, {
|
|
218
|
-
duration: DURATION, ease: 'power3.inOut', absolute: true,
|
|
219
|
-
stagger: { amount: STAGGER, from: 'start' },
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// Outlast the stagger (failure #3).
|
|
223
|
-
const height = gsap.to(grid, {
|
|
224
|
-
height: to,
|
|
225
|
-
duration: DURATION + STAGGER,
|
|
226
|
-
ease: 'power3.inOut',
|
|
227
|
-
onComplete: () => gsap.set(grid, { clearProps: 'height' }),
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
return () => { flip.kill(); height.kill(); gsap.set(grid, { clearProps: 'height' }); };
|
|
231
|
-
}, [active]);
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
```css
|
|
235
|
-
.grid {
|
|
236
|
-
position: relative; /* REQUIRED: failure #2 */
|
|
237
|
-
display: grid;
|
|
238
|
-
grid-auto-rows: …;
|
|
239
|
-
}
|
|
240
|
-
.grid > [data-flip] {
|
|
241
|
-
/* Do NOT transition any property passed to Flip's `props` (failure #4).
|
|
242
|
-
Opacity is safe here only because Flip never touches it. */
|
|
243
|
-
transition: opacity 0.6s var(--ease-out);
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
**Ordering rule:** all measurement happens before any `gsap.set` that mutates
|
|
248
|
-
layout. Never `set(height:'auto')` → measure → tween *after* `Flip.from` has
|
|
249
|
-
captured target geometry; that mutates layout mid-flight and silently corrupts
|
|
250
|
-
the capture.
|
|
251
|
-
|
|
252
|
-
### 7.3 `scale: true` is a trade, not a default
|
|
253
|
-
|
|
254
|
-
`scale: true` fakes resizing with `transform: scale`, which is cheaper, but it
|
|
255
|
-
**distorts children**: `object-fit: cover` images stretch, text warps, borders and
|
|
256
|
-
radii scale non-uniformly.
|
|
257
|
-
|
|
258
|
-
- Items that genuinely change dimensions (bento tile `span 1` → `span 2`, card →
|
|
259
|
-
modal): **omit `scale`.** Let Flip tween width/height. Correctness beats cheap.
|
|
260
|
-
- Uniform items that only *move* (reordering a same-size grid), or brief/small
|
|
261
|
-
morphs where distortion is imperceptible: `scale: true` is a fine win.
|
|
262
|
-
|
|
263
|
-
### 7.4 Verifying Flip
|
|
264
|
-
|
|
265
|
-
**Typecheck and lint cannot see any of these failures.** A fully broken Flip
|
|
266
|
-
passes `tsc --noEmit` and ESLint clean, which is exactly how a wrong fix survives
|
|
267
|
-
review and reaches the user. Flip is only verified by watching it: click the
|
|
268
|
-
control and confirm (a) nothing below the container moves, (b) no item overlaps
|
|
269
|
-
the next section, (c) the final beat lands without a jump. Check the last one
|
|
270
|
-
specifically; failure #3 hides until the very end.
|
|
271
|
-
|
|
272
|
-
---
|
|
273
|
-
|
|
274
|
-
## 8. The `ease: "none"` rule
|
|
275
|
-
|
|
276
|
-
Anything **scrubbed** tracks scroll linearly. Scroll itself is the easing; an ease on top feels broken:
|
|
277
|
-
|
|
278
|
-
- Scrub-tied values (parallax drift, marquee, the horizontal-scroll track, continuous dial): `ease: "none"`.
|
|
279
|
-
- **Every tween whose trigger uses `containerAnimation`** (triggers inside a moving horizontal track): `ease: "none"` on the track tween, or the trigger math breaks.
|
|
280
|
-
- The exception: *segment* eases (`power2.out`/`power2.in`) **inside** a scrubbed timeline for phase swaps. Each swap keeps shape while the timeline as a whole stays scroll-locked ([pinning-scrub.md](pinning-scrub.md) §1).
|
|
281
|
-
|
|
282
|
-
---
|
|
283
|
-
|
|
284
|
-
## 9. DrawSVG & MorphSVG (pointer)
|
|
285
|
-
|
|
286
|
-
Both plugins are free (no token) and register in the `@/lib/gsap` module alongside `Flip` ([setup.md](setup.md) §1). The two one-liners the other files rely on:
|
|
287
|
-
|
|
288
|
-
```ts
|
|
289
|
-
gsap.from(path, { drawSVG: 0 }); // self-drawing stroke
|
|
290
|
-
gsap.to("#shapeA", { morphSVG: "#shapeB" }); // shape-to-shape morph
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
Full DrawSVG and MorphSVG recipes live in [svg-motion.md](svg-motion.md).
|