@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
@@ -0,0 +1,77 @@
1
+ import type { CanvasHTMLAttributes, Ref } from 'react';
2
+ export type Texture = 'clean' | 'halftone';
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
+ /** CSS custom-property name for the "neutralised" (caught) color. */
30
+ caughtColorVar?: string;
31
+ /** Force a single static frame (also auto-true under reduced motion). */
32
+ paused?: boolean;
33
+ /** Enable the interactive shooter easter egg (halftone only). Default: false. */
34
+ game?: boolean;
35
+ /** Card dimensions for spawn-exclusion when game is enabled. */
36
+ excludeCardSize?: {
37
+ width: number;
38
+ height: number;
39
+ };
40
+ }
41
+ export interface EngineOptions {
42
+ texture: Texture;
43
+ /** Grid cell size in px. */
44
+ spacing: number;
45
+ /** Seconds for one L→R sweep pass. */
46
+ sweepPeriod: number;
47
+ /** How far from the scan line (px) dots react. */
48
+ bloomRadius: number;
49
+ /** Minimum seconds between accent events. */
50
+ anomalyInterval: number;
51
+ /** Global opacity multiplier (0–1). */
52
+ intensity: number;
53
+ /** Peak alpha of bloomed dots before `intensity`. */
54
+ bloomAlpha: number;
55
+ /** Halftone only: cap on a bloomed pixel's edge length (px). */
56
+ maxDotSize: number;
57
+ /** Sweep-line tilt in degrees (0 = vertical). */
58
+ tilt: number;
59
+ /** CSS custom-property names resolved at runtime. */
60
+ dotColorVar: string;
61
+ accentColorVar: string;
62
+ baseColorVar: string;
63
+ /** CSS custom-property name for the "caught" (neutralised) color. */
64
+ caughtColorVar: string;
65
+ }
66
+ export interface GameStats {
67
+ /** Lifetime catches — drives the arming gate (≥ 5); reset only by exitGame. */
68
+ kills: number;
69
+ /** Threats stopped THIS round (HIT numerator). */
70
+ stopped: number;
71
+ /** Threats that timed out unshot THIS round. */
72
+ escaped: number;
73
+ /** Attacks spawned this round (caps at ROUND_ATTACKS). */
74
+ spawned: number;
75
+ /** Round complete — full wave spawned AND field cleared. */
76
+ done: boolean;
77
+ }
@@ -0,0 +1,3 @@
1
+ import type { MutableRefObject, RefObject } from 'react';
2
+ import type { SweepEngine } from './index';
3
+ export declare const useGameKeyboard: (engineRef: RefObject<SweepEngine | null>, game: boolean, armed: boolean, roundOver: boolean, hasStartedRoundRef: MutableRefObject<boolean>) => void;
@@ -0,0 +1,73 @@
1
+ import { useEffect } from "react";
2
+ const useGameKeyboard = (engineRef, game, armed, roundOver, hasStartedRoundRef)=>{
3
+ useEffect(()=>{
4
+ if (!game || !armed || roundOver) return;
5
+ const held = {
6
+ left: false,
7
+ right: false
8
+ };
9
+ function updateDir() {
10
+ const dir = (held.right ? 1 : 0) - (held.left ? 1 : 0);
11
+ engineRef.current?.setCannonDir(dir);
12
+ }
13
+ function onKeyDown(e) {
14
+ if ('ArrowLeft' === e.key) {
15
+ e.preventDefault();
16
+ held.left = true;
17
+ updateDir();
18
+ } else if ('ArrowRight' === e.key) {
19
+ e.preventDefault();
20
+ held.right = true;
21
+ updateDir();
22
+ } else if (' ' === e.key || 'Spacebar' === e.key) {
23
+ e.preventDefault();
24
+ if (!e.repeat) engineRef.current?.setFiring(true);
25
+ } else if ('Escape' === e.key) {
26
+ e.preventDefault();
27
+ engineRef.current?.exitGame();
28
+ hasStartedRoundRef.current = false;
29
+ }
30
+ }
31
+ function onKeyUp(e) {
32
+ if ('ArrowLeft' === e.key) {
33
+ held.left = false;
34
+ updateDir();
35
+ } else if ('ArrowRight' === e.key) {
36
+ held.right = false;
37
+ updateDir();
38
+ } else if (' ' === e.key || 'Spacebar' === e.key) engineRef.current?.setFiring(false);
39
+ }
40
+ window.addEventListener('keydown', onKeyDown);
41
+ window.addEventListener('keyup', onKeyUp);
42
+ return ()=>{
43
+ window.removeEventListener('keydown', onKeyDown);
44
+ window.removeEventListener('keyup', onKeyUp);
45
+ engineRef.current?.setFiring(false);
46
+ engineRef.current?.setCannonDir(0);
47
+ };
48
+ }, [
49
+ game,
50
+ armed,
51
+ roundOver,
52
+ engineRef,
53
+ hasStartedRoundRef
54
+ ]);
55
+ useEffect(()=>{
56
+ if (!game || !roundOver) return;
57
+ function onKeyDown(e) {
58
+ if ('Escape' === e.key) {
59
+ e.preventDefault();
60
+ engineRef.current?.exitGame();
61
+ hasStartedRoundRef.current = false;
62
+ }
63
+ }
64
+ window.addEventListener('keydown', onKeyDown);
65
+ return ()=>window.removeEventListener('keydown', onKeyDown);
66
+ }, [
67
+ game,
68
+ roundOver,
69
+ engineRef,
70
+ hasStartedRoundRef
71
+ ]);
72
+ };
73
+ export { useGameKeyboard };
@@ -7,7 +7,7 @@ declare const flexVariants: (props?: ({
7
7
  align?: "center" | "end" | "baseline" | "start" | "stretch" | null | undefined;
8
8
  justify?: "center" | "end" | "start" | "between" | "around" | "evenly" | null | undefined;
9
9
  wrap?: "reverse" | "nowrap" | "wrap" | null | undefined;
10
- gap?: 1 | 2 | 4 | 6 | 8 | 44 | 16 | 32 | 20 | 80 | 12 | 24 | 28 | 36 | 40 | 48 | 56 | 64 | 96 | 112 | 128 | 144 | null | undefined;
10
+ gap?: 1 | 2 | 4 | 6 | 8 | 16 | 44 | 80 | 24 | 40 | 48 | 12 | 20 | 32 | 28 | 36 | 56 | 64 | 96 | 112 | 128 | 144 | null | undefined;
11
11
  grow?: boolean | null | undefined;
12
12
  shrink?: boolean | null | undefined;
13
13
  fullWidth?: boolean | null | undefined;
@@ -2,7 +2,7 @@ import type { ComponentRef, FC, Ref } from 'react';
2
2
  import { type VariantProps } from 'class-variance-authority';
3
3
  import { Separator, type SeparatorProps } from '../Separator';
4
4
  declare const segmentedControlSeparatorVariants: (props?: ({
5
- mx?: 1 | 2 | 4 | 6 | 8 | 44 | 16 | 32 | 20 | 80 | 12 | 24 | 28 | 36 | 40 | 48 | 56 | 64 | 96 | 112 | 128 | 144 | null | undefined;
5
+ mx?: 1 | 2 | 4 | 6 | 8 | 16 | 44 | 80 | 24 | 40 | 48 | 12 | 20 | 32 | 28 | 36 | 56 | 64 | 96 | 112 | 128 | 144 | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  type SegmentedControlSeparatorVariants = VariantProps<typeof segmentedControlSeparatorVariants>;
8
8
  /**
@@ -3,7 +3,7 @@ import { type VariantProps } from 'class-variance-authority';
3
3
  import type { TestableProps } from '../../utils/testId';
4
4
  declare const separatorVariants: (props?: ({
5
5
  orientation?: "horizontal" | "vertical" | null | undefined;
6
- spacing?: 1 | 2 | 4 | 6 | 8 | 44 | 16 | 32 | 20 | 80 | 12 | 24 | 28 | 36 | 40 | 48 | 56 | 64 | 96 | 112 | 128 | 144 | null | undefined;
6
+ spacing?: 1 | 2 | 4 | 6 | 8 | 16 | 44 | 80 | 24 | 40 | 48 | 12 | 20 | 32 | 28 | 36 | 56 | 64 | 96 | 112 | 128 | 144 | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  export type SeparatorProps = ComponentPropsWithoutRef<'div'> & VariantProps<typeof separatorVariants> & TestableProps & {
9
9
  ref?: Ref<HTMLDivElement>;
@@ -3,7 +3,7 @@ import { type VariantProps } from 'class-variance-authority';
3
3
  import type { TestableProps } from '../../utils/testId';
4
4
  declare const skeletonVariants: (props?: ({
5
5
  transparent?: boolean | null | undefined;
6
- rounded?: 2 | 4 | 6 | "none" | 8 | 16 | 32 | "full" | 12 | 24 | null | undefined;
6
+ rounded?: 2 | 4 | 6 | "none" | 8 | 16 | 24 | 12 | "full" | 32 | null | undefined;
7
7
  withDimensions?: boolean | null | undefined;
8
8
  withChildren?: boolean | null | undefined;
9
9
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -10,7 +10,7 @@ declare const stackVariants: (props?: ({
10
10
  fullWidth?: boolean | null | undefined;
11
11
  flexGrow?: boolean | null | undefined;
12
12
  flexShrink?: boolean | null | undefined;
13
- gap?: 0 | 1 | 2 | 4 | 6 | 8 | 44 | 16 | 32 | 20 | 80 | 12 | 24 | 28 | 36 | 40 | 48 | 56 | 64 | 96 | 112 | 128 | 144 | null | undefined;
13
+ gap?: 0 | 1 | 2 | 4 | 6 | 8 | 16 | 44 | 80 | 24 | 40 | 48 | 12 | 20 | 32 | 28 | 36 | 56 | 64 | 96 | 112 | 128 | 144 | null | undefined;
14
14
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
15
15
  type StackNativeProps = HTMLAttributes<HTMLDivElement>;
16
16
  type StackVariantProps = VariantProps<typeof stackVariants>;
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.54.0",
3
- "generatedAt": "2026-06-04T22:49:52.517Z",
2
+ "version": "0.55.0",
3
+ "generatedAt": "2026-06-08T15:41:56.689Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -3406,12 +3406,31 @@
3406
3406
  "required": false,
3407
3407
  "description": "CSS custom-property name for the base fill."
3408
3408
  },
3409
+ {
3410
+ "name": "caughtColorVar",
3411
+ "type": "string | undefined",
3412
+ "required": false,
3413
+ "description": "CSS custom-property name for the \"neutralised\" (caught) color."
3414
+ },
3409
3415
  {
3410
3416
  "name": "paused",
3411
3417
  "type": "boolean | undefined",
3412
3418
  "required": false,
3413
3419
  "description": "Force a single static frame (also auto-true under reduced motion)."
3414
3420
  },
3421
+ {
3422
+ "name": "game",
3423
+ "type": "boolean | undefined",
3424
+ "required": false,
3425
+ "description": "Enable the interactive shooter easter egg (halftone only). Default: false.",
3426
+ "defaultValue": "false"
3427
+ },
3428
+ {
3429
+ "name": "excludeCardSize",
3430
+ "type": "{ width: number; height: number; } | undefined",
3431
+ "required": false,
3432
+ "description": "Card dimensions for spawn-exclusion when game is enabled."
3433
+ },
3415
3434
  {
3416
3435
  "name": "height",
3417
3436
  "type": "string | number | undefined",
@@ -3693,16 +3712,7 @@
3693
3712
  "description": "@see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part}"
3694
3713
  }
3695
3714
  ],
3696
- "variants": [
3697
- {
3698
- "name": "texture",
3699
- "options": [
3700
- "clean",
3701
- "halftone"
3702
- ],
3703
- "defaultValue": "halftone"
3704
- }
3705
- ],
3715
+ "variants": [],
3706
3716
  "subComponents": [],
3707
3717
  "examples": [
3708
3718
  {
@@ -3711,7 +3721,7 @@
3711
3721
  },
3712
3722
  {
3713
3723
  "name": "WithCard",
3714
- "code": "() => (\n <div className='relative h-screen w-screen'>\n <AnimatedBackground />\n\n <Card className='absolute top-1/2 left-1/2 -translate-1/2 w-[300px] h-max'>\n <CardHeader>\n <CardTitle>Sign In</CardTitle>\n </CardHeader>\n <CardContent>\n <p className='text-sm text-text-secondary'>\n Decorative background renders behind interactive content.\n </p>\n </CardContent>\n </Card>\n </div>\n)"
3724
+ "code": "() => (\n <div className='relative h-screen w-screen'>\n <AnimatedBackground game />\n\n <Card className='absolute top-1/2 left-1/2 -translate-1/2 w-[300px] h-max'>\n <CardHeader>\n <CardTitle>Sign In</CardTitle>\n </CardHeader>\n <CardContent>\n <p className='text-sm text-text-secondary'>\n Decorative background renders behind interactive content.\n </p>\n </CardContent>\n </Card>\n </div>\n)"
3715
3725
  }
3716
3726
  ]
3717
3727
  },
@@ -1,9 +1,45 @@
1
+ @font-face {
2
+ font-family: 'Press Start 2P';
3
+ font-style: normal;
4
+ font-weight: 400;
5
+ font-display: swap;
6
+ src: url('https://fonts.gstatic.com/s/pressstart2p/v15/e3t4euO8T-267oIAQAu6jDQyK3nVivM.woff2') format('woff2');
7
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
8
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
9
+ U+2212, U+2215, U+FEFF, U+FFFD;
10
+ }
11
+
1
12
  :root {
2
13
  --animated-bg-dot: var(--color-slate-500);
3
14
  --animated-bg-accent-dot: var(--color-red-400);
15
+ --animated-bg-caught-dot: var(--color-green-500);
4
16
  }
5
17
 
6
18
  [data-theme="dark"] {
7
19
  --animated-bg-dot: var(--color-slate-500);
8
20
  --animated-bg-accent-dot: var(--color-red-400);
21
+ --animated-bg-caught-dot: var(--color-green-300);
22
+ }
23
+
24
+ @keyframes hud-in {
25
+ from {
26
+ opacity: 0;
27
+ transform: translateY(-4px);
28
+ }
29
+ to {
30
+ opacity: 1;
31
+ transform: translateY(0);
32
+ }
33
+ }
34
+
35
+ @keyframes catch-pop {
36
+ 0% {
37
+ transform: scale(1);
38
+ }
39
+ 40% {
40
+ transform: scale(1.25);
41
+ }
42
+ 100% {
43
+ transform: scale(1);
44
+ }
9
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.55.0",
3
+ "version": "0.55.1",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,3 +0,0 @@
1
- export declare const animatedBackgroundVariants: (props?: ({
2
- texture?: "clean" | "halftone" | null | undefined;
3
- } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -1,13 +0,0 @@
1
- import { cva } from "class-variance-authority";
2
- const animatedBackgroundVariants = cva('h-full w-full pointer-events-none', {
3
- variants: {
4
- texture: {
5
- clean: '',
6
- halftone: ''
7
- }
8
- },
9
- defaultVariants: {
10
- texture: 'halftone'
11
- }
12
- });
13
- export { animatedBackgroundVariants };
@@ -1,9 +0,0 @@
1
- import type { EngineOptions } from './types';
2
- export interface SweepEngine {
3
- start(): void;
4
- stop(): void;
5
- setOptions(next: Partial<EngineOptions>): void;
6
- renderStatic(t?: number): void;
7
- resize(): void;
8
- }
9
- export declare function createSweepEngine(canvas: HTMLCanvasElement, options: EngineOptions): SweepEngine;
@@ -1,217 +0,0 @@
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_BASE = '#f8fafc';
12
- const ALPHA_STEPS = 16;
13
- function buildPalette(rgb) {
14
- const p = new Array(ALPHA_STEPS + 1);
15
- for(let i = 0; i <= ALPHA_STEPS; i++)p[i] = `rgba(${rgb.r},${rgb.g},${rgb.b},${(i / ALPHA_STEPS).toFixed(3)})`;
16
- return p;
17
- }
18
- function alphaIdx(a) {
19
- return Math.min(ALPHA_STEPS, a * ALPHA_STEPS + 0.5 | 0);
20
- }
21
- function parseColor(value) {
22
- const v = value.trim();
23
- if (!v) return null;
24
- if ('#' === v[0]) {
25
- let hex = v.slice(1);
26
- if (3 === hex.length) hex = hex.split('').map((c)=>c + c).join('');
27
- if (6 !== hex.length) return null;
28
- const n = parseInt(hex, 16);
29
- if (Number.isNaN(n)) return null;
30
- return {
31
- r: n >> 16 & 255,
32
- g: n >> 8 & 255,
33
- b: 255 & n
34
- };
35
- }
36
- const m = v.match(/rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
37
- if (m?.[1] && m[2] && m[3]) return {
38
- r: +m[1],
39
- g: +m[2],
40
- b: +m[3]
41
- };
42
- return null;
43
- }
44
- function buildGrid(w, h, sp) {
45
- const cap = 20000;
46
- const safeSp = Math.max(sp, Math.sqrt(w * h / cap));
47
- const a = [];
48
- for(let y = safeSp / 2; y < h; y += safeSp)for(let x = safeSp / 2; x < w; x += safeSp)a.push({
49
- x,
50
- y
51
- });
52
- return a;
53
- }
54
- function sweepX(t, w, period) {
55
- return t % period / period * (w + 140) - 70;
56
- }
57
- function createSweepEngine(canvas, options) {
58
- const maybeCtx = canvas.getContext('2d');
59
- if (!maybeCtx) throw new Error('2D canvas context unavailable');
60
- const ctx = maybeCtx;
61
- let opts = {
62
- ...options
63
- };
64
- let w = 0;
65
- let h = 0;
66
- let dots = [];
67
- let baseColor = FALLBACK_BASE;
68
- let dotPalette = buildPalette(FALLBACK_DOT);
69
- let accentPalette = buildPalette(FALLBACK_ACCENT);
70
- let anomalyTime = -1 / 0;
71
- let anomalyX = 0;
72
- let anomalyY = 0;
73
- const latched = [];
74
- let lastLatch = -1 / 0;
75
- let rafId = null;
76
- let running = false;
77
- function resolveColors() {
78
- const cs = getComputedStyle(canvas);
79
- const dot = parseColor(cs.getPropertyValue(opts.dotColorVar)) ?? FALLBACK_DOT;
80
- const accent = parseColor(cs.getPropertyValue(opts.accentColorVar)) ?? FALLBACK_ACCENT;
81
- baseColor = cs.getPropertyValue(opts.baseColorVar).trim() || FALLBACK_BASE;
82
- dotPalette = buildPalette(dot);
83
- accentPalette = buildPalette(accent);
84
- }
85
- function resize() {
86
- const rect = canvas.getBoundingClientRect();
87
- w = Math.max(1, Math.round(rect.width));
88
- h = Math.max(1, Math.round(rect.height));
89
- const dpr = Math.min(window.devicePixelRatio || 1, 2);
90
- canvas.width = Math.round(w * dpr);
91
- canvas.height = Math.round(h * dpr);
92
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
93
- dots = buildGrid(w, h, opts.spacing);
94
- resolveColors();
95
- if (!running) renderStatic();
96
- }
97
- function drawClean(t) {
98
- const sx = sweepX(t, w, opts.sweepPeriod);
99
- const tanT = Math.tan(opts.tilt * Math.PI / 180);
100
- const k = opts.intensity;
101
- for (const d of dots){
102
- const dist = Math.abs(d.x - (sx + (h / 2 - d.y) * tanT));
103
- const inBloom = dist < opts.bloomRadius;
104
- const a = inBloom ? (0.13 + (opts.bloomAlpha - 0.13) * (1 - dist / opts.bloomRadius)) * k : 0.13 * k;
105
- const idx = alphaIdx(a);
106
- if (0 === idx) continue;
107
- ctx.fillStyle = dotPalette[idx];
108
- const half = inBloom ? 1.5 : 1;
109
- ctx.fillRect(d.x - half, d.y - half, 2 * half, 2 * half);
110
- }
111
- const sweepAt = (y)=>sx + (h / 2 - y) * tanT;
112
- if (t - lastLatch > opts.anomalyInterval && Math.random() < 0.05 && dots.length) {
113
- const cand = dots[Math.random() * dots.length | 0];
114
- if (cand.x < sweepAt(cand.y) - 8) {
115
- lastLatch = t;
116
- latched.push({
117
- x: cand.x,
118
- y: cand.y,
119
- t0: t
120
- });
121
- }
122
- }
123
- let kept = 0;
124
- for (const l of latched){
125
- const age = t - l.t0;
126
- if (age >= 3.2) continue;
127
- latched[kept++] = l;
128
- const fade = 1 - age / 3.2;
129
- const glowIdx = alphaIdx(0.35 * fade);
130
- if (glowIdx > 0) {
131
- ctx.fillStyle = accentPalette[glowIdx];
132
- ctx.fillRect(l.x - 5, l.y - 5, 10, 10);
133
- }
134
- ctx.fillStyle = accentPalette[alphaIdx(0.95 * fade)];
135
- ctx.fillRect(l.x - 2.4, l.y - 2.4, 4.8, 4.8);
136
- }
137
- latched.length = kept;
138
- }
139
- function drawHalftone(t) {
140
- const sx = sweepX(t, w, opts.sweepPeriod);
141
- const tanT = Math.tan(opts.tilt * Math.PI / 180);
142
- const k = opts.intensity;
143
- const halfCap = opts.maxDotSize / 2;
144
- if (t - anomalyTime > opts.anomalyInterval) {
145
- anomalyTime = t;
146
- anomalyX = 30 + Math.random() * Math.max(1, w - 60);
147
- anomalyY = 16 + Math.random() * Math.max(1, h - 32);
148
- }
149
- const aEnv = Math.max(0, Math.sin((t - anomalyTime) / 2.4 * Math.PI));
150
- for (const p of dots){
151
- const sxAt = sx + (h / 2 - p.y) * tanT;
152
- const amb = 0.11 * (0.5 + 0.5 * Math.sin(0.045 * p.x + 0.032 * p.y + 0.9 * t));
153
- const d = Math.abs(p.x - sxAt);
154
- const bloom = d < opts.bloomRadius ? 0.62 * (1 - d / opts.bloomRadius) : 0;
155
- const ad = Math.hypot(p.x - anomalyX, p.y - anomalyY);
156
- const ao = ad < 42 && p.x < sxAt ? aEnv * (1 - ad / 42) : 0;
157
- const val = Math.min(1, Math.max(amb + bloom, ao));
158
- if (val < 0.05) continue;
159
- const step = Math.round(5 * val);
160
- const half = Math.min(1.05 * step + 0.5, halfCap);
161
- if (ao > 0.06) ctx.fillStyle = accentPalette[alphaIdx(Math.min(1, 0.45 + ao))];
162
- else ctx.fillStyle = dotPalette[alphaIdx((0.12 + val * (opts.bloomAlpha - 0.12)) * k)];
163
- ctx.fillRect(p.x - half, p.y - half, 2 * half, 2 * half);
164
- }
165
- }
166
- function frame(t) {
167
- ctx.fillStyle = baseColor;
168
- ctx.fillRect(0, 0, w, h);
169
- if ('halftone' === opts.texture) drawHalftone(t);
170
- else drawClean(t);
171
- }
172
- function tick(ts) {
173
- if (!running) return;
174
- frame(ts / 1000);
175
- rafId = requestAnimationFrame(tick);
176
- }
177
- function onVisibility() {
178
- if (document.hidden) {
179
- if (null !== rafId) cancelAnimationFrame(rafId);
180
- rafId = null;
181
- } else if (running && null === rafId) rafId = requestAnimationFrame(tick);
182
- }
183
- function start() {
184
- if (running) return;
185
- running = true;
186
- document.addEventListener('visibilitychange', onVisibility);
187
- if (!document.hidden) rafId = requestAnimationFrame(tick);
188
- }
189
- function stop() {
190
- running = false;
191
- if (null !== rafId) cancelAnimationFrame(rafId);
192
- rafId = null;
193
- document.removeEventListener('visibilitychange', onVisibility);
194
- }
195
- function renderStatic(t = 1.4) {
196
- frame(t);
197
- }
198
- function setOptions(next) {
199
- const spacingChanged = void 0 !== next.spacing && next.spacing !== opts.spacing;
200
- opts = {
201
- ...opts,
202
- ...next
203
- };
204
- if (spacingChanged) dots = buildGrid(w, h, opts.spacing);
205
- resolveColors();
206
- if (!running) renderStatic();
207
- }
208
- resize();
209
- return {
210
- start,
211
- stop,
212
- setOptions,
213
- renderStatic,
214
- resize
215
- };
216
- }
217
- export { createSweepEngine };
@@ -1,3 +0,0 @@
1
- import type { AnimatedBackgroundProps } from './AnimatedBackground';
2
- import type { EngineOptions } from './types';
3
- export declare const resolveOptions: (props: AnimatedBackgroundProps) => EngineOptions;
@@ -1,24 +0,0 @@
1
- export type Texture = 'clean' | 'halftone';
2
- export interface EngineOptions {
3
- texture: Texture;
4
- /** Grid cell size in px. */
5
- spacing: number;
6
- /** Seconds for one L→R sweep pass. */
7
- sweepPeriod: number;
8
- /** How far from the scan line (px) dots react. */
9
- bloomRadius: number;
10
- /** Minimum seconds between accent events. */
11
- anomalyInterval: number;
12
- /** Global opacity multiplier (0–1). */
13
- intensity: number;
14
- /** Peak alpha of bloomed dots before `intensity`. */
15
- bloomAlpha: number;
16
- /** Halftone only: cap on a bloomed pixel's edge length (px). */
17
- maxDotSize: number;
18
- /** Sweep-line tilt in degrees (0 = vertical). */
19
- tilt: number;
20
- /** CSS custom-property names resolved at runtime. */
21
- dotColorVar: string;
22
- accentColorVar: string;
23
- baseColorVar: string;
24
- }