react-fidget-spinner
Version:
Turn any React component into an interactive clickable fidget spinner! 🪿
863 lines (862 loc) • 21.9 kB
JavaScript
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