UNPKG

react-fidget-spinner

Version:

Turn any React component into an interactive clickable fidget spinner! 🪿

863 lines (862 loc) • 21.9 kB
import { jsx as L, jsxs as ze } from "react/jsx-runtime"; import { useRef as r, useCallback as d, useEffect as me, useState as X, useMemo as de } from "react"; import { useDebounceCallback as Fe } from "usehooks-ts"; import * as e from "valibot"; import nn from "bezier-easing"; const ue = (n, o = !0) => { const t = r(0), a = r(0), i = d( (l) => { if (a.current != null) { const g = l - a.current; n(g); } a.current = l, t.current = requestAnimationFrame(i); }, [n] ); me(() => { if (o) return t.current = requestAnimationFrame(i), () => cancelAnimationFrame(t.current); }, [o, i]); }, le = e.tuple([e.number(), e.number(), e.number(), e.number()]), _ = (n) => nn(n[0], n[1], n[2], n[3]); var D = /* @__PURE__ */ ((n) => (n.Plus = "Plus", n.Minus = "Minus", n.PlusMinus = "PlusMinus", n))(D || {}), N = /* @__PURE__ */ ((n) => (n.Percent = "Percent", n.Absolute = "Absolute", n))(N || {}); const qe = e.object({ type: e.union([e.literal( "Plus" /* Plus */ ), e.literal( "Minus" /* Minus */ ), e.literal( "PlusMinus" /* PlusMinus */ )]), unit: e.union([e.literal( "Percent" /* Percent */ ), e.literal( "Absolute" /* Absolute */ )]), value: e.number() }); e.object({ value: e.number(), variation: e.optional(qe) }); const b = e.union([ e.number(), e.object({ value: e.number(), variation: e.optional(qe) }) ]), p = (n) => { if (typeof n == "number") return n; if (!n.variation) return n.value; const { variation: o, value: t } = n, a = Math.random() * 2 - 1; if (o.unit === "Absolute") { if (o.type === "Plus") return t + Math.random() * o.value; if (o.type === "Minus") return t - Math.random() * o.value; if (o.type === "PlusMinus") return t + a * o.value; throw new Error("Invalid variation type"); } else if (o.unit === "Percent") { const i = o.value / 100; if (o.type === "Plus") return t + t * Math.random() * i; if (o.type === "Minus") return t - t * Math.random() * i; if (o.type === "PlusMinus") return t + t * a * i; throw new Error("Invalid variation type"); } else throw new Error("Invalid variation unit"); }, Ue = e.object({ active: e.boolean(), components: e.array(e.string()), durationMs: b, scaleEnd: b, frameRate: e.pipe(e.number(), e.toMinValue(0)), spawnIntervalMs: b, onRemove: e.function(), onSpawn: e.function(), opacityEasing: le, opacityEnd: b, opacityStart: b, scaleEasing: le, scaleStart: b, wobbleAmplitude: b, wobbleFrequency: b, yEasing: le, yEnd: b, yStart: b, xStart: b }), tn = { active: !1, components: ["💸", "🔥"], durationMs: { value: 1e3, variation: { type: D.Plus, unit: N.Absolute, value: 1e3 } }, scaleEnd: { value: 2, variation: { type: D.PlusMinus, unit: N.Percent, value: 20 } }, frameRate: 60, spawnIntervalMs: { value: 600, variation: { type: D.PlusMinus, unit: N.Absolute, value: 400 } }, onRemove: () => { }, onSpawn: () => { }, opacityEasing: [0.25, -0.75, 0.8, 1.2], opacityEnd: 0, opacityStart: 1, scaleEasing: [0.25, -0.75, 0.8, 1.2], scaleStart: { value: 1, variation: { type: D.PlusMinus, unit: N.Percent, value: 50 } }, wobbleAmplitude: { value: 1, variation: { type: D.Plus, unit: N.Absolute, value: 40 } }, wobbleFrequency: { value: 0.1, variation: { type: D.Plus, unit: N.Absolute, value: 0.4 } }, yEasing: [0.25, 0, 0.8, 1.2], yEnd: { value: 100, variation: { type: D.Plus, unit: N.Absolute, value: 200 } }, yStart: 0, xStart: { value: 0, variation: { type: D.PlusMinus, unit: N.Absolute, value: 100 } } }, Me = (n = {}) => { const o = { ...tn, ...n }; return e.parse(Ue, o); }, We = () => Math.random().toString(36).substring(2, 15), on = (n) => { const { spawnIntervalMs: o, // minSpawnIntervalMs, // maxSpawnIntervalMs, components: t, durationMs: a, // durationMsRandomness, opacityEasing: i, opacityStart: l, opacityEnd: g, scaleStart: C, // scaleStartRandomness, scaleEasing: u, scaleEnd: M, // scaleEndRandomness, wobbleFrequency: S, // wobbleFrequencyRandomness, wobbleAmplitude: E, // wobbleAmplitudeRandomness, // xOffsetRandomness, onSpawn: y, onRemove: V, yEasing: $, frameRate: z, yStart: I, yEnd: j, xStart: A, // yRandomness, active: F } = Me(n), [s, h] = X({}), w = de(() => Object.values(s), [s]), B = d( (m, R) => { h((k) => ({ ...k, [m]: R })); }, [h] ), q = d( (m) => { h((R) => { const k = { ...R }; return delete k[m], k; }); }, [h] ), f = r(performance.now()), P = r(p(o)), v = d(() => { const m = performance.now(); if (m - f.current > P.current) { f.current = m; const k = p(o); P.current = k; const T = p(E), H = p(S), Y = Math.random() < 0.5 ? -1 : 1, J = (O) => { const ne = O / 1e3, se = Math.sin(ne * Math.PI * 2 * H) * T * 0.6 + Math.cos(ne * Math.PI * 3.7 * H) * T * 0.4 + Math.sin(ne * Math.PI * 5.3 * H) * T * 0.2; return Y * se; }, G = p(a), U = -p(j), W = We(), ee = t[Math.floor(Math.random() * t.length)], K = { id: W, durationMs: G, scaleStart: p(C), scaleEnd: p(M), scaleEasing: _(u), opacityStart: p(l), opacityEnd: p(g), opacityEasing: _(i), yStart: p(I), yEnd: U, yEasing: _($), xStart: p(A), xWobbleFunction: J, cleanup: () => { q(W); }, children: ee, frameRate: z, onSpawn: y, onRemove: V }; B(W, K); } }, [ E, S, a, C, M, u, i, l, g, j, $, I, t, z, y, V, B, q, A, o ]); return ue(v, F), /* @__PURE__ */ L( "div", { style: { position: "relative" }, children: w.map((m) => /* @__PURE__ */ L(an, { ...m }, m.id)) } ); }, an = ({ durationMs: n, scaleStart: o, scaleEnd: t, scaleEasing: a, opacityStart: i, opacityEnd: l, opacityEasing: g, yStart: C, yEnd: u, yEasing: M, xStart: S, xWobbleFunction: E, cleanup: y, frameRate: V, children: $, onSpawn: z, onRemove: I }) => { const j = r(performance.now()), A = r(o), F = r(i), s = r(S), h = r(C), [w, B] = X({ x: S, y: C, scale: o, opacity: i }), q = 1e3 / V, f = Fe(B, q, { maxWait: q }), [P, v] = X(!0); me(() => (z(), () => { I(); }), [z, I]); const m = d(() => { const R = performance.now() - j.current, k = Math.min(R / n, 1), T = g(k); F.current = i + (l - i) * T; const H = M(k); h.current = C + H * (u - C); const Y = a(k); A.current = o + (t - o) * Y; const J = E(R) + S; s.current = J, f({ x: J, y: h.current, scale: A.current, opacity: F.current }), k >= 1 && (v(!1), y()); }, [ i, l, C, u, E, o, t, g, M, a, S, n, y, f ]); return ue(m, P), P ? /* @__PURE__ */ L( "div", { style: { left: "50%", top: "50%", position: "absolute", scale: w.scale, opacity: w.opacity.toString(), userSelect: "none", WebkitUserSelect: "none", MozUserSelect: "none", transform: `translate(calc(${w.x}px - 50%), calc(${w.y}px - 50%)) scale(${w.scale})` }, children: $ } ) : null; }, De = e.object({ active: e.boolean(), components: e.array(e.any()), distanceEasing: e.tuple([e.number(), e.number(), e.number(), e.number()]), distanceStart: b, distanceEnd: b, durationMs: b, frameRate: b, intensity: b, onRemove: e.function(), onSpawn: e.function(), opacityEasing: e.tuple([e.number(), e.number(), e.number(), e.number()]), opacityEnd: b, opacityStart: b, scaleEasing: e.tuple([e.number(), e.number(), e.number(), e.number()]), scaleEnd: b, scaleStart: b, spawnIntervalMs: b }), sn = { active: !0, components: ["💸", "🔥"], distanceEasing: [0.25, 0, 0.8, 1.2], distanceStart: 0, durationMs: 1e3, frameRate: 50, intensity: 1, distanceEnd: { value: 400, variation: { type: D.PlusMinus, unit: N.Percent, value: 50 } }, onRemove: () => { }, onSpawn: () => { }, opacityEasing: [0.25, 0, 0.8, 1.2], opacityEnd: 0, opacityStart: 1, scaleEasing: [0.25, 0, 0.8, 1.2], scaleEnd: 5, scaleStart: 0.5, spawnIntervalMs: { value: 500, variation: { type: D.PlusMinus, unit: N.Percent, value: 50 } } }, ke = (n = {}) => e.parse(De, { ...sn, ...n }), rn = (n) => { const { components: o, durationMs: t, distanceStart: a, distanceEnd: i, distanceEasing: l, opacityEasing: g, opacityStart: C, opacityEnd: u, scaleEasing: M, scaleStart: S, scaleEnd: E, onSpawn: y, onRemove: V, frameRate: $, active: z, spawnIntervalMs: I } = ke(n), [j, A] = X({}), F = de(() => Object.values(j), [j]), s = d( (f, P) => { A((v) => ({ ...v, [f]: P })); }, [A] ), h = d( (f) => { A((P) => { const v = { ...P }; return delete v[f], v; }); }, [A] ), w = r(performance.now()), B = r(p(I)), q = d(() => { const f = performance.now(); if (f - w.current > B.current) { w.current = f, B.current = p(I); const v = We(), m = o[Math.floor(Math.random() * o.length)], R = Math.random() * 2 * Math.PI, k = { id: v, durationMs: p(t), frameRate: p($), opacityStart: p(C), opacityEnd: p(u), opacityEasing: g, distanceStart: p(a), distanceEnd: p(i), distanceEasing: l, onSpawn: y, onRemove: V, cleanup: () => { h(v); }, angleRadians: R, scaleStart: p(S), scaleEnd: p(E), scaleEasing: M, Component: m }; s(v, k); } }, [ w, s, h, o, t, $, C, u, g, l, y, V, S, E, M, a, I, i ]); return ue(q, z), /* @__PURE__ */ L("div", { style: { position: "relative" }, children: F.map((f) => /* @__PURE__ */ L(cn, { ...f }, f.id)) }); }, cn = ({ durationMs: n, frameRate: o, angleRadians: t, scaleStart: a, scaleEnd: i, scaleEasing: l, opacityStart: g, opacityEnd: C, opacityEasing: u, distanceStart: M, distanceEnd: S, distanceEasing: E, onSpawn: y, onRemove: V, cleanup: $, Component: z }) => { const I = r(performance.now()), j = Math.cos(t) * M, A = Math.sin(t) * M, F = r(j), s = r(A), h = r(a), w = r(g), [B, q] = X(!0); me(() => (y(), () => { V(); }), [y, V]); const [f, P] = X({ x: j, y: A, scale: a, opacity: g }), v = 1e3 / o, m = Fe(P, v, { maxWait: v }), R = d(() => { const k = performance.now() - I.current, T = Math.min(k / n, 1), H = _(u)(T); w.current = g + (C - g) * H; const Y = _(l)(T); h.current = a + (i - a) * Y; const J = _(E)(T), G = M + (S - M) * J, U = t; F.current = Math.cos(U) * G, s.current = Math.sin(U) * G, m({ x: F.current, y: s.current, opacity: w.current, scale: h.current }), T >= 1 && (q(!1), $()); }, [ m, $, M, S, E, g, C, u, a, i, l, t, n ]); return ue(R, B), B ? /* @__PURE__ */ L( "div", { style: { left: "50%", top: "50%", position: "absolute", userSelect: "none", WebkitUserSelect: "none", MozUserSelect: "none", opacity: f.opacity, transform: `translate(calc(${f.x}px - 50%), calc(${f.y}px - 50%)) scale(${f.scale})` }, children: z } ) : null; }, Ne = e.object({ dampingCoefficient: e.pipe(e.number(), e.toMinValue(0), e.toMaxValue(1)), initialAngle: e.pipe(e.number(), e.toMinValue(0), e.toMaxValue(Math.PI * 2)), initialAngularVelocity: e.pipe(e.number(), e.toMinValue(0)), maxAngularVelocity: e.pipe(e.number(), e.toMinValue(0)), onMaxAngularVelocity: e.function(), onClick: e.function(), direction: e.union([e.literal("clockwise"), e.literal("antiClockwise")]) }), ln = { dampingCoefficient: 0.5, initialAngle: 0, initialAngularVelocity: 0, maxAngularVelocity: Math.PI * 20, onMaxAngularVelocity: () => { }, onClick: () => { }, direction: "clockwise" }, Oe = (n = {}) => e.parse(Ne, { ...ln, ...n }), Xe = e.object({ onScaleChange: e.function(), onScaleEnd: e.function(), onScaleStart: e.function(), scale: e.pipe(e.number(), e.toMinValue(0)), scaleDurationMs: e.pipe(e.number(), e.toMinValue(0)), scaleEasing: le }), un = { onScaleChange: () => { }, onScaleEnd: () => { }, onScaleStart: () => { }, scale: 1, scaleDurationMs: 500, scaleEasing: [0.25, -0.75, 0.8, 1.2] }, Je = (n = {}) => e.parse(Xe, { ...un, ...n }), Le = e.object({ durationMs: e.pipe(e.number(), e.toMinValue(0)), easing: le, onResetStart: e.function(), onResetEnd: e.function(), onResetCancel: e.function() }), fn = { durationMs: 200, easing: [0.25, -0.75, 0.8, 1.2], onResetStart: () => { }, onResetEnd: () => { }, onResetCancel: () => { } }, He = (n = {}) => e.parse(Le, { ...fn, ...n }), Ye = e.object({ angularVelocityPerClick: b, onSpawn: e.function(), onRemove: e.function(), active: e.boolean() }), pn = { angularVelocityPerClick: Math.PI * 2, onSpawn: () => { }, onRemove: () => { }, active: !0 }, Ge = (n = {}) => e.parse(Ye, { ...pn, ...n }), gn = e.object({ scaleConfig: Xe, bubbleConfig: Ue, sparkConfig: De, resetConfig: Le, spinnerConfig: Ne, clickConfig: Ye }), Ke = e.object({ breakpoint: e.pipe(e.number(), e.toMinValue(0), e.toMaxValue(1)), config: gn }), bn = e.array(Ke), mn = [ { breakpoint: 0.9, config: { scaleConfig: { scale: 3 } } }, { breakpoint: 0.7, config: { scaleConfig: { scale: 2 } } }, { breakpoint: 0.3, config: { scaleConfig: { scale: 1.5 } } } ], dn = (n) => { const { breakpoint: o, config: t } = n, { scaleConfig: a, bubbleConfig: i, sparkConfig: l, resetConfig: g, spinnerConfig: C, clickConfig: u } = t; return e.parse(Ke, { breakpoint: o, config: { scaleConfig: Je(a), bubbleConfig: Me(i), sparkConfig: ke(l), resetConfig: He(g), spinnerConfig: Oe(C), clickConfig: Ge(u) } }); }, Cn = (n = mn, o) => { const t = n.map((a) => { const i = a.config, l = { scaleConfig: { ...o.scaleConfig, ...i.scaleConfig }, bubbleConfig: { ...o.bubbleConfig, ...i.bubbleConfig }, sparkConfig: { ...o.sparkConfig, ...i.sparkConfig }, resetConfig: { ...o.resetConfig, ...i.resetConfig }, spinnerConfig: { ...o.spinnerConfig, ...i.spinnerConfig }, clickConfig: { ...o.clickConfig, ...i.clickConfig } }; return dn({ ...a, config: l }); }); return e.parse(bn, t); }, ae = (n = {}, o) => { const t = o(n), [a, i] = X(t), l = de(() => JSON.stringify(n), [n]); me(() => { const C = o(JSON.parse(l)); i(C); }, [l, o]); const g = d(() => { i(t); }, [t]); return [a, i, t, g]; }, yn = "fidget-spinner-container", wn = ({ bubbleConfig: n, children: o, resetConfig: t, scaleConfig: a, sparkConfig: i, spinnerConfig: l, velocityBreakpoints: g, clickConfig: C }) => { const [u, M, S, E] = ae( a, Je ), [y, V, $, z] = ae( t, He ), [I, j, A, F] = ae( i, ke ), [s, h, w, B] = ae( l, Oe ), [q, f, P, v] = ae( n, Me ), [m, R, k, T] = ae( C, Ge ), Y = Cn(g, { scaleConfig: S, resetConfig: $, bubbleConfig: P, sparkConfig: A, spinnerConfig: w, clickConfig: k }), [J, G] = X(s.initialAngle), U = r(s.initialAngle), W = r(s.initialAngularVelocity), ee = r(!1), K = r(null), O = r(null), ne = r(null), se = r(null), Ce = r(null), he = r(!1), we = r(S.scale), re = r(null), [Qe, Ae] = X(S.scale), [fe, Ee] = X(!1), Pe = de(() => [...Y].sort((x, Q) => Q.breakpoint - x.breakpoint), [Y]), Re = d(() => { const x = Math.abs(W.current) / s.maxAngularVelocity; return Pe.find((te) => x >= te.breakpoint) || null; }, [Pe, s.maxAngularVelocity]), xe = d(() => { F(), v(), E(), z(), B(), T(); }, [F, v, E, z, B, T]), ye = d( ({ newScale: x = 1 }) => { ne.current = performance.now(), se.current = we.current, Ce.current = x, he.current = !0, u.onScaleStart(), u.onScaleChange(x); }, [u] ), Ve = d(() => { ne.current = null, se.current = null, Ce.current = null, he.current = !1, u.onScaleEnd(); }, [u]), Ie = d(() => { const x = u.scaleDurationMs, Q = ne.current, te = se.current, oe = Ce.current; if (!Q || !te || !oe) return; const c = performance.now() - Q, pe = Math.min(c / x, 1), ie = _(u.scaleEasing)(pe), Z = te + (oe - te) * ie; we.current = Z, Ae(Z), pe >= 1 && Ve(); }, [Ae, Ve, u.scaleDurationMs, u.scaleEasing]), je = d(() => { G(s.initialAngle), U.current = s.initialAngle, W.current = s.initialAngularVelocity, ee.current = !1, K.current = null, O.current = null, re.current = null; }, [s.initialAngle, s.initialAngularVelocity]), Be = d(() => { y.onResetStart(), ee.current = !0, K.current = performance.now(), O.current = U.current; }, [y]), Te = d(() => { ee.current = !1, K.current = null, O.current = null, y.onResetCancel(); }, [y]), $e = d( (x) => { if (ee.current) { K.current === null && (K.current = performance.now()); const ie = performance.now() - K.current, Z = Math.min(ie / y.durationMs, 1), ge = _(y.easing)(Z); O.current === null && (O.current = U.current); const be = O.current < 0 ? -2 * Math.PI : 0, ce = O.current + (be - O.current) * ge; if (Z >= 1) { je(), y.onResetEnd(), Ee(!1); return; } U.current = ce, G(ce); return; } const Q = x / 1e3, te = W.current < 15 ? s.dampingCoefficient * 6 : s.dampingCoefficient, oe = Math.min( W.current * Math.exp(-te * Q), s.maxAngularVelocity ); oe === s.maxAngularVelocity && s.onMaxAngularVelocity(); const c = Re(); if (c && c.breakpoint !== re.current) { re.current = c == null ? void 0 : c.breakpoint; const ie = c == null ? void 0 : c.config.scaleConfig; ie && (M(ie), ye({ newScale: ie.scale })); const Z = c == null ? void 0 : c.config.bubbleConfig; Z && f(Z); const ve = c == null ? void 0 : c.config.sparkConfig; ve && j(ve); const ge = c == null ? void 0 : c.config.resetConfig; ge && V(ge); const be = c == null ? void 0 : c.config.spinnerConfig; be && h(be); const ce = c == null ? void 0 : c.config.clickConfig; ce && R(ce); } else !c && re.current !== null && (re.current = null, ye({ newScale: S.scale }), xe()); oe < 2 && Be(); const pe = oe * Q, Se = (U.current + pe) % (2 * Math.PI); U.current = Se, W.current = oe, G(Se); }, [ Be, je, ye, s, y, Re, xe, S, M, f, j, V, h, R ] ), Ze = d( (x) => { fe && ($e(x), Ie()); }, [fe, $e, Ie] ), _e = d(() => { ee.current === !0 && Te(), Ee(!0), W.current = W.current + p(m.angularVelocityPerClick); }, [Te, m]); ue(Ze); const en = s.direction === "clockwise" ? J : -J; return /* @__PURE__ */ ze( "div", { id: yn, style: { position: "relative", userSelect: "none", MozUserSelect: "none", WebkitUserSelect: "none" }, children: [ /* @__PURE__ */ L( "div", { onClick: (x) => { _e(), m.active && Sn(x, m); }, style: { cursor: "pointer", position: "absolute", left: "50%", top: "50%", userSelect: "none", MozUserSelect: "none", WebkitUserSelect: "none", WebkitTapHighlightColor: "transparent", transform: `translate(-50%, -50%) rotate(${en}rad) scale(${Qe})`, zIndex: 100 }, children: o } ), /* @__PURE__ */ ze("div", { style: { position: "relative", left: "50%", top: "50%" }, children: [ /* @__PURE__ */ L(on, { ...q, active: fe }), /* @__PURE__ */ L(rn, { ...I, active: fe }) ] }) ] } ); }; function Sn(n, o) { const t = document.createElement("div"); t.style.left = `${n.pageX}px`, t.style.top = `${n.pageY}px`, t.style.zIndex = "200", t.style.background = "rgb(255 0 0 / 80%)", t.style.borderRadius = "50%", t.style.height = "40px", t.style.pointerEvents = "none", t.style.position = "absolute", t.style.transform = "translate(-50%, -50%) scale(0)", t.style.transition = "all 300ms ease-out", t.style.width = "40px", t.style.zIndex = "200"; const a = 300; document.body.appendChild(t), o.onSpawn(), requestAnimationFrame(() => { t.style.transform = "translate(-50%, -50%) scale(1)", t.style.opacity = "0"; }), setTimeout(() => { t.remove(), o.onRemove(); }, a); } export { on as Bubbles, wn as FidgetSpinner, rn as Sparks, Me as buildBubbleConfig, Ge as buildClickConfig, He as buildResetConfig, Je as buildScaleConfig, ke as buildSparkConfig, Oe as buildSpinnerConfig, Cn as buildVelocityBreakpointConfigs }; //# sourceMappingURL=index.js.map