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 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
@@ -43,5 +43,5 @@
43
43
  },
44
44
  "type": "module",
45
45
  "types": "./dist/index.d.ts",
46
- "version": "0.18.0"
46
+ "version": "0.19.0"
47
47
  }