@freegamestore/games 0.16.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +106 -8
  2. package/TOKENS.md +132 -0
  3. package/dist/Confetti.d.ts +18 -0
  4. package/dist/Confetti.d.ts.map +1 -0
  5. package/dist/Confetti.js +43 -0
  6. package/dist/Confetti.js.map +1 -0
  7. package/dist/GameAuth.d.ts.map +1 -1
  8. package/dist/GameAuth.js +5 -87
  9. package/dist/GameAuth.js.map +1 -1
  10. package/dist/GameButton.d.ts +2 -1
  11. package/dist/GameButton.d.ts.map +1 -1
  12. package/dist/GameButton.js +16 -67
  13. package/dist/GameButton.js.map +1 -1
  14. package/dist/GameConfirm.d.ts +1 -1
  15. package/dist/GameConfirm.d.ts.map +1 -1
  16. package/dist/GameConfirm.js +1 -7
  17. package/dist/GameConfirm.js.map +1 -1
  18. package/dist/GameModal.d.ts +3 -2
  19. package/dist/GameModal.d.ts.map +1 -1
  20. package/dist/GameModal.js +3 -40
  21. package/dist/GameModal.js.map +1 -1
  22. package/dist/GameOverScreen.d.ts +3 -2
  23. package/dist/GameOverScreen.d.ts.map +1 -1
  24. package/dist/GameOverScreen.js +3 -37
  25. package/dist/GameOverScreen.js.map +1 -1
  26. package/dist/GameResultScreen.d.ts +41 -0
  27. package/dist/GameResultScreen.d.ts.map +1 -0
  28. package/dist/GameResultScreen.js +25 -0
  29. package/dist/GameResultScreen.js.map +1 -0
  30. package/dist/GameShell.d.ts +3 -0
  31. package/dist/GameShell.d.ts.map +1 -1
  32. package/dist/GameShell.js +4 -43
  33. package/dist/GameShell.js.map +1 -1
  34. package/dist/GameTextSizeToggle.d.ts +1 -1
  35. package/dist/GameTextSizeToggle.d.ts.map +1 -1
  36. package/dist/GameTextSizeToggle.js +1 -16
  37. package/dist/GameTextSizeToggle.js.map +1 -1
  38. package/dist/GameThemeToggle.d.ts +1 -1
  39. package/dist/GameThemeToggle.d.ts.map +1 -1
  40. package/dist/GameThemeToggle.js +44 -21
  41. package/dist/GameThemeToggle.js.map +1 -1
  42. package/dist/GameTopbar.d.ts +12 -7
  43. package/dist/GameTopbar.d.ts.map +1 -1
  44. package/dist/GameTopbar.js +54 -188
  45. package/dist/GameTopbar.js.map +1 -1
  46. package/dist/GameTopbar.module.css +201 -0
  47. package/dist/Leaderboard.d.ts.map +1 -1
  48. package/dist/Leaderboard.js +2 -5
  49. package/dist/Leaderboard.js.map +1 -1
  50. package/dist/StartScreen.d.ts +41 -0
  51. package/dist/StartScreen.d.ts.map +1 -0
  52. package/dist/StartScreen.js +30 -0
  53. package/dist/StartScreen.js.map +1 -0
  54. package/dist/feel/ScorePopup.d.ts +20 -0
  55. package/dist/feel/ScorePopup.d.ts.map +1 -0
  56. package/dist/feel/ScorePopup.js +51 -0
  57. package/dist/feel/ScorePopup.js.map +1 -0
  58. package/dist/feel/easing.d.ts +14 -0
  59. package/dist/feel/easing.d.ts.map +1 -0
  60. package/dist/feel/easing.js +12 -0
  61. package/dist/feel/easing.js.map +1 -0
  62. package/dist/feel/haptics.d.ts +19 -0
  63. package/dist/feel/haptics.d.ts.map +1 -0
  64. package/dist/feel/haptics.js +24 -0
  65. package/dist/feel/haptics.js.map +1 -0
  66. package/dist/feel/index.d.ts +17 -0
  67. package/dist/feel/index.d.ts.map +1 -0
  68. package/dist/feel/index.js +17 -0
  69. package/dist/feel/index.js.map +1 -0
  70. package/dist/feel/particles.d.ts +46 -0
  71. package/dist/feel/particles.d.ts.map +1 -0
  72. package/dist/feel/particles.js +61 -0
  73. package/dist/feel/particles.js.map +1 -0
  74. package/dist/feel/reducedMotion.d.ts +6 -0
  75. package/dist/feel/reducedMotion.d.ts.map +1 -0
  76. package/dist/feel/reducedMotion.js +10 -0
  77. package/dist/feel/reducedMotion.js.map +1 -0
  78. package/dist/feel/shake.d.ts +6 -0
  79. package/dist/feel/shake.d.ts.map +1 -0
  80. package/dist/feel/shake.js +17 -0
  81. package/dist/feel/shake.js.map +1 -0
  82. package/dist/feel/tween.d.ts +9 -0
  83. package/dist/feel/tween.d.ts.map +1 -0
  84. package/dist/feel/tween.js +15 -0
  85. package/dist/feel/tween.js.map +1 -0
  86. package/dist/feel/useHaptics.d.ts +12 -0
  87. package/dist/feel/useHaptics.d.ts.map +1 -0
  88. package/dist/feel/useHaptics.js +16 -0
  89. package/dist/feel/useHaptics.js.map +1 -0
  90. package/dist/feel/useParticles.d.ts +18 -0
  91. package/dist/feel/useParticles.d.ts.map +1 -0
  92. package/dist/feel/useParticles.js +79 -0
  93. package/dist/feel/useParticles.js.map +1 -0
  94. package/dist/feel/useShake.d.ts +14 -0
  95. package/dist/feel/useShake.d.ts.map +1 -0
  96. package/dist/feel/useShake.js +17 -0
  97. package/dist/feel/useShake.js.map +1 -0
  98. package/dist/index.d.ts +5 -2
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +14 -2
  101. package/dist/index.js.map +1 -1
  102. package/dist/theme.css +93 -0
  103. package/dist/tokens.d.ts +112 -0
  104. package/dist/tokens.d.ts.map +1 -0
  105. package/dist/tokens.js +106 -0
  106. package/dist/tokens.js.map +1 -0
  107. package/dist/useGameSounds.d.ts.map +1 -1
  108. package/dist/useGameSounds.js +37 -15
  109. package/dist/useGameSounds.js.map +1 -1
  110. package/dist/useLeaderboard.d.ts +8 -1
  111. package/dist/useLeaderboard.d.ts.map +1 -1
  112. package/dist/useLeaderboard.js +21 -11
  113. package/dist/useLeaderboard.js.map +1 -1
  114. package/package.json +23 -7
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useState } from 'react';
3
+ import { GameButton } from './GameButton.js';
4
+ import { GameModal } from './GameModal.js';
5
+ /** The numbered steps of a game's rules. */
6
+ function HowToPlaySteps({ steps }) {
7
+ return (_jsx("ol", { className: "m-0 flex list-none flex-col gap-[0.85rem] p-0", children: steps.map((step, i) => (_jsxs("li", { className: "flex animate-fgs-rise-step items-start gap-3 motion-reduce:animate-none",
8
+ // Dynamic: the entrance delay is staggered by the step's index.
9
+ style: { '--i': i }, children: [_jsx("span", { className: "grid size-7 flex-none place-items-center rounded-lg bg-accent font-display font-extrabold text-white shadow-[inset_0_1px_0_rgb(255_255_255/0.35),0_2px_4px_rgb(0_0_0/0.2)]", "aria-hidden": "true", children: i + 1 }), _jsxs("div", { children: [_jsx("strong", { className: "block font-bold text-ink", children: step.heading }), _jsx("p", { className: "m-0 mt-[0.15rem] text-[0.9rem] leading-[1.4] text-muted", children: step.text })] })] }, step.heading))) }));
10
+ }
11
+ /**
12
+ * A game's start screen: artwork, title, tagline, then Play and How to play.
13
+ * Motion is the `animate-fgs-*` utilities from the SDK's `theme.css`, and it
14
+ * stops under prefers-reduced-motion. The card sits on an opaque `bg-panel`.
15
+ *
16
+ * Colours come from the platform tokens through the game's `@theme` mapping.
17
+ */
18
+ export function StartScreen({ title, tagline, artwork, playLabel = 'Play', onPlay, onHowToPlay, howToPlay, }) {
19
+ const [helpOpen, setHelpOpen] = useState(false);
20
+ const titleId = useId();
21
+ const showsHelp = onHowToPlay !== undefined || howToPlay !== undefined;
22
+ const openHelp = () => {
23
+ if (onHowToPlay)
24
+ onHowToPlay();
25
+ else
26
+ setHelpOpen(true);
27
+ };
28
+ return (_jsxs("section", { "data-fgs": "start", className: "relative flex h-full flex-col items-center justify-center gap-[clamp(0.75rem,3svh,1.5rem)] overflow-hidden bg-paper bg-[radial-gradient(120%_70%_at_50%_0%,var(--color-accent-soft)_0%,transparent_65%)] p-4", "aria-labelledby": titleId, children: [artwork !== undefined && artwork !== null && (_jsx("div", { "data-fgs": "start-artwork", className: "w-[min(11rem,52vw)] animate-fgs-rise [--delay:0ms] motion-reduce:animate-none", "aria-hidden": "true", children: _jsx("div", { className: "animate-fgs-float motion-reduce:animate-none", children: artwork }) })), _jsxs("div", { className: "flex w-[min(20rem,100%)] animate-fgs-rise flex-col items-center gap-2 rounded-fgs border border-line bg-panel px-5 pb-5 pt-6 text-center shadow-[inset_0_1px_0_rgb(255_255_255/0.4),0_1px_2px_rgb(0_0_0/0.06),0_18px_32px_-18px_rgb(0_0_0/0.35)] [--delay:120ms] motion-reduce:animate-none", children: [_jsx("h1", { id: titleId, className: "m-0 text-balance font-display text-fgs-display font-extrabold leading-[0.95] tracking-[-0.03em] text-ink [font-optical-sizing:auto] [font-variation-settings:'opsz'_144]", children: title }), tagline && (_jsx("p", { className: "m-0 mb-2 max-w-72 text-[0.95rem] leading-[1.4] text-muted", children: tagline })), _jsxs("div", { className: "mt-1 flex w-full flex-col gap-2", children: [_jsx(GameButton, { variant: "primary", size: "lg", block: true, onClick: onPlay, children: playLabel }), showsHelp && (_jsx(GameButton, { variant: "secondary", size: "md", block: true, onClick: openHelp, children: "How to play" }))] })] }), onHowToPlay === undefined && howToPlay !== undefined && (_jsx(GameModal, { open: helpOpen, onClose: () => setHelpOpen(false), title: "How to play", children: _jsx(HowToPlaySteps, { steps: howToPlay }) }))] }));
29
+ }
30
+ //# sourceMappingURL=StartScreen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StartScreen.js","sourceRoot":"","sources":["../src/StartScreen.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAmC3C,4CAA4C;AAC5C,SAAS,cAAc,CAAC,EAAE,KAAK,EAAyC;IACtE,OAAO,CACL,aAAI,SAAS,EAAC,+CAA+C,YAC1D,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CACtB,cAEE,SAAS,EAAC,yEAAyE;YACnF,gEAAgE;YAChE,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,EAAyB,aAE1C,eACE,SAAS,EAAC,4KAA4K,iBAC1K,MAAM,YAEjB,CAAC,GAAG,CAAC,GACD,EACP,0BACE,iBAAQ,SAAS,EAAC,0BAA0B,YAAE,IAAI,CAAC,OAAO,GAAU,EACpE,YAAG,SAAS,EAAC,yDAAyD,YAAE,IAAI,CAAC,IAAI,GAAK,IAClF,KAdD,IAAI,CAAC,OAAO,CAed,CACN,CAAC,GACC,CACN,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,KAAK,EACL,OAAO,EACP,OAAO,EACP,SAAS,GAAG,MAAM,EAClB,MAAM,EACN,WAAW,EACX,SAAS,GACQ;IACjB,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IACxB,MAAM,SAAS,GAAG,WAAW,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,CAAC;IAEvE,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,IAAI,WAAW;YAAE,WAAW,EAAE,CAAC;;YAC1B,WAAW,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC,CAAC;IAEF,OAAO,CACL,+BACW,OAAO,EAChB,SAAS,EAAC,8MAA8M,qBACvM,OAAO,aAEvB,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,IAAI,IAAI,CAC5C,0BACW,eAAe,EACxB,SAAS,EAAC,+EAA+E,iBAC7E,MAAM,YAElB,cAAK,SAAS,EAAC,8CAA8C,YAAE,OAAO,GAAO,GACzE,CACP,EACD,eAAK,SAAS,EAAC,6RAA6R,aAC1S,aACE,EAAE,EAAE,OAAO,EACX,SAAS,EAAC,0KAA0K,YAEnL,KAAK,GACH,EACJ,OAAO,IAAI,CACV,YAAG,SAAS,EAAC,2DAA2D,YAAE,OAAO,GAAK,CACvF,EACD,eAAK,SAAS,EAAC,iCAAiC,aAC9C,KAAC,UAAU,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,EAAC,KAAK,QAAC,OAAO,EAAE,MAAM,YAC1D,SAAS,GACC,EACZ,SAAS,IAAI,CACZ,KAAC,UAAU,IAAC,OAAO,EAAC,WAAW,EAAC,IAAI,EAAC,IAAI,EAAC,KAAK,QAAC,OAAO,EAAE,QAAQ,4BAEpD,CACd,IACG,IACF,EACL,WAAW,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,IAAI,CACvD,KAAC,SAAS,IAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,KAAK,EAAC,aAAa,YAC/E,KAAC,cAAc,IAAC,KAAK,EAAE,SAAS,GAAI,GAC1B,CACb,IACO,CACX,CAAC;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type * as React from 'react';
2
+ /** How long a pop-up stays on screen, in ms. */
3
+ export declare const SCORE_POP_MS = 700;
4
+ export interface ScorePopupProps {
5
+ /** The points to show, as `+N`. */
6
+ points: number;
7
+ /** Horizontal centre, in the pixel space of its positioned ancestor. */
8
+ x: number;
9
+ /** Vertical centre, in the pixel space of its positioned ancestor. */
10
+ y: number;
11
+ /** Called once the pop-up has finished. The parent should remove it. */
12
+ onDone: () => void;
13
+ }
14
+ /**
15
+ * A floating `+N` that rises and fades from (x, y), then calls `onDone`.
16
+ * Under reduced motion it holds still for the same time, so the number is still
17
+ * seen and `onDone` still fires. Place it inside a `position: relative` container.
18
+ */
19
+ export declare function ScorePopup({ points, x, y, onDone }: ScorePopupProps): React.JSX.Element;
20
+ //# sourceMappingURL=ScorePopup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScorePopup.d.ts","sourceRoot":"","sources":["../../src/feel/ScorePopup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAMpC,gDAAgD;AAChD,eAAO,MAAM,YAAY,MAAM,CAAC;AAKhC,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,CAAC,EAAE,MAAM,CAAC;IACV,sEAAsE;IACtE,CAAC,EAAE,MAAM,CAAC;IACV,wEAAwE;IACxE,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,eAAe,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA8CvF"}
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { easeInQuad, easeOutQuad } from './easing.js';
4
+ import { prefersReducedMotion } from './reducedMotion.js';
5
+ import { tweenProgress, tweenValue } from './tween.js';
6
+ /** How long a pop-up stays on screen, in ms. */
7
+ export const SCORE_POP_MS = 700;
8
+ /** How far a pop-up rises before it fades, in px. */
9
+ const RISE_PX = 40;
10
+ /**
11
+ * A floating `+N` that rises and fades from (x, y), then calls `onDone`.
12
+ * Under reduced motion it holds still for the same time, so the number is still
13
+ * seen and `onDone` still fires. Place it inside a `position: relative` container.
14
+ */
15
+ export function ScorePopup({ points, x, y, onDone }) {
16
+ const ref = useRef(null);
17
+ const onDoneRef = useRef(onDone);
18
+ useEffect(() => {
19
+ onDoneRef.current = onDone;
20
+ }, [onDone]);
21
+ useEffect(() => {
22
+ const element = ref.current;
23
+ if (!element)
24
+ return;
25
+ if (prefersReducedMotion()) {
26
+ const id = setTimeout(() => onDoneRef.current(), SCORE_POP_MS);
27
+ return () => clearTimeout(id);
28
+ }
29
+ let started = null;
30
+ let frame = 0;
31
+ const step = (now) => {
32
+ started ??= now;
33
+ const elapsed = now - started;
34
+ const rise = tweenValue(0, -RISE_PX, elapsed, SCORE_POP_MS, easeOutQuad);
35
+ element.style.transform = `translate(-50%, calc(-50% + ${rise}px))`;
36
+ element.style.opacity = String(tweenValue(1, 0, elapsed, SCORE_POP_MS, easeInQuad));
37
+ if (tweenProgress(elapsed, SCORE_POP_MS) < 1) {
38
+ frame = requestAnimationFrame(step);
39
+ }
40
+ else {
41
+ onDoneRef.current();
42
+ }
43
+ };
44
+ frame = requestAnimationFrame(step);
45
+ return () => cancelAnimationFrame(frame);
46
+ }, []);
47
+ return (_jsx("span", { ref: ref, "data-fgs": "score-popup", className: "pointer-events-none absolute z-[750] whitespace-nowrap font-display text-fgs-stat font-extrabold text-accent-ink will-change-[transform,opacity] [text-shadow:0_1px_2px_rgb(0_0_0/0.55)] [transform:translate(-50%,-50%)]",
48
+ // Dynamic: the pop-up is placed at the caller's x and y; the rise and fade are set from script.
49
+ style: { left: x, top: y }, children: `+${points}` }));
50
+ }
51
+ //# sourceMappingURL=ScorePopup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScorePopup.js","sourceRoot":"","sources":["../../src/feel/ScorePopup.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD,gDAAgD;AAChD,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAC;AAEhC,qDAAqD;AACrD,MAAM,OAAO,GAAG,EAAE,CAAC;AAanB;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAmB;IAClE,MAAM,GAAG,GAAG,MAAM,CAAkB,IAAI,CAAC,CAAC;IAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAEjC,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,CAAC,OAAO,GAAG,MAAM,CAAC;IAC7B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,IAAI,oBAAoB,EAAE,EAAE,CAAC;YAC3B,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;YAC/D,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAChC,CAAC;QAED,IAAI,OAAO,GAAkB,IAAI,CAAC;QAClC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE;YAC3B,OAAO,KAAK,GAAG,CAAC;YAChB,MAAM,OAAO,GAAG,GAAG,GAAG,OAAO,CAAC;YAC9B,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;YACzE,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,+BAA+B,IAAI,MAAM,CAAC;YACpE,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC;YACpF,IAAI,aAAa,CAAC,OAAO,EAAE,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC7C,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;YACtC,CAAC;iBAAM,CAAC;gBACN,SAAS,CAAC,OAAO,EAAE,CAAC;YACtB,CAAC;QACH,CAAC,CAAC;QACF,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;QACpC,OAAO,GAAG,EAAE,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,CACL,eACE,GAAG,EAAE,GAAG,cACC,aAAa,EACtB,SAAS,EAAC,2NAA2N;QACrO,gGAAgG;QAChG,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,YAEzB,IAAI,MAAM,EAAE,GACR,CACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Easing curves. Each maps progress t in [0, 1] to an eased progress, with
3
+ * 0 → 0 and 1 → 1. Pure functions, so they can be used anywhere and tree-shaken.
4
+ */
5
+ export type Easing = (t: number) => number;
6
+ export declare const linear: Easing;
7
+ export declare const easeInQuad: Easing;
8
+ /** Starts fast and settles. The default for entrances and moves. */
9
+ export declare const easeOutQuad: Easing;
10
+ export declare const easeOutCubic: Easing;
11
+ export declare const easeInOutQuad: Easing;
12
+ /** Overshoots its target and settles back, a light spring. Use it for pops, not for long moves. */
13
+ export declare function easeOutBack(overshoot?: number): Easing;
14
+ //# sourceMappingURL=easing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"easing.d.ts","sourceRoot":"","sources":["../../src/feel/easing.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;AAE3C,eAAO,MAAM,MAAM,EAAE,MAAiB,CAAC;AAEvC,eAAO,MAAM,UAAU,EAAE,MAAqB,CAAC;AAE/C,oEAAoE;AACpE,eAAO,MAAM,WAAW,EAAE,MAAqC,CAAC;AAEhE,eAAO,MAAM,YAAY,EAAE,MAAgC,CAAC;AAE5D,eAAO,MAAM,aAAa,EAAE,MAAiE,CAAC;AAE9F,mGAAmG;AACnG,wBAAgB,WAAW,CAAC,SAAS,SAAU,GAAG,MAAM,CAGvD"}
@@ -0,0 +1,12 @@
1
+ export const linear = (t) => t;
2
+ export const easeInQuad = (t) => t * t;
3
+ /** Starts fast and settles. The default for entrances and moves. */
4
+ export const easeOutQuad = (t) => 1 - (1 - t) * (1 - t);
5
+ export const easeOutCubic = (t) => 1 - (1 - t) ** 3;
6
+ export const easeInOutQuad = (t) => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2);
7
+ /** Overshoots its target and settles back, a light spring. Use it for pops, not for long moves. */
8
+ export function easeOutBack(overshoot = 1.70158) {
9
+ const c3 = overshoot + 1;
10
+ return (t) => 1 + c3 * (t - 1) ** 3 + overshoot * (t - 1) ** 2;
11
+ }
12
+ //# sourceMappingURL=easing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"easing.js","sourceRoot":"","sources":["../../src/feel/easing.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,MAAM,GAAW,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAEvC,MAAM,CAAC,MAAM,UAAU,GAAW,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;AAE/C,oEAAoE;AACpE,MAAM,CAAC,MAAM,WAAW,GAAW,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAEhE,MAAM,CAAC,MAAM,YAAY,GAAW,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AAE5D,MAAM,CAAC,MAAM,aAAa,GAAW,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE9F,mGAAmG;AACnG,MAAM,UAAU,WAAW,CAAC,SAAS,GAAG,OAAO;IAC7C,MAAM,EAAE,GAAG,SAAS,GAAG,CAAC,CAAC;IACzB,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Haptic pulses through the Vibration API. Pure except for the one `vibrate` call,
3
+ * so the patterns can be checked on their own.
4
+ */
5
+ /** Milliseconds to vibrate, or alternating vibrate and pause durations. */
6
+ export type VibrationPattern = number | readonly number[];
7
+ /** Named pulses for common game events. Each stays short so it reads as feedback, not buzzing. */
8
+ export declare const HAPTICS: {
9
+ /** A match clears: a crisp double tick. */
10
+ readonly clear: readonly [12, 40, 18];
11
+ /** The game ends: a heavier, slower pulse. */
12
+ readonly gameOver: readonly [60, 80, 120];
13
+ };
14
+ /**
15
+ * Ask the device to vibrate. Returns false when the Vibration API is missing, which
16
+ * covers desktop browsers and iOS Safari. Nothing else happens in that case.
17
+ */
18
+ export declare function vibrate(pattern: VibrationPattern, nav?: Pick<Navigator, 'vibrate'> | undefined): boolean;
19
+ //# sourceMappingURL=haptics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"haptics.d.ts","sourceRoot":"","sources":["../../src/feel/haptics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;AAE1D,kGAAkG;AAClG,eAAO,MAAM,OAAO;IAClB,2CAA2C;;IAE3C,8CAA8C;;CAEK,CAAC;AAEtD;;;GAGG;AACH,wBAAgB,OAAO,CACrB,OAAO,EAAE,gBAAgB,EACzB,GAAG,GAAE,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,SAA6B,GAC9D,OAAO,CAGT"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Haptic pulses through the Vibration API. Pure except for the one `vibrate` call,
3
+ * so the patterns can be checked on their own.
4
+ */
5
+ /** Named pulses for common game events. Each stays short so it reads as feedback, not buzzing. */
6
+ export const HAPTICS = {
7
+ /** A match clears: a crisp double tick. */
8
+ clear: [12, 40, 18],
9
+ /** The game ends: a heavier, slower pulse. */
10
+ gameOver: [60, 80, 120],
11
+ };
12
+ /**
13
+ * Ask the device to vibrate. Returns false when the Vibration API is missing, which
14
+ * covers desktop browsers and iOS Safari. Nothing else happens in that case.
15
+ */
16
+ export function vibrate(pattern, nav = globalNavigator()) {
17
+ if (typeof nav?.vibrate !== 'function')
18
+ return false;
19
+ return nav.vibrate(pattern);
20
+ }
21
+ function globalNavigator() {
22
+ return typeof navigator === 'undefined' ? undefined : navigator;
23
+ }
24
+ //# sourceMappingURL=haptics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"haptics.js","sourceRoot":"","sources":["../../src/feel/haptics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,kGAAkG;AAClG,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,2CAA2C;IAC3C,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;IACnB,8CAA8C;IAC9C,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC;CAC4B,CAAC;AAEtD;;;GAGG;AACH,MAAM,UAAU,OAAO,CACrB,OAAyB,EACzB,MAA8C,eAAe,EAAE;IAE/D,IAAI,OAAO,GAAG,EAAE,OAAO,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IACrD,OAAO,GAAG,CAAC,OAAO,CAAC,OAA4B,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,eAAe;IACtB,OAAO,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AAClE,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The feel kit: particles, tweens and easing, shake, haptics and score pop-ups.
3
+ * Every export is a separate module with no side effects on import, so a bundler
4
+ * keeps only what a game uses. Import it from `@freegamestore/games/feel`, or
5
+ * from the package root.
6
+ */
7
+ export { type Easing, easeInOutQuad, easeInQuad, easeOutBack, easeOutCubic, easeOutQuad, linear, } from './easing.js';
8
+ export { HAPTICS, type VibrationPattern, vibrate } from './haptics.js';
9
+ export { type BurstOptions, createBurst, GRAVITY, MAX_PARTICLES, type Particle, particleAlpha, stepParticles, } from './particles.js';
10
+ export { prefersReducedMotion } from './reducedMotion.js';
11
+ export { SCORE_POP_MS, ScorePopup, type ScorePopupProps } from './ScorePopup.js';
12
+ export { shakeKeyframes } from './shake.js';
13
+ export { tweenProgress, tweenValue } from './tween.js';
14
+ export { type Haptics, useHaptics } from './useHaptics.js';
15
+ export { type ParticleEmitter, useParticles } from './useParticles.js';
16
+ export { type ShakeOptions, useShake } from './useShake.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/feel/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,MAAM,EACX,aAAa,EACb,UAAU,EACV,WAAW,EACX,YAAY,EACZ,WAAW,EACX,MAAM,GACP,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,OAAO,EAAE,KAAK,gBAAgB,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EACL,KAAK,YAAY,EACjB,WAAW,EACX,OAAO,EACP,aAAa,EACb,KAAK,QAAQ,EACb,aAAa,EACb,aAAa,GACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,KAAK,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,KAAK,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,KAAK,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The feel kit: particles, tweens and easing, shake, haptics and score pop-ups.
3
+ * Every export is a separate module with no side effects on import, so a bundler
4
+ * keeps only what a game uses. Import it from `@freegamestore/games/feel`, or
5
+ * from the package root.
6
+ */
7
+ export { easeInOutQuad, easeInQuad, easeOutBack, easeOutCubic, easeOutQuad, linear, } from './easing.js';
8
+ export { HAPTICS, vibrate } from './haptics.js';
9
+ export { createBurst, GRAVITY, MAX_PARTICLES, particleAlpha, stepParticles, } from './particles.js';
10
+ export { prefersReducedMotion } from './reducedMotion.js';
11
+ export { SCORE_POP_MS, ScorePopup } from './ScorePopup.js';
12
+ export { shakeKeyframes } from './shake.js';
13
+ export { tweenProgress, tweenValue } from './tween.js';
14
+ export { useHaptics } from './useHaptics.js';
15
+ export { useParticles } from './useParticles.js';
16
+ export { useShake } from './useShake.js';
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/feel/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEL,aAAa,EACb,UAAU,EACV,WAAW,EACX,YAAY,EACZ,WAAW,EACX,MAAM,GACP,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,OAAO,EAAyB,OAAO,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAEL,WAAW,EACX,OAAO,EACP,aAAa,EAEb,aAAa,EACb,aAAa,GACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAwB,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAwB,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAqB,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Particle bursts: a pure model with no DOM or canvas, so it can be tested on its own
3
+ * and drawn by any renderer. `useParticles` draws it to a canvas.
4
+ */
5
+ /** One particle. Positions and sizes are in CSS pixels, times in seconds. */
6
+ export interface Particle {
7
+ x: number;
8
+ y: number;
9
+ vx: number;
10
+ vy: number;
11
+ size: number;
12
+ colour: string;
13
+ age: number;
14
+ life: number;
15
+ }
16
+ export interface BurstOptions {
17
+ /** How many particles to emit. */
18
+ count: number;
19
+ /** Launch speed range, in px per second. */
20
+ speed: readonly [number, number];
21
+ /** Square size range, in px. */
22
+ size: readonly [number, number];
23
+ /** Seconds a particle lives. Each one varies by up to a quarter. */
24
+ life: number;
25
+ /** Centre of the launch arc, in radians. 0 points right and π/2 points down. Default 0. */
26
+ angle?: number;
27
+ /** Width of the launch arc, in radians. Default 2π, which is a full radial burst. */
28
+ spread?: number;
29
+ }
30
+ /** Downward pull, in px per second squared. */
31
+ export declare const GRAVITY = 900;
32
+ /** Upper bound on live particles, so a burst on a slow device cannot pile up. */
33
+ export declare const MAX_PARTICLES = 160;
34
+ /**
35
+ * Emit a burst of `options.count` particles from (x, y). Colours are used in
36
+ * turn. `random` returns values in [0, 1), and tests can pass a fixed one.
37
+ */
38
+ export declare function createBurst(x: number, y: number, colours: readonly string[], options: BurstOptions, random?: () => number): Particle[];
39
+ /**
40
+ * Advance every particle by `dt` seconds, drop the ones past their life, and
41
+ * return the survivors. Works in place, so a frame allocates nothing.
42
+ */
43
+ export declare function stepParticles(particles: Particle[], dt: number, gravity?: number): Particle[];
44
+ /** Opacity for a particle: fully opaque when born, fading to nothing at the end of its life. */
45
+ export declare function particleAlpha(particle: Particle): number;
46
+ //# sourceMappingURL=particles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../src/feel/particles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,gCAAgC;IAChC,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,+CAA+C;AAC/C,eAAO,MAAM,OAAO,MAAM,CAAC;AAE3B,iFAAiF;AACjF,eAAO,MAAM,aAAa,MAAM,CAAC;AAQjC;;;GAGG;AACH,wBAAgB,WAAW,CACzB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,YAAY,EACrB,MAAM,GAAE,MAAM,MAAoB,GACjC,QAAQ,EAAE,CAqBZ;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,SAAU,GAAG,QAAQ,EAAE,CAY9F;AAED,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAExD"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Particle bursts: a pure model with no DOM or canvas, so it can be tested on its own
3
+ * and drawn by any renderer. `useParticles` draws it to a canvas.
4
+ */
5
+ /** Downward pull, in px per second squared. */
6
+ export const GRAVITY = 900;
7
+ /** Upper bound on live particles, so a burst on a slow device cannot pile up. */
8
+ export const MAX_PARTICLES = 160;
9
+ const FALLBACK_COLOUR = '#888888';
10
+ function lerp(a, b, t) {
11
+ return a + (b - a) * t;
12
+ }
13
+ /**
14
+ * Emit a burst of `options.count` particles from (x, y). Colours are used in
15
+ * turn. `random` returns values in [0, 1), and tests can pass a fixed one.
16
+ */
17
+ export function createBurst(x, y, colours, options, random = Math.random) {
18
+ if (colours.length === 0)
19
+ return [];
20
+ const angle = options.angle ?? 0;
21
+ const spread = options.spread ?? Math.PI * 2;
22
+ const particles = [];
23
+ for (let i = 0; i < options.count; i++) {
24
+ const direction = angle + (random() - 0.5) * spread;
25
+ const speed = lerp(options.speed[0], options.speed[1], random());
26
+ particles.push({
27
+ x,
28
+ y,
29
+ vx: Math.cos(direction) * speed,
30
+ vy: Math.sin(direction) * speed,
31
+ size: lerp(options.size[0], options.size[1], random()),
32
+ colour: colours[i % colours.length] ?? FALLBACK_COLOUR,
33
+ age: 0,
34
+ life: options.life * (0.75 + 0.25 * random()),
35
+ });
36
+ }
37
+ return particles;
38
+ }
39
+ /**
40
+ * Advance every particle by `dt` seconds, drop the ones past their life, and
41
+ * return the survivors. Works in place, so a frame allocates nothing.
42
+ */
43
+ export function stepParticles(particles, dt, gravity = GRAVITY) {
44
+ let alive = 0;
45
+ for (const particle of particles) {
46
+ particle.age += dt;
47
+ if (particle.age >= particle.life)
48
+ continue;
49
+ particle.vy += gravity * dt;
50
+ particle.x += particle.vx * dt;
51
+ particle.y += particle.vy * dt;
52
+ particles[alive++] = particle;
53
+ }
54
+ particles.length = alive;
55
+ return particles;
56
+ }
57
+ /** Opacity for a particle: fully opaque when born, fading to nothing at the end of its life. */
58
+ export function particleAlpha(particle) {
59
+ return Math.max(0, 1 - particle.age / particle.life);
60
+ }
61
+ //# sourceMappingURL=particles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particles.js","sourceRoot":"","sources":["../../src/feel/particles.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * True when the player has asked the system for less motion. Safe to call where
3
+ * `matchMedia` is missing (server render, jsdom, old browsers), where it returns false.
4
+ */
5
+ export declare function prefersReducedMotion(): boolean;
6
+ //# sourceMappingURL=reducedMotion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reducedMotion.d.ts","sourceRoot":"","sources":["../../src/feel/reducedMotion.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAI9C"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * True when the player has asked the system for less motion. Safe to call where
3
+ * `matchMedia` is missing (server render, jsdom, old browsers), where it returns false.
4
+ */
5
+ export function prefersReducedMotion() {
6
+ return typeof window !== 'undefined' && typeof window.matchMedia === 'function'
7
+ ? window.matchMedia('(prefers-reduced-motion: reduce)').matches
8
+ : false;
9
+ }
10
+ //# sourceMappingURL=reducedMotion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reducedMotion.js","sourceRoot":"","sources":["../../src/feel/reducedMotion.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;QAC7E,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,kCAAkC,CAAC,CAAC,OAAO;QAC/D,CAAC,CAAC,KAAK,CAAC;AACZ,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Keyframes for a short, decaying shake: a few side-to-side swings that shrink to rest.
3
+ * Pure, so the motion can be checked on its own and passed to `element.animate`.
4
+ */
5
+ export declare function shakeKeyframes(amplitude: number): Keyframe[];
6
+ //# sourceMappingURL=shake.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shake.d.ts","sourceRoot":"","sources":["../../src/feel/shake.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,EAAE,CAW5D"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Keyframes for a short, decaying shake: a few side-to-side swings that shrink to rest.
3
+ * Pure, so the motion can be checked on its own and passed to `element.animate`.
4
+ */
5
+ export function shakeKeyframes(amplitude) {
6
+ const a = amplitude;
7
+ return [
8
+ { transform: 'translate3d(0, 0, 0)', offset: 0 },
9
+ { transform: `translate3d(${-a}px, 0, 0)`, offset: 0.15 },
10
+ { transform: `translate3d(${a * 0.8}px, 0, 0)`, offset: 0.3 },
11
+ { transform: `translate3d(${-a * 0.6}px, 0, 0)`, offset: 0.45 },
12
+ { transform: `translate3d(${a * 0.4}px, 0, 0)`, offset: 0.6 },
13
+ { transform: `translate3d(${-a * 0.2}px, 0, 0)`, offset: 0.8 },
14
+ { transform: 'translate3d(0, 0, 0)', offset: 1 },
15
+ ];
16
+ }
17
+ //# sourceMappingURL=shake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shake.js","sourceRoot":"","sources":["../../src/feel/shake.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,MAAM,CAAC,GAAG,SAAS,CAAC;IACpB,OAAO;QACL,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,EAAE,CAAC,EAAE;QAChD,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;QACzD,EAAE,SAAS,EAAE,eAAe,CAAC,GAAG,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE;QAC7D,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC,GAAG,GAAG,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;QAC/D,EAAE,SAAS,EAAE,eAAe,CAAC,GAAG,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE;QAC7D,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC,GAAG,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE;QAC9D,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,EAAE,CAAC,EAAE;KACjD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { type Easing } from './easing.js';
2
+ /** Progress through a tween, from 0 to 1. Clamped, so a zero duration or a time past the end is safe. */
3
+ export declare function tweenProgress(elapsed: number, duration: number): number;
4
+ /**
5
+ * The value of a tween `elapsed` ms into a move from `from` to `to` over `duration` ms.
6
+ * Pure: the caller keeps the clock and decides when to draw the next frame.
7
+ */
8
+ export declare function tweenValue(from: number, to: number, elapsed: number, duration: number, easing?: Easing): number;
9
+ //# sourceMappingURL=tween.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tween.d.ts","sourceRoot":"","sources":["../../src/feel/tween.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAe,MAAM,aAAa,CAAC;AAEvD,yGAAyG;AACzG,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGvE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAoB,GAC3B,MAAM,CAER"}
@@ -0,0 +1,15 @@
1
+ import { easeOutQuad } from './easing.js';
2
+ /** Progress through a tween, from 0 to 1. Clamped, so a zero duration or a time past the end is safe. */
3
+ export function tweenProgress(elapsed, duration) {
4
+ if (duration <= 0)
5
+ return 1;
6
+ return Math.min(1, Math.max(0, elapsed / duration));
7
+ }
8
+ /**
9
+ * The value of a tween `elapsed` ms into a move from `from` to `to` over `duration` ms.
10
+ * Pure: the caller keeps the clock and decides when to draw the next frame.
11
+ */
12
+ export function tweenValue(from, to, elapsed, duration, easing = easeOutQuad) {
13
+ return from + (to - from) * easing(tweenProgress(elapsed, duration));
14
+ }
15
+ //# sourceMappingURL=tween.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tween.js","sourceRoot":"","sources":["../../src/feel/tween.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,WAAW,EAAE,MAAM,aAAa,CAAC;AAEvD,yGAAyG;AACzG,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,QAAgB;IAC7D,IAAI,QAAQ,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAC5B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC;AACtD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,EAAU,EACV,OAAe,EACf,QAAgB,EAChB,SAAiB,WAAW;IAE5B,OAAO,IAAI,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;AACvE,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { type VibrationPattern } from './haptics.js';
2
+ export interface Haptics {
3
+ /** Vibrate with a pattern. Silent when haptics are off or unsupported. */
4
+ pulse(pattern: VibrationPattern): void;
5
+ }
6
+ /**
7
+ * Haptic pulses for a game. Haptics are not visual motion, so reduced motion does
8
+ * not switch them off. Pass `enabled: false` to mute them, for example from a
9
+ * settings toggle. The returned object is stable while `enabled` is unchanged.
10
+ */
11
+ export declare function useHaptics(enabled?: boolean): Haptics;
12
+ //# sourceMappingURL=useHaptics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHaptics.d.ts","sourceRoot":"","sources":["../../src/feel/useHaptics.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,gBAAgB,EAAW,MAAM,cAAc,CAAC;AAE9D,MAAM,WAAW,OAAO;IACtB,0EAA0E;IAC1E,KAAK,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACxC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,UAAO,GAAG,OAAO,CASlD"}
@@ -0,0 +1,16 @@
1
+ import { useMemo } from 'react';
2
+ import { vibrate } from './haptics.js';
3
+ /**
4
+ * Haptic pulses for a game. Haptics are not visual motion, so reduced motion does
5
+ * not switch them off. Pass `enabled: false` to mute them, for example from a
6
+ * settings toggle. The returned object is stable while `enabled` is unchanged.
7
+ */
8
+ export function useHaptics(enabled = true) {
9
+ return useMemo(() => ({
10
+ pulse(pattern) {
11
+ if (enabled)
12
+ vibrate(pattern);
13
+ },
14
+ }), [enabled]);
15
+ }
16
+ //# sourceMappingURL=useHaptics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHaptics.js","sourceRoot":"","sources":["../../src/feel/useHaptics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,EAAyB,OAAO,EAAE,MAAM,cAAc,CAAC;AAO9D;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAAO,GAAG,IAAI;IACvC,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC;QACL,KAAK,CAAC,OAAO;YACX,IAAI,OAAO;gBAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;KACF,CAAC,EACF,CAAC,OAAO,CAAC,CACV,CAAC;AACJ,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type RefObject } from 'react';
2
+ import { type BurstOptions } from './particles.js';
3
+ export interface ParticleEmitter {
4
+ /**
5
+ * Launch a burst from (x, y), in the canvas's own CSS-pixel space. Does nothing
6
+ * under reduced motion, or before the canvas is mounted.
7
+ */
8
+ burst(x: number, y: number, colours: readonly string[], options: BurstOptions): void;
9
+ }
10
+ /**
11
+ * Particle bursts drawn to a full-size canvas. Pass the ref of an `<canvas>`
12
+ * that is positioned over the play area and has `pointer-events: none`. The
13
+ * frame loop runs only while particles are alive, so an idle board costs
14
+ * nothing. The live count is written to `data-particles`, so tests can read it
15
+ * without a re-render per frame.
16
+ */
17
+ export declare function useParticles(canvasRef: RefObject<HTMLCanvasElement | null>): ParticleEmitter;
18
+ //# sourceMappingURL=useParticles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useParticles.d.ts","sourceRoot":"","sources":["../../src/feel/useParticles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA8B,MAAM,OAAO,CAAC;AACnE,OAAO,EACL,KAAK,YAAY,EAMlB,MAAM,gBAAgB,CAAC;AASxB,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;CACtF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,eAAe,CA2C5F"}
@@ -0,0 +1,79 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { createBurst, MAX_PARTICLES, particleAlpha, stepParticles, } from './particles.js';
3
+ import { prefersReducedMotion } from './reducedMotion.js';
4
+ /** Highest device pixel ratio used for the canvas. Higher ratios cost fill time for no visible gain. */
5
+ const MAX_DPR = 2;
6
+ /** Longest step a single frame may take, in seconds, so a tab switch does not fling particles across the screen. */
7
+ const MAX_FRAME_DT = 0.1;
8
+ /**
9
+ * Particle bursts drawn to a full-size canvas. Pass the ref of an `<canvas>`
10
+ * that is positioned over the play area and has `pointer-events: none`. The
11
+ * frame loop runs only while particles are alive, so an idle board costs
12
+ * nothing. The live count is written to `data-particles`, so tests can read it
13
+ * without a re-render per frame.
14
+ */
15
+ export function useParticles(canvasRef) {
16
+ const particlesRef = useRef([]);
17
+ const frameRef = useRef(0);
18
+ const lastRef = useRef(null);
19
+ const runningRef = useRef(false);
20
+ useEffect(() => () => {
21
+ cancelAnimationFrame(frameRef.current);
22
+ runningRef.current = false;
23
+ }, []);
24
+ return useMemo(() => {
25
+ const frame = (now) => {
26
+ // The first frame after a burst has no previous time to measure from, so it takes no step.
27
+ const last = lastRef.current;
28
+ const dt = last === null ? 0 : Math.min(Math.max((now - last) / 1000, 0), MAX_FRAME_DT);
29
+ lastRef.current = now;
30
+ const particles = stepParticles(particlesRef.current, dt);
31
+ const canvas = canvasRef.current;
32
+ if (canvas)
33
+ drawFrame(canvas, particles);
34
+ if (particles.length === 0) {
35
+ runningRef.current = false;
36
+ return;
37
+ }
38
+ frameRef.current = requestAnimationFrame(frame);
39
+ };
40
+ return {
41
+ burst(x, y, colours, options) {
42
+ if (!canvasRef.current || prefersReducedMotion())
43
+ return;
44
+ const particles = particlesRef.current;
45
+ particles.push(...createBurst(x, y, colours, options));
46
+ if (particles.length > MAX_PARTICLES)
47
+ particles.splice(0, particles.length - MAX_PARTICLES);
48
+ if (runningRef.current)
49
+ return;
50
+ runningRef.current = true;
51
+ lastRef.current = null;
52
+ frameRef.current = requestAnimationFrame(frame);
53
+ },
54
+ };
55
+ }, [canvasRef]);
56
+ }
57
+ /** Match the backing store to the CSS size, scaled for the screen, then draw one frame. */
58
+ function drawFrame(canvas, particles) {
59
+ canvas.dataset.particles = String(particles.length);
60
+ const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
61
+ const width = canvas.clientWidth;
62
+ const height = canvas.clientHeight;
63
+ if (canvas.width !== Math.round(width * dpr) || canvas.height !== Math.round(height * dpr)) {
64
+ canvas.width = Math.round(width * dpr);
65
+ canvas.height = Math.round(height * dpr);
66
+ }
67
+ const ctx = canvas.getContext('2d');
68
+ if (!ctx)
69
+ return;
70
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
71
+ ctx.clearRect(0, 0, width, height);
72
+ for (const particle of particles) {
73
+ ctx.globalAlpha = particleAlpha(particle);
74
+ ctx.fillStyle = particle.colour;
75
+ ctx.fillRect(particle.x - particle.size / 2, particle.y - particle.size / 2, particle.size, particle.size);
76
+ }
77
+ ctx.globalAlpha = 1;
78
+ }
79
+ //# sourceMappingURL=useParticles.js.map