solid-drift 0.18.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 +22 -0
- package/dist/depixelate.d.ts +57 -0
- package/dist/depixelate.js +190 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1104,6 +1104,28 @@ const reveal = createMintReveal(() => card, {
|
|
|
1104
1104
|
|
|
1105
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`.
|
|
1106
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
|
+
|
|
1107
1129
|
### `createConnectButton(ref, options?)`
|
|
1108
1130
|
|
|
1109
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
|
@@ -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";
|
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";
|
package/package.json
CHANGED