@wallarm-org/design-system 0.55.0 → 0.55.1

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 (40) hide show
  1. package/dist/components/AnimatedBackground/AnimatedBackground.d.ts +2 -31
  2. package/dist/components/AnimatedBackground/AnimatedBackground.js +102 -25
  3. package/dist/components/AnimatedBackground/GameHud.d.ts +15 -0
  4. package/dist/components/AnimatedBackground/GameHud.js +141 -0
  5. package/dist/components/AnimatedBackground/index.d.ts +2 -3
  6. package/dist/components/AnimatedBackground/index.js +1 -2
  7. package/dist/components/AnimatedBackground/module/constants.d.ts +28 -0
  8. package/dist/components/AnimatedBackground/module/constants.js +29 -0
  9. package/dist/components/AnimatedBackground/module/engine-colors.d.ts +16 -0
  10. package/dist/components/AnimatedBackground/module/engine-colors.js +49 -0
  11. package/dist/components/AnimatedBackground/module/engine-grid.d.ts +6 -0
  12. package/dist/components/AnimatedBackground/module/engine-grid.js +14 -0
  13. package/dist/components/AnimatedBackground/module/engine.d.ts +33 -0
  14. package/dist/components/AnimatedBackground/module/engine.js +206 -0
  15. package/dist/components/AnimatedBackground/module/game-logic.d.ts +87 -0
  16. package/dist/components/AnimatedBackground/module/game-logic.js +472 -0
  17. package/dist/components/AnimatedBackground/module/game-renderer.d.ts +16 -0
  18. package/dist/components/AnimatedBackground/module/game-renderer.js +121 -0
  19. package/dist/components/AnimatedBackground/module/index.d.ts +4 -0
  20. package/dist/components/AnimatedBackground/module/index.js +4 -0
  21. package/dist/components/AnimatedBackground/module/lib.d.ts +2 -0
  22. package/dist/components/AnimatedBackground/{lib.js → module/lib.js} +2 -1
  23. package/dist/components/AnimatedBackground/module/types.d.ts +77 -0
  24. package/dist/components/AnimatedBackground/module/useGameKeyboard.d.ts +3 -0
  25. package/dist/components/AnimatedBackground/module/useGameKeyboard.js +73 -0
  26. package/dist/components/Flex/Flex.d.ts +1 -1
  27. package/dist/components/SegmentedControl/SegmentedControlSeparator.d.ts +1 -1
  28. package/dist/components/Separator/Separator.d.ts +1 -1
  29. package/dist/components/Skeleton/Skeleton.d.ts +1 -1
  30. package/dist/components/Stack/Stack.d.ts +1 -1
  31. package/dist/metadata/components.json +23 -13
  32. package/dist/theme/components/login-background.css +36 -0
  33. package/package.json +1 -1
  34. package/dist/components/AnimatedBackground/classes.d.ts +0 -3
  35. package/dist/components/AnimatedBackground/classes.js +0 -13
  36. package/dist/components/AnimatedBackground/engine.d.ts +0 -9
  37. package/dist/components/AnimatedBackground/engine.js +0 -217
  38. package/dist/components/AnimatedBackground/lib.d.ts +0 -3
  39. package/dist/components/AnimatedBackground/types.d.ts +0 -24
  40. /package/dist/components/AnimatedBackground/{types.js → module/types.js} +0 -0
@@ -1,32 +1,3 @@
1
- import type { CanvasHTMLAttributes, FC, Ref } from 'react';
2
- import type { Texture } from './types';
3
- export interface AnimatedBackgroundProps extends Omit<CanvasHTMLAttributes<HTMLCanvasElement>, 'children'> {
4
- ref?: Ref<HTMLCanvasElement>;
5
- /** Variant A (`clean`) vs Variant B (`halftone`). */
6
- texture?: Texture;
7
- /** Grid cell size in px. */
8
- spacing?: number;
9
- /** Seconds for one L->R sweep pass. */
10
- sweepPeriod?: number;
11
- /** How far from the scan line (px) dots react. */
12
- bloomRadius?: number;
13
- /** Minimum seconds between orange events. Higher = rarer. */
14
- anomalyInterval?: number;
15
- /** Global opacity/strength multiplier (0-1). */
16
- intensity?: number;
17
- /** Peak alpha of fully-bloomed (emphasized) dots, before intensity. */
18
- bloomAlpha?: number;
19
- /** Halftone only: cap on a bloomed pixel's full edge length (px). */
20
- maxDotSize?: number;
21
- /** Sweep-line tilt in degrees (0 = vertical; positive leans the top right). */
22
- tilt?: number;
23
- /** CSS custom-property name for the dot color. */
24
- dotColorVar?: string;
25
- /** CSS custom-property name for the "caught" accent. */
26
- accentColorVar?: string;
27
- /** CSS custom-property name for the base fill. */
28
- baseColorVar?: string;
29
- /** Force a single static frame (also auto-true under reduced motion). */
30
- paused?: boolean;
31
- }
1
+ import type { FC } from 'react';
2
+ import type { AnimatedBackgroundProps } from './module';
32
3
  export declare const AnimatedBackground: FC<AnimatedBackgroundProps>;
@@ -1,30 +1,50 @@
1
- import { jsx } from "react/jsx-runtime";
2
- import { useEffect, useRef } from "react";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { cn } from "../../utils/cn.js";
4
- import { animatedBackgroundVariants } from "./classes.js";
5
- import { createSweepEngine } from "./engine.js";
6
- import { resolveOptions } from "./lib.js";
4
+ import { GameHud } from "./GameHud.js";
5
+ import { createSweepEngine, resolveOptions, useGameKeyboard } from "./module/index.js";
6
+ const GATE_TARGET = 5;
7
7
  const AnimatedBackground = (props)=>{
8
- const { ref, texture, paused, className, ...rest } = props;
8
+ const { ref, texture, paused, game = false, excludeCardSize, className, ...rest } = props;
9
9
  const internalRef = useRef(null);
10
10
  const engineRef = useRef(null);
11
+ const reducedMotionRef = useRef(null);
11
12
  const options = resolveOptions(props);
12
13
  const optionsRef = useRef(options);
13
14
  const pausedRef = useRef(paused);
15
+ const gameRef = useRef(game);
14
16
  useEffect(()=>{
15
17
  optionsRef.current = options;
16
18
  pausedRef.current = paused;
19
+ gameRef.current = game;
20
+ }, []);
21
+ const [stats, setStats] = useState({
22
+ kills: 0,
23
+ stopped: 0,
24
+ escaped: 0,
25
+ spawned: 0,
26
+ done: false
17
27
  });
18
- const signature = JSON.stringify({
19
- ...options,
20
- paused: !!paused
21
- });
28
+ const [catchKey, setCatchKey] = useState(0);
29
+ const isHalftone = (texture ?? 'halftone') === 'halftone';
30
+ const gameActive = game && isHalftone;
31
+ const caught = stats.kills;
32
+ const armed = game && caught >= GATE_TARGET;
33
+ const roundOver = armed && stats.done;
34
+ const faced = stats.stopped + stats.escaped;
35
+ const accuracy = faced > 0 ? Math.round(stats.stopped / faced * 100) : 100;
36
+ const hasStartedRoundRef = useRef(false);
22
37
  useEffect(()=>{
23
38
  const canvas = internalRef.current;
24
39
  if (!canvas) return;
25
40
  const engine = createSweepEngine(canvas, optionsRef.current);
26
41
  engineRef.current = engine;
42
+ engine.onStats((s)=>{
43
+ setStats(s);
44
+ if (gameRef.current && !s.done) setCatchKey((prev)=>prev + 1);
45
+ });
27
46
  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
47
+ reducedMotionRef.current = reduced;
28
48
  const apply = ()=>{
29
49
  if (reduced.matches || pausedRef.current) {
30
50
  engine.stop();
@@ -49,6 +69,7 @@ const AnimatedBackground = (props)=>{
49
69
  });
50
70
  return ()=>{
51
71
  reduced.removeEventListener('change', apply);
72
+ reducedMotionRef.current = null;
52
73
  ro.disconnect();
53
74
  themeObserver.disconnect();
54
75
  cancelAnimationFrame(frame);
@@ -56,30 +77,86 @@ const AnimatedBackground = (props)=>{
56
77
  engineRef.current = null;
57
78
  };
58
79
  }, []);
80
+ const syncKey = `${JSON.stringify(options)}|${paused}`;
59
81
  useEffect(()=>{
60
82
  const engine = engineRef.current;
61
83
  if (!engine) return;
62
84
  engine.setOptions(optionsRef.current);
63
- const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
64
- if (reduced.matches || pausedRef.current) {
85
+ const reduced = reducedMotionRef.current;
86
+ if (reduced?.matches || pausedRef.current) {
65
87
  engine.stop();
66
88
  engine.renderStatic();
67
89
  } else engine.start();
68
90
  }, [
69
- signature
91
+ syncKey
92
+ ]);
93
+ useEffect(()=>{
94
+ engineRef.current?.setGameActive(game && isHalftone);
95
+ }, [
96
+ game,
97
+ isHalftone
98
+ ]);
99
+ const exW = excludeCardSize?.width;
100
+ const exH = excludeCardSize?.height;
101
+ useEffect(()=>{
102
+ engineRef.current?.setExclusion(null != exW && null != exH ? {
103
+ width: exW,
104
+ height: exH
105
+ } : null);
106
+ }, [
107
+ exW,
108
+ exH
70
109
  ]);
71
- return /*#__PURE__*/ jsx("canvas", {
72
- ...rest,
73
- ref: (node)=>{
74
- internalRef.current = node;
75
- if ('function' == typeof ref) ref(node);
76
- else if (ref) ref.current = node;
77
- },
78
- "data-slot": "login-background",
79
- "aria-hidden": "true",
80
- className: cn(animatedBackgroundVariants({
81
- texture
82
- }), className)
110
+ useEffect(()=>{
111
+ if (!game || !armed) return;
112
+ if (hasStartedRoundRef.current) return void engineRef.current?.setMode('armed');
113
+ hasStartedRoundRef.current = true;
114
+ engineRef.current?.startRound();
115
+ }, [
116
+ game,
117
+ armed
118
+ ]);
119
+ useGameKeyboard(engineRef, game, armed, roundOver, hasStartedRoundRef);
120
+ const onPointerDown = useCallback((e)=>{
121
+ if (!game) return;
122
+ const canvas = internalRef.current;
123
+ if (!canvas) return;
124
+ const rect = canvas.getBoundingClientRect();
125
+ const x = e.clientX - rect.left;
126
+ const y = e.clientY - rect.top;
127
+ engineRef.current?.catchAt(x, y);
128
+ }, [
129
+ game
130
+ ]);
131
+ const handleTryAgain = useCallback(()=>{
132
+ engineRef.current?.startRound();
133
+ }, []);
134
+ return /*#__PURE__*/ jsxs(Fragment, {
135
+ children: [
136
+ /*#__PURE__*/ jsx("canvas", {
137
+ ...rest,
138
+ ref: (node)=>{
139
+ internalRef.current = node;
140
+ if ('function' == typeof ref) ref(node);
141
+ else if (ref) ref.current = node;
142
+ },
143
+ "data-slot": "animated-background",
144
+ "aria-hidden": "true",
145
+ className: cn('h-full w-full pointer-events-none', gameActive && 'pointer-events-auto', className),
146
+ onPointerDown: gameActive ? onPointerDown : void 0
147
+ }),
148
+ gameActive && /*#__PURE__*/ jsx(GameHud, {
149
+ caught: caught,
150
+ armed: armed,
151
+ roundOver: roundOver,
152
+ stats: stats,
153
+ accuracy: accuracy,
154
+ faced: faced,
155
+ catchKey: catchKey,
156
+ gateTarget: GATE_TARGET,
157
+ onTryAgain: handleTryAgain
158
+ })
159
+ ]
83
160
  });
84
161
  };
85
162
  AnimatedBackground.displayName = 'AnimatedBackground';
@@ -0,0 +1,15 @@
1
+ import type { FC } from 'react';
2
+ import type { GameStats } from './module';
3
+ interface GameHudProps {
4
+ caught: number;
5
+ armed: boolean;
6
+ roundOver: boolean;
7
+ stats: GameStats;
8
+ accuracy: number;
9
+ faced: number;
10
+ catchKey: number;
11
+ gateTarget: number;
12
+ onTryAgain: () => void;
13
+ }
14
+ export declare const GameHud: FC<GameHudProps>;
15
+ export {};
@@ -0,0 +1,141 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../../utils/cn.js";
3
+ const dotColor = 'var(--animated-bg-dot)';
4
+ const caughtColor = 'var(--animated-bg-caught-dot)';
5
+ const labelStyle = {
6
+ fontFeatureSettings: '"liga" 0',
7
+ color: dotColor
8
+ };
9
+ const GameHud = ({ caught, armed, roundOver, stats, accuracy, faced, catchKey, gateTarget, onTryAgain })=>{
10
+ const showCounter = caught > 0;
11
+ return /*#__PURE__*/ jsxs(Fragment, {
12
+ children: [
13
+ showCounter && /*#__PURE__*/ jsxs("div", {
14
+ className: "fixed top-24 right-24 flex flex-col items-end gap-6 font-mono pointer-events-none",
15
+ style: {
16
+ animation: 'hud-in 0.3s ease-out'
17
+ },
18
+ children: [
19
+ /*#__PURE__*/ jsx("div", {
20
+ className: cn('flex flex-col items-center justify-center border', armed ? 'h-113 w-119' : 'h-52 w-119'),
21
+ style: {
22
+ borderColor: dotColor,
23
+ backgroundColor: `color-mix(in srgb, ${dotColor} 8%, transparent)`
24
+ },
25
+ children: armed ? /*#__PURE__*/ jsxs(Fragment, {
26
+ children: [
27
+ /*#__PURE__*/ jsxs("div", {
28
+ className: "flex flex-col items-center gap-4",
29
+ children: [
30
+ /*#__PURE__*/ jsxs("span", {
31
+ className: "text-base leading-xl font-bold",
32
+ style: {
33
+ fontFeatureSettings: '"tnum"',
34
+ color: caughtColor
35
+ },
36
+ children: [
37
+ stats.stopped,
38
+ /*#__PURE__*/ jsxs("span", {
39
+ className: "opacity-45",
40
+ children: [
41
+ " / ",
42
+ faced
43
+ ]
44
+ })
45
+ ]
46
+ }),
47
+ /*#__PURE__*/ jsx("span", {
48
+ className: "text-xs leading-xs uppercase",
49
+ style: labelStyle,
50
+ children: "HIT"
51
+ })
52
+ ]
53
+ }),
54
+ /*#__PURE__*/ jsx("div", {
55
+ className: "h-px w-full my-8",
56
+ style: {
57
+ backgroundColor: dotColor
58
+ }
59
+ }),
60
+ /*#__PURE__*/ jsxs("div", {
61
+ className: "flex flex-col items-center gap-4",
62
+ children: [
63
+ /*#__PURE__*/ jsxs("span", {
64
+ className: "text-base leading-xl font-bold",
65
+ style: {
66
+ fontFeatureSettings: '"tnum"',
67
+ color: caughtColor
68
+ },
69
+ children: [
70
+ accuracy,
71
+ "%"
72
+ ]
73
+ }),
74
+ /*#__PURE__*/ jsx("span", {
75
+ className: "text-xs leading-xs uppercase",
76
+ style: labelStyle,
77
+ children: "SCORE"
78
+ })
79
+ ]
80
+ })
81
+ ]
82
+ }) : /*#__PURE__*/ jsxs("div", {
83
+ className: "flex flex-col items-center gap-4",
84
+ children: [
85
+ /*#__PURE__*/ jsxs("span", {
86
+ className: "text-base leading-xl font-bold",
87
+ style: {
88
+ fontFeatureSettings: '"tnum"',
89
+ animation: 'catch-pop 0.25s ease-out',
90
+ color: caughtColor
91
+ },
92
+ children: [
93
+ caught,
94
+ /*#__PURE__*/ jsxs("span", {
95
+ className: "opacity-45",
96
+ children: [
97
+ " / ",
98
+ gateTarget
99
+ ]
100
+ })
101
+ ]
102
+ }, catchKey),
103
+ /*#__PURE__*/ jsx("span", {
104
+ className: "text-xs leading-xs uppercase",
105
+ style: labelStyle,
106
+ children: "INSERT COIN"
107
+ })
108
+ ]
109
+ })
110
+ }),
111
+ roundOver && /*#__PURE__*/ jsx("button", {
112
+ type: "button",
113
+ className: "pointer-events-auto text-2xs leading-sm font-mono uppercase underline underline-offset-4 hover:text-[var(--animated-bg-accent-dot)]",
114
+ style: {
115
+ color: dotColor
116
+ },
117
+ onClick: onTryAgain,
118
+ children: "Try again"
119
+ }),
120
+ armed && !roundOver && /*#__PURE__*/ jsx("span", {
121
+ className: "text-2xs leading-sm",
122
+ style: {
123
+ color: dotColor
124
+ },
125
+ children: '← → move · space fire · esc to exit'
126
+ })
127
+ ]
128
+ }),
129
+ caught > 0 && !armed && /*#__PURE__*/ jsx("div", {
130
+ className: "fixed bottom-24 left-1/2 -translate-x-1/2 text-xs pointer-events-none",
131
+ style: {
132
+ animation: 'hud-in 0.3s ease-out',
133
+ color: dotColor
134
+ },
135
+ children: 'Click the red anomalies \u2014 catch 5 to arm the cannon, then \u2190 \u2192 move \u00B7 space fire'
136
+ })
137
+ ]
138
+ });
139
+ };
140
+ GameHud.displayName = 'GameHud';
141
+ export { GameHud };
@@ -1,3 +1,2 @@
1
- export { AnimatedBackground, type AnimatedBackgroundProps } from './AnimatedBackground';
2
- export { animatedBackgroundVariants } from './classes';
3
- export type { EngineOptions, Texture } from './types';
1
+ export { AnimatedBackground } from './AnimatedBackground';
2
+ export type { AnimatedBackgroundProps } from './module';
@@ -1,3 +1,2 @@
1
1
  import { AnimatedBackground } from "./AnimatedBackground.js";
2
- import { animatedBackgroundVariants } from "./classes.js";
3
- export { AnimatedBackground, animatedBackgroundVariants };
2
+ export { AnimatedBackground };
@@ -0,0 +1,28 @@
1
+ export declare const NOISE_FREQ_X = 0.045;
2
+ export declare const NOISE_FREQ_Y = 0.032;
3
+ export declare const NOISE_FREQ_T = 0.9;
4
+ export declare const AMB_SCALE = 0.11;
5
+ export declare const HALFTONE_BLOOM_PEAK = 0.62;
6
+ export declare const HALFTONE_BASE_ALPHA = 0.12;
7
+ export declare const ANOMALY_ALPHA_BASE = 0.45;
8
+ export declare const ANOMALY_VIS_THRESHOLD = 0.06;
9
+ export declare const MIN_DOT_VALUE = 0.05;
10
+ export declare const DOT_STEP_SCALE = 1.05;
11
+ export declare const DOT_STEP_BASE = 0.5;
12
+ export declare const CLEAN_BASE_ALPHA = 0.13;
13
+ export declare const CLEAN_DOT_HALF_BLOOM = 1.5;
14
+ export declare const CLEAN_DOT_HALF_BASE = 1;
15
+ export declare const GLOW_LIFETIME = 3.2;
16
+ export declare const GLOW_OUTER_ALPHA = 0.35;
17
+ export declare const GLOW_OUTER_HALF = 5;
18
+ export declare const GLOW_INNER_ALPHA = 0.95;
19
+ export declare const GLOW_INNER_HALF = 2.4;
20
+ export declare const SPAWN_PROBABILITY = 0.05;
21
+ export declare const SWEEP_OFFSET_GUARD = 8;
22
+ export declare const IDLE_MARGIN_X = 30;
23
+ export declare const IDLE_MARGIN_Y = 16;
24
+ export declare const LABEL_MIN_Y = 15;
25
+ export declare const LABEL_OFFSET_Y = 26;
26
+ export declare const LABEL_DRIFT = 8;
27
+ export declare const LABEL_SHADOW_ALPHA = 0.3;
28
+ export declare const LABEL_ALPHA_BOOST = 1.15;
@@ -0,0 +1,29 @@
1
+ const NOISE_FREQ_X = 0.045;
2
+ const NOISE_FREQ_Y = 0.032;
3
+ const NOISE_FREQ_T = 0.9;
4
+ const AMB_SCALE = 0.11;
5
+ const HALFTONE_BLOOM_PEAK = 0.62;
6
+ const HALFTONE_BASE_ALPHA = 0.12;
7
+ const ANOMALY_ALPHA_BASE = 0.45;
8
+ const ANOMALY_VIS_THRESHOLD = 0.06;
9
+ const MIN_DOT_VALUE = 0.05;
10
+ const DOT_STEP_SCALE = 1.05;
11
+ const DOT_STEP_BASE = 0.5;
12
+ const CLEAN_BASE_ALPHA = 0.13;
13
+ const CLEAN_DOT_HALF_BLOOM = 1.5;
14
+ const CLEAN_DOT_HALF_BASE = 1;
15
+ const GLOW_LIFETIME = 3.2;
16
+ const GLOW_OUTER_ALPHA = 0.35;
17
+ const GLOW_OUTER_HALF = 5;
18
+ const GLOW_INNER_ALPHA = 0.95;
19
+ const GLOW_INNER_HALF = 2.4;
20
+ const SPAWN_PROBABILITY = 0.05;
21
+ const SWEEP_OFFSET_GUARD = 8;
22
+ const IDLE_MARGIN_X = 30;
23
+ const IDLE_MARGIN_Y = 16;
24
+ const LABEL_MIN_Y = 15;
25
+ const LABEL_OFFSET_Y = 26;
26
+ const LABEL_DRIFT = 8;
27
+ const LABEL_SHADOW_ALPHA = 0.3;
28
+ const LABEL_ALPHA_BOOST = 1.15;
29
+ export { AMB_SCALE, ANOMALY_ALPHA_BASE, ANOMALY_VIS_THRESHOLD, CLEAN_BASE_ALPHA, CLEAN_DOT_HALF_BASE, CLEAN_DOT_HALF_BLOOM, DOT_STEP_BASE, DOT_STEP_SCALE, GLOW_INNER_ALPHA, GLOW_INNER_HALF, GLOW_LIFETIME, GLOW_OUTER_ALPHA, GLOW_OUTER_HALF, HALFTONE_BASE_ALPHA, HALFTONE_BLOOM_PEAK, IDLE_MARGIN_X, IDLE_MARGIN_Y, LABEL_ALPHA_BOOST, LABEL_DRIFT, LABEL_MIN_Y, LABEL_OFFSET_Y, LABEL_SHADOW_ALPHA, MIN_DOT_VALUE, NOISE_FREQ_T, NOISE_FREQ_X, NOISE_FREQ_Y, SPAWN_PROBABILITY, SWEEP_OFFSET_GUARD };
@@ -0,0 +1,16 @@
1
+ export interface RGB {
2
+ r: number;
3
+ g: number;
4
+ b: number;
5
+ }
6
+ export declare const FALLBACK_DOT: RGB;
7
+ export declare const FALLBACK_ACCENT: RGB;
8
+ export declare const FALLBACK_CAUGHT: RGB;
9
+ export declare const FALLBACK_BASE = "#f8fafc";
10
+ /** Number of discrete alpha levels in pre-computed palettes. */
11
+ export declare const ALPHA_STEPS = 16;
12
+ /** Pre-compute RGBA strings for 0-ALPHA_STEPS so the hot loop never allocates. */
13
+ export declare function buildPalette(rgb: RGB): string[];
14
+ /** Map a 0-1 alpha to the nearest palette index. */
15
+ export declare function alphaIdx(a: number): number;
16
+ export declare function parseColor(value: string): RGB | null;
@@ -0,0 +1,49 @@
1
+ const FALLBACK_DOT = {
2
+ r: 69,
3
+ g: 85,
4
+ b: 108
5
+ };
6
+ const FALLBACK_ACCENT = {
7
+ r: 251,
8
+ g: 44,
9
+ b: 54
10
+ };
11
+ const FALLBACK_CAUGHT = {
12
+ r: 0,
13
+ g: 201,
14
+ b: 80
15
+ };
16
+ const FALLBACK_BASE = '#f8fafc';
17
+ const ALPHA_STEPS = 16;
18
+ function buildPalette(rgb) {
19
+ const p = new Array(ALPHA_STEPS + 1);
20
+ for(let i = 0; i <= ALPHA_STEPS; i++)p[i] = `rgba(${rgb.r},${rgb.g},${rgb.b},${(i / ALPHA_STEPS).toFixed(3)})`;
21
+ return p;
22
+ }
23
+ function alphaIdx(a) {
24
+ return Math.min(ALPHA_STEPS, a * ALPHA_STEPS + 0.5 | 0);
25
+ }
26
+ function parseColor(value) {
27
+ const v = value.trim();
28
+ if (!v) return null;
29
+ if ('#' === v[0]) {
30
+ let hex = v.slice(1);
31
+ if (3 === hex.length) hex = hex.split('').map((c)=>c + c).join('');
32
+ if (6 !== hex.length) return null;
33
+ const n = parseInt(hex, 16);
34
+ if (Number.isNaN(n)) return null;
35
+ return {
36
+ r: n >> 16 & 255,
37
+ g: n >> 8 & 255,
38
+ b: 255 & n
39
+ };
40
+ }
41
+ const m = v.match(/rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
42
+ if (m?.[1] && m[2] && m[3]) return {
43
+ r: +m[1],
44
+ g: +m[2],
45
+ b: +m[3]
46
+ };
47
+ return null;
48
+ }
49
+ export { ALPHA_STEPS, FALLBACK_ACCENT, FALLBACK_BASE, FALLBACK_CAUGHT, FALLBACK_DOT, alphaIdx, buildPalette, parseColor };
@@ -0,0 +1,6 @@
1
+ export interface Dot {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ export declare function buildGrid(w: number, h: number, sp: number): Dot[];
6
+ export declare function sweepX(t: number, w: number, period: number): number;
@@ -0,0 +1,14 @@
1
+ function buildGrid(w, h, sp) {
2
+ const cap = 20000;
3
+ const safeSp = Math.max(sp, Math.sqrt(w * h / cap));
4
+ const a = [];
5
+ for(let y = safeSp / 2; y < h; y += safeSp)for(let x = safeSp / 2; x < w; x += safeSp)a.push({
6
+ x,
7
+ y
8
+ });
9
+ return a;
10
+ }
11
+ function sweepX(t, w, period) {
12
+ return t % period / period * (w + 140) - 70;
13
+ }
14
+ export { buildGrid, sweepX };
@@ -0,0 +1,33 @@
1
+ import type { EngineOptions, GameStats } from './types';
2
+ /**
3
+ * Framework-agnostic "detection sweep" canvas engine.
4
+ * A scan line crosses a dot grid; dots react as it passes; rare accent events
5
+ * appear behind the sweep. Two textures share this engine, selected via `texture`.
6
+ *
7
+ * Performance notes:
8
+ * - All dots use `fillRect` (no `arc`/`beginPath` overhead).
9
+ * - Color strings are pre-computed in palettes — zero allocation in the hot loop.
10
+ * - No `shadowBlur` — glow is faked with layered rects.
11
+ * - Game sim folds into the existing frame() behind mode guards.
12
+ * - Bounded pools (MAX_TARGETS, MAX_BULLETS). No per-frame allocation.
13
+ */
14
+ export interface SweepEngine {
15
+ start(): void;
16
+ stop(): void;
17
+ setOptions(next: Partial<EngineOptions>): void;
18
+ renderStatic(t?: number): void;
19
+ resize(): void;
20
+ setGameActive(active: boolean): void;
21
+ catchAt(x: number, y: number): boolean;
22
+ setMode(mode: 'idle' | 'armed'): void;
23
+ startRound(): void;
24
+ exitGame(): void;
25
+ setCannonDir(dir: number): void;
26
+ setFiring(on: boolean): void;
27
+ onStats(cb: (s: GameStats) => void): void;
28
+ setExclusion(box: {
29
+ width: number;
30
+ height: number;
31
+ } | null): void;
32
+ }
33
+ export declare function createSweepEngine(canvas: HTMLCanvasElement, options: EngineOptions): SweepEngine;