solid-drift 0.20.0 → 0.22.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 +107 -0
- package/dist/dom.d.ts +163 -0
- package/dist/dom.js +316 -0
- package/dist/fun.d.ts +167 -0
- package/dist/fun.js +448 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1338,6 +1338,113 @@ const packet = createRedPacket({ amount: 88, coins: 14 })
|
|
|
1338
1338
|
|
|
1339
1339
|
Options: `coins` (default `12`), `amount` (total, default `88`), `spread` (burst size in px, default `160`), `gravity` (px/s^2, default `900`), `openDuration` (ms, default `500`), `burstDuration` (ms, default `1600`), `revealDuration` (ms, default `800`), `onOpen`, `onReveal(amount)`. Returns `{ status, coins, revealed, open, reset }`. SSR-safe and reduced-motion safe: `open()` jumps straight to revealed with no burst.
|
|
1340
1340
|
|
|
1341
|
+
### `createConfetti(canvas, options?)`
|
|
1342
|
+
|
|
1343
|
+
Canvas confetti bursts: celebration physics with gravity, drag, sway, and tumbling paper flutter, rendered on the shared animation clock. Give it a canvas (a fullscreen fixed overlay with `pointer-events: none` is the classic setup) and call `burst()` from party moments: mints, wins, onboarding completions. Bursts accumulate, so rapid celebrations stack instead of replacing. The canvas is fitted to its CSS size times the device pixel ratio automatically.
|
|
1344
|
+
|
|
1345
|
+
```tsx
|
|
1346
|
+
import { createConfetti } from "solid-drift"
|
|
1347
|
+
|
|
1348
|
+
let cvs!: HTMLCanvasElement
|
|
1349
|
+
const confetti = createConfetti(() => cvs, {
|
|
1350
|
+
onDone: () => console.log("party over"),
|
|
1351
|
+
})
|
|
1352
|
+
<canvas ref={cvs} style={{ position: "fixed", inset: "0", "pointer-events": "none" }} />
|
|
1353
|
+
<button onClick={() => confetti.burst()}>Celebrate</button>
|
|
1354
|
+
<button onClick={() => confetti.burst({ x: 0.2, y: 0.8 })}>Side popper</button>
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
Options: `count` (particles per burst, default `120`), `colors` (default a festive palette), `spread` (launch cone in degrees around straight up, default `70`), `power` (launch speed in px/s, default `900`), `gravity` (px/s^2, default `1100`), `drag` (default `1.2`), `size` ([min, max] px, default `[6, 12]`), `shapes` (default `["rect", "circle"]`), `lifetime` (ms, default `2600`), `onDone` (called when the last particle fades). Returns `{ active, burst, clear }`: `burst(origin?)` fires from a normalized origin (default `{ x: 0.5, y: 0.6 }`), `clear()` removes every particle immediately. SSR-safe: `burst()` is a no-op on the server. Under reduced motion `burst()` skips the particles but still calls `onDone`, so chained logic (show the prize after the celebration) keeps working. Tip: pair with `useLowPowerMode` to drop the count on weak devices.
|
|
1358
|
+
|
|
1359
|
+
### `createEmojiBurst(canvas, options?)`
|
|
1360
|
+
|
|
1361
|
+
Emoji celebration burst: the same particle physics as confetti, but the particles are emoji glyphs that rise, tumble gently, and fade. Reactions, likes, level-ups, chat celebrations. Same canvas setup, same safety rules.
|
|
1362
|
+
|
|
1363
|
+
```tsx
|
|
1364
|
+
import { createEmojiBurst } from "solid-drift"
|
|
1365
|
+
|
|
1366
|
+
let cvs!: HTMLCanvasElement
|
|
1367
|
+
const burst = createEmojiBurst(() => cvs, { emoji: ["❤️", "🔥"] })
|
|
1368
|
+
<canvas ref={cvs} style={{ position: "fixed", inset: "0", "pointer-events": "none" }} />
|
|
1369
|
+
<button onClick={() => burst.burst()}>Send love</button>
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
Options: `emoji` (default `["🎉", "✨", "💥", "⭐", "💖", "🥳"]`), `count` (default `24`), `power` (default `650`), `gravity` (default `700`: floatier than confetti), `drag` (default `1.6`), `size` ([min, max] px, default `[24, 48]`), `spread` (default `90`), `lifetime` (default `1800`), `onDone`. Returns `{ active, burst, clear }`.
|
|
1373
|
+
|
|
1374
|
+
### `createScratch(canvas, options?)`
|
|
1375
|
+
|
|
1376
|
+
Scratch-off cover: a lottery-ticket foil over hidden content. The canvas paints an opaque cover (silver holographic foil by default, or your own art via `paint`) and pointer drags erase through it with `destination-out`. The cleared fraction is sampled from the alpha channel on a throttled cadence, and `onComplete` fires once past `threshold`. Layer it over the prize with absolute positioning, and set `touch-action: none` on the canvas so touch scratches do not scroll the page.
|
|
1377
|
+
|
|
1378
|
+
```tsx
|
|
1379
|
+
import { createScratch } from "solid-drift"
|
|
1380
|
+
|
|
1381
|
+
let foil!: HTMLCanvasElement
|
|
1382
|
+
const scratch = createScratch(() => foil, {
|
|
1383
|
+
onComplete: () => console.log("revealed!"),
|
|
1384
|
+
})
|
|
1385
|
+
<div style={{ position: "relative" }}>
|
|
1386
|
+
<div>YOU WON 50 STARS</div>
|
|
1387
|
+
<canvas ref={foil} style={{ position: "absolute", inset: "0", "touch-action": "none" }} />
|
|
1388
|
+
</div>
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
Options: `threshold` (fraction cleared to complete, default `0.45`), `brush` (eraser radius in px, default `26`), `paint(ctx, w, h)` (custom cover art), `onComplete`. Returns `{ cleared, done, reset }`: `cleared()` is the 0..1 fraction erased, `reset()` repaints the cover. Scratching is direct manipulation, so it works identically under reduced motion. SSR-safe: `cleared()` stays 0.
|
|
1392
|
+
|
|
1393
|
+
### DOM utilities
|
|
1394
|
+
|
|
1395
|
+
Everyday DOM glue, signal-native: debounced and throttled signal transforms, a persisted signal, a live media query, outside-press dismissal, body scroll locking, and infinite scroll. All SSR-safe.
|
|
1396
|
+
|
|
1397
|
+
```tsx
|
|
1398
|
+
import {
|
|
1399
|
+
createDebounced,
|
|
1400
|
+
createThrottled,
|
|
1401
|
+
createLocalStorage,
|
|
1402
|
+
createMediaQuery,
|
|
1403
|
+
createClickOutside,
|
|
1404
|
+
createScrollLock,
|
|
1405
|
+
createInfiniteScroll,
|
|
1406
|
+
} from "solid-drift"
|
|
1407
|
+
|
|
1408
|
+
// Debounced search: the query waits for a 300ms pause before firing.
|
|
1409
|
+
const [query, setQuery] = createSignal("")
|
|
1410
|
+
const debounced = createDebounced(query, 300)
|
|
1411
|
+
createEffect(() => { if (debounced()) search(debounced()) })
|
|
1412
|
+
|
|
1413
|
+
// Throttled scroll position: at most one update per 100ms.
|
|
1414
|
+
const throttledY = createThrottled(scrollY, 100)
|
|
1415
|
+
|
|
1416
|
+
// Persisted theme, synced across tabs.
|
|
1417
|
+
const theme = createLocalStorage<"light" | "dark">("theme", "light")
|
|
1418
|
+
theme.set("dark")
|
|
1419
|
+
|
|
1420
|
+
// Live media query.
|
|
1421
|
+
const wide = createMediaQuery("(min-width: 1024px)")
|
|
1422
|
+
|
|
1423
|
+
// Dismiss a menu on outside press.
|
|
1424
|
+
let menu!: HTMLDivElement
|
|
1425
|
+
createClickOutside(() => menu, () => setOpen(false))
|
|
1426
|
+
|
|
1427
|
+
// Lock body scroll while a modal is open (nested locks stack).
|
|
1428
|
+
const scroll = createScrollLock()
|
|
1429
|
+
createEffect(() => { modalOpen() ? scroll.lock() : scroll.unlock() })
|
|
1430
|
+
|
|
1431
|
+
// Infinite scroll: prefetch as the sentinel approaches.
|
|
1432
|
+
let sentinel!: HTMLDivElement
|
|
1433
|
+
createInfiniteScroll(() => sentinel, {
|
|
1434
|
+
onLoadMore: () => loadPage(),
|
|
1435
|
+
disabled: () => !hasMore(),
|
|
1436
|
+
})
|
|
1437
|
+
<div ref={sentinel} />
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
- `createDebounced(source, delay)` returns an `Accessor<T>` that follows the source after it stops changing for `delay` ms (trailing edge).
|
|
1441
|
+
- `createThrottled(source, interval)` returns an `Accessor<T>` that updates at most once per `interval` ms: leading change applies immediately, the rest collapse into one trailing update.
|
|
1442
|
+
- `createLocalStorage<T>(key, initialValue, options?)` returns `{ value, set, remove }`: reads the stored value on creation (falling back on missing or corrupt JSON), writes through on every set, and stays in sync across tabs via the `storage` event (`sync: true` default). Custom `serialize`/`deserialize` supported. Behaves like a plain signal where storage is unavailable.
|
|
1443
|
+
- `createMediaQuery(query)` returns an `Accessor<boolean>` that tracks the query live (`false` on the server).
|
|
1444
|
+
- `createClickOutside(ref, handler, options?)` calls `handler` on `pointerdown` (default, configurable via `events`) outside the element. Shadow-DOM aware via `composedPath`. No-op on the server.
|
|
1445
|
+
- `createScrollLock()` returns `{ locked, lock, unlock }`: sets `document.body.style.overflow = "hidden"`, restores the previous value when the last lock releases, and reference-counts nested locks so stacked modals cannot unlock each other early. Unmounting releases the locks.
|
|
1446
|
+
- `createInfiniteScroll(ref, options)` observes a sentinel with IntersectionObserver and calls `onLoadMore` as it approaches the viewport (`threshold` px prefetch via `rootMargin`, default `200`). `disabled` is a reactive kill switch (e.g. `() => !hasMore()`).
|
|
1447
|
+
|
|
1341
1448
|
### Easings
|
|
1342
1449
|
|
|
1343
1450
|
Named easings: `linear`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeInQuart`, `easeOutQuart`, `easeInOutQuart`, `easeOutExpo`, `easeOutBack`, plus the cartoon set: `easeInBack` (anticipation dip before movement), `easeInOutBack` (wind-up, overshoot, settle), `easeOutElastic` (decaying rubber-band oscillation), `easeOutBounce` (shrinking cartoon bounces). Also `cubicBezier(x1, y1, x2, y2)` for CSS-style curves. Pass a name or a custom `(t) => number` function anywhere an easing is accepted.
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { type Accessor, type Setter } from "solid-js";
|
|
2
|
+
type MaybeElement = () => Element | null | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* A signal that follows the source after it stops changing for `delay`
|
|
5
|
+
* ms. The classic search-input pattern: the UI stays live while the
|
|
6
|
+
* query signal waits for a pause before firing network requests.
|
|
7
|
+
*
|
|
8
|
+
* Trailing edge only. The pending update is cancelled on cleanup.
|
|
9
|
+
* SSR-safe: holds the source's initial value on the server.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const [query, setQuery] = createSignal("")
|
|
13
|
+
* const debounced = createDebounced(query, 300)
|
|
14
|
+
* createEffect(() => {
|
|
15
|
+
* const q = debounced()
|
|
16
|
+
* if (q) search(q) // fires only after a 300ms pause
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function createDebounced<T>(source: Accessor<T>, delay: number): Accessor<T>;
|
|
21
|
+
/**
|
|
22
|
+
* A signal that follows the source at most once per `interval` ms.
|
|
23
|
+
* The first change applies immediately (leading edge); changes inside
|
|
24
|
+
* the window collapse into one trailing update. For scroll or resize
|
|
25
|
+
* handlers that feed expensive work.
|
|
26
|
+
*
|
|
27
|
+
* SSR-safe: holds the source's initial value on the server.
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* const throttledY = createThrottled(scrollY, 100)
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function createThrottled<T>(source: Accessor<T>, interval: number): Accessor<T>;
|
|
34
|
+
export interface LocalStorageOptions<T> {
|
|
35
|
+
/** Serialize a value. Default JSON.stringify. */
|
|
36
|
+
serialize?: (value: T) => string;
|
|
37
|
+
/** Deserialize a stored string. Default JSON.parse. */
|
|
38
|
+
deserialize?: (raw: string) => T;
|
|
39
|
+
/**
|
|
40
|
+
* React to `storage` events from other tabs. Default true: the
|
|
41
|
+
* signal stays in sync across tabs.
|
|
42
|
+
*/
|
|
43
|
+
sync?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface LocalStorageControls<T> {
|
|
46
|
+
value: Accessor<T>;
|
|
47
|
+
set: Setter<T>;
|
|
48
|
+
/** Remove the key from storage and reset to the initial value. */
|
|
49
|
+
remove: () => void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A signal persisted to `localStorage`. Reads the stored value on
|
|
53
|
+
* creation (falling back to `initialValue` when missing or corrupt),
|
|
54
|
+
* writes through on every set, and stays in sync across tabs via the
|
|
55
|
+
* `storage` event.
|
|
56
|
+
*
|
|
57
|
+
* SSR-safe and storage-less-safe: without `window.localStorage` it
|
|
58
|
+
* behaves like a plain signal.
|
|
59
|
+
*
|
|
60
|
+
* ```tsx
|
|
61
|
+
* const theme = createLocalStorage<"light" | "dark">("theme", "light")
|
|
62
|
+
* theme.set("dark") // localStorage.theme = '"dark"'
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function createLocalStorage<T>(key: string, initialValue: T, options?: LocalStorageOptions<T>): LocalStorageControls<T>;
|
|
66
|
+
/**
|
|
67
|
+
* A boolean signal tracking a CSS media query, updating live when the
|
|
68
|
+
* query starts or stops matching. The primitive behind
|
|
69
|
+
* `usePrefersReducedMotion`, generalized.
|
|
70
|
+
*
|
|
71
|
+
* SSR-safe (and safe where `matchMedia` is missing): constant `false`.
|
|
72
|
+
*
|
|
73
|
+
* ```tsx
|
|
74
|
+
* const wide = createMediaQuery("(min-width: 1024px)")
|
|
75
|
+
* const columns = () => (wide() ? 4 : 2)
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function createMediaQuery(query: string): Accessor<boolean>;
|
|
79
|
+
export interface ClickOutsideOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Events that count as "outside". Default `["pointerdown"]`: fires
|
|
82
|
+
* before `click`, so menus dismiss without a visible flash, and
|
|
83
|
+
* covers touch via Pointer Events.
|
|
84
|
+
*/
|
|
85
|
+
events?: string[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Call `handler` when the user interacts outside the element:
|
|
89
|
+
* dropdowns, popovers, and menus that dismiss on outside press.
|
|
90
|
+
*
|
|
91
|
+
* Listens on `window` (so it works even when the press lands on an
|
|
92
|
+
* overlaying element), ignores presses inside the ref, and cleans up
|
|
93
|
+
* on unmount. SSR-safe: no-op on the server.
|
|
94
|
+
*
|
|
95
|
+
* ```tsx
|
|
96
|
+
* let menu!: HTMLDivElement
|
|
97
|
+
* const [open, setOpen] = createSignal(false)
|
|
98
|
+
* createClickOutside(() => menu, () => setOpen(false))
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function createClickOutside(ref: MaybeElement, handler: (event: Event) => void, options?: ClickOutsideOptions): void;
|
|
102
|
+
export interface ScrollLockControls {
|
|
103
|
+
/** Whether the body scroll is currently locked. */
|
|
104
|
+
locked: Accessor<boolean>;
|
|
105
|
+
/** Lock body scroll. Nested locks stack; all must unlock. */
|
|
106
|
+
lock: () => void;
|
|
107
|
+
/** Release one lock. Restores the original overflow when empty. */
|
|
108
|
+
unlock: () => void;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Lock body scroll while overlays are open: modals, drawers, the
|
|
112
|
+
* bottom sheet. `overflow: hidden` goes on `document.body`, the
|
|
113
|
+
* previous value is restored when the last lock releases, and nested
|
|
114
|
+
* locks are reference-counted so two stacked modals cannot unlock
|
|
115
|
+
* each other early. Unmounting while locked releases the lock.
|
|
116
|
+
*
|
|
117
|
+
* SSR-safe: no-op on the server.
|
|
118
|
+
*
|
|
119
|
+
* ```tsx
|
|
120
|
+
* const scroll = createScrollLock()
|
|
121
|
+
* createEffect(() => {
|
|
122
|
+
* if (modalOpen()) scroll.lock()
|
|
123
|
+
* else scroll.unlock()
|
|
124
|
+
* })
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export declare function createScrollLock(): ScrollLockControls;
|
|
128
|
+
export interface InfiniteScrollOptions {
|
|
129
|
+
/** Called when the sentinel approaches the viewport. */
|
|
130
|
+
onLoadMore: () => void;
|
|
131
|
+
/**
|
|
132
|
+
* Trigger distance in px before the sentinel enters: the observer's
|
|
133
|
+
* rootMargin. Default 200.
|
|
134
|
+
*/
|
|
135
|
+
threshold?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Reactive kill switch, e.g. `() => !hasMore()`. While true the
|
|
138
|
+
* sentinel is unobserved.
|
|
139
|
+
*/
|
|
140
|
+
disabled?: Accessor<boolean>;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Infinite scroll: observe a sentinel element at the end of a list
|
|
144
|
+
* and call `onLoadMore` as it approaches the viewport, prefetching
|
|
145
|
+
* before the user hits the bottom.
|
|
146
|
+
*
|
|
147
|
+
* Built on IntersectionObserver with a `rootMargin` prefetch zone.
|
|
148
|
+
* SSR-safe (and safe without IntersectionObserver): never fires.
|
|
149
|
+
*
|
|
150
|
+
* ```tsx
|
|
151
|
+
* let sentinel!: HTMLDivElement
|
|
152
|
+
* const [items, setItems] = createSignal<string[]>([])
|
|
153
|
+
* const [hasMore, setHasMore] = createSignal(true)
|
|
154
|
+
* createInfiniteScroll(() => sentinel, {
|
|
155
|
+
* onLoadMore: () => loadPage(),
|
|
156
|
+
* disabled: () => !hasMore(),
|
|
157
|
+
* })
|
|
158
|
+
* <For each={items()}>{(item) => <Row item={item} />}</For>
|
|
159
|
+
* <div ref={sentinel} />
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export declare function createInfiniteScroll(ref: MaybeElement, options: InfiniteScrollOptions): void;
|
|
163
|
+
export {};
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { createEffect, createSignal, onCleanup, untrack, } from "solid-js";
|
|
2
|
+
/* ------------------------------------------------------------------ */
|
|
3
|
+
/* createDebounced */
|
|
4
|
+
/* ------------------------------------------------------------------ */
|
|
5
|
+
/**
|
|
6
|
+
* A signal that follows the source after it stops changing for `delay`
|
|
7
|
+
* ms. The classic search-input pattern: the UI stays live while the
|
|
8
|
+
* query signal waits for a pause before firing network requests.
|
|
9
|
+
*
|
|
10
|
+
* Trailing edge only. The pending update is cancelled on cleanup.
|
|
11
|
+
* SSR-safe: holds the source's initial value on the server.
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const [query, setQuery] = createSignal("")
|
|
15
|
+
* const debounced = createDebounced(query, 300)
|
|
16
|
+
* createEffect(() => {
|
|
17
|
+
* const q = debounced()
|
|
18
|
+
* if (q) search(q) // fires only after a 300ms pause
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function createDebounced(source, delay) {
|
|
23
|
+
const [value, setValue] = createSignal(untrack(source));
|
|
24
|
+
if (typeof window === "undefined")
|
|
25
|
+
return value;
|
|
26
|
+
let timer;
|
|
27
|
+
createEffect(() => {
|
|
28
|
+
const next = source();
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
timer = setTimeout(() => setValue(() => next), Math.max(0, delay));
|
|
31
|
+
});
|
|
32
|
+
onCleanup(() => clearTimeout(timer));
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/* ------------------------------------------------------------------ */
|
|
36
|
+
/* createThrottled */
|
|
37
|
+
/* ------------------------------------------------------------------ */
|
|
38
|
+
/**
|
|
39
|
+
* A signal that follows the source at most once per `interval` ms.
|
|
40
|
+
* The first change applies immediately (leading edge); changes inside
|
|
41
|
+
* the window collapse into one trailing update. For scroll or resize
|
|
42
|
+
* handlers that feed expensive work.
|
|
43
|
+
*
|
|
44
|
+
* SSR-safe: holds the source's initial value on the server.
|
|
45
|
+
*
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const throttledY = createThrottled(scrollY, 100)
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function createThrottled(source, interval) {
|
|
51
|
+
const [value, setValue] = createSignal(untrack(source));
|
|
52
|
+
if (typeof window === "undefined")
|
|
53
|
+
return value;
|
|
54
|
+
const wait = Math.max(0, interval);
|
|
55
|
+
let last = 0;
|
|
56
|
+
let timer;
|
|
57
|
+
createEffect(() => {
|
|
58
|
+
const next = source();
|
|
59
|
+
const t = Date.now();
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
timer = undefined;
|
|
62
|
+
if (t - last >= wait) {
|
|
63
|
+
last = t;
|
|
64
|
+
setValue(() => next);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
timer = setTimeout(() => {
|
|
68
|
+
last = Date.now();
|
|
69
|
+
timer = undefined;
|
|
70
|
+
setValue(() => next);
|
|
71
|
+
}, wait - (t - last));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
onCleanup(() => clearTimeout(timer));
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A signal persisted to `localStorage`. Reads the stored value on
|
|
79
|
+
* creation (falling back to `initialValue` when missing or corrupt),
|
|
80
|
+
* writes through on every set, and stays in sync across tabs via the
|
|
81
|
+
* `storage` event.
|
|
82
|
+
*
|
|
83
|
+
* SSR-safe and storage-less-safe: without `window.localStorage` it
|
|
84
|
+
* behaves like a plain signal.
|
|
85
|
+
*
|
|
86
|
+
* ```tsx
|
|
87
|
+
* const theme = createLocalStorage<"light" | "dark">("theme", "light")
|
|
88
|
+
* theme.set("dark") // localStorage.theme = '"dark"'
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function createLocalStorage(key, initialValue, options = {}) {
|
|
92
|
+
const { serialize = JSON.stringify, deserialize = JSON.parse, sync = true, } = options;
|
|
93
|
+
const storage = () => {
|
|
94
|
+
if (typeof window === "undefined")
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
return window.localStorage;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null; // private mode / blocked storage
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const read = () => {
|
|
104
|
+
const store = storage();
|
|
105
|
+
if (!store)
|
|
106
|
+
return initialValue;
|
|
107
|
+
try {
|
|
108
|
+
const raw = store.getItem(key);
|
|
109
|
+
return raw === null ? initialValue : deserialize(raw);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return initialValue; // corrupt JSON: fall back, don't crash
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const [value, setValue] = createSignal(read());
|
|
116
|
+
const set = ((next) => {
|
|
117
|
+
const resolved = typeof next === "function"
|
|
118
|
+
? next(untrack(value))
|
|
119
|
+
: next;
|
|
120
|
+
setValue(() => resolved);
|
|
121
|
+
try {
|
|
122
|
+
storage()?.setItem(key, serialize(resolved));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Quota exceeded or blocked: the signal still updates.
|
|
126
|
+
}
|
|
127
|
+
return resolved;
|
|
128
|
+
});
|
|
129
|
+
const remove = () => {
|
|
130
|
+
try {
|
|
131
|
+
storage()?.removeItem(key);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// ignore
|
|
135
|
+
}
|
|
136
|
+
setValue(() => initialValue);
|
|
137
|
+
};
|
|
138
|
+
if (typeof window !== "undefined" && sync) {
|
|
139
|
+
const onStorage = (event) => {
|
|
140
|
+
if (event.key !== key)
|
|
141
|
+
return;
|
|
142
|
+
try {
|
|
143
|
+
setValue(() => event.newValue === null ? initialValue : deserialize(event.newValue));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// ignore corrupt cross-tab writes
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
window.addEventListener("storage", onStorage);
|
|
150
|
+
onCleanup(() => window.removeEventListener("storage", onStorage));
|
|
151
|
+
}
|
|
152
|
+
return { value, set, remove };
|
|
153
|
+
}
|
|
154
|
+
/* ------------------------------------------------------------------ */
|
|
155
|
+
/* createMediaQuery */
|
|
156
|
+
/* ------------------------------------------------------------------ */
|
|
157
|
+
/**
|
|
158
|
+
* A boolean signal tracking a CSS media query, updating live when the
|
|
159
|
+
* query starts or stops matching. The primitive behind
|
|
160
|
+
* `usePrefersReducedMotion`, generalized.
|
|
161
|
+
*
|
|
162
|
+
* SSR-safe (and safe where `matchMedia` is missing): constant `false`.
|
|
163
|
+
*
|
|
164
|
+
* ```tsx
|
|
165
|
+
* const wide = createMediaQuery("(min-width: 1024px)")
|
|
166
|
+
* const columns = () => (wide() ? 4 : 2)
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export function createMediaQuery(query) {
|
|
170
|
+
const no = () => false;
|
|
171
|
+
if (typeof window === "undefined" ||
|
|
172
|
+
typeof window.matchMedia !== "function") {
|
|
173
|
+
return no;
|
|
174
|
+
}
|
|
175
|
+
const mql = window.matchMedia(query);
|
|
176
|
+
const [matches, setMatches] = createSignal(mql.matches);
|
|
177
|
+
const onChange = (event) => {
|
|
178
|
+
setMatches(event.matches);
|
|
179
|
+
};
|
|
180
|
+
mql.addEventListener("change", onChange);
|
|
181
|
+
onCleanup(() => mql.removeEventListener("change", onChange));
|
|
182
|
+
return matches;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Call `handler` when the user interacts outside the element:
|
|
186
|
+
* dropdowns, popovers, and menus that dismiss on outside press.
|
|
187
|
+
*
|
|
188
|
+
* Listens on `window` (so it works even when the press lands on an
|
|
189
|
+
* overlaying element), ignores presses inside the ref, and cleans up
|
|
190
|
+
* on unmount. SSR-safe: no-op on the server.
|
|
191
|
+
*
|
|
192
|
+
* ```tsx
|
|
193
|
+
* let menu!: HTMLDivElement
|
|
194
|
+
* const [open, setOpen] = createSignal(false)
|
|
195
|
+
* createClickOutside(() => menu, () => setOpen(false))
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export function createClickOutside(ref, handler, options = {}) {
|
|
199
|
+
if (typeof window === "undefined")
|
|
200
|
+
return;
|
|
201
|
+
const { events = ["pointerdown"] } = options;
|
|
202
|
+
const onEvent = (event) => {
|
|
203
|
+
const el = ref();
|
|
204
|
+
if (!el)
|
|
205
|
+
return;
|
|
206
|
+
const target = event.target;
|
|
207
|
+
// Shadow-DOM aware: composedPath pierces shadow roots.
|
|
208
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
209
|
+
const inside = (target !== null && el.contains(target)) || path.includes(el);
|
|
210
|
+
if (!inside)
|
|
211
|
+
handler(event);
|
|
212
|
+
};
|
|
213
|
+
for (const name of events) {
|
|
214
|
+
window.addEventListener(name, onEvent);
|
|
215
|
+
}
|
|
216
|
+
onCleanup(() => {
|
|
217
|
+
for (const name of events) {
|
|
218
|
+
window.removeEventListener(name, onEvent);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
let lockCount = 0;
|
|
223
|
+
let savedOverflow = "";
|
|
224
|
+
/**
|
|
225
|
+
* Lock body scroll while overlays are open: modals, drawers, the
|
|
226
|
+
* bottom sheet. `overflow: hidden` goes on `document.body`, the
|
|
227
|
+
* previous value is restored when the last lock releases, and nested
|
|
228
|
+
* locks are reference-counted so two stacked modals cannot unlock
|
|
229
|
+
* each other early. Unmounting while locked releases the lock.
|
|
230
|
+
*
|
|
231
|
+
* SSR-safe: no-op on the server.
|
|
232
|
+
*
|
|
233
|
+
* ```tsx
|
|
234
|
+
* const scroll = createScrollLock()
|
|
235
|
+
* createEffect(() => {
|
|
236
|
+
* if (modalOpen()) scroll.lock()
|
|
237
|
+
* else scroll.unlock()
|
|
238
|
+
* })
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
export function createScrollLock() {
|
|
242
|
+
const [locked, setLocked] = createSignal(false);
|
|
243
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
244
|
+
return { locked, lock: () => { }, unlock: () => { } };
|
|
245
|
+
}
|
|
246
|
+
let held = 0;
|
|
247
|
+
const lock = () => {
|
|
248
|
+
if (lockCount === 0) {
|
|
249
|
+
savedOverflow = document.body.style.overflow;
|
|
250
|
+
document.body.style.overflow = "hidden";
|
|
251
|
+
}
|
|
252
|
+
lockCount++;
|
|
253
|
+
held++;
|
|
254
|
+
setLocked(true);
|
|
255
|
+
};
|
|
256
|
+
const unlock = () => {
|
|
257
|
+
if (held === 0)
|
|
258
|
+
return;
|
|
259
|
+
held--;
|
|
260
|
+
lockCount--;
|
|
261
|
+
if (lockCount === 0) {
|
|
262
|
+
document.body.style.overflow = savedOverflow;
|
|
263
|
+
setLocked(false);
|
|
264
|
+
}
|
|
265
|
+
else if (held === 0) {
|
|
266
|
+
setLocked(false);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
onCleanup(() => {
|
|
270
|
+
while (held > 0)
|
|
271
|
+
unlock();
|
|
272
|
+
});
|
|
273
|
+
return { locked, lock, unlock };
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Infinite scroll: observe a sentinel element at the end of a list
|
|
277
|
+
* and call `onLoadMore` as it approaches the viewport, prefetching
|
|
278
|
+
* before the user hits the bottom.
|
|
279
|
+
*
|
|
280
|
+
* Built on IntersectionObserver with a `rootMargin` prefetch zone.
|
|
281
|
+
* SSR-safe (and safe without IntersectionObserver): never fires.
|
|
282
|
+
*
|
|
283
|
+
* ```tsx
|
|
284
|
+
* let sentinel!: HTMLDivElement
|
|
285
|
+
* const [items, setItems] = createSignal<string[]>([])
|
|
286
|
+
* const [hasMore, setHasMore] = createSignal(true)
|
|
287
|
+
* createInfiniteScroll(() => sentinel, {
|
|
288
|
+
* onLoadMore: () => loadPage(),
|
|
289
|
+
* disabled: () => !hasMore(),
|
|
290
|
+
* })
|
|
291
|
+
* <For each={items()}>{(item) => <Row item={item} />}</For>
|
|
292
|
+
* <div ref={sentinel} />
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
export function createInfiniteScroll(ref, options) {
|
|
296
|
+
const { onLoadMore, threshold = 200, disabled } = options;
|
|
297
|
+
if (typeof window === "undefined" ||
|
|
298
|
+
typeof IntersectionObserver === "undefined") {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
createEffect(() => {
|
|
302
|
+
const el = ref();
|
|
303
|
+
if (!el)
|
|
304
|
+
return;
|
|
305
|
+
if (disabled?.() === true)
|
|
306
|
+
return;
|
|
307
|
+
const observer = new IntersectionObserver((entries) => {
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (entry.isIntersecting)
|
|
310
|
+
onLoadMore();
|
|
311
|
+
}
|
|
312
|
+
}, { rootMargin: `${threshold}px` });
|
|
313
|
+
observer.observe(el);
|
|
314
|
+
onCleanup(() => observer.disconnect());
|
|
315
|
+
});
|
|
316
|
+
}
|
package/dist/fun.d.ts
CHANGED
|
@@ -149,3 +149,170 @@ export interface RedPacketControls {
|
|
|
149
149
|
* ```
|
|
150
150
|
*/
|
|
151
151
|
export declare function createRedPacket(options?: RedPacketOptions): RedPacketControls;
|
|
152
|
+
export interface ConfettiOptions {
|
|
153
|
+
/** Particles per burst. Default 120. */
|
|
154
|
+
count?: number;
|
|
155
|
+
/** Confetti colors. Default a festive palette. */
|
|
156
|
+
colors?: string[];
|
|
157
|
+
/** Launch cone in degrees around straight up. Default 70. */
|
|
158
|
+
spread?: number;
|
|
159
|
+
/** Launch speed in px/s. Default 900. */
|
|
160
|
+
power?: number;
|
|
161
|
+
/** Gravity in px/s^2. Default 1100. */
|
|
162
|
+
gravity?: number;
|
|
163
|
+
/** Air drag. Default 1.2. */
|
|
164
|
+
drag?: number;
|
|
165
|
+
/** Particle size range in px. Default [6, 12]. */
|
|
166
|
+
size?: [min: number, max: number];
|
|
167
|
+
/** Particle shapes. Default ["rect", "circle"]. */
|
|
168
|
+
shapes?: Array<"rect" | "circle">;
|
|
169
|
+
/** Particle lifetime in ms. Default 2600. */
|
|
170
|
+
lifetime?: number;
|
|
171
|
+
/** Called when the last particle fades. */
|
|
172
|
+
onDone?: () => void;
|
|
173
|
+
}
|
|
174
|
+
export interface ConfettiControls {
|
|
175
|
+
/** True while any particles are alive. */
|
|
176
|
+
active: Accessor<boolean>;
|
|
177
|
+
/**
|
|
178
|
+
* Fire a burst from a normalized origin (0..1 across the canvas).
|
|
179
|
+
* Default `{ x: 0.5, y: 0.6 }`.
|
|
180
|
+
*/
|
|
181
|
+
burst: (origin?: {
|
|
182
|
+
x: number;
|
|
183
|
+
y: number;
|
|
184
|
+
}) => void;
|
|
185
|
+
/** Remove every particle immediately. */
|
|
186
|
+
clear: () => void;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Canvas confetti bursts: celebration physics with gravity, drag,
|
|
190
|
+
* sway, and tumbling paper flutter, rendered on the shared clock.
|
|
191
|
+
*
|
|
192
|
+
* Give it a canvas (a fullscreen fixed overlay with
|
|
193
|
+
* `pointer-events: none` is the classic setup) and call `burst()`
|
|
194
|
+
* from party moments: mints, wins, onboarding completions. Bursts
|
|
195
|
+
* accumulate, so rapid celebrations stack instead of replacing.
|
|
196
|
+
*
|
|
197
|
+
* SSR-safe: `burst()` is a no-op on the server. Under reduced motion
|
|
198
|
+
* `burst()` skips the particles but still calls `onDone`, so chained
|
|
199
|
+
* logic (show the prize after the celebration) keeps working.
|
|
200
|
+
*
|
|
201
|
+
* ```tsx
|
|
202
|
+
* import { createConfetti } from "solid-drift"
|
|
203
|
+
*
|
|
204
|
+
* let cvs!: HTMLCanvasElement
|
|
205
|
+
* const confetti = createConfetti(() => cvs, {
|
|
206
|
+
* onDone: () => console.log("party over"),
|
|
207
|
+
* })
|
|
208
|
+
* <canvas ref={cvs} style={{ position: "fixed", inset: "0", "pointer-events": "none" }} />
|
|
209
|
+
* <button onClick={() => confetti.burst()}>Celebrate</button>
|
|
210
|
+
* <button onClick={() => confetti.burst({ x: 0.2, y: 0.8 })}>Side popper</button>
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export declare function createConfetti(canvas: () => HTMLCanvasElement | null | undefined, options?: ConfettiOptions): ConfettiControls;
|
|
214
|
+
export interface EmojiBurstOptions {
|
|
215
|
+
/** Emoji pool. Default ["\u{1F389}", "\u2728", "\u{1F4A5}", "\u2B50", "\u{1F496}", "\u{1F973}"]. */
|
|
216
|
+
emoji?: string[];
|
|
217
|
+
/** Particles per burst. Default 24. */
|
|
218
|
+
count?: number;
|
|
219
|
+
/** Launch speed in px/s. Default 650. */
|
|
220
|
+
power?: number;
|
|
221
|
+
/** Gravity in px/s^2. Default 700: floatier than confetti. */
|
|
222
|
+
gravity?: number;
|
|
223
|
+
/** Air drag. Default 1.6. */
|
|
224
|
+
drag?: number;
|
|
225
|
+
/** Font size range in px. Default [24, 48]. */
|
|
226
|
+
size?: [min: number, max: number];
|
|
227
|
+
/** Launch cone in degrees around straight up. Default 90. */
|
|
228
|
+
spread?: number;
|
|
229
|
+
/** Particle lifetime in ms. Default 1800. */
|
|
230
|
+
lifetime?: number;
|
|
231
|
+
/** Called when the last particle fades. */
|
|
232
|
+
onDone?: () => void;
|
|
233
|
+
}
|
|
234
|
+
export interface EmojiBurstControls {
|
|
235
|
+
/** True while any particles are alive. */
|
|
236
|
+
active: Accessor<boolean>;
|
|
237
|
+
/**
|
|
238
|
+
* Fire a burst from a normalized origin (0..1 across the canvas).
|
|
239
|
+
* Default `{ x: 0.5, y: 0.6 }`.
|
|
240
|
+
*/
|
|
241
|
+
burst: (origin?: {
|
|
242
|
+
x: number;
|
|
243
|
+
y: number;
|
|
244
|
+
}) => void;
|
|
245
|
+
/** Remove every particle immediately. */
|
|
246
|
+
clear: () => void;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Emoji celebration burst: the same particle physics as confetti,
|
|
250
|
+
* but the particles are emoji glyphs that rise, tumble gently, and
|
|
251
|
+
* fade. Reactions, likes, level-ups, chat celebrations.
|
|
252
|
+
*
|
|
253
|
+
* Same canvas setup and safety rules as `createConfetti`: SSR-safe,
|
|
254
|
+
* and under reduced motion `burst()` skips the particles but still
|
|
255
|
+
* calls `onDone`.
|
|
256
|
+
*
|
|
257
|
+
* ```tsx
|
|
258
|
+
* import { createEmojiBurst } from "solid-drift"
|
|
259
|
+
*
|
|
260
|
+
* let cvs!: HTMLCanvasElement
|
|
261
|
+
* const burst = createEmojiBurst(() => cvs, { emoji: ["\u2764\uFE0F", "\u{1F525}"] })
|
|
262
|
+
* <canvas ref={cvs} style={{ position: "fixed", inset: "0", "pointer-events": "none" }} />
|
|
263
|
+
* <button onClick={() => burst.burst()}>Send love</button>
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export declare function createEmojiBurst(canvas: () => HTMLCanvasElement | null | undefined, options?: EmojiBurstOptions): EmojiBurstControls;
|
|
267
|
+
export interface ScratchOptions {
|
|
268
|
+
/**
|
|
269
|
+
* Fraction of the cover that must be cleared to complete, 0 to 1.
|
|
270
|
+
* Default 0.45.
|
|
271
|
+
*/
|
|
272
|
+
threshold?: number;
|
|
273
|
+
/** Eraser brush radius in px. Default 26. */
|
|
274
|
+
brush?: number;
|
|
275
|
+
/**
|
|
276
|
+
* Paint the cover yourself: foil art, branding, "scratch here" copy.
|
|
277
|
+
* Receives the 2D context and the canvas size in px. Default is a
|
|
278
|
+
* silver holographic foil.
|
|
279
|
+
*/
|
|
280
|
+
paint?: (ctx: CanvasRenderingContext2D, w: number, h: number) => void;
|
|
281
|
+
/** Called once when clearing passes the threshold. */
|
|
282
|
+
onComplete?: () => void;
|
|
283
|
+
}
|
|
284
|
+
export interface ScratchControls {
|
|
285
|
+
/** 0..1 fraction of the cover cleared. */
|
|
286
|
+
cleared: Accessor<number>;
|
|
287
|
+
/** True after clearing passes the threshold. */
|
|
288
|
+
done: Accessor<boolean>;
|
|
289
|
+
/** Repaint the cover and reset progress. */
|
|
290
|
+
reset: () => void;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Scratch-off cover: a lottery-ticket foil over hidden content.
|
|
294
|
+
*
|
|
295
|
+
* The canvas paints an opaque cover (silver holographic foil by
|
|
296
|
+
* default, or your own art via `paint`). Pointer drags erase through
|
|
297
|
+
* it with `destination-out`; the cleared fraction is sampled from the
|
|
298
|
+
* alpha channel on a throttled cadence, and `onComplete` fires once
|
|
299
|
+
* past `threshold`. Layer it over the prize with absolute positioning.
|
|
300
|
+
*
|
|
301
|
+
* Set `touch-action: none` on the canvas so touch scratches do not
|
|
302
|
+
* scroll the page. Scratching is direct manipulation, so it works
|
|
303
|
+
* identically under reduced motion. SSR-safe: `cleared()` stays 0.
|
|
304
|
+
*
|
|
305
|
+
* ```tsx
|
|
306
|
+
* import { createScratch } from "solid-drift"
|
|
307
|
+
*
|
|
308
|
+
* let foil!: HTMLCanvasElement
|
|
309
|
+
* const scratch = createScratch(() => foil, {
|
|
310
|
+
* onComplete: () => console.log("revealed!"),
|
|
311
|
+
* })
|
|
312
|
+
* <div style={{ position: "relative" }}>
|
|
313
|
+
* <div>YOU WON 50 STARS</div>
|
|
314
|
+
* <canvas ref={foil} style={{ position: "absolute", inset: "0", "touch-action": "none" }} />
|
|
315
|
+
* </div>
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
export declare function createScratch(canvas: () => HTMLCanvasElement | null | undefined, options?: ScratchOptions): ScratchControls;
|
package/dist/fun.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal, onCleanup, } from "solid-js";
|
|
1
|
+
import { createEffect, createSignal, onCleanup, } from "solid-js";
|
|
2
2
|
import { now, schedule } from "./engine.js";
|
|
3
3
|
import { resolveEasing } from "./easing.js";
|
|
4
4
|
import { createTween } from "./tween.js";
|
|
@@ -274,3 +274,450 @@ export function createRedPacket(options = {}) {
|
|
|
274
274
|
onCleanup(() => cancel?.());
|
|
275
275
|
return { status, coins: coinList, revealed, open, reset };
|
|
276
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* A canvas particle layer with gravity, drag, sway, and flutter, run
|
|
279
|
+
* on the shared animation clock. One rAF loop per layer, only while
|
|
280
|
+
* particles are alive. The canvas is fitted to its CSS size times the
|
|
281
|
+
* device pixel ratio on every frame, so a fullscreen fixed overlay
|
|
282
|
+
* canvas just works.
|
|
283
|
+
*/
|
|
284
|
+
function createParticleLayer(canvas, gravity, drag, onDone) {
|
|
285
|
+
const server = typeof window === "undefined";
|
|
286
|
+
const [active, setActive] = createSignal(false);
|
|
287
|
+
let particles = [];
|
|
288
|
+
let cancel = null;
|
|
289
|
+
let lastT = 0;
|
|
290
|
+
const MAX_PARTICLES = 1200;
|
|
291
|
+
const loop = (t) => {
|
|
292
|
+
const cvs = canvas();
|
|
293
|
+
if (!cvs) {
|
|
294
|
+
particles = [];
|
|
295
|
+
setActive(false);
|
|
296
|
+
cancel = null;
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
300
|
+
const w = cvs.clientWidth;
|
|
301
|
+
const h = cvs.clientHeight;
|
|
302
|
+
if (w === 0 || h === 0)
|
|
303
|
+
return true; // no layout yet; keep waiting
|
|
304
|
+
const pw = Math.round(w * dpr);
|
|
305
|
+
const ph = Math.round(h * dpr);
|
|
306
|
+
if (cvs.width !== pw || cvs.height !== ph) {
|
|
307
|
+
cvs.width = pw;
|
|
308
|
+
cvs.height = ph;
|
|
309
|
+
}
|
|
310
|
+
const ctx = cvs.getContext("2d");
|
|
311
|
+
if (!ctx) {
|
|
312
|
+
particles = [];
|
|
313
|
+
setActive(false);
|
|
314
|
+
cancel = null;
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
const dt = lastT === 0 ? 0.016 : Math.min(0.05, Math.max(0.001, (t - lastT) / 1000));
|
|
318
|
+
lastT = t;
|
|
319
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
320
|
+
ctx.clearRect(0, 0, w, h);
|
|
321
|
+
const dk = Math.max(0, 1 - drag * dt);
|
|
322
|
+
particles = particles.filter((p) => {
|
|
323
|
+
const age = t - p.born;
|
|
324
|
+
if (age >= p.life)
|
|
325
|
+
return false;
|
|
326
|
+
p.vx *= dk;
|
|
327
|
+
p.vy = p.vy * dk + gravity * dt;
|
|
328
|
+
p.swayPhase += p.swaySpeed * dt;
|
|
329
|
+
p.x += (p.vx + Math.sin(p.swayPhase) * p.sway) * dt;
|
|
330
|
+
p.y += p.vy * dt;
|
|
331
|
+
p.rot += p.vr * dt;
|
|
332
|
+
// Fade over the last quarter of life.
|
|
333
|
+
ctx.globalAlpha = Math.min(1, (p.life - age) / (p.life * 0.25));
|
|
334
|
+
if (p.shape === "circle") {
|
|
335
|
+
ctx.fillStyle = p.color;
|
|
336
|
+
ctx.beginPath();
|
|
337
|
+
ctx.arc(p.x, p.y, p.size / 2, 0, Math.PI * 2);
|
|
338
|
+
ctx.fill();
|
|
339
|
+
}
|
|
340
|
+
else if (p.shape === "emoji") {
|
|
341
|
+
ctx.font = `${p.size}px serif`;
|
|
342
|
+
ctx.textAlign = "center";
|
|
343
|
+
ctx.textBaseline = "middle";
|
|
344
|
+
ctx.save();
|
|
345
|
+
ctx.translate(p.x, p.y);
|
|
346
|
+
ctx.rotate(p.rot * 0.3);
|
|
347
|
+
ctx.fillText(p.text, 0, 0);
|
|
348
|
+
ctx.restore();
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Paper flutter: the rect tumbles and its width breathes.
|
|
352
|
+
const squash = 0.35 + 0.65 * Math.abs(Math.sin(p.swayPhase * 2));
|
|
353
|
+
ctx.fillStyle = p.color;
|
|
354
|
+
ctx.save();
|
|
355
|
+
ctx.translate(p.x, p.y);
|
|
356
|
+
ctx.rotate(p.rot);
|
|
357
|
+
ctx.scale(1, squash);
|
|
358
|
+
ctx.fillRect(-p.size / 2, -p.size / 4, p.size, p.size / 2);
|
|
359
|
+
ctx.restore();
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
});
|
|
363
|
+
ctx.globalAlpha = 1;
|
|
364
|
+
if (particles.length === 0) {
|
|
365
|
+
setActive(false);
|
|
366
|
+
cancel = null;
|
|
367
|
+
onDone?.();
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
};
|
|
372
|
+
const spawn = (make) => {
|
|
373
|
+
if (server)
|
|
374
|
+
return;
|
|
375
|
+
particles.push(make());
|
|
376
|
+
if (particles.length > MAX_PARTICLES) {
|
|
377
|
+
particles.splice(0, particles.length - MAX_PARTICLES);
|
|
378
|
+
}
|
|
379
|
+
if (!cancel) {
|
|
380
|
+
lastT = 0;
|
|
381
|
+
setActive(true);
|
|
382
|
+
cancel = schedule(loop);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
const clear = () => {
|
|
386
|
+
cancel?.();
|
|
387
|
+
cancel = null;
|
|
388
|
+
particles = [];
|
|
389
|
+
setActive(false);
|
|
390
|
+
};
|
|
391
|
+
if (!server) {
|
|
392
|
+
onCleanup(() => {
|
|
393
|
+
cancel?.();
|
|
394
|
+
cancel = null;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return { active, spawn, clear };
|
|
398
|
+
}
|
|
399
|
+
const CONFETTI_COLORS = [
|
|
400
|
+
"#ff4757",
|
|
401
|
+
"#ffa502",
|
|
402
|
+
"#2ed573",
|
|
403
|
+
"#1e90ff",
|
|
404
|
+
"#eccc68",
|
|
405
|
+
"#ff6b81",
|
|
406
|
+
"#7bed9f",
|
|
407
|
+
"#70a1ff",
|
|
408
|
+
"#f368e0",
|
|
409
|
+
"#48dbfb",
|
|
410
|
+
];
|
|
411
|
+
/**
|
|
412
|
+
* Canvas confetti bursts: celebration physics with gravity, drag,
|
|
413
|
+
* sway, and tumbling paper flutter, rendered on the shared clock.
|
|
414
|
+
*
|
|
415
|
+
* Give it a canvas (a fullscreen fixed overlay with
|
|
416
|
+
* `pointer-events: none` is the classic setup) and call `burst()`
|
|
417
|
+
* from party moments: mints, wins, onboarding completions. Bursts
|
|
418
|
+
* accumulate, so rapid celebrations stack instead of replacing.
|
|
419
|
+
*
|
|
420
|
+
* SSR-safe: `burst()` is a no-op on the server. Under reduced motion
|
|
421
|
+
* `burst()` skips the particles but still calls `onDone`, so chained
|
|
422
|
+
* logic (show the prize after the celebration) keeps working.
|
|
423
|
+
*
|
|
424
|
+
* ```tsx
|
|
425
|
+
* import { createConfetti } from "solid-drift"
|
|
426
|
+
*
|
|
427
|
+
* let cvs!: HTMLCanvasElement
|
|
428
|
+
* const confetti = createConfetti(() => cvs, {
|
|
429
|
+
* onDone: () => console.log("party over"),
|
|
430
|
+
* })
|
|
431
|
+
* <canvas ref={cvs} style={{ position: "fixed", inset: "0", "pointer-events": "none" }} />
|
|
432
|
+
* <button onClick={() => confetti.burst()}>Celebrate</button>
|
|
433
|
+
* <button onClick={() => confetti.burst({ x: 0.2, y: 0.8 })}>Side popper</button>
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
export function createConfetti(canvas, options = {}) {
|
|
437
|
+
const { count = 120, colors = CONFETTI_COLORS, spread = 70, power = 900, gravity = 1100, drag = 1.2, size = [6, 12], shapes = ["rect", "circle"], lifetime = 2600, onDone, } = options;
|
|
438
|
+
const server = typeof window === "undefined";
|
|
439
|
+
const layer = createParticleLayer(canvas, gravity, drag, onDone);
|
|
440
|
+
const burst = (origin = { x: 0.5, y: 0.6 }) => {
|
|
441
|
+
if (server || prefersReducedMotion()) {
|
|
442
|
+
onDone?.();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const cvs = canvas();
|
|
446
|
+
if (!cvs || cvs.clientWidth === 0)
|
|
447
|
+
return;
|
|
448
|
+
const ox = origin.x * cvs.clientWidth;
|
|
449
|
+
const oy = origin.y * cvs.clientHeight;
|
|
450
|
+
const [minSize, maxSize] = size;
|
|
451
|
+
for (let i = 0; i < count; i++) {
|
|
452
|
+
const angle = ((-90 + (Math.random() - 0.5) * spread) * Math.PI) / 180;
|
|
453
|
+
const speed = power * (0.4 + Math.random() * 0.8);
|
|
454
|
+
const shape = shapes[(Math.random() * shapes.length) | 0];
|
|
455
|
+
layer.spawn(() => ({
|
|
456
|
+
x: ox,
|
|
457
|
+
y: oy,
|
|
458
|
+
vx: Math.cos(angle) * speed,
|
|
459
|
+
vy: Math.sin(angle) * speed,
|
|
460
|
+
rot: Math.random() * Math.PI * 2,
|
|
461
|
+
vr: (Math.random() - 0.5) * 20,
|
|
462
|
+
sway: 40 + Math.random() * 60,
|
|
463
|
+
swaySpeed: 4 + Math.random() * 6,
|
|
464
|
+
swayPhase: Math.random() * Math.PI * 2,
|
|
465
|
+
size: minSize + Math.random() * (maxSize - minSize),
|
|
466
|
+
color: colors[(Math.random() * colors.length) | 0],
|
|
467
|
+
shape,
|
|
468
|
+
text: "",
|
|
469
|
+
born: now(),
|
|
470
|
+
life: lifetime * (0.7 + Math.random() * 0.6),
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
return { active: layer.active, burst, clear: layer.clear };
|
|
475
|
+
}
|
|
476
|
+
const BURST_EMOJI = ["\u{1F389}", "\u2728", "\u{1F4A5}", "\u2B50", "\u{1F496}", "\u{1F973}"];
|
|
477
|
+
/**
|
|
478
|
+
* Emoji celebration burst: the same particle physics as confetti,
|
|
479
|
+
* but the particles are emoji glyphs that rise, tumble gently, and
|
|
480
|
+
* fade. Reactions, likes, level-ups, chat celebrations.
|
|
481
|
+
*
|
|
482
|
+
* Same canvas setup and safety rules as `createConfetti`: SSR-safe,
|
|
483
|
+
* and under reduced motion `burst()` skips the particles but still
|
|
484
|
+
* calls `onDone`.
|
|
485
|
+
*
|
|
486
|
+
* ```tsx
|
|
487
|
+
* import { createEmojiBurst } from "solid-drift"
|
|
488
|
+
*
|
|
489
|
+
* let cvs!: HTMLCanvasElement
|
|
490
|
+
* const burst = createEmojiBurst(() => cvs, { emoji: ["\u2764\uFE0F", "\u{1F525}"] })
|
|
491
|
+
* <canvas ref={cvs} style={{ position: "fixed", inset: "0", "pointer-events": "none" }} />
|
|
492
|
+
* <button onClick={() => burst.burst()}>Send love</button>
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
export function createEmojiBurst(canvas, options = {}) {
|
|
496
|
+
const { emoji = BURST_EMOJI, count = 24, power = 650, gravity = 700, drag = 1.6, size = [24, 48], spread = 90, lifetime = 1800, onDone, } = options;
|
|
497
|
+
const server = typeof window === "undefined";
|
|
498
|
+
const layer = createParticleLayer(canvas, gravity, drag, onDone);
|
|
499
|
+
const burst = (origin = { x: 0.5, y: 0.6 }) => {
|
|
500
|
+
if (server || prefersReducedMotion()) {
|
|
501
|
+
onDone?.();
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const cvs = canvas();
|
|
505
|
+
if (!cvs || cvs.clientWidth === 0)
|
|
506
|
+
return;
|
|
507
|
+
const ox = origin.x * cvs.clientWidth;
|
|
508
|
+
const oy = origin.y * cvs.clientHeight;
|
|
509
|
+
const [minSize, maxSize] = size;
|
|
510
|
+
for (let i = 0; i < count; i++) {
|
|
511
|
+
const angle = ((-90 + (Math.random() - 0.5) * spread) * Math.PI) / 180;
|
|
512
|
+
const speed = power * (0.5 + Math.random() * 0.7);
|
|
513
|
+
layer.spawn(() => ({
|
|
514
|
+
x: ox,
|
|
515
|
+
y: oy,
|
|
516
|
+
vx: Math.cos(angle) * speed,
|
|
517
|
+
vy: Math.sin(angle) * speed,
|
|
518
|
+
rot: (Math.random() - 0.5) * Math.PI,
|
|
519
|
+
vr: (Math.random() - 0.5) * 6,
|
|
520
|
+
sway: 30 + Math.random() * 40,
|
|
521
|
+
swaySpeed: 3 + Math.random() * 4,
|
|
522
|
+
swayPhase: Math.random() * Math.PI * 2,
|
|
523
|
+
size: minSize + Math.random() * (maxSize - minSize),
|
|
524
|
+
color: "#ffffff",
|
|
525
|
+
shape: "emoji",
|
|
526
|
+
text: emoji[(Math.random() * emoji.length) | 0],
|
|
527
|
+
born: now(),
|
|
528
|
+
life: lifetime * (0.7 + Math.random() * 0.6),
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
return { active: layer.active, burst, clear: layer.clear };
|
|
533
|
+
}
|
|
534
|
+
/** Default cover: a silver holographic foil with sheen streaks. */
|
|
535
|
+
function paintFoil(ctx, w, h) {
|
|
536
|
+
const g = ctx.createLinearGradient(0, 0, w, h);
|
|
537
|
+
g.addColorStop(0, "#dde2e9");
|
|
538
|
+
g.addColorStop(0.45, "#a9b2bf");
|
|
539
|
+
g.addColorStop(0.55, "#c3ccd7");
|
|
540
|
+
g.addColorStop(1, "#98a2b1");
|
|
541
|
+
ctx.fillStyle = g;
|
|
542
|
+
ctx.fillRect(0, 0, w, h);
|
|
543
|
+
// Diagonal sheen streaks.
|
|
544
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.35)";
|
|
545
|
+
ctx.lineWidth = Math.max(2, w * 0.02);
|
|
546
|
+
for (let i = -2; i < 5; i++) {
|
|
547
|
+
ctx.beginPath();
|
|
548
|
+
ctx.moveTo((i * w) / 3, -10);
|
|
549
|
+
ctx.lineTo((i * w) / 3 + h * 0.6, h + 10);
|
|
550
|
+
ctx.stroke();
|
|
551
|
+
}
|
|
552
|
+
// Speckle.
|
|
553
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
|
|
554
|
+
const n = Math.floor((w * h) / 900);
|
|
555
|
+
for (let i = 0; i < n; i++) {
|
|
556
|
+
const x = Math.random() * w;
|
|
557
|
+
const y = Math.random() * h;
|
|
558
|
+
ctx.fillRect(x, y, 1.5, 1.5);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Scratch-off cover: a lottery-ticket foil over hidden content.
|
|
563
|
+
*
|
|
564
|
+
* The canvas paints an opaque cover (silver holographic foil by
|
|
565
|
+
* default, or your own art via `paint`). Pointer drags erase through
|
|
566
|
+
* it with `destination-out`; the cleared fraction is sampled from the
|
|
567
|
+
* alpha channel on a throttled cadence, and `onComplete` fires once
|
|
568
|
+
* past `threshold`. Layer it over the prize with absolute positioning.
|
|
569
|
+
*
|
|
570
|
+
* Set `touch-action: none` on the canvas so touch scratches do not
|
|
571
|
+
* scroll the page. Scratching is direct manipulation, so it works
|
|
572
|
+
* identically under reduced motion. SSR-safe: `cleared()` stays 0.
|
|
573
|
+
*
|
|
574
|
+
* ```tsx
|
|
575
|
+
* import { createScratch } from "solid-drift"
|
|
576
|
+
*
|
|
577
|
+
* let foil!: HTMLCanvasElement
|
|
578
|
+
* const scratch = createScratch(() => foil, {
|
|
579
|
+
* onComplete: () => console.log("revealed!"),
|
|
580
|
+
* })
|
|
581
|
+
* <div style={{ position: "relative" }}>
|
|
582
|
+
* <div>YOU WON 50 STARS</div>
|
|
583
|
+
* <canvas ref={foil} style={{ position: "absolute", inset: "0", "touch-action": "none" }} />
|
|
584
|
+
* </div>
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
587
|
+
export function createScratch(canvas, options = {}) {
|
|
588
|
+
const { threshold = 0.45, brush = 26, paint, onComplete } = options;
|
|
589
|
+
const server = typeof window === "undefined";
|
|
590
|
+
const [cleared, setCleared] = createSignal(0);
|
|
591
|
+
const [done, setDone] = createSignal(false);
|
|
592
|
+
if (server) {
|
|
593
|
+
return { cleared, done, reset: () => { } };
|
|
594
|
+
}
|
|
595
|
+
const paintCover = () => {
|
|
596
|
+
const cvs = canvas();
|
|
597
|
+
if (!cvs)
|
|
598
|
+
return;
|
|
599
|
+
const rect = cvs.getBoundingClientRect();
|
|
600
|
+
const w = Math.max(1, Math.round(rect.width));
|
|
601
|
+
const h = Math.max(1, Math.round(rect.height));
|
|
602
|
+
if (cvs.width !== w || cvs.height !== h) {
|
|
603
|
+
cvs.width = w;
|
|
604
|
+
cvs.height = h;
|
|
605
|
+
}
|
|
606
|
+
const ctx = cvs.getContext("2d");
|
|
607
|
+
if (!ctx)
|
|
608
|
+
return;
|
|
609
|
+
ctx.globalCompositeOperation = "source-over";
|
|
610
|
+
ctx.globalAlpha = 1;
|
|
611
|
+
if (paint)
|
|
612
|
+
paint(ctx, w, h);
|
|
613
|
+
else
|
|
614
|
+
paintFoil(ctx, w, h);
|
|
615
|
+
};
|
|
616
|
+
/** Fraction of sampled pixels erased, via the alpha channel. */
|
|
617
|
+
const sampleCleared = () => {
|
|
618
|
+
const cvs = canvas();
|
|
619
|
+
if (!cvs)
|
|
620
|
+
return;
|
|
621
|
+
const ctx = cvs.getContext("2d");
|
|
622
|
+
if (!ctx)
|
|
623
|
+
return;
|
|
624
|
+
const { width: w, height: h } = cvs;
|
|
625
|
+
if (w === 0 || h === 0)
|
|
626
|
+
return;
|
|
627
|
+
let data;
|
|
628
|
+
try {
|
|
629
|
+
data = ctx.getImageData(0, 0, w, h).data;
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
return; // tainted canvas: cannot read pixels
|
|
633
|
+
}
|
|
634
|
+
const step = 6;
|
|
635
|
+
let clear = 0;
|
|
636
|
+
let total = 0;
|
|
637
|
+
for (let y = 0; y < h; y += step) {
|
|
638
|
+
for (let x = 0; x < w; x += step) {
|
|
639
|
+
total++;
|
|
640
|
+
if (data[(y * w + x) * 4 + 3] < 128)
|
|
641
|
+
clear++;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const frac = total === 0 ? 0 : clear / total;
|
|
645
|
+
setCleared(frac);
|
|
646
|
+
if (!done() && frac >= threshold) {
|
|
647
|
+
setDone(true);
|
|
648
|
+
onComplete?.();
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
const eraseAt = (clientX, clientY) => {
|
|
652
|
+
const cvs = canvas();
|
|
653
|
+
if (!cvs)
|
|
654
|
+
return;
|
|
655
|
+
const rect = cvs.getBoundingClientRect();
|
|
656
|
+
if (rect.width === 0 || rect.height === 0)
|
|
657
|
+
return;
|
|
658
|
+
const ctx = cvs.getContext("2d");
|
|
659
|
+
if (!ctx)
|
|
660
|
+
return;
|
|
661
|
+
const sx = cvs.width / rect.width;
|
|
662
|
+
const sy = cvs.height / rect.height;
|
|
663
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
664
|
+
ctx.beginPath();
|
|
665
|
+
ctx.arc((clientX - rect.left) * sx, (clientY - rect.top) * sy, brush * Math.max(sx, sy), 0, Math.PI * 2);
|
|
666
|
+
ctx.fill();
|
|
667
|
+
ctx.globalCompositeOperation = "source-over";
|
|
668
|
+
};
|
|
669
|
+
// Throttle the (relatively expensive) pixel sampling.
|
|
670
|
+
let lastSample = 0;
|
|
671
|
+
const maybeSample = () => {
|
|
672
|
+
const t = now();
|
|
673
|
+
if (t - lastSample < 120)
|
|
674
|
+
return;
|
|
675
|
+
lastSample = t;
|
|
676
|
+
sampleCleared();
|
|
677
|
+
};
|
|
678
|
+
const onDown = (event) => {
|
|
679
|
+
if (event.isPrimary === false)
|
|
680
|
+
return;
|
|
681
|
+
const cvs = canvas();
|
|
682
|
+
if (!cvs)
|
|
683
|
+
return;
|
|
684
|
+
eraseAt(event.clientX, event.clientY);
|
|
685
|
+
maybeSample();
|
|
686
|
+
try {
|
|
687
|
+
cvs.setPointerCapture(event.pointerId);
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
// setPointerCapture may throw for synthetic events; the window
|
|
691
|
+
// fallback below is not needed since moves stay on the canvas.
|
|
692
|
+
}
|
|
693
|
+
const move = (ev) => {
|
|
694
|
+
eraseAt(ev.clientX, ev.clientY);
|
|
695
|
+
maybeSample();
|
|
696
|
+
};
|
|
697
|
+
const up = () => {
|
|
698
|
+
cvs.removeEventListener("pointermove", move);
|
|
699
|
+
cvs.removeEventListener("pointerup", up);
|
|
700
|
+
cvs.removeEventListener("pointercancel", up);
|
|
701
|
+
sampleCleared(); // final accurate read
|
|
702
|
+
};
|
|
703
|
+
cvs.addEventListener("pointermove", move);
|
|
704
|
+
cvs.addEventListener("pointerup", up);
|
|
705
|
+
cvs.addEventListener("pointercancel", up);
|
|
706
|
+
};
|
|
707
|
+
// Late-bound refs (Solid assigns `ref` after mount) get the cover
|
|
708
|
+
// painted and the gesture attached as soon as they exist.
|
|
709
|
+
createEffect(() => {
|
|
710
|
+
const cvs = canvas();
|
|
711
|
+
if (!cvs)
|
|
712
|
+
return;
|
|
713
|
+
paintCover();
|
|
714
|
+
cvs.addEventListener("pointerdown", onDown);
|
|
715
|
+
onCleanup(() => cvs.removeEventListener("pointerdown", onDown));
|
|
716
|
+
});
|
|
717
|
+
const reset = () => {
|
|
718
|
+
setCleared(0);
|
|
719
|
+
setDone(false);
|
|
720
|
+
paintCover();
|
|
721
|
+
};
|
|
722
|
+
return { cleared, done, reset };
|
|
723
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,8 +12,9 @@ export { createScrollProgress, type ScrollTarget } from "./scroll.js";
|
|
|
12
12
|
export { createInView, type InViewOptions } from "./inview.js";
|
|
13
13
|
export { usePrefersReducedMotion, prefersReducedMotion, } from "./reduced-motion.js";
|
|
14
14
|
export { isLowPowerMode, useLowPowerMode, type LowPowerOptions, } from "./power.js";
|
|
15
|
-
export { createSlotMachine, type SlotMachineControls, type SlotMachineOptions, type SlotMachineStatus, createRedPacket, type RedPacketCoin, type RedPacketControls, type RedPacketOptions, type RedPacketStatus, } from "./fun.js";
|
|
15
|
+
export { createSlotMachine, type SlotMachineControls, type SlotMachineOptions, type SlotMachineStatus, createRedPacket, type RedPacketCoin, type RedPacketControls, type RedPacketOptions, type RedPacketStatus, createConfetti, type ConfettiControls, type ConfettiOptions, createEmojiBurst, type EmojiBurstControls, type EmojiBurstOptions, createScratch, type ScratchControls, type ScratchOptions, } from "./fun.js";
|
|
16
16
|
export { createStagger } from "./stagger.js";
|
|
17
|
+
export { createDebounced, createThrottled, createLocalStorage, type LocalStorageOptions, type LocalStorageControls, createMediaQuery, createClickOutside, type ClickOutsideOptions, createScrollLock, type ScrollLockControls, createInfiniteScroll, type InfiniteScrollOptions, } from "./dom.js";
|
|
17
18
|
export { createHorizontalScroll, type HorizontalScrollOptions, type HorizontalScrollResult, } from "./horizontal.js";
|
|
18
19
|
export { createScrub, type ScrubKeyframe, type ScrubOptions } from "./scrub.js";
|
|
19
20
|
export { createScrollColor, type ScrollColorStop, type ScrollColorOptions, type ScrollColorFormat, createScrollTracking, type ScrollTrackingOptions, createScrollLine, type ScrollLineOptions, type ScrollLineStyle, type ScrollLineAxis, type ScrollLineOrigin, } from "./scrollfx.js";
|
package/dist/index.js
CHANGED
|
@@ -12,8 +12,9 @@ export { createScrollProgress } from "./scroll.js";
|
|
|
12
12
|
export { createInView } from "./inview.js";
|
|
13
13
|
export { usePrefersReducedMotion, prefersReducedMotion, } from "./reduced-motion.js";
|
|
14
14
|
export { isLowPowerMode, useLowPowerMode, } from "./power.js";
|
|
15
|
-
export { createSlotMachine, createRedPacket, } from "./fun.js";
|
|
15
|
+
export { createSlotMachine, createRedPacket, createConfetti, createEmojiBurst, createScratch, } from "./fun.js";
|
|
16
16
|
export { createStagger } from "./stagger.js";
|
|
17
|
+
export { createDebounced, createThrottled, createLocalStorage, createMediaQuery, createClickOutside, createScrollLock, createInfiniteScroll, } from "./dom.js";
|
|
17
18
|
export { createHorizontalScroll, } from "./horizontal.js";
|
|
18
19
|
export { createScrub } from "./scrub.js";
|
|
19
20
|
export { createScrollColor, createScrollTracking, createScrollLine, } from "./scrollfx.js";
|
package/package.json
CHANGED