@struq/skill 0.3.0 → 0.4.1

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.
Files changed (129) hide show
  1. package/README.md +22 -36
  2. package/package.json +4 -2
  3. package/skills/struq/SKILL.md +139 -307
  4. package/skills/struq/blocks/CATALOG.md +426 -0
  5. package/skills/struq/blocks/backgrounds/background-aurora.tsx +818 -0
  6. package/skills/struq/blocks/backgrounds/background-facets.tsx +1283 -0
  7. package/skills/struq/blocks/backgrounds/background-silk.tsx +906 -0
  8. package/skills/struq/blocks/catalog.json +1302 -0
  9. package/skills/struq/blocks/footers/footer-contact.tsx +789 -0
  10. package/skills/struq/blocks/footers/footer-reveal.tsx +313 -0
  11. package/skills/struq/blocks/headers/header-bar.tsx +652 -0
  12. package/skills/struq/blocks/headers/menu-curtain.tsx +790 -0
  13. package/skills/struq/blocks/heroes/hero-framed.tsx +627 -0
  14. package/skills/struq/blocks/heroes/hero-index.tsx +855 -0
  15. package/skills/struq/blocks/heroes/hero-poster.tsx +1086 -0
  16. package/skills/struq/blocks/heroes/hero-split.tsx +685 -0
  17. package/skills/struq/blocks/heroes/hero-still.tsx +862 -0
  18. package/skills/struq/{recipes/components/struq → blocks/kit}/Cursor.tsx +1 -1
  19. package/skills/struq/{recipes/components/struq → blocks/kit}/HeroCanvas.tsx +4 -4
  20. package/skills/struq/{recipes/components/struq → blocks/kit}/HorizontalGallery.tsx +2 -2
  21. package/skills/struq/{recipes/components/struq → blocks/kit}/MagneticButton.tsx +1 -1
  22. package/skills/struq/{recipes/components/struq → blocks/kit}/MediaFrame.tsx +2 -2
  23. package/skills/struq/{recipes/components/struq → blocks/kit}/PinnedZoom.tsx +14 -4
  24. package/skills/struq/{recipes/components/struq → blocks/kit}/Reveal.tsx +1 -1
  25. package/skills/struq/{recipes/components/struq → blocks/kit}/SmoothScroll.tsx +1 -1
  26. package/skills/struq/{recipes/components/struq → blocks/kit}/SplitHeadline.tsx +1 -1
  27. package/skills/struq/blocks/kit/blocks.css +2102 -0
  28. package/skills/struq/blocks/kit/images.ts +17 -0
  29. package/skills/struq/blocks/kit/photo-read.ts +102 -0
  30. package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +706 -0
  31. package/skills/struq/blocks/page-transitions/page-transition-slices.tsx +469 -0
  32. package/skills/struq/blocks/page-transitions/page-transition-stack.tsx +583 -0
  33. package/skills/struq/blocks/preloaders/preloader-gate.tsx +358 -0
  34. package/skills/struq/blocks/preloaders/preloader-slices-uncovered.ts +50 -0
  35. package/skills/struq/blocks/preloaders/preloader-slices.tsx +289 -0
  36. package/skills/struq/blocks/preloaders/preloader-stack.tsx +367 -0
  37. package/skills/struq/blocks/preloaders/preloader-wordmark.tsx +568 -0
  38. package/skills/struq/blocks/sections/about-studio.tsx +436 -0
  39. package/skills/struq/blocks/sections/approach.tsx +661 -0
  40. package/skills/struq/blocks/sections/contact-form.tsx +664 -0
  41. package/skills/struq/blocks/sections/cta-closing.tsx +763 -0
  42. package/skills/struq/blocks/sections/fanned-cards.tsx +547 -0
  43. package/skills/struq/blocks/sections/film-strip.tsx +385 -0
  44. package/skills/struq/blocks/sections/grid-assembly.tsx +616 -0
  45. package/skills/struq/blocks/sections/hover-list.tsx +467 -0
  46. package/skills/struq/blocks/sections/image-trail.tsx +432 -0
  47. package/skills/struq/blocks/sections/manifesto.tsx +691 -0
  48. package/skills/struq/blocks/sections/panel-accordion.tsx +380 -0
  49. package/skills/struq/blocks/sections/reveals.tsx +416 -0
  50. package/skills/struq/blocks/sections/split-pair.tsx +670 -0
  51. package/skills/struq/blocks/sections/sticky-chapters.tsx +256 -0
  52. package/skills/struq/blocks/sections/velocity-type.tsx +269 -0
  53. package/skills/struq/{cinematic/showpieces/toonstuk-fill-rows.tsx → blocks/showpieces/fill-rows.tsx} +1 -1
  54. package/skills/struq/{cinematic/showpieces/toonstuk-hero-opener.tsx → blocks/showpieces/hero-opener.tsx} +5 -5
  55. package/skills/struq/{cinematic/showpieces/toonstuk-image-to-fullbleed.tsx → blocks/showpieces/image-to-fullbleed.tsx} +16 -5
  56. package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +48 -0
  57. package/skills/struq/{cinematic/showpieces/toonstuk-panel-wipe.tsx → blocks/showpieces/panel-wipe.tsx} +2 -2
  58. package/skills/struq/{cinematic/showpieces/toonstuk-pixel-resolve.tsx → blocks/showpieces/pixel-resolve.tsx} +1 -1
  59. package/skills/struq/{cinematic/showpieces/toonstuk-project-stack.tsx → blocks/showpieces/project-stack.tsx} +2 -2
  60. package/skills/struq/blocks/showpieces/scrub-connector.tsx +307 -0
  61. package/skills/struq/{cinematic/showpieces/toonstuk-wordmark-finale.tsx → blocks/showpieces/wordmark-finale.tsx} +1 -1
  62. package/skills/struq/blocks/transitions/zoom-through.tsx +613 -0
  63. package/skills/struq/canon/canon.json +627 -0
  64. package/skills/struq/cinematic/README.md +7 -18
  65. package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +1 -1
  66. package/skills/struq/cinematic/examples/code/shared/styles.css +1 -1
  67. package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +1 -1
  68. package/skills/struq/cinematic/references/01-core-mental-model.md +1 -1
  69. package/skills/struq/cinematic/references/02-vocabulary.md +1 -1
  70. package/skills/struq/cinematic/references/07-stack-integration.md +2 -2
  71. package/skills/struq/recipes/app/fonts.ts +12 -22
  72. package/skills/struq/recipes/app/globals.css +34 -177
  73. package/skills/struq/recipes/app/layout.tsx +13 -11
  74. package/skills/struq/references/craft.md +77 -0
  75. package/skills/struq/references/iconography.md +28 -230
  76. package/skills/struq/references/stack-targets.md +23 -74
  77. package/skills/struq/references/tells.md +52 -0
  78. package/skills/struq/references/triage.md +6 -7
  79. package/skills/struq/scripts/blocks.mjs +156 -0
  80. package/skills/struq/scripts/canon.mjs +106 -281
  81. package/skills/struq/scripts/check.mjs +92 -126
  82. package/skills/struq/scripts/images.mjs +2 -2
  83. package/skills/struq/scripts/log.mjs +6 -23
  84. package/skills/struq/scripts/tokens.mjs +222 -0
  85. package/skills/struq/cinematic/showpieces/README.md +0 -18
  86. package/skills/struq/cinematic/showpieces/toonstuk-manifesto-scrub.tsx +0 -81
  87. package/skills/struq/cinematic/showpieces/toonstuk-scrub-connector.tsx +0 -78
  88. package/skills/struq/references/anti-patterns.md +0 -330
  89. package/skills/struq/references/baseline-prompt.md +0 -122
  90. package/skills/struq/references/brand.md +0 -143
  91. package/skills/struq/references/brief-router.md +0 -218
  92. package/skills/struq/references/composition.md +0 -190
  93. package/skills/struq/references/depth-parallax.md +0 -235
  94. package/skills/struq/references/direction.md +0 -188
  95. package/skills/struq/references/feel-layer.md +0 -107
  96. package/skills/struq/references/gsap-api.md +0 -293
  97. package/skills/struq/references/hero-scenes.md +0 -107
  98. package/skills/struq/references/imagery.md +0 -125
  99. package/skills/struq/references/loaders.md +0 -287
  100. package/skills/struq/references/micro-interactions.md +0 -270
  101. package/skills/struq/references/mobile-composition.md +0 -70
  102. package/skills/struq/references/nav-header.md +0 -296
  103. package/skills/struq/references/niche-packets.md +0 -338
  104. package/skills/struq/references/page-architecture.md +0 -134
  105. package/skills/struq/references/page-entrances.md +0 -198
  106. package/skills/struq/references/performance.md +0 -235
  107. package/skills/struq/references/pinning-scrub.md +0 -340
  108. package/skills/struq/references/pitfalls.md +0 -290
  109. package/skills/struq/references/route-transitions.md +0 -281
  110. package/skills/struq/references/scaling.md +0 -151
  111. package/skills/struq/references/scroll-reveals.md +0 -279
  112. package/skills/struq/references/sections.md +0 -141
  113. package/skills/struq/references/seo.md +0 -219
  114. package/skills/struq/references/setup.md +0 -397
  115. package/skills/struq/references/showcase.md +0 -87
  116. package/skills/struq/references/struq-imagery.md +0 -107
  117. package/skills/struq/references/svg-motion.md +0 -390
  118. package/skills/struq/references/transitions.md +0 -160
  119. package/skills/struq/references/typography.md +0 -720
  120. package/skills/struq/references/vibe-presets.md +0 -287
  121. package/skills/struq/references/webgl-r3f.md +0 -274
  122. package/skills/struq/references/webgl-scenes.md +0 -593
  123. package/skills/struq/scripts/font-names.mjs +0 -40
  124. package/skills/struq/scripts/recipes.mjs +0 -135
  125. /package/skills/struq/{recipes/lib → blocks/kit}/gsap.ts +0 -0
  126. /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/contours.ts +0 -0
  127. /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/dots.ts +0 -0
  128. /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/ripple.ts +0 -0
  129. /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).