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 +52 -0
- package/dist/depixelate.d.ts +57 -0
- package/dist/depixelate.js +190 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/pointer.d.ts +61 -0
- package/dist/pointer.js +118 -0
- package/package.json +1 -1
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