solid-drift 0.18.0 → 0.20.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 +48 -0
- package/dist/depixelate.d.ts +57 -0
- package/dist/depixelate.js +190 -0
- package/dist/gesture.d.ts +82 -0
- package/dist/gesture.js +107 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -412,6 +412,32 @@ const { x, y, status } = createDrag(() => card, {
|
|
|
412
412
|
|
|
413
413
|
Returns `{ x, y, status }`: the drag offset in pixels and `status` (`"idle"`, `"dragging"`, `"settling"`). SSR-safe: everything rests at 0. Under reduced motion the drag still tracks the pointer (direct manipulation is not animation) but release snaps instantly to the constrained target with no glide.
|
|
414
414
|
|
|
415
|
+
### `createSwipe(ref, options?)`
|
|
416
|
+
|
|
417
|
+
Touch swipe gesture recognition: swipe-to-dismiss, carousels. While `createDrag` tracks the pointer continuously, `createSwipe` makes the discrete decision: was that gesture a swipe, and which way? On pointerup it compares travel, duration, and velocity against the thresholds and fires the matching callbacks plus the `lastSwipe` signal. Pointer Events give touch parity for free: mouse, touch, and pen run through the same path. For touch, set `touch-action: pan-y` on a horizontal swipe surface (or `pan-x` for vertical) so the browser does not hijack the gesture; use `none` when recognizing both axes.
|
|
418
|
+
|
|
419
|
+
```tsx
|
|
420
|
+
import { createSwipe } from "solid-drift"
|
|
421
|
+
|
|
422
|
+
let deck!: HTMLDivElement
|
|
423
|
+
const { lastSwipe } = createSwipe(() => deck, {
|
|
424
|
+
onSwipeLeft: () => dismiss(),
|
|
425
|
+
onSwipeRight: () => keep(),
|
|
426
|
+
})
|
|
427
|
+
<div ref={deck} style={{ "touch-action": "pan-y" }}>card</div>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
| Option | Default | Description |
|
|
431
|
+
| -------------- | ------- | ------------------------------------------------------------------------ |
|
|
432
|
+
| `threshold` | `48` | Minimum travel in px to count as a swipe |
|
|
433
|
+
| `maxDuration` | `800` | Maximum gesture duration in ms |
|
|
434
|
+
| `minVelocity` | `0.4` | Minimum velocity in px/ms; a fast flick below `threshold` still counts |
|
|
435
|
+
| `axis` | `"both"`| `"x"`, `"y"`, or `"both"` |
|
|
436
|
+
| `onSwipe` | none | Called for every recognized swipe with `{ direction, distance, velocity, duration, from, to }` |
|
|
437
|
+
| `onSwipeLeft` / `onSwipeRight` / `onSwipeUp` / `onSwipeDown` | none | Per-direction callbacks |
|
|
438
|
+
|
|
439
|
+
Returns `{ lastSwipe, reset }`. A swipe counts when travel passes `threshold` inside `maxDuration`, or velocity passes `minVelocity`; slow long drags are not swipes. Recognition is not animation, so it works identically under reduced motion; the host decides how to animate the response. SSR-safe: `lastSwipe()` stays null and callbacks never fire.
|
|
440
|
+
|
|
415
441
|
### `createTrail(source, options?)`
|
|
416
442
|
|
|
417
443
|
A signal that replays another signal's past: it returns the value the source had `delay` milliseconds ago, interpolated between samples. Chain trails off one source for follower effects (a cursor with a comet tail, cascading highlights), or trail a scroll progress for a delayed echo of the page. The trail catches up and parks exactly on the latest value when the source rests. The follow loop runs only while the trail is behind.
|
|
@@ -1104,6 +1130,28 @@ const reveal = createMintReveal(() => card, {
|
|
|
1104
1130
|
|
|
1105
1131
|
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`.
|
|
1106
1132
|
|
|
1133
|
+
### `createDepixelate(image, canvas, options?)`
|
|
1134
|
+
|
|
1135
|
+
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.
|
|
1136
|
+
|
|
1137
|
+
```tsx
|
|
1138
|
+
let img!: HTMLImageElement
|
|
1139
|
+
let cvs!: HTMLCanvasElement
|
|
1140
|
+
const reveal = createDepixelate(() => img, () => cvs, { duration: 1800 })
|
|
1141
|
+
<img ref={img} src={artUrl} style={{ display: "none" }} />
|
|
1142
|
+
<canvas ref={cvs} />
|
|
1143
|
+
<button onClick={() => reveal.play()}>Reveal</button>
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
| Option | Default | Description |
|
|
1147
|
+
| ------------ | ----------------- | ---------------------------------------------- |
|
|
1148
|
+
| `levels` | `10` | Discrete pixelation steps from blocky to sharp |
|
|
1149
|
+
| `duration` | `1600` | Full reveal duration in ms |
|
|
1150
|
+
| `easing` | `"easeInOutCubic"`| Easing for the reveal progress |
|
|
1151
|
+
| `onComplete` | none | Called when the reveal reaches sharp |
|
|
1152
|
+
|
|
1153
|
+
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.
|
|
1154
|
+
|
|
1107
1155
|
### `createConnectButton(ref, options?)`
|
|
1108
1156
|
|
|
1109
1157
|
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/gesture.d.ts
CHANGED
|
@@ -105,4 +105,86 @@ export interface DragControls {
|
|
|
105
105
|
* ```
|
|
106
106
|
*/
|
|
107
107
|
export declare function createDrag(ref: MaybeElement, options?: DragOptions): DragControls;
|
|
108
|
+
/** Cardinal direction of a recognized swipe. */
|
|
109
|
+
export type SwipeDirection = "left" | "right" | "up" | "down";
|
|
110
|
+
/** Snapshot delivered for a recognized swipe. */
|
|
111
|
+
export interface SwipeDetails {
|
|
112
|
+
direction: SwipeDirection;
|
|
113
|
+
/** Primary-axis travel in px. Always positive. */
|
|
114
|
+
distance: number;
|
|
115
|
+
/** Primary-axis velocity in px/ms. Always positive. */
|
|
116
|
+
velocity: number;
|
|
117
|
+
/** Gesture duration in ms. */
|
|
118
|
+
duration: number;
|
|
119
|
+
/** Pointer path endpoints in client pixels. */
|
|
120
|
+
from: {
|
|
121
|
+
x: number;
|
|
122
|
+
y: number;
|
|
123
|
+
};
|
|
124
|
+
to: {
|
|
125
|
+
x: number;
|
|
126
|
+
y: number;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export interface SwipeOptions {
|
|
130
|
+
/** Minimum travel in px to count as a swipe. Default 48. */
|
|
131
|
+
threshold?: number;
|
|
132
|
+
/** Maximum gesture duration in ms. Default 800. */
|
|
133
|
+
maxDuration?: number;
|
|
134
|
+
/**
|
|
135
|
+
* Minimum primary-axis velocity in px/ms. A fast flick shorter than
|
|
136
|
+
* `threshold` still counts as a swipe. Default 0.4.
|
|
137
|
+
*/
|
|
138
|
+
minVelocity?: number;
|
|
139
|
+
/** Lock recognition to an axis. Default "both". */
|
|
140
|
+
axis?: DragAxis;
|
|
141
|
+
/** Called for every recognized swipe. */
|
|
142
|
+
onSwipe?: (details: SwipeDetails) => void;
|
|
143
|
+
/** Called for a leftward swipe. */
|
|
144
|
+
onSwipeLeft?: (details: SwipeDetails) => void;
|
|
145
|
+
/** Called for a rightward swipe. */
|
|
146
|
+
onSwipeRight?: (details: SwipeDetails) => void;
|
|
147
|
+
/** Called for an upward swipe. */
|
|
148
|
+
onSwipeUp?: (details: SwipeDetails) => void;
|
|
149
|
+
/** Called for a downward swipe. */
|
|
150
|
+
onSwipeDown?: (details: SwipeDetails) => void;
|
|
151
|
+
}
|
|
152
|
+
export interface SwipeControls {
|
|
153
|
+
/** The most recent recognized swipe, or null if none yet. */
|
|
154
|
+
lastSwipe: Accessor<SwipeDetails | null>;
|
|
155
|
+
/** Clear the last swipe. */
|
|
156
|
+
reset: () => void;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Touch swipe gesture recognition: swipe-to-dismiss, carousels.
|
|
160
|
+
*
|
|
161
|
+
* While `createDrag` tracks the pointer continuously, `createSwipe`
|
|
162
|
+
* makes the discrete decision: was that gesture a swipe, and which
|
|
163
|
+
* way? On pointerup it compares travel, duration, and velocity
|
|
164
|
+
* against the thresholds and fires the matching callbacks plus the
|
|
165
|
+
* `lastSwipe` signal.
|
|
166
|
+
*
|
|
167
|
+
* Pointer Events give touch parity for free: mouse, touch, and pen
|
|
168
|
+
* run through the same path. For touch, set `touch-action: pan-y` on
|
|
169
|
+
* a horizontal swipe surface (or `pan-x` for vertical) so the browser
|
|
170
|
+
* does not hijack the gesture; use `none` when recognizing both axes.
|
|
171
|
+
*
|
|
172
|
+
* A swipe counts when travel passes `threshold` inside `maxDuration`,
|
|
173
|
+
* or when velocity passes `minVelocity` (a fast flick). Slow long
|
|
174
|
+
* drags are not swipes; they belong to `createDrag`.
|
|
175
|
+
*
|
|
176
|
+
* Recognition is not animation, so it works identically under
|
|
177
|
+
* reduced motion; the host decides how to animate the response.
|
|
178
|
+
* SSR-safe: `lastSwipe()` stays null and callbacks never fire.
|
|
179
|
+
*
|
|
180
|
+
* ```tsx
|
|
181
|
+
* let deck!: HTMLDivElement
|
|
182
|
+
* const { lastSwipe } = createSwipe(() => deck, {
|
|
183
|
+
* onSwipeLeft: () => dismiss(),
|
|
184
|
+
* onSwipeRight: () => keep(),
|
|
185
|
+
* })
|
|
186
|
+
* <div ref={deck} style={{ "touch-action": "pan-y" }}>card</div>
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export declare function createSwipe(ref: MaybeElement, options?: SwipeOptions): SwipeControls;
|
|
108
190
|
export {};
|
package/dist/gesture.js
CHANGED
|
@@ -205,3 +205,110 @@ export function createDrag(ref, options = {}) {
|
|
|
205
205
|
onCleanup(() => stopSettle());
|
|
206
206
|
return { x, y, status };
|
|
207
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Touch swipe gesture recognition: swipe-to-dismiss, carousels.
|
|
210
|
+
*
|
|
211
|
+
* While `createDrag` tracks the pointer continuously, `createSwipe`
|
|
212
|
+
* makes the discrete decision: was that gesture a swipe, and which
|
|
213
|
+
* way? On pointerup it compares travel, duration, and velocity
|
|
214
|
+
* against the thresholds and fires the matching callbacks plus the
|
|
215
|
+
* `lastSwipe` signal.
|
|
216
|
+
*
|
|
217
|
+
* Pointer Events give touch parity for free: mouse, touch, and pen
|
|
218
|
+
* run through the same path. For touch, set `touch-action: pan-y` on
|
|
219
|
+
* a horizontal swipe surface (or `pan-x` for vertical) so the browser
|
|
220
|
+
* does not hijack the gesture; use `none` when recognizing both axes.
|
|
221
|
+
*
|
|
222
|
+
* A swipe counts when travel passes `threshold` inside `maxDuration`,
|
|
223
|
+
* or when velocity passes `minVelocity` (a fast flick). Slow long
|
|
224
|
+
* drags are not swipes; they belong to `createDrag`.
|
|
225
|
+
*
|
|
226
|
+
* Recognition is not animation, so it works identically under
|
|
227
|
+
* reduced motion; the host decides how to animate the response.
|
|
228
|
+
* SSR-safe: `lastSwipe()` stays null and callbacks never fire.
|
|
229
|
+
*
|
|
230
|
+
* ```tsx
|
|
231
|
+
* let deck!: HTMLDivElement
|
|
232
|
+
* const { lastSwipe } = createSwipe(() => deck, {
|
|
233
|
+
* onSwipeLeft: () => dismiss(),
|
|
234
|
+
* onSwipeRight: () => keep(),
|
|
235
|
+
* })
|
|
236
|
+
* <div ref={deck} style={{ "touch-action": "pan-y" }}>card</div>
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export function createSwipe(ref, options = {}) {
|
|
240
|
+
const none = () => null;
|
|
241
|
+
if (typeof window === "undefined") {
|
|
242
|
+
return { lastSwipe: none, reset: () => { } };
|
|
243
|
+
}
|
|
244
|
+
const { threshold = 48, maxDuration = 800, minVelocity = 0.4, axis = "both", onSwipe, onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown, } = options;
|
|
245
|
+
const [lastSwipe, setLastSwipe] = createSignal(null);
|
|
246
|
+
const fire = (details) => {
|
|
247
|
+
setLastSwipe(details);
|
|
248
|
+
onSwipe?.(details);
|
|
249
|
+
if (details.direction === "left")
|
|
250
|
+
onSwipeLeft?.(details);
|
|
251
|
+
else if (details.direction === "right")
|
|
252
|
+
onSwipeRight?.(details);
|
|
253
|
+
else if (details.direction === "up")
|
|
254
|
+
onSwipeUp?.(details);
|
|
255
|
+
else
|
|
256
|
+
onSwipeDown?.(details);
|
|
257
|
+
};
|
|
258
|
+
const onDown = (event) => {
|
|
259
|
+
if (event.isPrimary === false)
|
|
260
|
+
return;
|
|
261
|
+
const el = ref();
|
|
262
|
+
if (!el)
|
|
263
|
+
return;
|
|
264
|
+
const startX = event.clientX;
|
|
265
|
+
const startY = event.clientY;
|
|
266
|
+
const startT = now();
|
|
267
|
+
const up = (ev) => {
|
|
268
|
+
window.removeEventListener("pointerup", up);
|
|
269
|
+
window.removeEventListener("pointercancel", cancel);
|
|
270
|
+
const dx = ev.clientX - startX;
|
|
271
|
+
const dy = ev.clientY - startY;
|
|
272
|
+
const dt = Math.max(now() - startT, 1);
|
|
273
|
+
// Primary axis, honoring the axis lock.
|
|
274
|
+
const horizontal = axis === "x" || (axis === "both" && Math.abs(dx) >= Math.abs(dy));
|
|
275
|
+
const travel = horizontal ? dx : dy;
|
|
276
|
+
const distance = Math.abs(travel);
|
|
277
|
+
const velocity = distance / dt;
|
|
278
|
+
const isSwipe = (distance >= threshold && dt <= maxDuration) ||
|
|
279
|
+
velocity >= minVelocity;
|
|
280
|
+
if (!isSwipe || distance === 0)
|
|
281
|
+
return;
|
|
282
|
+
const direction = horizontal
|
|
283
|
+
? travel > 0
|
|
284
|
+
? "right"
|
|
285
|
+
: "left"
|
|
286
|
+
: travel > 0
|
|
287
|
+
? "down"
|
|
288
|
+
: "up";
|
|
289
|
+
fire({
|
|
290
|
+
direction,
|
|
291
|
+
distance,
|
|
292
|
+
velocity,
|
|
293
|
+
duration: dt,
|
|
294
|
+
from: { x: startX, y: startY },
|
|
295
|
+
to: { x: ev.clientX, y: ev.clientY },
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
const cancel = () => {
|
|
299
|
+
window.removeEventListener("pointerup", up);
|
|
300
|
+
window.removeEventListener("pointercancel", cancel);
|
|
301
|
+
};
|
|
302
|
+
window.addEventListener("pointerup", up);
|
|
303
|
+
window.addEventListener("pointercancel", cancel);
|
|
304
|
+
};
|
|
305
|
+
// Late-bound refs (Solid assigns `ref` after mount) still get the grab.
|
|
306
|
+
createEffect(() => {
|
|
307
|
+
const el = ref();
|
|
308
|
+
if (!el)
|
|
309
|
+
return;
|
|
310
|
+
el.addEventListener("pointerdown", onDown);
|
|
311
|
+
onCleanup(() => el.removeEventListener("pointerdown", onDown));
|
|
312
|
+
});
|
|
313
|
+
return { lastSwipe, reset: () => setLastSwipe(null) };
|
|
314
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export { createScrollColor, type ScrollColorStop, type ScrollColorOptions, type
|
|
|
20
20
|
export { createVelocity, type VelocityOptions } from "./velocity.js";
|
|
21
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";
|
|
@@ -29,7 +30,7 @@ export { createFontSwap, type FontSwapOptions, type FontSwapResult, createTyping
|
|
|
29
30
|
export { easings, cubicBezier, linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeOutExpo, easeOutBack, easeInBack, easeInOutBack, easeOutElastic, easeOutBounce, resolveEasing, type Easing, type EasingName, } from "./easing.js";
|
|
30
31
|
export { createKineticType, createScenePlayer, createShowreel, createCamera, createColorShift, createTransition, createBeat, createBeatCuts, } from "./motion.js";
|
|
31
32
|
export type { KineticTypeFrom, KineticTypeOptions, KineticTypeStatus, KineticTypeControls, MotionScene, ScenePlayerStatus, ScenePlayerControls, ShowreelScene, ShowreelSceneKind, CameraKeyframe, CameraOptions, ColorShiftOptions, ColorShiftStatus, ColorShiftControls, TransitionType, TransitionDirection, TransitionOptions, TransitionLayerStyle, TransitionStatus, TransitionControls, BeatOptions, BeatStatus, BeatControls, BeatCutOptions, } from "./motion.js";
|
|
32
|
-
export { createDrag, type DragStatus, type DragAxis, type DragConstraints, type DragEndInfo, type DragOptions, type DragControls, } from "./gesture.js";
|
|
33
|
+
export { createDrag, type DragStatus, type DragAxis, type DragConstraints, type DragEndInfo, type DragOptions, type DragControls, createSwipe, type SwipeDirection, type SwipeDetails, type SwipeOptions, type SwipeControls, } from "./gesture.js";
|
|
33
34
|
export { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
|
|
34
35
|
export type { StreamRevealStatus, StreamRevealOptions, StreamRevealControls, AgentState, AgentStateTransition, AgentStateOptions, AgentStateControls, DriftSpecPrimitive, DriftSpecStep, DriftSpec, SpecPlayerStatus, SpecPlayerHooks, SpecPlayerControls, } from "./ai.js";
|
|
35
36
|
export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, createAgentTx, } from "./web3.js";
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export { createScrollColor, createScrollTracking, createScrollLine, } from "./sc
|
|
|
20
20
|
export { createVelocity } from "./velocity.js";
|
|
21
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";
|
|
@@ -28,7 +29,7 @@ export { createGravity, createPendulum, createFling, } from "./physics.js";
|
|
|
28
29
|
export { createFontSwap, createTyping, createTextPhysics, createTextTunnel, createTextCutout, createTextGradient, createTextScramble, createTextWave, createCountUp, } from "./typography.js";
|
|
29
30
|
export { easings, cubicBezier, linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeOutExpo, easeOutBack, easeInBack, easeInOutBack, easeOutElastic, easeOutBounce, resolveEasing, } from "./easing.js";
|
|
30
31
|
export { createKineticType, createScenePlayer, createShowreel, createCamera, createColorShift, createTransition, createBeat, createBeatCuts, } from "./motion.js";
|
|
31
|
-
export { createDrag, } from "./gesture.js";
|
|
32
|
+
export { createDrag, createSwipe, } from "./gesture.js";
|
|
32
33
|
export { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
|
|
33
34
|
export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, createAgentTx, } from "./web3.js";
|
|
34
35
|
export { createPoll, shortenAddress, isAddress, formatUnits, parseUnits, CHAINS, createChain, createTokenPrice, createPriceChange, createPriceCompare, createGasPrice, createBalance, createTxReceipt, createBlockNumber, createChainlinkPrice, createNFTMetadata, createENS, createIdenticon, } from "./web3data.js";
|
package/package.json
CHANGED