solid-drift 0.17.0 → 0.19.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 CHANGED
@@ -345,6 +345,36 @@ const { rotateX, rotateY } = createTilt(() => card, { maxAngle: 12 })
345
345
 
346
346
  Returns `{ rotateX, rotateY }`: spring-smoothed tilt in degrees. SSR-safe and reduced-motion safe: both return constant `0` accessors.
347
347
 
348
+ ### `createTiltCard(ref, options?)`
349
+
350
+ Holographic trading-card tilt: 3D lean plus every signal a holo foil needs. Beyond `createTilt`'s rotation, this tracks `glareX`/`glareY` (pointer position 0..1, for a radial glare overlay), `holoAngle` (a rainbow angle that sweeps with the pointer, for a gradient foil overlay), `shine` (0..1 overlay intensity that fades in on hover and out on leave), `scale` (hover pop), `hovering`, and a ready-made `transform()` string (perspective, rotateX/rotateY, scale). SSR-safe and reduced-motion safe: static constants, no tilt, no shine. Touch drags tilt while touching, release settles back.
351
+
352
+ ```tsx
353
+ import { createTiltCard } from "solid-drift"
354
+
355
+ let card!: HTMLDivElement
356
+ const c = createTiltCard(() => card, { maxAngle: 14 })
357
+ <div style={{ transform: c.transform() }} ref={card}>
358
+ {art}
359
+ <div style={{
360
+ background: `radial-gradient(circle at ${c.glareX() * 100}% ${c.glareY() * 100}%, rgba(255,255,255,0.6), transparent 60%)`,
361
+ opacity: c.shine(),
362
+ }} />
363
+ <div style={{
364
+ background: `linear-gradient(${c.holoAngle()}deg, #ff0080, #ff8000, #ffff00, #00ff80, #0080ff, #8000ff)`,
365
+ "mix-blend-mode": "color-dodge",
366
+ opacity: c.shine() * 0.55,
367
+ }} />
368
+ </div>
369
+ ```
370
+
371
+ | Option | Default | Description |
372
+ | ------------- | ------- | ------------------------------------ |
373
+ | `maxAngle` | `12` | Maximum tilt in degrees at the edge |
374
+ | `scale` | `1.04` | Scale while hovering |
375
+ | `perspective` | `900` | Perspective distance in px |
376
+ | `spring` | default | Spring physics for tilt, shine, pop |
377
+
348
378
  ### `createDrag(ref, options?)`
349
379
 
350
380
  Pointer drag with spring physics, constraints, and momentum. The gesture workhorse: draggable cards, sliders, bottom-sheet handles, sortable rows. While the pointer is down the element tracks it 1:1; on release it glides with inertia and springs into its constraints, stretching elastically past the edges while dragged. Set `touch-action: none` on the draggable element so touch drags do not fight the page scroll. For the physics-toy flavor (exponential friction plus bouncing off walls), see `createFling` instead.
@@ -1074,6 +1104,28 @@ const reveal = createMintReveal(() => card, {
1074
1104
 
1075
1105
  Returns `{ play, reset, status }`. `status()` walks `"idle"`, `"anticipating"`, `"flipping"`, `"revealed"`. Under reduced motion (and on the server) `play()` applies the revealed state immediately and still calls `onFlip`.
1076
1106
 
1107
+ ### `createDepixelate(image, canvas, options?)`
1108
+
1109
+ Pixel-to-sharp image reveal, the classic NFT mint ceremony. An image renders into a canvas fully pixelated, then resolves to sharp in discrete chunky steps on the shared animation clock. Owns the canvas drawing: give it an image and a canvas, call `play()` when the art should reveal. The pixelated teaser frame paints itself as soon as the image loads, so the pre-reveal state needs no manual setup. Pair with `createMintReveal` for the full ceremony: flip the card, depixelate the art.
1110
+
1111
+ ```tsx
1112
+ let img!: HTMLImageElement
1113
+ let cvs!: HTMLCanvasElement
1114
+ const reveal = createDepixelate(() => img, () => cvs, { duration: 1800 })
1115
+ <img ref={img} src={artUrl} style={{ display: "none" }} />
1116
+ <canvas ref={cvs} />
1117
+ <button onClick={() => reveal.play()}>Reveal</button>
1118
+ ```
1119
+
1120
+ | Option | Default | Description |
1121
+ | ------------ | ----------------- | ---------------------------------------------- |
1122
+ | `levels` | `10` | Discrete pixelation steps from blocky to sharp |
1123
+ | `duration` | `1600` | Full reveal duration in ms |
1124
+ | `easing` | `"easeInOutCubic"`| Easing for the reveal progress |
1125
+ | `onComplete` | none | Called when the reveal reaches sharp |
1126
+
1127
+ Returns `{ pixelSize, progress, status, play, complete, reset, stop }`. `status()` walks `"idle"`, `"revealing"`, `"revealed"`; `pixelSize()` is the current block size in px (1 means sharp). `stop()` halts mid-reveal and resolves the pending `play()` promise; `reset()` repaints the teaser; `complete()` jumps to sharp. Under reduced motion (and on the server) the art is sharp immediately.
1128
+
1077
1129
  ### `createConnectButton(ref, options?)`
1078
1130
 
1079
1131
  Wallet connect button micro-interactions: magnetic pull toward the pointer, a press scale, an animated check overlay for copy-address feedback, and a chain pulse ring. Pointer handling is global (presses that start inside still count if released outside), and everything cleans up on unmount.
@@ -0,0 +1,57 @@
1
+ import { type Accessor } from "solid-js";
2
+ import { type Easing, type EasingName } from "./easing.js";
3
+ /**
4
+ * Pixel-to-sharp image reveal, the classic NFT mint ceremony.
5
+ *
6
+ * An image renders into a canvas fully pixelated, then resolves to
7
+ * sharp in discrete chunky steps. Owns the canvas drawing: give it
8
+ * an image and a canvas, call `play()` when the art should reveal.
9
+ *
10
+ * ```tsx
11
+ * let img!: HTMLImageElement
12
+ * let cvs!: HTMLCanvasElement
13
+ * const reveal = createDepixelate(() => img, () => cvs, { duration: 1800 })
14
+ * <img ref={img} src={artUrl} style={{ display: "none" }} />
15
+ * <canvas ref={cvs} />
16
+ * <button onClick={() => reveal.play()}>Reveal</button>
17
+ * ```
18
+ *
19
+ * The pixelated teaser frame paints itself as soon as the image
20
+ * loads, so the pre-reveal state needs no manual setup. Call `play()`
21
+ * after the image has loaded; if the image or canvas is missing,
22
+ * `play()` resolves immediately as revealed.
23
+ *
24
+ * SSR-safe: `status()` starts `"revealed"` and `play()` resolves
25
+ * immediately. Reduced-motion safe: `play()` and `complete()` jump
26
+ * straight to sharp.
27
+ */
28
+ export type DepixelateStatus = "idle" | "revealing" | "revealed";
29
+ export interface DepixelateOptions {
30
+ /**
31
+ * Discrete pixelation steps from blocky to sharp. Default 10.
32
+ * More steps make a smoother, longer-feeling resolve.
33
+ */
34
+ levels?: number;
35
+ /** Full reveal duration in ms. Default 1600. */
36
+ duration?: number;
37
+ /** Easing for the reveal progress. Default "easeInOutCubic". */
38
+ easing?: Easing | EasingName;
39
+ /** Called when the reveal reaches sharp. */
40
+ onComplete?: () => void;
41
+ }
42
+ export interface DepixelateControls {
43
+ /** Current pixel block size in px. 1 (or 0) means fully sharp. */
44
+ pixelSize: Accessor<number>;
45
+ /** Reveal progress 0..1. */
46
+ progress: Accessor<number>;
47
+ status: Accessor<DepixelateStatus>;
48
+ /** Start (or restart) the reveal. Resolves when sharp or stopped. */
49
+ play: () => Promise<void>;
50
+ /** Jump straight to sharp. */
51
+ complete: () => void;
52
+ /** Back to the fully pixelated teaser frame. */
53
+ reset: () => void;
54
+ /** Stop the reveal where it is. */
55
+ stop: () => void;
56
+ }
57
+ export declare function createDepixelate(image: () => HTMLImageElement | null | undefined, canvas: () => HTMLCanvasElement | null | undefined, options?: DepixelateOptions): DepixelateControls;
@@ -0,0 +1,190 @@
1
+ import { createEffect, createSignal, onCleanup, } from "solid-js";
2
+ import { now, schedule } from "./engine.js";
3
+ import { resolveEasing } from "./easing.js";
4
+ import { prefersReducedMotion } from "./reduced-motion.js";
5
+ export function createDepixelate(image, canvas, options = {}) {
6
+ const server = typeof window === "undefined";
7
+ const { levels = 10, duration = 1600, easing: easingOpt = "easeInOutCubic", onComplete, } = options;
8
+ const easing = resolveEasing(easingOpt);
9
+ const steps = Math.max(1, Math.round(levels));
10
+ const [pixelSize, setPixelSize] = createSignal(1);
11
+ const [progress, setProgress] = createSignal(0);
12
+ const [status, setStatus] = createSignal(server ? "revealed" : "idle");
13
+ let off = null;
14
+ const scratch = () => {
15
+ if (off || typeof document === "undefined")
16
+ return off;
17
+ off = document.createElement("canvas");
18
+ return off;
19
+ };
20
+ /** Largest block size: chunky enough to hide the art, small enough to hint at it. */
21
+ const startBlock = (img) => Math.max(8, Math.floor(Math.min(img.naturalWidth, img.naturalHeight) / 12));
22
+ const blockFor = (step, start) => {
23
+ if (step >= steps - 1)
24
+ return 1;
25
+ const t = 1 - step / Math.max(1, steps - 1);
26
+ return Math.max(2, Math.round(start * t * t));
27
+ };
28
+ const draw = (block) => {
29
+ const img = image();
30
+ const cvs = canvas();
31
+ if (!img || !cvs)
32
+ return;
33
+ if (!img.complete || img.naturalWidth === 0)
34
+ return;
35
+ const w = img.naturalWidth;
36
+ const h = img.naturalHeight;
37
+ if (cvs.width !== w || cvs.height !== h) {
38
+ cvs.width = w;
39
+ cvs.height = h;
40
+ }
41
+ const ctx = cvs.getContext("2d");
42
+ if (!ctx)
43
+ return;
44
+ if (block <= 1) {
45
+ ctx.imageSmoothingEnabled = true;
46
+ ctx.clearRect(0, 0, w, h);
47
+ ctx.drawImage(img, 0, 0, w, h);
48
+ return;
49
+ }
50
+ const buf = scratch();
51
+ if (!buf)
52
+ return;
53
+ const sw = Math.max(1, Math.round(w / block));
54
+ const sh = Math.max(1, Math.round(h / block));
55
+ buf.width = sw;
56
+ buf.height = sh;
57
+ const bctx = buf.getContext("2d");
58
+ if (!bctx)
59
+ return;
60
+ bctx.imageSmoothingEnabled = true;
61
+ bctx.drawImage(img, 0, 0, sw, sh);
62
+ ctx.imageSmoothingEnabled = false;
63
+ ctx.clearRect(0, 0, w, h);
64
+ ctx.drawImage(buf, 0, 0, sw, sh, 0, 0, w, h);
65
+ };
66
+ const paintTeaser = () => {
67
+ const img = image();
68
+ if (!img)
69
+ return;
70
+ const block = startBlock(img);
71
+ draw(block);
72
+ setPixelSize(block);
73
+ setProgress(0);
74
+ };
75
+ const paintSharp = () => {
76
+ draw(1);
77
+ setPixelSize(1);
78
+ setProgress(1);
79
+ };
80
+ // Paint the teaser frame as soon as the image is ready, so the
81
+ // pre-reveal state works with no manual setup. Under reduced motion
82
+ // the teaser is the sharp art, since the reveal itself is skipped.
83
+ if (!server) {
84
+ createEffect(() => {
85
+ const img = image();
86
+ if (!img || !img.complete || img.naturalWidth === 0)
87
+ return;
88
+ if (!canvas())
89
+ return;
90
+ if (status() !== "idle")
91
+ return;
92
+ if (prefersReducedMotion()) {
93
+ paintSharp();
94
+ }
95
+ else {
96
+ paintTeaser();
97
+ }
98
+ });
99
+ }
100
+ let cancelTask = null;
101
+ let finish = null;
102
+ // Creation-time owner: cancel any in-flight reveal on unmount so a
103
+ // disposed component never keeps drawing into a dead canvas.
104
+ if (!server) {
105
+ onCleanup(() => {
106
+ cancelTask?.();
107
+ cancelTask = null;
108
+ finish?.();
109
+ finish = null;
110
+ });
111
+ }
112
+ const settle = (final) => {
113
+ cancelTask?.();
114
+ cancelTask = null;
115
+ setStatus(final);
116
+ finish?.();
117
+ finish = null;
118
+ };
119
+ const play = () => {
120
+ cancelTask?.();
121
+ cancelTask = null;
122
+ finish?.();
123
+ finish = null;
124
+ if (server) {
125
+ setStatus("revealed");
126
+ return Promise.resolve();
127
+ }
128
+ const img = image();
129
+ const cvs = canvas();
130
+ if (!img || !cvs || !img.complete || img.naturalWidth === 0) {
131
+ setStatus("revealed");
132
+ return Promise.resolve();
133
+ }
134
+ if (prefersReducedMotion() || duration <= 0) {
135
+ paintSharp();
136
+ setStatus("revealed");
137
+ onComplete?.();
138
+ return Promise.resolve();
139
+ }
140
+ const start = startBlock(img);
141
+ paintTeaser();
142
+ setStatus("revealing");
143
+ const t0 = now();
144
+ let resolvePromise;
145
+ const done = new Promise((resolve) => {
146
+ resolvePromise = resolve;
147
+ });
148
+ finish = resolvePromise;
149
+ cancelTask = schedule((t) => {
150
+ const p = Math.min(1, (t - t0) / duration);
151
+ const eased = easing(p);
152
+ const step = Math.min(steps - 1, Math.floor(eased * steps));
153
+ const block = blockFor(step, start);
154
+ draw(block);
155
+ setPixelSize(block);
156
+ setProgress(eased);
157
+ if (p >= 1) {
158
+ paintSharp();
159
+ settle("revealed");
160
+ onComplete?.();
161
+ return false;
162
+ }
163
+ return true;
164
+ });
165
+ return done;
166
+ };
167
+ const stop = () => {
168
+ if (status() === "revealing") {
169
+ settle("idle");
170
+ }
171
+ };
172
+ const complete = () => {
173
+ cancelTask?.();
174
+ cancelTask = null;
175
+ finish?.();
176
+ finish = null;
177
+ paintSharp();
178
+ setStatus("revealed");
179
+ onComplete?.();
180
+ };
181
+ const reset = () => {
182
+ cancelTask?.();
183
+ cancelTask = null;
184
+ finish?.();
185
+ finish = null;
186
+ paintTeaser();
187
+ setStatus("idle");
188
+ };
189
+ return { pixelSize, progress, status, play, complete, reset, stop };
190
+ }
package/dist/index.d.ts CHANGED
@@ -18,8 +18,9 @@ export { createHorizontalScroll, type HorizontalScrollOptions, type HorizontalSc
18
18
  export { createScrub, type ScrubKeyframe, type ScrubOptions } from "./scrub.js";
19
19
  export { createScrollColor, type ScrollColorStop, type ScrollColorOptions, type ScrollColorFormat, createScrollTracking, type ScrollTrackingOptions, createScrollLine, type ScrollLineOptions, type ScrollLineStyle, type ScrollLineAxis, type ScrollLineOrigin, } from "./scrollfx.js";
20
20
  export { createVelocity, type VelocityOptions } from "./velocity.js";
21
- export { createMagnetic, type MagneticOptions, type MagneticResult, createTilt, type TiltOptions, type TiltResult, } from "./pointer.js";
21
+ export { createMagnetic, type MagneticOptions, type MagneticResult, createTilt, type TiltOptions, type TiltResult, createTiltCard, type TiltCardOptions, type TiltCardResult, } from "./pointer.js";
22
22
  export { createTrail, type TrailOptions } from "./trail.js";
23
+ export { createDepixelate, type DepixelateOptions, type DepixelateStatus, type DepixelateControls, } from "./depixelate.js";
23
24
  export { createTimeline, type TimelineStep, type TimelineStatus, type TimelineControls, } from "./timeline.js";
24
25
  export { animateFlip, type FlipOptions, createSharedLayout, type SharedLayoutOptions, type SharedLayoutResult, } from "./flip.js";
25
26
  export { createToast, type Toast, type ToastControls, type ToastKind, type ToastOptions, type ToastQueueOptions, type ToastState, } from "./toast.js";
package/dist/index.js CHANGED
@@ -18,8 +18,9 @@ export { createHorizontalScroll, } from "./horizontal.js";
18
18
  export { createScrub } from "./scrub.js";
19
19
  export { createScrollColor, createScrollTracking, createScrollLine, } from "./scrollfx.js";
20
20
  export { createVelocity } from "./velocity.js";
21
- export { createMagnetic, createTilt, } from "./pointer.js";
21
+ export { createMagnetic, createTilt, createTiltCard, } from "./pointer.js";
22
22
  export { createTrail } from "./trail.js";
23
+ export { createDepixelate, } from "./depixelate.js";
23
24
  export { createTimeline, } from "./timeline.js";
24
25
  export { animateFlip, createSharedLayout, } from "./flip.js";
25
26
  export { createToast, } from "./toast.js";
package/dist/pointer.d.ts CHANGED
@@ -74,3 +74,64 @@ export declare function createMagnetic(ref: () => Element | null | undefined, op
74
74
  * ```
75
75
  */
76
76
  export declare function createTilt(ref: () => Element | null | undefined, options?: TiltOptions): TiltResult;
77
+ export interface TiltCardOptions {
78
+ /** Max tilt angle in degrees. Default 12. */
79
+ maxAngle?: number;
80
+ /** Scale while hovering. Default 1.04. */
81
+ scale?: number;
82
+ /** Perspective distance in px for the transform. Default 900. */
83
+ perspective?: number;
84
+ /** Spring options for tilt, shine, and scale. */
85
+ spring?: SpringOptions;
86
+ }
87
+ export interface TiltCardResult {
88
+ /** Spring-smoothed rotateX in degrees. */
89
+ rotateX: Accessor<number>;
90
+ /** Spring-smoothed rotateY in degrees. */
91
+ rotateY: Accessor<number>;
92
+ /** Pointer position 0..1 across the card, for glare placement. */
93
+ glareX: Accessor<number>;
94
+ /** Pointer position 0..1 down the card, for glare placement. */
95
+ glareY: Accessor<number>;
96
+ /** Rainbow angle in degrees, follows the pointer. Spring-smoothed. */
97
+ holoAngle: Accessor<number>;
98
+ /** 0..1 shine intensity, spring-smoothed, 0 when not hovering. */
99
+ shine: Accessor<number>;
100
+ /** Spring-smoothed scale. */
101
+ scale: Accessor<number>;
102
+ /** Whether the pointer is over the card. */
103
+ hovering: Accessor<boolean>;
104
+ /** Ready-made transform: perspective, rotateX/rotateY, scale. */
105
+ transform: Accessor<string>;
106
+ }
107
+ /**
108
+ * Holographic trading-card tilt: 3D lean plus the signals a holo
109
+ * foil needs.
110
+ *
111
+ * Beyond `createTilt`'s spring-smoothed rotation, this tracks the
112
+ * pointer as `glareX`/`glareY` (0..1, for a radial glare overlay),
113
+ * `holoAngle` (a rainbow angle that sweeps with the pointer, for a
114
+ * gradient foil overlay), `shine` (0..1 overlay intensity that fades
115
+ * in on hover and out on leave), and a hover `scale` pop. Bind them
116
+ * to two absolutely-positioned overlays over your card art:
117
+ *
118
+ * ```tsx
119
+ * const card = createTiltCard(() => el, { maxAngle: 14 });
120
+ * <div style={{ transform: card.transform() }}>
121
+ * {art}
122
+ * <div style={{
123
+ * background: `radial-gradient(circle at ${card.glareX() * 100}% ${card.glareY() * 100}%, rgba(255,255,255,0.6), transparent 60%)`,
124
+ * opacity: card.shine(),
125
+ * }} />
126
+ * <div style={{
127
+ * background: `linear-gradient(${card.holoAngle()}deg, #ff0080, #ff8000, #ffff00, #00ff80, #0080ff, #8000ff)`,
128
+ * "mix-blend-mode": "color-dodge",
129
+ * opacity: card.shine() * 0.55,
130
+ * }} />
131
+ * </div>
132
+ * ```
133
+ *
134
+ * SSR-safe and reduced-motion safe: static constants (no tilt, no
135
+ * shine). Touch drags tilt while touching, release settles back.
136
+ */
137
+ export declare function createTiltCard(ref: () => Element | null | undefined, options?: TiltCardOptions): TiltCardResult;
package/dist/pointer.js CHANGED
@@ -121,3 +121,121 @@ export function createTilt(ref, options = {}) {
121
121
  onCleanup(() => window.removeEventListener("pointermove", onMove));
122
122
  return { rotateX, rotateY };
123
123
  }
124
+ /**
125
+ * Holographic trading-card tilt: 3D lean plus the signals a holo
126
+ * foil needs.
127
+ *
128
+ * Beyond `createTilt`'s spring-smoothed rotation, this tracks the
129
+ * pointer as `glareX`/`glareY` (0..1, for a radial glare overlay),
130
+ * `holoAngle` (a rainbow angle that sweeps with the pointer, for a
131
+ * gradient foil overlay), `shine` (0..1 overlay intensity that fades
132
+ * in on hover and out on leave), and a hover `scale` pop. Bind them
133
+ * to two absolutely-positioned overlays over your card art:
134
+ *
135
+ * ```tsx
136
+ * const card = createTiltCard(() => el, { maxAngle: 14 });
137
+ * <div style={{ transform: card.transform() }}>
138
+ * {art}
139
+ * <div style={{
140
+ * background: `radial-gradient(circle at ${card.glareX() * 100}% ${card.glareY() * 100}%, rgba(255,255,255,0.6), transparent 60%)`,
141
+ * opacity: card.shine(),
142
+ * }} />
143
+ * <div style={{
144
+ * background: `linear-gradient(${card.holoAngle()}deg, #ff0080, #ff8000, #ffff00, #00ff80, #0080ff, #8000ff)`,
145
+ * "mix-blend-mode": "color-dodge",
146
+ * opacity: card.shine() * 0.55,
147
+ * }} />
148
+ * </div>
149
+ * ```
150
+ *
151
+ * SSR-safe and reduced-motion safe: static constants (no tilt, no
152
+ * shine). Touch drags tilt while touching, release settles back.
153
+ */
154
+ export function createTiltCard(ref, options = {}) {
155
+ const half = () => 0.5;
156
+ const one = () => 1;
157
+ const no = () => false;
158
+ if (typeof window === "undefined" || prefersReducedMotion()) {
159
+ const p = options.perspective ?? 900;
160
+ return {
161
+ rotateX: zero,
162
+ rotateY: zero,
163
+ glareX: half,
164
+ glareY: half,
165
+ holoAngle: zero,
166
+ shine: zero,
167
+ scale: one,
168
+ hovering: no,
169
+ transform: () => `perspective(${p}px) rotateX(0deg) rotateY(0deg) scale(1)`,
170
+ };
171
+ }
172
+ const { maxAngle = 12, scale: hoverScale = 1.04, perspective = 900, spring, } = options;
173
+ const [targetRX, setTargetRX] = createSignal(0);
174
+ const [targetRY, setTargetRY] = createSignal(0);
175
+ const [targetHolo, setTargetHolo] = createSignal(0);
176
+ const [targetShine, setTargetShine] = createSignal(0);
177
+ const [targetScale, setTargetScale] = createSignal(1);
178
+ const [glareX, setGlareX] = createSignal(0.5);
179
+ const [glareY, setGlareY] = createSignal(0.5);
180
+ const [hovering, setHovering] = createSignal(false);
181
+ const rotateX = createSpring(targetRX, spring);
182
+ const rotateY = createSpring(targetRY, spring);
183
+ const holoAngle = createSpring(targetHolo, spring);
184
+ const shine = createSpring(targetShine, spring);
185
+ const scale = createSpring(targetScale, spring);
186
+ const onMove = (event) => {
187
+ const el = ref();
188
+ if (!el)
189
+ return;
190
+ const rect = el.getBoundingClientRect();
191
+ if (rect.width === 0 || rect.height === 0)
192
+ return;
193
+ if (event.clientX < rect.left ||
194
+ event.clientX > rect.right ||
195
+ event.clientY < rect.top ||
196
+ event.clientY > rect.bottom) {
197
+ return; // outside: pointerleave resets the card
198
+ }
199
+ const px = (event.clientX - rect.left) / rect.width;
200
+ const py = (event.clientY - rect.top) / rect.height;
201
+ const cx = px - 0.5;
202
+ const cy = py - 0.5;
203
+ setTargetRY(cx * 2 * maxAngle);
204
+ setTargetRX(-cy * 2 * maxAngle);
205
+ setGlareX(px);
206
+ setGlareY(py);
207
+ setTargetHolo((cx + cy) * 180);
208
+ setTargetShine(1);
209
+ setTargetScale(hoverScale);
210
+ setHovering(true);
211
+ };
212
+ const onLeave = () => {
213
+ setTargetRX(0);
214
+ setTargetRY(0);
215
+ setTargetHolo(0);
216
+ setTargetShine(0);
217
+ setTargetScale(1);
218
+ setHovering(false);
219
+ };
220
+ window.addEventListener("pointermove", onMove, { passive: true });
221
+ // Late-bound refs (Solid assigns `ref` after mount) still get the reset.
222
+ createEffect(() => {
223
+ const el = ref();
224
+ if (!el)
225
+ return;
226
+ el.addEventListener("pointerleave", onLeave);
227
+ onCleanup(() => el.removeEventListener("pointerleave", onLeave));
228
+ });
229
+ onCleanup(() => window.removeEventListener("pointermove", onMove));
230
+ return {
231
+ rotateX,
232
+ rotateY,
233
+ glareX,
234
+ glareY,
235
+ holoAngle,
236
+ shine,
237
+ scale,
238
+ hovering,
239
+ transform: () => `perspective(${perspective}px) rotateX(${rotateX()}deg) rotateY(${rotateY()}deg) scale(${scale()})`,
240
+ };
241
+ }
package/package.json CHANGED
@@ -43,5 +43,5 @@
43
43
  },
44
44
  "type": "module",
45
45
  "types": "./dist/index.d.ts",
46
- "version": "0.17.0"
46
+ "version": "0.19.0"
47
47
  }