solid-drift 0.14.0 → 0.15.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
@@ -1164,6 +1164,36 @@ const machine = createSlotMachine({
1164
1164
 
1165
1165
  Options: `symbols` (required, at least 2), `reels` (default `3`), `duration` (ms for the first reel, default `1400`), `stagger` (extra ms per subsequent reel, default `500`), `minSpins` (full rotations before stopping, default `3`), `easing` (default `"easeOutQuart"`), `onTick(reel, symbol)`, `onDone(result)`. Returns `{ values, result, status, spin, stop, reset }`: `values()` is the visible symbol per reel, `result()` the final symbols of the last spin, `status()` is `"idle"`, `"spinning"`, or `"done"`. `stop()` halts at the current symbols; `reset()` returns to idle. SSR-safe and reduced-motion safe: `spin()` jumps straight to the result.
1166
1166
 
1167
+ ### `createRedPacket(options?)`
1168
+
1169
+ Crypto red packet ceremony: tap to open, coins burst out with physics, the amount counts up. The primitive owns the ceremony state machine (`"sealed"`, `"opening"`, `"bursting"`, `"revealed"`) and the coin particle physics; you render the envelope and the coins. Each coin carries position, rotation, size, opacity, and its share of the total, split randomly like a real red packet grab.
1170
+
1171
+ ```tsx
1172
+ import { createRedPacket } from "solid-drift"
1173
+
1174
+ const packet = createRedPacket({ amount: 88, coins: 14 })
1175
+
1176
+ <button onClick={() => packet.open()}>
1177
+ {packet.status() === "sealed"
1178
+ ? "🧧 Tap to open"
1179
+ : `$${packet.revealed().toFixed(2)}`}
1180
+ </button>
1181
+ <For each={packet.coins()}>
1182
+ {(coin) => (
1183
+ <div
1184
+ class="coin"
1185
+ style={{
1186
+ transform: `translate(${coin.x}px, ${coin.y}px) rotate(${coin.rotation}deg)`,
1187
+ opacity: coin.opacity,
1188
+ width: `${coin.size}px`,
1189
+ }}
1190
+ />
1191
+ )}
1192
+ </For>
1193
+ ```
1194
+
1195
+ 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.
1196
+
1167
1197
  ### Easings
1168
1198
 
1169
1199
  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/fun.d.ts CHANGED
@@ -62,3 +62,90 @@ export interface SlotMachineControls<T = string> {
62
62
  * ```
63
63
  */
64
64
  export declare function createSlotMachine<T = string>(options: SlotMachineOptions<T>): SlotMachineControls<T>;
65
+ export type RedPacketStatus = "sealed" | "opening" | "bursting" | "revealed";
66
+ export interface RedPacketCoin {
67
+ id: number;
68
+ /** Pixels right from the packet center. */
69
+ x: number;
70
+ /** Pixels down from the packet center. */
71
+ y: number;
72
+ vx: number;
73
+ vy: number;
74
+ /** Degrees. */
75
+ rotation: number;
76
+ /** Degrees per second. */
77
+ vr: number;
78
+ /** Diameter in pixels. */
79
+ size: number;
80
+ /** 0..1, fades at the end of life. */
81
+ opacity: number;
82
+ /** This coin's share of the total amount. */
83
+ amount: number;
84
+ }
85
+ export interface RedPacketOptions {
86
+ /** Coins in the burst. Default 12. */
87
+ coins?: number;
88
+ /** Total amount, split randomly across coins like a real red packet. Default 88. */
89
+ amount?: number;
90
+ /** Burst size in pixels, scales launch speed. Default 160. */
91
+ spread?: number;
92
+ /** Gravity in px/s^2. Default 900. */
93
+ gravity?: number;
94
+ /** Envelope opening ceremony in ms. Default 500. */
95
+ openDuration?: number;
96
+ /** Coin burst in ms before the reveal. Default 1600. */
97
+ burstDuration?: number;
98
+ /** Amount count-up in ms once revealed. Default 800. */
99
+ revealDuration?: number;
100
+ /** Called when the packet is tapped open. */
101
+ onOpen?: () => void;
102
+ /** Called with the total amount when the reveal lands. */
103
+ onReveal?: (amount: number) => void;
104
+ }
105
+ export interface RedPacketControls {
106
+ status: Accessor<RedPacketStatus>;
107
+ /** Live coin particles, centered on the packet. Render them absolutely. */
108
+ coins: Accessor<RedPacketCoin[]>;
109
+ /** Amount counted up so far. Reaches the total when revealed. */
110
+ revealed: Accessor<number>;
111
+ /** Tap the packet: sealed -> opening -> bursting -> revealed. */
112
+ open: () => void;
113
+ /** Back to sealed. */
114
+ reset: () => void;
115
+ }
116
+ /**
117
+ * Crypto red packet ceremony: tap to open, coins burst out with physics,
118
+ * the amount counts up. The hongbao moment, as a signal-native primitive.
119
+ *
120
+ * The primitive owns the ceremony state machine and the coin particle
121
+ * physics; you render the envelope and the coins however fits your design.
122
+ * Each coin carries position, rotation, size, opacity, and its share of
123
+ * the total amount, split randomly like a real red packet grab.
124
+ *
125
+ * SSR-safe: `open()` jumps straight to revealed on the server. Under
126
+ * reduced motion the packet opens instantly with no burst: the amount
127
+ * just appears.
128
+ *
129
+ * ```tsx
130
+ * import { createRedPacket } from "solid-drift"
131
+ *
132
+ * const packet = createRedPacket({ amount: 88, coins: 14 })
133
+ *
134
+ * <button onClick={() => packet.open()}>
135
+ * {packet.status() === "sealed" ? "🧧 Tap to open" : `$${packet.revealed().toFixed(2)}`}
136
+ * </button>
137
+ * <For each={packet.coins()}>
138
+ * {(coin) => (
139
+ * <div
140
+ * class="coin"
141
+ * style={{
142
+ * transform: `translate(${coin.x}px, ${coin.y}px) rotate(${coin.rotation}deg)`,
143
+ * opacity: coin.opacity,
144
+ * width: `${coin.size}px`,
145
+ * }}
146
+ * />
147
+ * )}
148
+ * </For>
149
+ * ```
150
+ */
151
+ export declare function createRedPacket(options?: RedPacketOptions): RedPacketControls;
package/dist/fun.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createSignal, onCleanup, } from "solid-js";
2
2
  import { now, schedule } from "./engine.js";
3
3
  import { resolveEasing } from "./easing.js";
4
+ import { createTween } from "./tween.js";
4
5
  import { prefersReducedMotion } from "./reduced-motion.js";
5
6
  /**
6
7
  * Gacha slot machine: reels launch fast, decelerate with momentum, and
@@ -131,3 +132,145 @@ export function createSlotMachine(options) {
131
132
  onCleanup(() => cancel?.());
132
133
  return { values, result, status, spin, stop, reset };
133
134
  }
135
+ /**
136
+ * Crypto red packet ceremony: tap to open, coins burst out with physics,
137
+ * the amount counts up. The hongbao moment, as a signal-native primitive.
138
+ *
139
+ * The primitive owns the ceremony state machine and the coin particle
140
+ * physics; you render the envelope and the coins however fits your design.
141
+ * Each coin carries position, rotation, size, opacity, and its share of
142
+ * the total amount, split randomly like a real red packet grab.
143
+ *
144
+ * SSR-safe: `open()` jumps straight to revealed on the server. Under
145
+ * reduced motion the packet opens instantly with no burst: the amount
146
+ * just appears.
147
+ *
148
+ * ```tsx
149
+ * import { createRedPacket } from "solid-drift"
150
+ *
151
+ * const packet = createRedPacket({ amount: 88, coins: 14 })
152
+ *
153
+ * <button onClick={() => packet.open()}>
154
+ * {packet.status() === "sealed" ? "🧧 Tap to open" : `$${packet.revealed().toFixed(2)}`}
155
+ * </button>
156
+ * <For each={packet.coins()}>
157
+ * {(coin) => (
158
+ * <div
159
+ * class="coin"
160
+ * style={{
161
+ * transform: `translate(${coin.x}px, ${coin.y}px) rotate(${coin.rotation}deg)`,
162
+ * opacity: coin.opacity,
163
+ * width: `${coin.size}px`,
164
+ * }}
165
+ * />
166
+ * )}
167
+ * </For>
168
+ * ```
169
+ */
170
+ export function createRedPacket(options = {}) {
171
+ const { coins: coinCount = 12, amount = 88, spread = 160, gravity = 900, openDuration = 500, burstDuration = 1600, revealDuration = 800, onOpen, onReveal, } = options;
172
+ const [status, setStatus] = createSignal("sealed");
173
+ const [coinList, setCoinList] = createSignal([]);
174
+ const [target, setTarget] = createSignal(0);
175
+ const tweened = createTween(target, {
176
+ duration: revealDuration,
177
+ easing: "easeOutExpo",
178
+ });
179
+ // On the server the tween effect never runs, so read the target directly.
180
+ const revealed = () => typeof window === "undefined" ? target() : tweened();
181
+ let cancel = null;
182
+ let burstAt = 0;
183
+ let lastT = 0;
184
+ let nextCoinId = 1;
185
+ const spawnCoins = () => {
186
+ const weights = Array.from({ length: coinCount }, () => 0.5 + Math.random());
187
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
188
+ const speedScale = spread / 160;
189
+ return weights.map((w, i) => {
190
+ const angle = ((Math.random() * 120 - 60) * Math.PI) / 180;
191
+ const speed = (200 + Math.random() * 220) * speedScale;
192
+ return {
193
+ id: nextCoinId++,
194
+ x: 0,
195
+ y: 0,
196
+ vx: Math.sin(angle) * speed,
197
+ vy: -Math.cos(angle) * speed,
198
+ rotation: Math.random() * 360,
199
+ vr: (Math.random() - 0.5) * 720,
200
+ size: 24 + Math.random() * 16,
201
+ opacity: 1,
202
+ amount: (amount * w) / totalWeight,
203
+ };
204
+ });
205
+ };
206
+ const stepCoins = (t) => {
207
+ // Every coin is born at burstAt, so they share one age and one fade.
208
+ const dt = Math.min(Math.max((t - lastT) / 1000, 0), 0.05);
209
+ lastT = t;
210
+ const age = t - burstAt;
211
+ if (age >= burstDuration) {
212
+ setCoinList([]);
213
+ return;
214
+ }
215
+ const fadeStart = burstDuration * 0.7;
216
+ const opacity = age < fadeStart
217
+ ? 1
218
+ : Math.max(1 - (age - fadeStart) / (burstDuration - fadeStart), 0);
219
+ setCoinList((prev) => prev.map((c) => {
220
+ const vy = c.vy + gravity * dt;
221
+ return {
222
+ ...c,
223
+ x: c.x + c.vx * dt,
224
+ y: c.y + vy * dt,
225
+ vy,
226
+ rotation: c.rotation + c.vr * dt,
227
+ opacity,
228
+ };
229
+ }));
230
+ };
231
+ const open = () => {
232
+ if (status() !== "sealed")
233
+ return;
234
+ onOpen?.();
235
+ if (typeof window === "undefined" || prefersReducedMotion()) {
236
+ // Accessibility / SSR: no ceremony, the amount just appears.
237
+ setTarget(amount);
238
+ setStatus("revealed");
239
+ onReveal?.(amount);
240
+ return;
241
+ }
242
+ setStatus("opening");
243
+ const t0 = now();
244
+ cancel?.();
245
+ cancel = schedule((t) => {
246
+ const s = status();
247
+ if (s === "opening" && t - t0 >= openDuration) {
248
+ burstAt = t;
249
+ lastT = t;
250
+ setCoinList(spawnCoins());
251
+ setStatus("bursting");
252
+ }
253
+ else if (s === "bursting") {
254
+ stepCoins(t);
255
+ if (t - burstAt >= burstDuration) {
256
+ setCoinList([]);
257
+ setTarget(amount);
258
+ setStatus("revealed");
259
+ onReveal?.(amount);
260
+ cancel = null;
261
+ return false;
262
+ }
263
+ }
264
+ return true;
265
+ });
266
+ };
267
+ const reset = () => {
268
+ cancel?.();
269
+ cancel = null;
270
+ setCoinList([]);
271
+ setTarget(0);
272
+ setStatus("sealed");
273
+ };
274
+ onCleanup(() => cancel?.());
275
+ return { status, coins: coinList, revealed, open, reset };
276
+ }
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ 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, } from "./fun.js";
15
+ export { createSlotMachine, type SlotMachineControls, type SlotMachineOptions, type SlotMachineStatus, createRedPacket, type RedPacketCoin, type RedPacketControls, type RedPacketOptions, type RedPacketStatus, } from "./fun.js";
16
16
  export { createStagger } from "./stagger.js";
17
17
  export { createHorizontalScroll, type HorizontalScrollOptions, type HorizontalScrollResult, } from "./horizontal.js";
18
18
  export { createScrub, type ScrubKeyframe, type ScrubOptions } from "./scrub.js";
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ 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, } from "./fun.js";
15
+ export { createSlotMachine, createRedPacket, } from "./fun.js";
16
16
  export { createStagger } from "./stagger.js";
17
17
  export { createHorizontalScroll, } from "./horizontal.js";
18
18
  export { createScrub } from "./scrub.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.14.0"
46
+ "version": "0.15.0"
47
47
  }