hyperframes 0.2.2 → 0.2.3-alpha.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.
- package/dist/cli.js +7926 -7426
- package/dist/skills/gsap/SKILL.md +222 -0
- package/dist/skills/gsap/references/effects.md +304 -0
- package/dist/skills/gsap/references/frameworks.md +56 -0
- package/dist/skills/gsap/references/plugins.md +194 -0
- package/dist/skills/gsap/references/react.md +80 -0
- package/dist/skills/gsap/references/scrolltrigger.md +147 -0
- package/dist/skills/gsap/references/utils.md +91 -0
- package/dist/skills/gsap/scripts/extract-audio-data.py +188 -0
- package/dist/skills/{hyperframes-compose → hyperframes}/SKILL.md +43 -43
- package/dist/skills/hyperframes/references/audio-reactive.md +76 -0
- package/dist/skills/hyperframes/references/captions.md +132 -0
- package/dist/skills/hyperframes/references/css-patterns.md +371 -0
- package/dist/skills/hyperframes/references/examples.md +146 -0
- package/dist/skills/hyperframes/references/marker-highlight.md +158 -0
- package/dist/skills/hyperframes/references/transitions/catalog.md +132 -0
- package/dist/skills/hyperframes/references/transitions/css-3d.md +12 -0
- package/dist/skills/hyperframes/references/transitions/css-blur.md +51 -0
- package/dist/skills/hyperframes/references/transitions/css-cover.md +43 -0
- package/dist/skills/hyperframes/references/transitions/css-destruction.md +95 -0
- package/dist/skills/hyperframes/references/transitions/css-dissolve.md +66 -0
- package/dist/skills/hyperframes/references/transitions/css-distortion.md +45 -0
- package/dist/skills/hyperframes/references/transitions/css-grid.md +10 -0
- package/dist/skills/hyperframes/references/transitions/css-light.md +49 -0
- package/dist/skills/hyperframes/references/transitions/css-mechanical.md +30 -0
- package/dist/skills/hyperframes/references/transitions/css-other.md +36 -0
- package/dist/skills/hyperframes/references/transitions/css-push.md +41 -0
- package/dist/skills/hyperframes/references/transitions/css-radial.md +37 -0
- package/dist/skills/hyperframes/references/transitions/css-scale.md +24 -0
- package/dist/skills/hyperframes/references/transitions/shader-setup.md +463 -0
- package/dist/skills/hyperframes/references/transitions/shader-transitions.md +329 -0
- package/dist/skills/hyperframes/references/transitions.md +96 -0
- package/dist/skills/hyperframes/references/tts.md +56 -0
- package/dist/skills/hyperframes-cli/SKILL.md +114 -0
- package/dist/templates/_shared/CLAUDE.md +5 -7
- package/dist/templates/blank/index.html +8 -10
- package/package.json +2 -4
- package/dist/skills/hyperframes-captions/SKILL.md +0 -212
- package/dist/skills/hyperframes-tts/SKILL.md +0 -79
- package/dist/templates/blank/compositions/captions.html +0 -95
- /package/dist/skills/{hyperframes-compose → hyperframes}/data-in-motion.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/house-style.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/palettes/bold-energetic.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/palettes/clean-corporate.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/palettes/dark-premium.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/palettes/jewel-rich.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/palettes/monochrome.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/palettes/nature-earth.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/palettes/neon-electric.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/palettes/pastel-soft.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/palettes/warm-editorial.md +0 -0
- /package/dist/skills/{hyperframes-compose → hyperframes}/patterns.md +0 -0
- /package/dist/skills/{hyperframes-captions → hyperframes/references}/dynamic-techniques.md +0 -0
- /package/dist/skills/{hyperframes-captions → hyperframes/references}/transcript-guide.md +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# GSAP Plugins
|
|
2
|
+
|
|
3
|
+
Register each plugin once before use:
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
import gsap from "gsap";
|
|
7
|
+
import { ScrollToPlugin } from "gsap/ScrollToPlugin";
|
|
8
|
+
import { Flip } from "gsap/Flip";
|
|
9
|
+
gsap.registerPlugin(ScrollToPlugin, Flip);
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [ScrollToPlugin](#scrolltoplugin)
|
|
15
|
+
- [ScrollSmoother](#scrollsmoother)
|
|
16
|
+
- [Flip](#flip)
|
|
17
|
+
- [Draggable + Inertia](#draggable)
|
|
18
|
+
- [Observer](#observer)
|
|
19
|
+
- [SplitText](#splittext)
|
|
20
|
+
- [ScrambleText](#scrambletext)
|
|
21
|
+
- [DrawSVG](#drawsvg)
|
|
22
|
+
- [MorphSVG](#morphsvg)
|
|
23
|
+
- [MotionPath](#motionpath)
|
|
24
|
+
- [CustomEase / EasePack](#customeaseeasepak)
|
|
25
|
+
- [Physics2D / PhysicsProps](#physics)
|
|
26
|
+
- [GSDevTools](#gsdevtools)
|
|
27
|
+
- [PixiPlugin](#pixiplugin)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ScrollToPlugin
|
|
32
|
+
|
|
33
|
+
Animate scroll position (window or scrollable element).
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
gsap.to(window, { scrollTo: { y: "#section", offsetY: 50 }, duration: 1 });
|
|
37
|
+
gsap.to(scrollContainer, { scrollTo: { x: "max" }, duration: 1 });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## ScrollSmoother
|
|
41
|
+
|
|
42
|
+
Smooth scroll wrapper. Requires ScrollTrigger + specific DOM structure (`#smooth-wrapper` > `#smooth-content`).
|
|
43
|
+
|
|
44
|
+
## Flip
|
|
45
|
+
|
|
46
|
+
FLIP layout transitions: capture state, change DOM, animate from old to new.
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
const state = Flip.getState(".item");
|
|
50
|
+
// change DOM (reorder, add/remove, change classes)
|
|
51
|
+
Flip.from(state, { duration: 0.5, ease: "power2.inOut" });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Options: `absolute`, `nested`, `scale`, `simple`, `duration`, `ease`.
|
|
55
|
+
|
|
56
|
+
## Draggable
|
|
57
|
+
|
|
58
|
+
Makes elements draggable/spinnable/throwable.
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
gsap.registerPlugin(Draggable, InertiaPlugin);
|
|
62
|
+
Draggable.create(".box", { type: "x,y", bounds: "#container", inertia: true });
|
|
63
|
+
Draggable.create(".knob", { type: "rotation" });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Types: `"x"`, `"y"`, `"x,y"`, `"rotation"`, `"scroll"`. Options: `bounds`, `inertia`, `edgeResistance`, `cursor`, drag callbacks.
|
|
67
|
+
|
|
68
|
+
### Inertia (InertiaPlugin)
|
|
69
|
+
|
|
70
|
+
Momentum after release with Draggable, or track velocity of any property:
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
InertiaPlugin.track(".box", "x");
|
|
74
|
+
gsap.to(obj, { inertia: { x: "auto" } });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Observer
|
|
78
|
+
|
|
79
|
+
Normalized pointer/scroll input across devices. Use for swipe/gesture detection.
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
Observer.create({
|
|
83
|
+
target: "#area",
|
|
84
|
+
onUp: () => {},
|
|
85
|
+
onDown: () => {},
|
|
86
|
+
tolerance: 10,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## SplitText
|
|
91
|
+
|
|
92
|
+
Split text into chars, words, lines for per-unit animation.
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
const split = SplitText.create(".heading", { type: "words, chars" });
|
|
96
|
+
gsap.from(split.chars, { opacity: 0, y: 20, stagger: 0.03 });
|
|
97
|
+
// later: split.revert()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Key options: `type` (comma-separated: chars/words/lines), `charsClass`/`wordsClass`/`linesClass`, `aria` ("auto"/"hidden"/"none"), `autoSplit` + `onSplit(self)` for font-safe re-splitting, `mask` (lines/words/chars for reveal effects), `tag`, `ignore`, `smartWrap`, `propIndex`.
|
|
101
|
+
|
|
102
|
+
Tips: Split only what's animated. For custom fonts, use `autoSplit: true` with `onSplit()`. Avoid `text-wrap: balance`.
|
|
103
|
+
|
|
104
|
+
## ScrambleText
|
|
105
|
+
|
|
106
|
+
Scramble/glitch text effect.
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
gsap.to(".text", { scrambleText: { text: "New message", chars: "01", revealDelay: 0.5 } });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## DrawSVG
|
|
113
|
+
|
|
114
|
+
Animate SVG stroke reveal (stroke-dashoffset/dasharray). Element must have `stroke` and `stroke-width`.
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
gsap.from("#path", { drawSVG: 0, duration: 1 }); // nothing to full stroke
|
|
118
|
+
gsap.to("#path", { drawSVG: "20% 80%", duration: 1 }); // partial segment
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`drawSVG` value = visible segment: `"start end"` in % or length. Single value (e.g. `0`) means start is 0.
|
|
122
|
+
|
|
123
|
+
## MorphSVG
|
|
124
|
+
|
|
125
|
+
Morph one SVG shape into another. Handles different point counts.
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
MorphSVGPlugin.convertToPath("circle, rect, ellipse, line");
|
|
129
|
+
gsap.to("#diamond", { morphSVG: "#lightning", duration: 1 });
|
|
130
|
+
// object form: { shape, type: "rotational", shapeIndex, smooth, curveMode }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Use `shapeIndex: "log"` to find optimal value. `type: "rotational"` avoids kinks.
|
|
134
|
+
|
|
135
|
+
## MotionPath
|
|
136
|
+
|
|
137
|
+
Animate along an SVG path.
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
gsap.to(".dot", {
|
|
141
|
+
motionPath: { path: "#path", align: "#path", alignOrigin: [0.5, 0.5], autoRotate: true },
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## CustomEase/EasePack
|
|
146
|
+
|
|
147
|
+
Custom curves beyond built-in eases:
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
const ease = CustomEase.create("name", ".17,.67,.83,.67");
|
|
151
|
+
// or SVG path data for complex curves
|
|
152
|
+
const hop = CustomEase.create("hop", "M0,0 C0,0 0.056,0.442 ...");
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
EasePack adds SlowMo, RoughEase, ExpoScaleEase. CustomWiggle for oscillation. CustomBounce for configurable bounces.
|
|
156
|
+
|
|
157
|
+
## Physics
|
|
158
|
+
|
|
159
|
+
### Physics2D
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
gsap.to(".ball", { physics2D: { velocity: 250, angle: 80, gravity: 500 }, duration: 2 });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### PhysicsProps
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
gsap.to(".obj", {
|
|
169
|
+
physicsProps: { x: { velocity: 100, end: 300 }, y: { velocity: -50, acceleration: 200 } },
|
|
170
|
+
duration: 2,
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## GSDevTools
|
|
175
|
+
|
|
176
|
+
Timeline scrubbing UI for development. **Do not ship to production.**
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
GSDevTools.create({ animation: tl });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## PixiPlugin
|
|
183
|
+
|
|
184
|
+
Integrates GSAP with PixiJS display objects.
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
gsap.to(sprite, { pixi: { x: 200, scale: 1.5 }, duration: 1 });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Do Not
|
|
191
|
+
|
|
192
|
+
- Use a plugin without registering it first.
|
|
193
|
+
- Ship GSDevTools to production.
|
|
194
|
+
- Forget to revert SplitText instances on unmount.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# GSAP with React
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install gsap @gsap/react
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## useGSAP() Hook (Preferred)
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
import { useGSAP } from "@gsap/react";
|
|
13
|
+
gsap.registerPlugin(useGSAP);
|
|
14
|
+
|
|
15
|
+
const containerRef = useRef(null);
|
|
16
|
+
useGSAP(
|
|
17
|
+
() => {
|
|
18
|
+
gsap.to(".box", { x: 100 });
|
|
19
|
+
},
|
|
20
|
+
{ scope: containerRef },
|
|
21
|
+
);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- Pass **scope** (ref) so selectors are scoped to the component.
|
|
25
|
+
- Cleanup runs automatically on unmount.
|
|
26
|
+
- Use **contextSafe** for callbacks created after useGSAP executes:
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
useGSAP(
|
|
30
|
+
(context, contextSafe) => {
|
|
31
|
+
const onClick = contextSafe(() => {
|
|
32
|
+
gsap.to(ref.current, { rotation: 180 });
|
|
33
|
+
});
|
|
34
|
+
ref.current.addEventListener("click", onClick);
|
|
35
|
+
return () => ref.current.removeEventListener("click", onClick);
|
|
36
|
+
},
|
|
37
|
+
{ scope: container },
|
|
38
|
+
);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Dependency Array and revertOnUpdate
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
useGSAP(
|
|
45
|
+
() => {
|
|
46
|
+
/* gsap code */
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
dependencies: [endX],
|
|
50
|
+
scope: container,
|
|
51
|
+
revertOnUpdate: true, // reverts + re-runs on dependency change
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## gsap.context() in useEffect (Fallback)
|
|
57
|
+
|
|
58
|
+
When @gsap/react isn't available:
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const ctx = gsap.context(() => {
|
|
63
|
+
gsap.to(".box", { x: 100 });
|
|
64
|
+
}, containerRef);
|
|
65
|
+
return () => ctx.revert();
|
|
66
|
+
}, []);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Always return `ctx.revert()` in cleanup.
|
|
70
|
+
|
|
71
|
+
## SSR (Next.js)
|
|
72
|
+
|
|
73
|
+
GSAP runs in the browser. Keep all GSAP code inside useGSAP or useEffect.
|
|
74
|
+
|
|
75
|
+
## Do Not
|
|
76
|
+
|
|
77
|
+
- Target by selector without a scope — always pass scope.
|
|
78
|
+
- Skip cleanup — always revert context or kill tweens on unmount.
|
|
79
|
+
- Run GSAP during SSR.
|
|
80
|
+
- Register plugins inside components that re-render — register once at app level.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# ScrollTrigger
|
|
2
|
+
|
|
3
|
+
## Registering
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Basic Trigger
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
gsap.to(".box", {
|
|
13
|
+
x: 500,
|
|
14
|
+
scrollTrigger: {
|
|
15
|
+
trigger: ".box",
|
|
16
|
+
start: "top center",
|
|
17
|
+
end: "bottom center",
|
|
18
|
+
toggleActions: "play reverse play reverse",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**start/end** format: `"triggerPosition viewportPosition"`. Examples: `"top top"`, `"center center"`, `"bottom 80%"`, numeric px `500`, relative `"+=300"`, `"+=100%"` (scroller height), `"max"`. Wrap in `clamp()` (v3.12+): `"clamp(top bottom)"`. Can be a function returning string/number.
|
|
24
|
+
|
|
25
|
+
## Key Config Options
|
|
26
|
+
|
|
27
|
+
| Property | Type | Description |
|
|
28
|
+
| ----------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| **trigger** | String/Element | Element whose position defines start. Required. |
|
|
30
|
+
| **start** | String/Number/Function | When active. Default `"top bottom"` (or `"top top"` if pinned). |
|
|
31
|
+
| **end** | String/Number/Function | When ends. Default `"bottom top"`. |
|
|
32
|
+
| **endTrigger** | String/Element | Different element for end calculation. |
|
|
33
|
+
| **scrub** | Boolean/Number | Link progress to scroll. `true` = direct; number = catch-up seconds. |
|
|
34
|
+
| **toggleActions** | String | Four actions: onEnter, onLeave, onEnterBack, onLeaveBack. Values: play/pause/resume/reset/restart/complete/reverse/none. Default `"play none none none"`. |
|
|
35
|
+
| **pin** | Boolean/String/Element | Pin element while active. `true` = pin trigger. Animate children, not the pinned element. |
|
|
36
|
+
| **pinSpacing** | Boolean/String | Default `true` (adds spacer). `false` or `"margin"`. |
|
|
37
|
+
| **horizontal** | Boolean | For horizontal scrolling. |
|
|
38
|
+
| **scroller** | String/Element | Scroll container (default: viewport). |
|
|
39
|
+
| **markers** | Boolean/Object | Dev markers. Remove in production. |
|
|
40
|
+
| **once** | Boolean | Kill after end reached once. |
|
|
41
|
+
| **snap** | Number/Array/Function/"labels"/Object | Snap to progress values. |
|
|
42
|
+
| **containerAnimation** | Tween/Timeline | For fake horizontal scroll (see below). |
|
|
43
|
+
| **toggleClass** | String/Object | Add/remove class when active. |
|
|
44
|
+
| **onEnter/onLeave/onEnterBack/onLeaveBack** | Function | Callbacks; receive ScrollTrigger instance. |
|
|
45
|
+
| **onUpdate/onToggle/onRefresh/onScrubComplete** | Function | Progress/state callbacks. |
|
|
46
|
+
|
|
47
|
+
**Standalone** (no linked tween): `ScrollTrigger.create({...})` with callbacks.
|
|
48
|
+
|
|
49
|
+
## Scrub
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
scrollTrigger: { trigger: ".box", start: "top center", end: "bottom center", scrub: true }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`scrub: true` = direct link; number (e.g. `1`) = smooth lag.
|
|
56
|
+
|
|
57
|
+
## Pinning
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
scrollTrigger: {
|
|
61
|
+
trigger: ".section", start: "top top", end: "+=1000", pin: true, scrub: 1
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Timeline + ScrollTrigger
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
const tl = gsap.timeline({
|
|
69
|
+
scrollTrigger: { trigger: ".container", start: "top top", end: "+=2000", scrub: 1, pin: true },
|
|
70
|
+
});
|
|
71
|
+
tl.to(".a", { x: 100 }).to(".b", { y: 50 });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## ScrollTrigger.batch()
|
|
75
|
+
|
|
76
|
+
Creates one ScrollTrigger per target, batches callbacks within a short interval. Good for staggered reveal of many elements.
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
ScrollTrigger.batch(".box", {
|
|
80
|
+
onEnter: (elements) => gsap.to(elements, { opacity: 1, y: 0, stagger: 0.15 }),
|
|
81
|
+
start: "top 80%",
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Options: `interval` (batch window), `batchMax` (max per batch). Callbacks receive `(targets, scrollTriggers)`.
|
|
86
|
+
|
|
87
|
+
## Horizontal Scroll (containerAnimation)
|
|
88
|
+
|
|
89
|
+
Pin a section, animate inner content's `x`/`xPercent` horizontally on vertical scroll:
|
|
90
|
+
|
|
91
|
+
1. Pin the section
|
|
92
|
+
2. Animate inner content with **ease: "none"** (required)
|
|
93
|
+
3. Attach ScrollTrigger with pin + scrub
|
|
94
|
+
4. Use `containerAnimation` on nested triggers
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
const scrollTween = gsap.to(scrollingEl, {
|
|
98
|
+
xPercent: () => Math.max(0, window.innerWidth - scrollingEl.offsetWidth),
|
|
99
|
+
ease: "none",
|
|
100
|
+
scrollTrigger: {
|
|
101
|
+
trigger: scrollingEl,
|
|
102
|
+
pin: scrollingEl.parentNode,
|
|
103
|
+
start: "top top",
|
|
104
|
+
end: "+=1000",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
gsap.to(".nested", {
|
|
109
|
+
y: 100,
|
|
110
|
+
scrollTrigger: { containerAnimation: scrollTween, trigger: ".wrapper", start: "left center" },
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Pinning and snapping unavailable on containerAnimation-based ScrollTriggers.
|
|
115
|
+
|
|
116
|
+
## ScrollTrigger.scrollerProxy()
|
|
117
|
+
|
|
118
|
+
Override scroll position reading for third-party smooth-scroll libraries. Call `ScrollTrigger.update` when the scroller updates.
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
ScrollTrigger.scrollerProxy(document.body, {
|
|
122
|
+
scrollTop(value) {
|
|
123
|
+
if (arguments.length) scrollbar.scrollTop = value;
|
|
124
|
+
return scrollbar.scrollTop;
|
|
125
|
+
},
|
|
126
|
+
getBoundingClientRect() {
|
|
127
|
+
return { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight };
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
scrollbar.addListener(ScrollTrigger.update);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Refresh and Cleanup
|
|
134
|
+
|
|
135
|
+
- `ScrollTrigger.refresh()` — recalculate after DOM/layout changes. Auto on resize (200ms debounce).
|
|
136
|
+
- Create ScrollTriggers top-to-bottom or set `refreshPriority`.
|
|
137
|
+
- Kill instances when removing elements: `ScrollTrigger.getAll().forEach(t => t.kill())` or `ScrollTrigger.getById("id")?.kill()`.
|
|
138
|
+
|
|
139
|
+
## Do Not
|
|
140
|
+
|
|
141
|
+
- Put ScrollTrigger on child tweens inside a timeline — put on the timeline.
|
|
142
|
+
- Nest ScrollTriggered animations inside a parent timeline.
|
|
143
|
+
- Use scrub and toggleActions together (scrub wins).
|
|
144
|
+
- Use an ease other than "none" on the horizontal animation with containerAnimation.
|
|
145
|
+
- Leave markers in production.
|
|
146
|
+
- Create triggers in random order without refreshPriority.
|
|
147
|
+
- Forget refresh() after layout changes.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# gsap.utils
|
|
2
|
+
|
|
3
|
+
Pure helpers on `gsap.utils`. No registration needed.
|
|
4
|
+
|
|
5
|
+
**Function form:** Most utils accept the value as the last argument. Omit it to get a reusable function: `gsap.utils.clamp(0, 100)(150)`. Exception: `random()` — pass `true` as the last argument for a reusable function.
|
|
6
|
+
|
|
7
|
+
## Clamping and Ranges
|
|
8
|
+
|
|
9
|
+
### clamp(min, max, value?)
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
gsap.utils.clamp(0, 100, 150); // 100
|
|
13
|
+
let c = gsap.utils.clamp(0, 100);
|
|
14
|
+
c(150); // 100
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### mapRange(inMin, inMax, outMin, outMax, value?)
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
gsap.utils.mapRange(0, 1, 0, 360, 0.5); // 180
|
|
21
|
+
let m = gsap.utils.mapRange(0, 100, 0, 500);
|
|
22
|
+
m(50); // 250
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### normalize(min, max, value?)
|
|
26
|
+
|
|
27
|
+
Returns 0-1 for the range.
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
gsap.utils.normalize(0, 100, 50); // 0.5
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### interpolate(start, end, progress?)
|
|
34
|
+
|
|
35
|
+
Numbers, colors, or objects with matching keys.
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
gsap.utils.interpolate(0, 100, 0.5); // 50
|
|
39
|
+
gsap.utils.interpolate("#ff0000", "#0000ff", 0.5); // mid color
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Random and Snap
|
|
43
|
+
|
|
44
|
+
### random(min, max[, snap, returnFunction]) / random(array[, returnFunction])
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
gsap.utils.random(-100, 100);
|
|
48
|
+
gsap.utils.random(0, 500, 5); // snapped to 5
|
|
49
|
+
let fn = gsap.utils.random(-200, 500, 10, true);
|
|
50
|
+
fn(); // reusable
|
|
51
|
+
gsap.utils.random(["red", "blue"]); // pick one
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**String form in tweens:** `x: "random(-100, 100, 5)"`.
|
|
55
|
+
|
|
56
|
+
### snap(snapTo, value?)
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
gsap.utils.snap(10, 23); // 20
|
|
60
|
+
gsap.utils.snap([0, 100, 200], 150); // nearest
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### shuffle(array)
|
|
64
|
+
|
|
65
|
+
Returns shuffled copy.
|
|
66
|
+
|
|
67
|
+
### distribute(config)
|
|
68
|
+
|
|
69
|
+
Returns a function assigning values by position. Config: `base`, `amount`/`each`, `from`, `grid`, `axis`, `ease`.
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
gsap.to(".class", { scale: gsap.utils.distribute({ base: 0.5, amount: 2.5, from: "center" }) });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Units and Parsing
|
|
76
|
+
|
|
77
|
+
- **getUnit(value)** — `gsap.utils.getUnit("100px")` → `"px"`
|
|
78
|
+
- **unitize(value, unit)** — `gsap.utils.unitize(100, "px")` → `"100px"`
|
|
79
|
+
- **splitColor(color, returnHSL?)** — `gsap.utils.splitColor("red")` → `[255, 0, 0]`. Pass `true` for HSL.
|
|
80
|
+
|
|
81
|
+
## Arrays and Collections
|
|
82
|
+
|
|
83
|
+
- **selector(scope)** — scoped selector: `gsap.utils.selector(ref)(".box")`
|
|
84
|
+
- **toArray(value, scope?)** — convert selector/NodeList/element to array
|
|
85
|
+
- **pipe(...fns)** — compose: `pipe(f1, f2)(value)` = `f2(f1(value))`
|
|
86
|
+
- **wrap(min, max, value?)** — cyclic wrap: `wrap(0, 360, 370)` → `10`
|
|
87
|
+
- **wrapYoyo(min, max, value?)** — bounce wrap: `wrapYoyo(0, 100, 150)` → `50`
|
|
88
|
+
|
|
89
|
+
## Do Not
|
|
90
|
+
|
|
91
|
+
- Assume mapRange/normalize handle units — they work on numbers. Use getUnit/unitize.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Extract per-frame audio visualization data from an audio or video file.
|
|
4
|
+
|
|
5
|
+
Outputs JSON with RMS amplitude and frequency band data at the target FPS,
|
|
6
|
+
ready to embed in a HyperFrames composition.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python extract-audio-data.py input.mp3 -o audio-data.json
|
|
10
|
+
python extract-audio-data.py input.mp4 --fps 30 --bands 16 -o audio-data.json
|
|
11
|
+
|
|
12
|
+
Requirements:
|
|
13
|
+
- Python 3.9+
|
|
14
|
+
- ffmpeg (for decoding audio)
|
|
15
|
+
- numpy (pip install numpy)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# FFT parameters
|
|
27
|
+
#
|
|
28
|
+
# A 4096-sample window gives ~10.8 Hz per bin at 44100Hz — enough to resolve
|
|
29
|
+
# low-frequency bands cleanly. The per-frame audio slice (44100/30 = 1470
|
|
30
|
+
# samples at 30fps) is too small and causes low bands to map to the same bins.
|
|
31
|
+
#
|
|
32
|
+
# Frequency range 30Hz–16kHz covers the useful range for music. Below 30Hz is
|
|
33
|
+
# sub-bass most speakers can't reproduce; above 16kHz is noise/harmonics that
|
|
34
|
+
# don't contribute to perceived rhythm or melody.
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
SAMPLE_RATE = 44100
|
|
38
|
+
FFT_SIZE = 4096
|
|
39
|
+
MIN_FREQ = 30.0
|
|
40
|
+
MAX_FREQ = 16000.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def decode_audio(path: str) -> np.ndarray:
|
|
44
|
+
"""Decode audio to mono float32 samples via ffmpeg."""
|
|
45
|
+
cmd = [
|
|
46
|
+
"ffmpeg", "-i", path,
|
|
47
|
+
"-vn", "-ac", "1", "-ar", str(SAMPLE_RATE),
|
|
48
|
+
"-f", "s16le", "-acodec", "pcm_s16le",
|
|
49
|
+
"-loglevel", "error",
|
|
50
|
+
"pipe:1",
|
|
51
|
+
]
|
|
52
|
+
result = subprocess.run(cmd, capture_output=True)
|
|
53
|
+
if result.returncode != 0:
|
|
54
|
+
print(f"ffmpeg error: {result.stderr.decode()}", file=sys.stderr)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
return np.frombuffer(result.stdout, dtype=np.int16).astype(np.float32) / 32768.0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def compute_band_edges(n_bands: int) -> np.ndarray:
|
|
60
|
+
"""Logarithmically-spaced frequency band edges from MIN_FREQ to MAX_FREQ."""
|
|
61
|
+
return np.array([
|
|
62
|
+
MIN_FREQ * (MAX_FREQ / MIN_FREQ) ** (i / n_bands)
|
|
63
|
+
for i in range(n_bands + 1)
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def compute_fft_bands(
|
|
68
|
+
windowed: np.ndarray, freq_per_bin: float, n_bins: int,
|
|
69
|
+
band_edges: np.ndarray, n_bands: int,
|
|
70
|
+
) -> np.ndarray:
|
|
71
|
+
"""Compute peak magnitude in logarithmically-spaced frequency bands."""
|
|
72
|
+
magnitudes = np.abs(np.fft.rfft(windowed))
|
|
73
|
+
|
|
74
|
+
bands = np.zeros(n_bands)
|
|
75
|
+
for b in range(n_bands):
|
|
76
|
+
low_bin = max(0, int(band_edges[b] / freq_per_bin))
|
|
77
|
+
high_bin = min(n_bins, int(band_edges[b + 1] / freq_per_bin))
|
|
78
|
+
if high_bin <= low_bin:
|
|
79
|
+
high_bin = low_bin + 1
|
|
80
|
+
# Clamp to valid range to avoid empty slices
|
|
81
|
+
low_bin = min(low_bin, n_bins - 1)
|
|
82
|
+
high_bin = min(high_bin, n_bins)
|
|
83
|
+
bands[b] = np.max(magnitudes[low_bin:high_bin])
|
|
84
|
+
|
|
85
|
+
return bands
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def extract(path: str, fps: int, n_bands: int) -> dict:
|
|
89
|
+
"""Extract per-frame audio data."""
|
|
90
|
+
print(f"Decoding audio from {path}...", file=sys.stderr)
|
|
91
|
+
samples = decode_audio(path)
|
|
92
|
+
duration = len(samples) / SAMPLE_RATE
|
|
93
|
+
frame_step = SAMPLE_RATE // fps
|
|
94
|
+
total_frames = int(duration * fps)
|
|
95
|
+
|
|
96
|
+
print(f"Duration: {duration:.1f}s, {total_frames} frames at {fps}fps", file=sys.stderr)
|
|
97
|
+
print(f"FFT window: {FFT_SIZE} samples ({SAMPLE_RATE / FFT_SIZE:.1f} Hz/bin)", file=sys.stderr)
|
|
98
|
+
print(f"Frequency range: {MIN_FREQ:.0f}-{MAX_FREQ:.0f} Hz, {n_bands} bands", file=sys.stderr)
|
|
99
|
+
|
|
100
|
+
# Precompute constants
|
|
101
|
+
hann = np.hanning(FFT_SIZE)
|
|
102
|
+
band_edges = compute_band_edges(n_bands)
|
|
103
|
+
freq_per_bin = SAMPLE_RATE / FFT_SIZE
|
|
104
|
+
n_bins = FFT_SIZE // 2 + 1
|
|
105
|
+
half_fft = FFT_SIZE // 2
|
|
106
|
+
|
|
107
|
+
# Pass 1: extract raw values
|
|
108
|
+
rms_values = np.zeros(total_frames)
|
|
109
|
+
band_values = np.zeros((total_frames, n_bands))
|
|
110
|
+
|
|
111
|
+
for f in range(total_frames):
|
|
112
|
+
# RMS from the frame's audio slice
|
|
113
|
+
rms_start = f * frame_step
|
|
114
|
+
rms_end = rms_start + frame_step
|
|
115
|
+
frame_slice = samples[rms_start:min(rms_end, len(samples))]
|
|
116
|
+
if len(frame_slice) > 0:
|
|
117
|
+
rms_values[f] = np.sqrt(np.mean(frame_slice ** 2))
|
|
118
|
+
|
|
119
|
+
# FFT from a centered 4096-sample window
|
|
120
|
+
center = rms_start + frame_step // 2
|
|
121
|
+
win_start = center - half_fft
|
|
122
|
+
win_end = center + half_fft
|
|
123
|
+
|
|
124
|
+
if win_start >= 0 and win_end <= len(samples):
|
|
125
|
+
window = samples[win_start:win_end] * hann
|
|
126
|
+
else:
|
|
127
|
+
# Zero-pad at edges
|
|
128
|
+
padded = np.zeros(FFT_SIZE)
|
|
129
|
+
src_start = max(0, win_start)
|
|
130
|
+
src_end = min(len(samples), win_end)
|
|
131
|
+
dst_start = src_start - win_start
|
|
132
|
+
dst_end = dst_start + (src_end - src_start)
|
|
133
|
+
padded[dst_start:dst_end] = samples[src_start:src_end]
|
|
134
|
+
window = padded * hann
|
|
135
|
+
|
|
136
|
+
band_values[f] = compute_fft_bands(window, freq_per_bin, n_bins, band_edges, n_bands)
|
|
137
|
+
|
|
138
|
+
# Pass 2: normalize
|
|
139
|
+
peak_rms = rms_values.max() if total_frames > 0 else 1.0
|
|
140
|
+
if peak_rms > 0:
|
|
141
|
+
rms_values /= peak_rms
|
|
142
|
+
|
|
143
|
+
# Per-band normalization so treble is visible alongside louder bass
|
|
144
|
+
band_peaks = band_values.max(axis=0)
|
|
145
|
+
band_peaks[band_peaks == 0] = 1.0
|
|
146
|
+
band_values /= band_peaks
|
|
147
|
+
|
|
148
|
+
# Build output
|
|
149
|
+
frames = []
|
|
150
|
+
for f in range(total_frames):
|
|
151
|
+
frames.append({
|
|
152
|
+
"time": round(f / fps, 4),
|
|
153
|
+
"rms": round(float(rms_values[f]), 4),
|
|
154
|
+
"bands": [round(float(b), 4) for b in band_values[f]],
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"duration": round(duration, 4),
|
|
159
|
+
"fps": fps,
|
|
160
|
+
"bands": n_bands,
|
|
161
|
+
"totalFrames": total_frames,
|
|
162
|
+
"frames": frames,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main():
|
|
167
|
+
parser = argparse.ArgumentParser(description="Extract per-frame audio visualization data")
|
|
168
|
+
parser.add_argument("input", help="Audio or video file")
|
|
169
|
+
parser.add_argument("-o", "--output", default="audio-data.json", help="Output JSON path")
|
|
170
|
+
parser.add_argument("--fps", type=int, default=30, help="Frames per second (default: 30)")
|
|
171
|
+
parser.add_argument("--bands", type=int, default=16, help="Number of frequency bands (default: 16)")
|
|
172
|
+
args = parser.parse_args()
|
|
173
|
+
|
|
174
|
+
if args.fps < 1:
|
|
175
|
+
parser.error("--fps must be at least 1")
|
|
176
|
+
if args.bands < 1:
|
|
177
|
+
parser.error("--bands must be at least 1")
|
|
178
|
+
|
|
179
|
+
data = extract(args.input, args.fps, args.bands)
|
|
180
|
+
|
|
181
|
+
with open(args.output, "w") as f:
|
|
182
|
+
json.dump(data, f)
|
|
183
|
+
|
|
184
|
+
print(f"Wrote {args.output} ({data['totalFrames']} frames, {data['bands']} bands)", file=sys.stderr)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
main()
|