solid-drift 0.2.0 → 0.8.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 +933 -10
- package/dist/ai.d.ts +207 -0
- package/dist/ai.js +570 -0
- package/dist/animate.d.ts +2 -2
- package/dist/animate.js +2 -2
- package/dist/cartoon.d.ts +190 -0
- package/dist/cartoon.js +334 -0
- package/dist/color.d.ts +53 -0
- package/dist/color.js +391 -0
- package/dist/directive.d.ts +5 -5
- package/dist/directive.js +15 -7
- package/dist/easing.d.ts +22 -1
- package/dist/easing.js +49 -1
- package/dist/flip.d.ts +44 -0
- package/dist/flip.js +108 -0
- package/dist/horizontal.d.ts +107 -0
- package/dist/horizontal.js +208 -0
- package/dist/index.d.ts +20 -3
- package/dist/index.js +17 -3
- package/dist/inview.d.ts +4 -4
- package/dist/inview.js +5 -5
- package/dist/motion.d.ts +404 -0
- package/dist/motion.js +761 -0
- package/dist/physics.d.ts +146 -0
- package/dist/physics.js +352 -0
- package/dist/pointer.d.ts +76 -0
- package/dist/pointer.js +123 -0
- package/dist/reduced-motion.d.ts +3 -3
- package/dist/reduced-motion.js +5 -4
- package/dist/scroll.d.ts +3 -3
- package/dist/scroll.js +5 -5
- package/dist/scrollfx.d.ts +156 -0
- package/dist/scrollfx.js +148 -0
- package/dist/scrub.d.ts +51 -0
- package/dist/scrub.js +67 -0
- package/dist/spring.d.ts +39 -4
- package/dist/spring.js +26 -7
- package/dist/stagger.d.ts +4 -4
- package/dist/stagger.js +4 -4
- package/dist/text.d.ts +27 -0
- package/dist/text.js +84 -0
- package/dist/timeline.d.ts +45 -0
- package/dist/timeline.js +93 -0
- package/dist/trail.d.ts +27 -0
- package/dist/trail.js +75 -0
- package/dist/tween.d.ts +3 -3
- package/dist/tween.js +3 -3
- package/dist/typography.d.ts +241 -0
- package/dist/typography.js +812 -0
- package/dist/velocity.d.ts +44 -0
- package/dist/velocity.js +88 -0
- package/dist/web3.d.ts +213 -0
- package/dist/web3.js +640 -0
- package/package.json +1 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
export interface GravityOptions {
|
|
3
|
+
/** Drop height in pixels above the floor. Default 240. */
|
|
4
|
+
from?: number;
|
|
5
|
+
/** Floor position in pixels: the y value the body rests at. Default 0. */
|
|
6
|
+
floor?: number;
|
|
7
|
+
/** Gravity in pixels per second squared. Default 2600. */
|
|
8
|
+
gravity?: number;
|
|
9
|
+
/** Bounciness (restitution) on each impact, 0 to 1. Default 0.55. */
|
|
10
|
+
bounciness?: number;
|
|
11
|
+
/** Horizontal velocity kept through each bounce, 0 to 1. Default 0.9. */
|
|
12
|
+
bounceFriction?: number;
|
|
13
|
+
/** Initial horizontal velocity in pixels per second. Default 0. */
|
|
14
|
+
velocityX?: number;
|
|
15
|
+
/** Speed below which an impact becomes a rest, in px/s. Default 40. */
|
|
16
|
+
restThreshold?: number;
|
|
17
|
+
/** Called once the body comes to rest. */
|
|
18
|
+
onRest?: () => void;
|
|
19
|
+
}
|
|
20
|
+
export interface GravityResult {
|
|
21
|
+
/** Horizontal position in pixels. */
|
|
22
|
+
x: Accessor<number>;
|
|
23
|
+
/** Vertical position in pixels, resting at `floor`. */
|
|
24
|
+
y: Accessor<number>;
|
|
25
|
+
/** True while falling or bouncing. */
|
|
26
|
+
moving: Accessor<boolean>;
|
|
27
|
+
/** Drop again, optionally from a new height. */
|
|
28
|
+
drop: (from?: number) => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Cartoon gravity: drop a body, watch it fall with acceleration, bounce
|
|
32
|
+
* with restitution, and settle on the floor.
|
|
33
|
+
*
|
|
34
|
+
* The simulation runs on the shared clock with semi-implicit Euler
|
|
35
|
+
* integration. Each floor impact reflects the vertical velocity scaled
|
|
36
|
+
* by `bounciness` and scrubs horizontal velocity by `bounceFriction`;
|
|
37
|
+
* when an impact arrives slower than `restThreshold` the body parks
|
|
38
|
+
* exactly on the floor. `drop()` replays the drop, so it pairs well
|
|
39
|
+
* with `createInView`: drop the anvil every time it scrolls into view.
|
|
40
|
+
*
|
|
41
|
+
* SSR-safe and reduced-motion safe: the body simply rests on the floor
|
|
42
|
+
* and `drop()` is a no-op.
|
|
43
|
+
*
|
|
44
|
+
* ```tsx
|
|
45
|
+
* const { x, y, moving, drop } = createGravity({ from: 300, bounciness: 0.6 })
|
|
46
|
+
* <div style={{ transform: `translate(${x()}px, ${y()}px)` }} />
|
|
47
|
+
* <button onClick={() => drop()}>Again!</button>
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function createGravity(options?: GravityOptions): GravityResult;
|
|
51
|
+
export interface PendulumOptions {
|
|
52
|
+
/** String length in pixels, pivot to bob. Default 160. */
|
|
53
|
+
length?: number;
|
|
54
|
+
/** Release angle in degrees. Default 32. */
|
|
55
|
+
amplitude?: number;
|
|
56
|
+
/** Damping coefficient per second. Default 0.25, a long gentle decay. */
|
|
57
|
+
damping?: number;
|
|
58
|
+
/** Gravity in pixels per second squared. Default 2600. */
|
|
59
|
+
gravity?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface PendulumResult {
|
|
62
|
+
/** Swing angle in degrees, 0 is hanging straight down. */
|
|
63
|
+
angle: Accessor<number>;
|
|
64
|
+
/** Bob offset from its rest position in pixels. */
|
|
65
|
+
x: Accessor<number>;
|
|
66
|
+
/** Bob offset from its rest position in pixels. */
|
|
67
|
+
y: Accessor<number>;
|
|
68
|
+
/** Release the pendulum from an angle in degrees. */
|
|
69
|
+
swing: (amplitude?: number) => void;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A pendulum that swings with real physics and a draggable amplitude.
|
|
73
|
+
*
|
|
74
|
+
* Integrates the pendulum equation (theta'' = -(g/L) sin theta - c theta')
|
|
75
|
+
* on the shared clock, so the period comes from `length` and `gravity`
|
|
76
|
+
* instead of a hand-tuned duration: long strings swing slow, short
|
|
77
|
+
* strings swing fast, exactly like the playground. The amplitude decays
|
|
78
|
+
* through `damping` until the bob hangs still.
|
|
79
|
+
*
|
|
80
|
+
* Grab the element and pull it aside to set the amplitude yourself:
|
|
81
|
+
* while dragging, the bob follows the pointer around its pivot (the
|
|
82
|
+
* pivot sits `length` pixels above the element's top center); release
|
|
83
|
+
* and it swings from exactly where you left it.
|
|
84
|
+
*
|
|
85
|
+
* SSR-safe and reduced-motion safe: the bob hangs at rest and `swing()`
|
|
86
|
+
* is a no-op.
|
|
87
|
+
*
|
|
88
|
+
* ```tsx
|
|
89
|
+
* let bob!: HTMLDivElement
|
|
90
|
+
* const { angle } = createPendulum(() => bob, { length: 200 })
|
|
91
|
+
* // The bob hangs from an invisible string 200px above it.
|
|
92
|
+
* <div ref={bob} style={{ transform: `rotate(${angle()}deg)`, "transform-origin": "50% -200px" }} />
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare function createPendulum(ref: () => Element | null | undefined, options?: PendulumOptions): PendulumResult;
|
|
96
|
+
export interface FlingOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Element to bounce inside; defaults to the viewport. Bounds are
|
|
99
|
+
* measured when the throw starts.
|
|
100
|
+
*/
|
|
101
|
+
bounds?: () => Element | null | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Exponential velocity decay per second of free flight: higher values
|
|
104
|
+
* stop the fling sooner. Default 1.4.
|
|
105
|
+
*/
|
|
106
|
+
friction?: number;
|
|
107
|
+
/** Bounciness on the edges, 0 to 1. Default 0.7. */
|
|
108
|
+
bounciness?: number;
|
|
109
|
+
/** Speed in px/s below which the body stops. Default 12. */
|
|
110
|
+
restThreshold?: number;
|
|
111
|
+
}
|
|
112
|
+
export interface FlingResult {
|
|
113
|
+
/** Horizontal offset in pixels from the layout position. */
|
|
114
|
+
x: Accessor<number>;
|
|
115
|
+
/** Vertical offset in pixels from the layout position. */
|
|
116
|
+
y: Accessor<number>;
|
|
117
|
+
/** Current horizontal velocity in px/s. */
|
|
118
|
+
vx: Accessor<number>;
|
|
119
|
+
/** Current vertical velocity in px/s. */
|
|
120
|
+
vy: Accessor<number>;
|
|
121
|
+
/** True while gliding after a throw. */
|
|
122
|
+
moving: Accessor<boolean>;
|
|
123
|
+
/** Stop dead wherever it is. */
|
|
124
|
+
stop: () => void;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Grab it, throw it: fling physics with momentum and edge bounce.
|
|
128
|
+
*
|
|
129
|
+
* Pointer down grabs the element and it follows the pointer; release
|
|
130
|
+
* and it keeps the release velocity, gliding with exponential friction
|
|
131
|
+
* and bouncing off the bounds (a container or the viewport) with
|
|
132
|
+
* restitution until it slows below `restThreshold`. The release
|
|
133
|
+
* velocity is measured from the last ~120ms of the drag, so a quick
|
|
134
|
+
* flick throws far and a gentle release barely moves.
|
|
135
|
+
*
|
|
136
|
+
* Touch works the same as mouse: drag with a finger, flick to throw.
|
|
137
|
+
* Under reduced motion the element still drags, but release drops it
|
|
138
|
+
* in place with no momentum. SSR-safe: everything rests at 0.
|
|
139
|
+
*
|
|
140
|
+
* ```tsx
|
|
141
|
+
* let card!: HTMLDivElement
|
|
142
|
+
* const { x, y } = createFling(() => card, { bounciness: 0.8 })
|
|
143
|
+
* <div ref={card} style={{ transform: `translate(${x()}px, ${y()}px)`, "touch-action": "none" }} />
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export declare function createFling(ref: () => Element | null | undefined, options?: FlingOptions): FlingResult;
|
package/dist/physics.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { createEffect, createSignal, onCleanup, untrack, } from "solid-js";
|
|
2
|
+
import { now, schedule } from "./engine.js";
|
|
3
|
+
import { prefersReducedMotion } from "./reduced-motion.js";
|
|
4
|
+
/** Element.addEventListener's type map omits pointer events in newer TS DOM libs. */
|
|
5
|
+
const toListener = (fn) => fn;
|
|
6
|
+
/**
|
|
7
|
+
* Cartoon gravity: drop a body, watch it fall with acceleration, bounce
|
|
8
|
+
* with restitution, and settle on the floor.
|
|
9
|
+
*
|
|
10
|
+
* The simulation runs on the shared clock with semi-implicit Euler
|
|
11
|
+
* integration. Each floor impact reflects the vertical velocity scaled
|
|
12
|
+
* by `bounciness` and scrubs horizontal velocity by `bounceFriction`;
|
|
13
|
+
* when an impact arrives slower than `restThreshold` the body parks
|
|
14
|
+
* exactly on the floor. `drop()` replays the drop, so it pairs well
|
|
15
|
+
* with `createInView`: drop the anvil every time it scrolls into view.
|
|
16
|
+
*
|
|
17
|
+
* SSR-safe and reduced-motion safe: the body simply rests on the floor
|
|
18
|
+
* and `drop()` is a no-op.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const { x, y, moving, drop } = createGravity({ from: 300, bounciness: 0.6 })
|
|
22
|
+
* <div style={{ transform: `translate(${x()}px, ${y()}px)` }} />
|
|
23
|
+
* <button onClick={() => drop()}>Again!</button>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function createGravity(options = {}) {
|
|
27
|
+
const { from = 240, floor = 0, gravity = 2600, bounciness = 0.55, bounceFriction = 0.9, velocityX = 0, restThreshold = 40, onRest, } = options;
|
|
28
|
+
if (typeof window === "undefined" || prefersReducedMotion()) {
|
|
29
|
+
const [x] = createSignal(0);
|
|
30
|
+
const [y] = createSignal(floor);
|
|
31
|
+
const [moving] = createSignal(false);
|
|
32
|
+
return { x, y, moving, drop: () => { } };
|
|
33
|
+
}
|
|
34
|
+
const [x, setX] = createSignal(0);
|
|
35
|
+
const [y, setY] = createSignal(floor - from);
|
|
36
|
+
const [moving, setMoving] = createSignal(false);
|
|
37
|
+
let vx = 0;
|
|
38
|
+
let vy = 0;
|
|
39
|
+
let height = from;
|
|
40
|
+
let cancel = null;
|
|
41
|
+
let lastT = 0;
|
|
42
|
+
const loop = (t) => {
|
|
43
|
+
const dt = Math.min(Math.max((t - lastT) / 1000, 0), 0.064);
|
|
44
|
+
lastT = t;
|
|
45
|
+
vy += gravity * dt;
|
|
46
|
+
let nx = untrack(x) + vx * dt;
|
|
47
|
+
let ny = untrack(y) + vy * dt;
|
|
48
|
+
if (ny >= floor) {
|
|
49
|
+
ny = floor;
|
|
50
|
+
if (Math.abs(vy) > restThreshold) {
|
|
51
|
+
vy = -vy * bounciness;
|
|
52
|
+
vx *= bounceFriction;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
vy = 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
setX(nx);
|
|
59
|
+
setY(ny);
|
|
60
|
+
if (ny >= floor && vy === 0 && Math.abs(vx) < restThreshold) {
|
|
61
|
+
setMoving(false);
|
|
62
|
+
cancel = null;
|
|
63
|
+
onRest?.();
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
68
|
+
const drop = (nextFrom) => {
|
|
69
|
+
if (nextFrom !== undefined)
|
|
70
|
+
height = nextFrom;
|
|
71
|
+
vx = velocityX;
|
|
72
|
+
vy = 0;
|
|
73
|
+
setX(0);
|
|
74
|
+
setY(floor - height);
|
|
75
|
+
setMoving(true);
|
|
76
|
+
if (!cancel) {
|
|
77
|
+
lastT = now();
|
|
78
|
+
cancel = schedule(loop);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
drop();
|
|
82
|
+
onCleanup(() => cancel?.());
|
|
83
|
+
return { x, y, moving, drop };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A pendulum that swings with real physics and a draggable amplitude.
|
|
87
|
+
*
|
|
88
|
+
* Integrates the pendulum equation (theta'' = -(g/L) sin theta - c theta')
|
|
89
|
+
* on the shared clock, so the period comes from `length` and `gravity`
|
|
90
|
+
* instead of a hand-tuned duration: long strings swing slow, short
|
|
91
|
+
* strings swing fast, exactly like the playground. The amplitude decays
|
|
92
|
+
* through `damping` until the bob hangs still.
|
|
93
|
+
*
|
|
94
|
+
* Grab the element and pull it aside to set the amplitude yourself:
|
|
95
|
+
* while dragging, the bob follows the pointer around its pivot (the
|
|
96
|
+
* pivot sits `length` pixels above the element's top center); release
|
|
97
|
+
* and it swings from exactly where you left it.
|
|
98
|
+
*
|
|
99
|
+
* SSR-safe and reduced-motion safe: the bob hangs at rest and `swing()`
|
|
100
|
+
* is a no-op.
|
|
101
|
+
*
|
|
102
|
+
* ```tsx
|
|
103
|
+
* let bob!: HTMLDivElement
|
|
104
|
+
* const { angle } = createPendulum(() => bob, { length: 200 })
|
|
105
|
+
* // The bob hangs from an invisible string 200px above it.
|
|
106
|
+
* <div ref={bob} style={{ transform: `rotate(${angle()}deg)`, "transform-origin": "50% -200px" }} />
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function createPendulum(ref, options = {}) {
|
|
110
|
+
const { length = 160, amplitude = 32, damping = 0.25, gravity = 2600, } = options;
|
|
111
|
+
if (typeof window === "undefined" || prefersReducedMotion()) {
|
|
112
|
+
const zero = () => 0;
|
|
113
|
+
return { angle: zero, x: zero, y: zero, swing: () => { } };
|
|
114
|
+
}
|
|
115
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
116
|
+
const [angle, setAngle] = createSignal(0);
|
|
117
|
+
const x = () => length * Math.sin(toRad(angle()));
|
|
118
|
+
const y = () => length * (1 - Math.cos(toRad(angle())));
|
|
119
|
+
let theta = 0;
|
|
120
|
+
let omega = 0;
|
|
121
|
+
let dragging = false;
|
|
122
|
+
let cancel = null;
|
|
123
|
+
let lastT = 0;
|
|
124
|
+
const loop = (t) => {
|
|
125
|
+
const dt = Math.min(Math.max((t - lastT) / 1000, 0), 0.064);
|
|
126
|
+
lastT = t;
|
|
127
|
+
const alpha = -(gravity / length) * Math.sin(theta) - damping * omega;
|
|
128
|
+
omega += alpha * dt;
|
|
129
|
+
theta += omega * dt;
|
|
130
|
+
setAngle((theta * 180) / Math.PI);
|
|
131
|
+
if (Math.abs(theta) < 0.0005 && Math.abs(omega) < 0.005) {
|
|
132
|
+
theta = 0;
|
|
133
|
+
omega = 0;
|
|
134
|
+
setAngle(0);
|
|
135
|
+
cancel = null;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
};
|
|
140
|
+
const kick = () => {
|
|
141
|
+
if (cancel || dragging)
|
|
142
|
+
return;
|
|
143
|
+
lastT = now();
|
|
144
|
+
cancel = schedule(loop);
|
|
145
|
+
};
|
|
146
|
+
const swing = (amp = amplitude) => {
|
|
147
|
+
theta = toRad(amp);
|
|
148
|
+
omega = 0;
|
|
149
|
+
setAngle(amp);
|
|
150
|
+
kick();
|
|
151
|
+
};
|
|
152
|
+
const onDown = (event) => {
|
|
153
|
+
const el = ref();
|
|
154
|
+
if (!el)
|
|
155
|
+
return;
|
|
156
|
+
dragging = true;
|
|
157
|
+
cancel?.();
|
|
158
|
+
cancel = null;
|
|
159
|
+
const rect = el.getBoundingClientRect();
|
|
160
|
+
const pivotX = rect.left + rect.width / 2;
|
|
161
|
+
const pivotY = rect.top - length;
|
|
162
|
+
const move = (ev) => {
|
|
163
|
+
const dx = ev.clientX - pivotX;
|
|
164
|
+
// Clamp below the pivot so the angle stays well-defined.
|
|
165
|
+
const dy = Math.max(ev.clientY - pivotY, 8);
|
|
166
|
+
theta = Math.atan2(dx, dy);
|
|
167
|
+
omega = 0;
|
|
168
|
+
setAngle((theta * 180) / Math.PI);
|
|
169
|
+
};
|
|
170
|
+
const up = () => {
|
|
171
|
+
window.removeEventListener("pointermove", move);
|
|
172
|
+
window.removeEventListener("pointerup", up);
|
|
173
|
+
window.removeEventListener("pointercancel", up);
|
|
174
|
+
dragging = false;
|
|
175
|
+
kick();
|
|
176
|
+
};
|
|
177
|
+
window.addEventListener("pointermove", move, { passive: true });
|
|
178
|
+
window.addEventListener("pointerup", up);
|
|
179
|
+
window.addEventListener("pointercancel", up);
|
|
180
|
+
move(event);
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
};
|
|
183
|
+
createEffect(() => {
|
|
184
|
+
const el = ref();
|
|
185
|
+
if (!el)
|
|
186
|
+
return;
|
|
187
|
+
el.addEventListener("pointerdown", toListener(onDown));
|
|
188
|
+
onCleanup(() => el.removeEventListener("pointerdown", toListener(onDown)));
|
|
189
|
+
});
|
|
190
|
+
onCleanup(() => cancel?.());
|
|
191
|
+
swing();
|
|
192
|
+
return { angle, x, y, swing };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Grab it, throw it: fling physics with momentum and edge bounce.
|
|
196
|
+
*
|
|
197
|
+
* Pointer down grabs the element and it follows the pointer; release
|
|
198
|
+
* and it keeps the release velocity, gliding with exponential friction
|
|
199
|
+
* and bouncing off the bounds (a container or the viewport) with
|
|
200
|
+
* restitution until it slows below `restThreshold`. The release
|
|
201
|
+
* velocity is measured from the last ~120ms of the drag, so a quick
|
|
202
|
+
* flick throws far and a gentle release barely moves.
|
|
203
|
+
*
|
|
204
|
+
* Touch works the same as mouse: drag with a finger, flick to throw.
|
|
205
|
+
* Under reduced motion the element still drags, but release drops it
|
|
206
|
+
* in place with no momentum. SSR-safe: everything rests at 0.
|
|
207
|
+
*
|
|
208
|
+
* ```tsx
|
|
209
|
+
* let card!: HTMLDivElement
|
|
210
|
+
* const { x, y } = createFling(() => card, { bounciness: 0.8 })
|
|
211
|
+
* <div ref={card} style={{ transform: `translate(${x()}px, ${y()}px)`, "touch-action": "none" }} />
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export function createFling(ref, options = {}) {
|
|
215
|
+
const { bounds, friction = 1.4, bounciness = 0.7, restThreshold = 12, } = options;
|
|
216
|
+
if (typeof window === "undefined") {
|
|
217
|
+
const zero = () => 0;
|
|
218
|
+
const no = () => false;
|
|
219
|
+
return { x: zero, y: zero, vx: zero, vy: zero, moving: no, stop: () => { } };
|
|
220
|
+
}
|
|
221
|
+
const [x, setX] = createSignal(0);
|
|
222
|
+
const [y, setY] = createSignal(0);
|
|
223
|
+
const [vx, setVx] = createSignal(0);
|
|
224
|
+
const [vy, setVy] = createSignal(0);
|
|
225
|
+
const [moving, setMoving] = createSignal(false);
|
|
226
|
+
let cancel = null;
|
|
227
|
+
let lastT = 0;
|
|
228
|
+
let minX = -Infinity;
|
|
229
|
+
let maxX = Infinity;
|
|
230
|
+
let minY = -Infinity;
|
|
231
|
+
let maxY = Infinity;
|
|
232
|
+
const measureBounds = () => {
|
|
233
|
+
const el = ref();
|
|
234
|
+
if (!el)
|
|
235
|
+
return;
|
|
236
|
+
const rect = el.getBoundingClientRect();
|
|
237
|
+
// Layout box: where the element would sit with no offset applied.
|
|
238
|
+
const layoutLeft = rect.left - untrack(x);
|
|
239
|
+
const layoutTop = rect.top - untrack(y);
|
|
240
|
+
const box = bounds?.()?.getBoundingClientRect();
|
|
241
|
+
const left = box?.left ?? 0;
|
|
242
|
+
const top = box?.top ?? 0;
|
|
243
|
+
const right = box?.right ?? window.innerWidth;
|
|
244
|
+
const bottom = box?.bottom ?? window.innerHeight;
|
|
245
|
+
minX = left - layoutLeft;
|
|
246
|
+
maxX = right - (layoutLeft + rect.width);
|
|
247
|
+
minY = top - layoutTop;
|
|
248
|
+
maxY = bottom - (layoutTop + rect.height);
|
|
249
|
+
};
|
|
250
|
+
const loop = (t) => {
|
|
251
|
+
const dt = Math.min(Math.max((t - lastT) / 1000, 0), 0.064);
|
|
252
|
+
lastT = t;
|
|
253
|
+
const decay = Math.exp(-friction * dt);
|
|
254
|
+
let nvx = untrack(vx) * decay;
|
|
255
|
+
let nvy = untrack(vy) * decay;
|
|
256
|
+
let nx = untrack(x) + nvx * dt;
|
|
257
|
+
let ny = untrack(y) + nvy * dt;
|
|
258
|
+
if (nx < minX) {
|
|
259
|
+
nx = minX;
|
|
260
|
+
nvx = Math.abs(nvx) * bounciness;
|
|
261
|
+
}
|
|
262
|
+
else if (nx > maxX) {
|
|
263
|
+
nx = maxX;
|
|
264
|
+
nvx = -Math.abs(nvx) * bounciness;
|
|
265
|
+
}
|
|
266
|
+
if (ny < minY) {
|
|
267
|
+
ny = minY;
|
|
268
|
+
nvy = Math.abs(nvy) * bounciness;
|
|
269
|
+
}
|
|
270
|
+
else if (ny > maxY) {
|
|
271
|
+
ny = maxY;
|
|
272
|
+
nvy = -Math.abs(nvy) * bounciness;
|
|
273
|
+
}
|
|
274
|
+
setX(nx);
|
|
275
|
+
setY(ny);
|
|
276
|
+
setVx(nvx);
|
|
277
|
+
setVy(nvy);
|
|
278
|
+
if (Math.hypot(nvx, nvy) < restThreshold) {
|
|
279
|
+
setVx(0);
|
|
280
|
+
setVy(0);
|
|
281
|
+
setMoving(false);
|
|
282
|
+
cancel = null;
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
};
|
|
287
|
+
const stop = () => {
|
|
288
|
+
cancel?.();
|
|
289
|
+
cancel = null;
|
|
290
|
+
setVx(0);
|
|
291
|
+
setVy(0);
|
|
292
|
+
setMoving(false);
|
|
293
|
+
};
|
|
294
|
+
const onDown = (event) => {
|
|
295
|
+
const el = ref();
|
|
296
|
+
if (!el)
|
|
297
|
+
return;
|
|
298
|
+
stop();
|
|
299
|
+
measureBounds();
|
|
300
|
+
const startPX = event.clientX;
|
|
301
|
+
const startPY = event.clientY;
|
|
302
|
+
const startX = untrack(x);
|
|
303
|
+
const startY = untrack(y);
|
|
304
|
+
const samples = [
|
|
305
|
+
[now(), startX, startY],
|
|
306
|
+
];
|
|
307
|
+
const move = (ev) => {
|
|
308
|
+
const nx = startX + (ev.clientX - startPX);
|
|
309
|
+
const ny = startY + (ev.clientY - startPY);
|
|
310
|
+
setX(nx);
|
|
311
|
+
setY(ny);
|
|
312
|
+
samples.push([now(), nx, ny]);
|
|
313
|
+
if (samples.length > 8)
|
|
314
|
+
samples.shift();
|
|
315
|
+
};
|
|
316
|
+
const up = () => {
|
|
317
|
+
window.removeEventListener("pointermove", move);
|
|
318
|
+
window.removeEventListener("pointerup", up);
|
|
319
|
+
window.removeEventListener("pointercancel", up);
|
|
320
|
+
// Release velocity from the last ~120ms of the drag.
|
|
321
|
+
const t = now();
|
|
322
|
+
let oi = 0;
|
|
323
|
+
while (oi < samples.length - 1 && t - samples[oi][0] > 120)
|
|
324
|
+
oi++;
|
|
325
|
+
const [ot, ox, oy] = samples[oi];
|
|
326
|
+
const [lt, lx, ly] = samples[samples.length - 1];
|
|
327
|
+
const dt = Math.max((lt - ot) / 1000, 0.008);
|
|
328
|
+
const rvx = (lx - ox) / dt;
|
|
329
|
+
const rvy = (ly - oy) / dt;
|
|
330
|
+
if (!prefersReducedMotion() && Math.hypot(rvx, rvy) >= restThreshold) {
|
|
331
|
+
setVx(rvx);
|
|
332
|
+
setVy(rvy);
|
|
333
|
+
setMoving(true);
|
|
334
|
+
lastT = now();
|
|
335
|
+
cancel = schedule(loop);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
window.addEventListener("pointermove", move, { passive: true });
|
|
339
|
+
window.addEventListener("pointerup", up);
|
|
340
|
+
window.addEventListener("pointercancel", up);
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
};
|
|
343
|
+
createEffect(() => {
|
|
344
|
+
const el = ref();
|
|
345
|
+
if (!el)
|
|
346
|
+
return;
|
|
347
|
+
el.addEventListener("pointerdown", toListener(onDown));
|
|
348
|
+
onCleanup(() => el.removeEventListener("pointerdown", toListener(onDown)));
|
|
349
|
+
});
|
|
350
|
+
onCleanup(() => cancel?.());
|
|
351
|
+
return { x, y, vx, vy, moving, stop };
|
|
352
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
import { type SpringOptions } from "./spring.js";
|
|
3
|
+
export interface MagneticOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Attraction radius in pixels around the element's center. Outside the
|
|
6
|
+
* radius the element springs back to rest. Default 140.
|
|
7
|
+
*/
|
|
8
|
+
radius?: number;
|
|
9
|
+
/** Pull strength at the center, 0 to 1. Default 0.35. */
|
|
10
|
+
strength?: number;
|
|
11
|
+
/** Spring physics for the pull and the release. */
|
|
12
|
+
spring?: SpringOptions;
|
|
13
|
+
}
|
|
14
|
+
export interface MagneticResult {
|
|
15
|
+
/** Horizontal pull in pixels, spring-smoothed. */
|
|
16
|
+
x: Accessor<number>;
|
|
17
|
+
/** Vertical pull in pixels, spring-smoothed. */
|
|
18
|
+
y: Accessor<number>;
|
|
19
|
+
}
|
|
20
|
+
export interface TiltOptions {
|
|
21
|
+
/** Maximum tilt in degrees at the element's edge. Default 10. */
|
|
22
|
+
maxAngle?: number;
|
|
23
|
+
/** Spring physics for the tilt and the settle-back. */
|
|
24
|
+
spring?: SpringOptions;
|
|
25
|
+
}
|
|
26
|
+
export interface TiltResult {
|
|
27
|
+
/** Tilt around the horizontal axis in degrees, spring-smoothed. */
|
|
28
|
+
rotateX: Accessor<number>;
|
|
29
|
+
/** Tilt around the vertical axis in degrees, spring-smoothed. */
|
|
30
|
+
rotateY: Accessor<number>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Magnetic attraction toward the pointer.
|
|
34
|
+
*
|
|
35
|
+
* When the pointer comes within `radius` of the element's center, the
|
|
36
|
+
* element is pulled toward it with a strength that fades with distance.
|
|
37
|
+
* when the pointer leaves, it springs back to rest. The pull itself is a
|
|
38
|
+
* spring, so arrivals and releases both glide instead of snapping.
|
|
39
|
+
*
|
|
40
|
+
* Built on `pointermove`, so touch drags work the same as mouse hovers.
|
|
41
|
+
* SSR-safe and reduced-motion safe: both return constant `0` accessors.
|
|
42
|
+
*
|
|
43
|
+
* ```tsx
|
|
44
|
+
* let btn!: HTMLButtonElement
|
|
45
|
+
* const { x, y } = createMagnetic(() => btn, { strength: 0.4 })
|
|
46
|
+
* <button ref={btn} style={{ transform: `translate(${x()}px, ${y()}px)` }}>
|
|
47
|
+
* Pull me
|
|
48
|
+
* </button>
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function createMagnetic(ref: () => Element | null | undefined, options?: MagneticOptions): MagneticResult;
|
|
52
|
+
/**
|
|
53
|
+
* 3D tilt that follows the pointer across an element.
|
|
54
|
+
*
|
|
55
|
+
* The pointer's position over the element maps to `rotateX`/`rotateY` in
|
|
56
|
+
* degrees, spring-smoothed so the card leans with weight instead of
|
|
57
|
+
* jittering. When the pointer leaves, the element settles back to flat.
|
|
58
|
+
* Pair with a CSS `perspective` on the parent for real depth.
|
|
59
|
+
*
|
|
60
|
+
* Touch drags tilt while touching, release settles back to flat.
|
|
61
|
+
* SSR-safe and reduced-motion safe: both return constant `0` accessors.
|
|
62
|
+
*
|
|
63
|
+
* ```tsx
|
|
64
|
+
* let card!: HTMLDivElement
|
|
65
|
+
* const { rotateX, rotateY } = createTilt(() => card, { maxAngle: 12 })
|
|
66
|
+
* <div style={{ perspective: "800px" }}>
|
|
67
|
+
* <div
|
|
68
|
+
* ref={card}
|
|
69
|
+
* style={{
|
|
70
|
+
* transform: `rotateX(${rotateX()}deg) rotateY(${rotateY()}deg)`,
|
|
71
|
+
* }}
|
|
72
|
+
* />
|
|
73
|
+
* </div>
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function createTilt(ref: () => Element | null | undefined, options?: TiltOptions): TiltResult;
|
package/dist/pointer.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createEffect, createSignal, onCleanup } from "solid-js";
|
|
2
|
+
import { createSpring } from "./spring.js";
|
|
3
|
+
import { prefersReducedMotion } from "./reduced-motion.js";
|
|
4
|
+
const zero = () => 0;
|
|
5
|
+
/**
|
|
6
|
+
* Magnetic attraction toward the pointer.
|
|
7
|
+
*
|
|
8
|
+
* When the pointer comes within `radius` of the element's center, the
|
|
9
|
+
* element is pulled toward it with a strength that fades with distance.
|
|
10
|
+
* when the pointer leaves, it springs back to rest. The pull itself is a
|
|
11
|
+
* spring, so arrivals and releases both glide instead of snapping.
|
|
12
|
+
*
|
|
13
|
+
* Built on `pointermove`, so touch drags work the same as mouse hovers.
|
|
14
|
+
* SSR-safe and reduced-motion safe: both return constant `0` accessors.
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* let btn!: HTMLButtonElement
|
|
18
|
+
* const { x, y } = createMagnetic(() => btn, { strength: 0.4 })
|
|
19
|
+
* <button ref={btn} style={{ transform: `translate(${x()}px, ${y()}px)` }}>
|
|
20
|
+
* Pull me
|
|
21
|
+
* </button>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function createMagnetic(ref, options = {}) {
|
|
25
|
+
if (typeof window === "undefined" || prefersReducedMotion()) {
|
|
26
|
+
return { x: zero, y: zero };
|
|
27
|
+
}
|
|
28
|
+
const { radius = 140, strength = 0.35, spring } = options;
|
|
29
|
+
const [targetX, setTargetX] = createSignal(0);
|
|
30
|
+
const [targetY, setTargetY] = createSignal(0);
|
|
31
|
+
const x = createSpring(targetX, spring);
|
|
32
|
+
const y = createSpring(targetY, spring);
|
|
33
|
+
const onMove = (event) => {
|
|
34
|
+
const el = ref();
|
|
35
|
+
if (!el) {
|
|
36
|
+
setTargetX(0);
|
|
37
|
+
setTargetY(0);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const rect = el.getBoundingClientRect();
|
|
41
|
+
const dx = event.clientX - (rect.left + rect.width / 2);
|
|
42
|
+
const dy = event.clientY - (rect.top + rect.height / 2);
|
|
43
|
+
const dist = Math.hypot(dx, dy);
|
|
44
|
+
if (dist >= radius) {
|
|
45
|
+
setTargetX(0);
|
|
46
|
+
setTargetY(0);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const falloff = 1 - dist / radius;
|
|
50
|
+
setTargetX(dx * strength * falloff);
|
|
51
|
+
setTargetY(dy * strength * falloff);
|
|
52
|
+
};
|
|
53
|
+
window.addEventListener("pointermove", onMove, { passive: true });
|
|
54
|
+
onCleanup(() => window.removeEventListener("pointermove", onMove));
|
|
55
|
+
return { x, y };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 3D tilt that follows the pointer across an element.
|
|
59
|
+
*
|
|
60
|
+
* The pointer's position over the element maps to `rotateX`/`rotateY` in
|
|
61
|
+
* degrees, spring-smoothed so the card leans with weight instead of
|
|
62
|
+
* jittering. When the pointer leaves, the element settles back to flat.
|
|
63
|
+
* Pair with a CSS `perspective` on the parent for real depth.
|
|
64
|
+
*
|
|
65
|
+
* Touch drags tilt while touching, release settles back to flat.
|
|
66
|
+
* SSR-safe and reduced-motion safe: both return constant `0` accessors.
|
|
67
|
+
*
|
|
68
|
+
* ```tsx
|
|
69
|
+
* let card!: HTMLDivElement
|
|
70
|
+
* const { rotateX, rotateY } = createTilt(() => card, { maxAngle: 12 })
|
|
71
|
+
* <div style={{ perspective: "800px" }}>
|
|
72
|
+
* <div
|
|
73
|
+
* ref={card}
|
|
74
|
+
* style={{
|
|
75
|
+
* transform: `rotateX(${rotateX()}deg) rotateY(${rotateY()}deg)`,
|
|
76
|
+
* }}
|
|
77
|
+
* />
|
|
78
|
+
* </div>
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function createTilt(ref, options = {}) {
|
|
82
|
+
if (typeof window === "undefined" || prefersReducedMotion()) {
|
|
83
|
+
return { rotateX: zero, rotateY: zero };
|
|
84
|
+
}
|
|
85
|
+
const { maxAngle = 10, spring } = options;
|
|
86
|
+
const [targetRX, setTargetRX] = createSignal(0);
|
|
87
|
+
const [targetRY, setTargetRY] = createSignal(0);
|
|
88
|
+
const rotateX = createSpring(targetRX, spring);
|
|
89
|
+
const rotateY = createSpring(targetRY, spring);
|
|
90
|
+
const onMove = (event) => {
|
|
91
|
+
const el = ref();
|
|
92
|
+
if (!el)
|
|
93
|
+
return;
|
|
94
|
+
const rect = el.getBoundingClientRect();
|
|
95
|
+
if (rect.width === 0 || rect.height === 0)
|
|
96
|
+
return;
|
|
97
|
+
if (event.clientX < rect.left ||
|
|
98
|
+
event.clientX > rect.right ||
|
|
99
|
+
event.clientY < rect.top ||
|
|
100
|
+
event.clientY > rect.bottom) {
|
|
101
|
+
return; // outside: pointerleave resets the tilt
|
|
102
|
+
}
|
|
103
|
+
const px = (event.clientX - rect.left) / rect.width - 0.5;
|
|
104
|
+
const py = (event.clientY - rect.top) / rect.height - 0.5;
|
|
105
|
+
setTargetRY(px * 2 * maxAngle);
|
|
106
|
+
setTargetRX(-py * 2 * maxAngle);
|
|
107
|
+
};
|
|
108
|
+
const onLeave = () => {
|
|
109
|
+
setTargetRX(0);
|
|
110
|
+
setTargetRY(0);
|
|
111
|
+
};
|
|
112
|
+
window.addEventListener("pointermove", onMove, { passive: true });
|
|
113
|
+
// Late-bound refs (Solid assigns `ref` after mount) still get the reset.
|
|
114
|
+
createEffect(() => {
|
|
115
|
+
const el = ref();
|
|
116
|
+
if (!el)
|
|
117
|
+
return;
|
|
118
|
+
el.addEventListener("pointerleave", onLeave);
|
|
119
|
+
onCleanup(() => el.removeEventListener("pointerleave", onLeave));
|
|
120
|
+
});
|
|
121
|
+
onCleanup(() => window.removeEventListener("pointermove", onMove));
|
|
122
|
+
return { rotateX, rotateY };
|
|
123
|
+
}
|