@caddis/cli 0.0.0 → 0.1.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 (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -1
  3. package/bundles/antigravity-plugin/agents/code-reviewer.md +47 -0
  4. package/bundles/antigravity-plugin/agents/preflight.md +53 -0
  5. package/bundles/antigravity-plugin/guard_agy.py +338 -0
  6. package/bundles/antigravity-plugin/hooks.json +36 -0
  7. package/bundles/antigravity-plugin/mcp_config.json +8 -0
  8. package/bundles/antigravity-plugin/mcp_ping_server.py +55 -0
  9. package/bundles/antigravity-plugin/plugin.json +5 -0
  10. package/bundles/antigravity-plugin/session_end_agy.py +57 -0
  11. package/bundles/antigravity-plugin/skills/_registry.md +115 -0
  12. package/bundles/antigravity-plugin/skills/add-rules/SKILL.md +45 -0
  13. package/bundles/antigravity-plugin/skills/api-design/SKILL.md +249 -0
  14. package/bundles/antigravity-plugin/skills/backend-development/SKILL.md +305 -0
  15. package/bundles/antigravity-plugin/skills/best-practices/SKILL.md +500 -0
  16. package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +263 -0
  17. package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +326 -0
  18. package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +245 -0
  19. package/bundles/antigravity-plugin/skills/best-practices/references/anti-patterns.md +571 -0
  20. package/bundles/antigravity-plugin/skills/best-practices/references/before-after-examples.md +1114 -0
  21. package/bundles/antigravity-plugin/skills/best-practices/references/best-practices-guide.md +513 -0
  22. package/bundles/antigravity-plugin/skills/best-practices/references/common-workflows.md +692 -0
  23. package/bundles/antigravity-plugin/skills/best-practices/references/prompt-patterns.md +547 -0
  24. package/bundles/antigravity-plugin/skills/brainstorming/SKILL.md +57 -0
  25. package/bundles/antigravity-plugin/skills/ci-cd-pipeline/SKILL.md +315 -0
  26. package/bundles/antigravity-plugin/skills/code-documentation/SKILL.md +271 -0
  27. package/bundles/antigravity-plugin/skills/code-review/SKILL.md +122 -0
  28. package/bundles/antigravity-plugin/skills/codebase-audit/SKILL.md +204 -0
  29. package/bundles/antigravity-plugin/skills/context-curator/SKILL.md +157 -0
  30. package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +40 -0
  31. package/bundles/antigravity-plugin/skills/css-architecture/SKILL.md +305 -0
  32. package/bundles/antigravity-plugin/skills/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
  33. package/bundles/antigravity-plugin/skills/database-design/SKILL.md +177 -0
  34. package/bundles/antigravity-plugin/skills/db-diagram/SKILL.md +148 -0
  35. package/bundles/antigravity-plugin/skills/db-diagram/scripts/sql_to_graph.py +1212 -0
  36. package/bundles/antigravity-plugin/skills/digress/SKILL.md +61 -0
  37. package/bundles/antigravity-plugin/skills/draw-io/SKILL.md +162 -0
  38. package/bundles/antigravity-plugin/skills/draw-io/references/aws-icons.md +677 -0
  39. package/bundles/antigravity-plugin/skills/draw-io/references/layout-guidelines.md +142 -0
  40. package/bundles/antigravity-plugin/skills/draw-io/references/troubleshooting.md +118 -0
  41. package/bundles/antigravity-plugin/skills/draw-io/references/workflows.md +103 -0
  42. package/bundles/antigravity-plugin/skills/draw-io/scripts/convert-drawio-to-png.sh +25 -0
  43. package/bundles/antigravity-plugin/skills/draw-io/scripts/find_aws_icon.py +79 -0
  44. package/bundles/antigravity-plugin/skills/error-handling/SKILL.md +260 -0
  45. package/bundles/antigravity-plugin/skills/excalidraw-db/SKILL.md +38 -0
  46. package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +300 -0
  47. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +198 -0
  48. package/bundles/antigravity-plugin/skills/frontend-design/SKILL.md +193 -0
  49. package/bundles/antigravity-plugin/skills/frontend-design/references/my-tech-stack.md +127 -0
  50. package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +195 -0
  51. package/bundles/antigravity-plugin/skills/git-commit/SKILL.md +285 -0
  52. package/bundles/antigravity-plugin/skills/golden-plan/SKILL.md +577 -0
  53. package/bundles/antigravity-plugin/skills/handoff/SKILL.md +100 -0
  54. package/bundles/antigravity-plugin/skills/implement/SKILL.md +114 -0
  55. package/bundles/antigravity-plugin/skills/javascript-typescript/SKILL.md +142 -0
  56. package/bundles/antigravity-plugin/skills/kb/SKILL.md +60 -0
  57. package/bundles/antigravity-plugin/skills/mermaid-db/SKILL.md +33 -0
  58. package/bundles/antigravity-plugin/skills/mermaid-diagrams/SKILL.md +236 -0
  59. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
  60. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
  61. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/advanced-features.md +556 -0
  62. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/architecture-diagrams.md +192 -0
  63. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/c4-diagrams.md +410 -0
  64. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/class-diagrams.md +361 -0
  65. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/erd-diagrams.md +510 -0
  66. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/flowcharts.md +450 -0
  67. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/sequence-diagrams.md +394 -0
  68. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/troubleshooting.md +335 -0
  69. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/workflows.md +418 -0
  70. package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +68 -0
  71. package/bundles/antigravity-plugin/skills/mockup/SKILL.md +242 -0
  72. package/bundles/antigravity-plugin/skills/particle-art/SKILL.md +243 -0
  73. package/bundles/antigravity-plugin/skills/particle-art/references/canvas-utils.ts +171 -0
  74. package/bundles/antigravity-plugin/skills/particle-art/references/dot-field.template.tsx +203 -0
  75. package/bundles/antigravity-plugin/skills/particle-art/references/flow-field.template.tsx +263 -0
  76. package/bundles/antigravity-plugin/skills/particle-art/references/node-shape.template.tsx +261 -0
  77. package/bundles/antigravity-plugin/skills/particle-art/references/shape-sampler.ts +281 -0
  78. package/bundles/antigravity-plugin/skills/particle-art/references/stipple-morph.template.tsx +167 -0
  79. package/bundles/antigravity-plugin/skills/particle-art/references/stipple.template.tsx +175 -0
  80. package/bundles/antigravity-plugin/skills/particle-art/references/trail-ghost.template.tsx +266 -0
  81. package/bundles/antigravity-plugin/skills/particle-art/references/usage-examples.md +320 -0
  82. package/bundles/antigravity-plugin/skills/playwright/API_REFERENCE.md +653 -0
  83. package/bundles/antigravity-plugin/skills/playwright/SKILL.md +454 -0
  84. package/bundles/antigravity-plugin/skills/playwright/lib/helpers.js +441 -0
  85. package/bundles/antigravity-plugin/skills/playwright/package.json +26 -0
  86. package/bundles/antigravity-plugin/skills/playwright/run.js +228 -0
  87. package/bundles/antigravity-plugin/skills/prd/SKILL.md +107 -0
  88. package/bundles/antigravity-plugin/skills/preflight/SKILL.md +435 -0
  89. package/bundles/antigravity-plugin/skills/python/SKILL.md +388 -0
  90. package/bundles/antigravity-plugin/skills/react-best-practices/SKILL.md +269 -0
  91. package/bundles/antigravity-plugin/skills/react-dev/README.md +404 -0
  92. package/bundles/antigravity-plugin/skills/react-dev/SKILL.md +459 -0
  93. package/bundles/antigravity-plugin/skills/react-dev/examples/generic-components.md +579 -0
  94. package/bundles/antigravity-plugin/skills/react-dev/examples/server-components.md +579 -0
  95. package/bundles/antigravity-plugin/skills/react-dev/references/event-handlers.md +574 -0
  96. package/bundles/antigravity-plugin/skills/react-dev/references/hooks.md +456 -0
  97. package/bundles/antigravity-plugin/skills/react-dev/references/react-19-patterns.md +638 -0
  98. package/bundles/antigravity-plugin/skills/react-dev/references/react-router.md +1002 -0
  99. package/bundles/antigravity-plugin/skills/react-dev/references/tanstack-router.md +587 -0
  100. package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +486 -0
  101. package/bundles/antigravity-plugin/skills/resume/SKILL.md +36 -0
  102. package/bundles/antigravity-plugin/skills/security-review/SKILL.md +196 -0
  103. package/bundles/antigravity-plugin/skills/setup-project-ai/SKILL.md +61 -0
  104. package/bundles/antigravity-plugin/skills/ship/SKILL.md +107 -0
  105. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +103 -0
  106. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +102 -0
  107. package/bundles/antigravity-plugin/skills/skill-creator/LICENSE.txt +202 -0
  108. package/bundles/antigravity-plugin/skills/skill-creator/SKILL.md +491 -0
  109. package/bundles/antigravity-plugin/skills/skill-creator/agents/analyzer.md +274 -0
  110. package/bundles/antigravity-plugin/skills/skill-creator/agents/comparator.md +202 -0
  111. package/bundles/antigravity-plugin/skills/skill-creator/agents/grader.md +223 -0
  112. package/bundles/antigravity-plugin/skills/skill-creator/assets/eval_review.html +146 -0
  113. package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  114. package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  115. package/bundles/antigravity-plugin/skills/skill-creator/references/schemas.md +430 -0
  116. package/bundles/antigravity-plugin/skills/skill-creator/scripts/__init__.py +0 -0
  117. package/bundles/antigravity-plugin/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  118. package/bundles/antigravity-plugin/skills/skill-creator/scripts/generate_report.py +326 -0
  119. package/bundles/antigravity-plugin/skills/skill-creator/scripts/improve_description.py +247 -0
  120. package/bundles/antigravity-plugin/skills/skill-creator/scripts/package_skill.py +136 -0
  121. package/bundles/antigravity-plugin/skills/skill-creator/scripts/quick_validate.py +103 -0
  122. package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_eval.py +310 -0
  123. package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_loop.py +328 -0
  124. package/bundles/antigravity-plugin/skills/skill-creator/scripts/utils.py +47 -0
  125. package/bundles/antigravity-plugin/skills/sql/SKILL.md +321 -0
  126. package/bundles/antigravity-plugin/skills/tdd/SKILL.md +37 -0
  127. package/bundles/antigravity-plugin/skills/tdd-workflow/SKILL.md +188 -0
  128. package/bundles/antigravity-plugin/skills/technical-writing/SKILL.md +286 -0
  129. package/bundles/antigravity-plugin/skills/test-strategy/SKILL.md +155 -0
  130. package/bundles/antigravity-plugin/skills/ui-brief/SKILL.md +84 -0
  131. package/bundles/antigravity-plugin/skills/ui-review/SKILL.md +176 -0
  132. package/bundles/antigravity-plugin/skills/ui-review/references/framework-fixes.md +471 -0
  133. package/bundles/antigravity-plugin/skills/ui-review/references/visual-checklist.md +236 -0
  134. package/bundles/antigravity-plugin/skills/usage-review/SKILL.md +77 -0
  135. package/bundles/antigravity-plugin/skills/use-model/SKILL.md +64 -0
  136. package/bundles/antigravity-plugin/skills/using-git-worktrees/SKILL.md +217 -0
  137. package/bundles/antigravity-plugin/skills/version/SKILL.md +18 -0
  138. package/bundles/antigravity-plugin/skills/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
  139. package/bundles/antigravity-plugin/skills/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
  140. package/bundles/antigravity-plugin/skills/warm-editorial-ui/SKILL.md +732 -0
  141. package/bundles/antigravity-plugin/skills/webapp-testing/LICENSE.txt +202 -0
  142. package/bundles/antigravity-plugin/skills/webapp-testing/SKILL.md +97 -0
  143. package/bundles/antigravity-plugin/skills/webapp-testing/examples/console_logging.py +35 -0
  144. package/bundles/antigravity-plugin/skills/webapp-testing/examples/element_discovery.py +40 -0
  145. package/bundles/antigravity-plugin/skills/webapp-testing/examples/static_html_automation.py +33 -0
  146. package/bundles/antigravity-plugin/skills/webapp-testing/scripts/with_server.py +106 -0
  147. package/bundles/antigravity-plugin/skills/windows-deployment/SKILL.md +880 -0
  148. package/bundles/antigravity-plugin/skills/writing-plans/SKILL.md +384 -0
  149. package/bundles/antigravity-plugin/statusline-command-agy.sh +91 -0
  150. package/bundles/antigravity-plugin/warm_start_agy.py +149 -0
  151. package/bundles/manifest.json +6 -0
  152. package/dist/cli.js +5363 -0
  153. package/package.json +61 -4
@@ -0,0 +1,203 @@
1
+ "use client";
2
+ /**
3
+ * dot-field.template.tsx — Gaussian canvas dot cloud with sparks template.
4
+ *
5
+ * USAGE:
6
+ * 1. Copy and rename (e.g. HeroDotField.tsx, CosmicField.tsx)
7
+ * 2. Adjust DOT_COUNT, SIGMA (spread), spark parameters to taste
8
+ * 3. Mouse moves dots away; they spring back slowly to their base positions
9
+ *
10
+ * THEMING: --accent-strong (inner/bright), --accent-soft (outer/dim) CSS vars
11
+ * DEPENDENCIES: none
12
+ */
13
+ import { useEffect, useRef } from "react";
14
+
15
+ // ── Constants ──────────────────────────────────────────────────────────────────
16
+ const DOT_COUNT = 120; // total dot count (lower for fine dot feel)
17
+ const SIGMA = 0.28; // Gaussian standard deviation [0–0.5]; lower = tighter cluster
18
+ const SPRING_BACK = 0.08; // how fast dots return to base position after repulsion
19
+ const REPEL_MULT = 0.2; // repulsion radius = size * REPEL_MULT
20
+ const SPARK_PROB = 0.008; // probability per frame to spawn a spark
21
+ const MAX_SPARKS = 8;
22
+
23
+ // ── Helpers ────────────────────────────────────────────────────────────────────
24
+ function themeRgb(v: string): [number, number, number] {
25
+ const ch = getComputedStyle(document.documentElement)
26
+ .getPropertyValue(v).trim().split(/\s+/).map(Number);
27
+ return [ch[0] ?? 100, ch[1] ?? 100, ch[2] ?? 100];
28
+ }
29
+
30
+ // Box-Muller: Gaussian sample with mean=0, std=sigma
31
+ function gaussian(sigma: number): number {
32
+ let u = 0, v = 0;
33
+ while (u === 0) u = Math.random();
34
+ while (v === 0) v = Math.random();
35
+ return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) * sigma;
36
+ }
37
+
38
+ interface Dot {
39
+ bx: number; by: number; // base position (centre of gravity)
40
+ x: number; y: number; // current position
41
+ r: number; // radius
42
+ distRatio: number; // normalised distance from centre [0, 1]
43
+ }
44
+
45
+ interface Spark {
46
+ x: number; y: number;
47
+ vx: number; vy: number;
48
+ life: number; // [1, 0] — fades and shrinks as it dies
49
+ }
50
+
51
+ function buildDots(size: number): Dot[] {
52
+ const cx = size / 2, cy = size / 2;
53
+ const dots: Dot[] = [];
54
+ for (let i = 0; i < DOT_COUNT; i++) {
55
+ const bx = cx + gaussian(SIGMA) * size;
56
+ const by = cy + gaussian(SIGMA) * size;
57
+ const dx = (bx - cx) / (size * 0.5);
58
+ const dy = (by - cy) / (size * 0.5);
59
+ dots.push({
60
+ bx, by, x: bx, y: by,
61
+ r: Math.random() * 1.8 + 0.8,
62
+ distRatio: Math.sqrt(dx * dx + dy * dy),
63
+ });
64
+ }
65
+ return dots;
66
+ }
67
+
68
+ // ── Component ──────────────────────────────────────────────────────────────────
69
+ export function DotFieldArt({ size = 280 }: { size?: number }) {
70
+ const ref = useRef<HTMLCanvasElement>(null);
71
+
72
+ useEffect(() => {
73
+ const el = ref.current;
74
+ if (!el) return;
75
+
76
+ const ctx = el.getContext("2d")!;
77
+ const dpr = window.devicePixelRatio || 1;
78
+ el.width = size * dpr;
79
+ el.height = size * dpr;
80
+ ctx.scale(dpr, dpr);
81
+
82
+ const dots = buildDots(size);
83
+ const sparks: Spark[] = [];
84
+ const REPEL = size * REPEL_MULT;
85
+
86
+ // Mouse tracking
87
+ const mouse = { x: -9999, y: -9999 };
88
+ const onMove = (e: MouseEvent) => {
89
+ const r = el.getBoundingClientRect();
90
+ mouse.x = e.clientX - r.left;
91
+ mouse.y = e.clientY - r.top;
92
+ };
93
+ const onLeave = () => { mouse.x = -9999; mouse.y = -9999; };
94
+ el.addEventListener("mousemove", onMove);
95
+ el.addEventListener("mouseleave", onLeave);
96
+
97
+ let raf: number;
98
+
99
+ const tick = () => {
100
+ ctx.clearRect(0, 0, size, size);
101
+ const [sr, sg, sb] = themeRgb("--accent-strong");
102
+ const [lr, lg, lb] = themeRgb("--accent-soft");
103
+ const { x: mx, y: my } = mouse;
104
+
105
+ // ── Update dots ────────────────────────────────────────────────────
106
+ dots.forEach(d => {
107
+ // Repulsion
108
+ const dx = d.x - mx, dy = d.y - my;
109
+ const dist = Math.sqrt(dx * dx + dy * dy);
110
+ if (dist < REPEL && dist > 0) {
111
+ const f = ((REPEL - dist) / REPEL) * 2.5;
112
+ d.x += (dx / dist) * f;
113
+ d.y += (dy / dist) * f;
114
+ }
115
+ // Spring back to base
116
+ d.x += (d.bx - d.x) * SPRING_BACK;
117
+ d.y += (d.by - d.y) * SPRING_BACK;
118
+
119
+ // Colour blend: inner → --accent-strong, outer → --accent-soft
120
+ const t = Math.min(1, d.distRatio * 1.6);
121
+ const r2 = sr + (lr - sr) * t;
122
+ const g2 = sg + (lg - sg) * t;
123
+ const b2 = sb + (lb - sb) * t;
124
+ const alpha = Math.max(0.12, 0.85 - d.distRatio * 0.7);
125
+
126
+ ctx.beginPath();
127
+ ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
128
+ ctx.fillStyle = `rgba(${Math.round(r2)},${Math.round(g2)},${Math.round(b2)},${alpha.toFixed(3)})`;
129
+ ctx.fill();
130
+ });
131
+
132
+ // ── Sparks ────────────────────────────────────────────────────────
133
+ if (Math.random() < SPARK_PROB && sparks.length < MAX_SPARKS) {
134
+ const angle = Math.random() * Math.PI * 2;
135
+ const spawnR = size * 0.08 + Math.random() * size * 0.15;
136
+ sparks.push({
137
+ x: size / 2 + Math.cos(angle) * spawnR,
138
+ y: size / 2 + Math.sin(angle) * spawnR,
139
+ vx: (Math.random() - 0.5) * 2.2,
140
+ vy: (Math.random() - 0.5) * 2.2,
141
+ life: 1,
142
+ });
143
+ }
144
+
145
+ for (let i = sparks.length - 1; i >= 0; i--) {
146
+ const s = sparks[i];
147
+ s.x += s.vx;
148
+ s.y += s.vy;
149
+ s.vx *= 0.96;
150
+ s.vy *= 0.96;
151
+ s.life -= 0.025;
152
+ if (s.life <= 0) { sparks.splice(i, 1); continue; }
153
+
154
+ ctx.beginPath();
155
+ ctx.arc(s.x, s.y, s.life * 1.8, 0, Math.PI * 2);
156
+ ctx.fillStyle = `rgba(${sr},${sg},${sb},${(s.life * 0.9).toFixed(3)})`;
157
+ ctx.fill();
158
+ }
159
+
160
+ raf = requestAnimationFrame(tick);
161
+ };
162
+
163
+ // Reduced-motion: draw one static frame of dots at base positions, skip the loop.
164
+ // Never return early before drawing — that leaves the canvas blank.
165
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
166
+ tick();
167
+ cancelAnimationFrame(raf);
168
+ return;
169
+ }
170
+
171
+ tick();
172
+ return () => {
173
+ cancelAnimationFrame(raf);
174
+ el.removeEventListener("mousemove", onMove);
175
+ el.removeEventListener("mouseleave", onLeave);
176
+ };
177
+ }, [size]);
178
+
179
+ return (
180
+ <div style={{ position: "relative", width: size, height: size }} aria-hidden="true">
181
+ <canvas
182
+ ref={ref}
183
+ style={{ width: size, height: size }}
184
+ />
185
+ {/* Centre ✦ as HTML so it scales with the canvas and stays crisp */}
186
+ <span
187
+ style={{
188
+ position: "absolute",
189
+ top: "50%",
190
+ left: "50%",
191
+ transform: "translate(-50%, -50%)",
192
+ fontSize: "1.4rem",
193
+ color: "rgb(var(--accent-strong))",
194
+ opacity: 0.72,
195
+ pointerEvents: "none",
196
+ userSelect: "none",
197
+ }}
198
+ >
199
+
200
+ </span>
201
+ </div>
202
+ );
203
+ }
@@ -0,0 +1,263 @@
1
+ "use client";
2
+ /**
3
+ * flow-field.template.tsx — Particles driven by 2D value noise; morph into shape on hover.
4
+ *
5
+ * USAGE:
6
+ * 1. Copy and rename (e.g. FlowField.tsx, HeroFlow.tsx)
7
+ * 2. Replace `buildTargets` with any shape from shape-sampler.ts
8
+ * 3. Tune FLOW_FORCE (current strength), FRICTION, and MORPH_SPEED
9
+ * 4. Ambient-only mode: remove letter targets entirely; particles just drift with noise
10
+ *
11
+ * KEY TECHNIQUE: every particle follows a `flowAngle` derived from two octaves of value noise
12
+ * (vnoise). On hover, spring forces toward letter targets compete with the noise field; morph
13
+ * controls the blend (0 = pure noise, 1 = 25% noise + 75% spring). Particles are drawn as
14
+ * velocity-aligned streaks to show flow direction.
15
+ *
16
+ * INTERACTION: mousemove → letter assembles from flowing particles; mouseleave → disperses back
17
+ * THEMING: --accent-strong (streak/node), --accent-soft (connection lines)
18
+ */
19
+ import { useEffect, useRef } from "react";
20
+
21
+ // ── 2D value noise ─────────────────────────────────────────────────────────────
22
+ //
23
+ // Coherent noise — same sin-hash base as shape-sampler.ts.
24
+ // Input: continuous (x, y). Output: 0..1, smoothly varying.
25
+ //
26
+ function vnoise(x: number, y: number): number {
27
+ const ix = Math.floor(x), iy = Math.floor(y);
28
+ const fx = x - ix, fy = y - iy;
29
+ const ux = fx * fx * (3 - 2 * fx); // smoothstep
30
+ const uy = fy * fy * (3 - 2 * fy);
31
+ const h = (a: number, b: number) =>
32
+ (Math.abs(Math.sin(a * 127.1 + b * 311.7) * 43758.5453)) % 1;
33
+ const aa = h(ix, iy), ba = h(ix+1, iy);
34
+ const ab = h(ix, iy+1), bb = h(ix+1, iy+1);
35
+ return aa + (ba-aa)*ux + (ab-aa)*uy + (aa-ba-ab+bb)*ux*uy;
36
+ }
37
+
38
+ // Two-octave flow angle — animates over time via `t`
39
+ // CUSTOMISE: adjust multipliers for scale (0.008 = large cells) and speed (0.00035 = slow drift)
40
+ function flowAngle(x: number, y: number, t: number): number {
41
+ const n1 = vnoise(x * 0.008 + t * 0.00035, y * 0.008);
42
+ const n2 = vnoise(x * 0.015 + t * 0.0005 + 100, y * 0.015 + 50);
43
+ return (n1 * 0.65 + n2 * 0.35) * Math.PI * 4;
44
+ }
45
+
46
+ // ── Target shape — replace or import from shape-sampler.ts ───────────────────
47
+ function cubicBezier(
48
+ t: number,
49
+ p0: [number,number], p1: [number,number],
50
+ p2: [number,number], p3: [number,number],
51
+ ): [number,number] {
52
+ const m = 1 - t;
53
+ return [
54
+ m**3*p0[0]+3*m**2*t*p1[0]+3*m*t**2*p2[0]+t**3*p3[0],
55
+ m**3*p0[1]+3*m**2*t*p1[1]+3*m*t**2*p2[1]+t**3*p3[1],
56
+ ];
57
+ }
58
+
59
+ interface Target { tx: number; ty: number; spring: number; r: number; alpha: number; isDot: boolean }
60
+
61
+ function buildTargets(size: number): Target[] {
62
+ const cx = size * 0.50;
63
+ const stemTop = size * 0.210, stemBot = size * 0.660;
64
+ const out: Target[] = [];
65
+
66
+ // Tittle
67
+ out.push({ tx: cx, ty: size * 0.130, spring: 0.030, r: 4.5, alpha: 1.0, isDot: true });
68
+
69
+ // Stem
70
+ for (let i = 0; i <= 12; i++)
71
+ out.push({ tx: cx, ty: stemTop + (i/12) * (stemBot-stemTop), spring: 0.028, r: 2.2, alpha: 0.92, isDot: false });
72
+
73
+ // Hook
74
+ const P0: [number,number] = [cx, stemBot];
75
+ const P1: [number,number] = [cx, stemBot + size*0.115];
76
+ const P2: [number,number] = [cx - size*0.115, stemBot + size*0.145];
77
+ const P3: [number,number] = [cx - size*0.155, stemBot + size*0.040];
78
+ for (let i = 1; i <= 9; i++) {
79
+ const [bx,by] = cubicBezier(i/9, P0, P1, P2, P3);
80
+ out.push({ tx: bx, ty: by, spring: 0.026, r: 2.2, alpha: 0.88, isDot: false });
81
+ }
82
+ return out;
83
+ }
84
+
85
+ // ── Types & theme helper ──────────────────────────────────────────────────────
86
+ interface Pt {
87
+ x: number; y: number; vx: number; vy: number;
88
+ tx: number; ty: number; spring: number;
89
+ r: number; alpha: number; isDot: boolean; isLetter: boolean;
90
+ }
91
+
92
+ function themeRgb(v: string): [number, number, number] {
93
+ const ch = getComputedStyle(document.documentElement)
94
+ .getPropertyValue(v).trim().split(/\s+/).map(Number);
95
+ return [ch[0], ch[1], ch[2]];
96
+ }
97
+
98
+ // ── Constants ─────────────────────────────────────────────────────────────────
99
+ const FLOW_FORCE = 0.06; // noise field push strength; raise for wilder currents
100
+ const FRICTION = 0.965; // less damping than spring components — particles glide
101
+ const MAX_SPEED = 1.8;
102
+ const MORPH_SPEED = 0.048; // lerp speed 0→1; lower = longer, dreamier transition
103
+ const LINK = 80; // connection line distance threshold (px)
104
+
105
+ // ── Component ─────────────────────────────────────────────────────────────────
106
+ export function FlowFieldArt({ size = 300 }: { size?: number }) {
107
+ const ref = useRef<HTMLCanvasElement>(null);
108
+ // useRef — never stale in the RAF loop
109
+ const active = useRef(false);
110
+ const morph = useRef(0);
111
+
112
+ useEffect(() => {
113
+ const el = ref.current;
114
+ if (!el) return;
115
+
116
+ const ctx = el.getContext("2d")!;
117
+ const dpr = window.devicePixelRatio || 1;
118
+ el.width = size * dpr;
119
+ el.height = size * dpr;
120
+ ctx.scale(dpr, dpr);
121
+
122
+ const cx = size * 0.50, cy = size * 0.46;
123
+
124
+ // Letter particles — scattered initially; attracted to letter on hover
125
+ const pts: Pt[] = buildTargets(size).map(t => ({
126
+ x: cx + (Math.random() - 0.5) * size * 0.6,
127
+ y: cy + (Math.random() - 0.5) * size * 0.6,
128
+ vx: (Math.random() - 0.5) * 0.5,
129
+ vy: (Math.random() - 0.5) * 0.5,
130
+ tx: t.tx, ty: t.ty, spring: t.spring,
131
+ r: t.r, alpha: t.alpha, isDot: t.isDot, isLetter: true,
132
+ }));
133
+
134
+ // Ambient flow-only particles — no letter target; fill the field
135
+ // CUSTOMISE: increase count for denser streams
136
+ for (let i = 0; i < 22; i++) {
137
+ pts.push({
138
+ x: Math.random() * size, y: Math.random() * size,
139
+ vx: (Math.random() - 0.5) * 0.4,
140
+ vy: (Math.random() - 0.5) * 0.4,
141
+ tx: cx, ty: cy, spring: 0,
142
+ r: Math.random() * 1.2 + 0.5,
143
+ alpha: 0.28 + Math.random() * 0.22,
144
+ isDot: false, isLetter: false,
145
+ });
146
+ }
147
+
148
+ const onMove = () => { active.current = true; };
149
+ const onLeave = () => { active.current = false; };
150
+ el.addEventListener("mousemove", onMove);
151
+ el.addEventListener("mouseleave", onLeave);
152
+
153
+ let raf: number;
154
+ let frame = 0;
155
+
156
+ const tick = () => {
157
+ ctx.clearRect(0, 0, size, size);
158
+ const [nr, ng, nb] = themeRgb("--accent-strong");
159
+ const [lr, lg, lb] = themeRgb("--accent-soft");
160
+
161
+ const m = morph.current + ((active.current ? 1 : 0) - morph.current) * MORPH_SPEED;
162
+ morph.current = m;
163
+
164
+ // Connection lines — grow more visible as letter forms
165
+ for (let i = 0; i < pts.length; i++) {
166
+ for (let j = i + 1; j < pts.length; j++) {
167
+ const dx = pts[i].x - pts[j].x, dy = pts[i].y - pts[j].y;
168
+ const d = Math.sqrt(dx*dx + dy*dy);
169
+ if (d < LINK) {
170
+ const a = (1 - d/LINK) * 0.14 * (0.4 + m*0.6);
171
+ ctx.beginPath();
172
+ ctx.moveTo(pts[i].x, pts[i].y);
173
+ ctx.lineTo(pts[j].x, pts[j].y);
174
+ ctx.strokeStyle = `rgba(${lr},${lg},${lb},${a.toFixed(3)})`;
175
+ ctx.lineWidth = 0.6;
176
+ ctx.stroke();
177
+ }
178
+ }
179
+ }
180
+
181
+ pts.forEach(p => {
182
+ // 1. Flow field force — attenuates as morph strengthens (25% remains at m=1)
183
+ const angle = flowAngle(p.x, p.y, frame);
184
+ const flowScale = 1 - m * 0.75;
185
+ p.vx += Math.cos(angle) * FLOW_FORCE * flowScale;
186
+ p.vy += Math.sin(angle) * FLOW_FORCE * flowScale;
187
+
188
+ // 2. Spring toward letter target (only for letter particles; scales with morph)
189
+ if (p.isLetter && m > 0.01) {
190
+ p.vx += (p.tx - p.x) * p.spring * m;
191
+ p.vy += (p.ty - p.y) * p.spring * m;
192
+ }
193
+
194
+ // 3. Edge wrap in idle (disable during morph to let letter form cleanly)
195
+ if (m < 0.3) {
196
+ if (p.x < 0) p.x = size;
197
+ if (p.x > size) p.x = 0;
198
+ if (p.y < 0) p.y = size;
199
+ if (p.y > size) p.y = 0;
200
+ }
201
+
202
+ p.vx *= FRICTION; p.vy *= FRICTION;
203
+ const spd = Math.sqrt(p.vx*p.vx + p.vy*p.vy);
204
+ if (spd > MAX_SPEED) { p.vx *= MAX_SPEED/spd; p.vy *= MAX_SPEED/spd; }
205
+ p.x += p.vx; p.y += p.vy;
206
+
207
+ // 4. Velocity-aligned streak — shows flow direction
208
+ // Length proportional to speed (particles moving faster cast longer streaks)
209
+ const streakLen = Math.sqrt(p.vx*p.vx + p.vy*p.vy) * 5;
210
+ const alpha = p.isLetter
211
+ ? p.alpha * (0.55 + m * 0.45) // letter particles brighten as form solidifies
212
+ : p.alpha;
213
+
214
+ ctx.beginPath();
215
+ ctx.moveTo(p.x - p.vx * streakLen, p.y - p.vy * streakLen);
216
+ ctx.lineTo(p.x, p.y);
217
+ ctx.strokeStyle = `rgba(${nr},${ng},${nb},${alpha.toFixed(3)})`;
218
+ ctx.lineWidth = p.r * 1.4;
219
+ ctx.lineCap = "round";
220
+ ctx.stroke();
221
+
222
+ // 5. Solid node dot at head — visible only once letter starts forming
223
+ if (m > 0.2 && p.isLetter) {
224
+ let nodeR = p.r;
225
+ if (p.isDot) {
226
+ nodeR = p.r + Math.sin(frame * 0.04) * 0.8;
227
+ // Pulsing glow ring for tittle
228
+ ctx.beginPath();
229
+ ctx.arc(p.x, p.y, nodeR + 4 + Math.sin(frame * 0.04) * 2, 0, Math.PI * 2);
230
+ ctx.strokeStyle = `rgba(${nr},${ng},${nb},${(0.18 * m).toFixed(3)})`;
231
+ ctx.lineWidth = 1;
232
+ ctx.stroke();
233
+ }
234
+ ctx.beginPath();
235
+ ctx.arc(p.x, p.y, nodeR * m, 0, Math.PI * 2);
236
+ ctx.fillStyle = `rgba(${nr},${ng},${nb},${(alpha * m).toFixed(3)})`;
237
+ ctx.fill();
238
+ }
239
+ });
240
+
241
+ frame++;
242
+ raf = requestAnimationFrame(tick);
243
+ };
244
+ tick();
245
+
246
+ // ⚠️ prefers-reduced-motion: do NOT early-return before drawing (leaves canvas blank).
247
+ // To support reduced-motion: snap pts to targets, call tick() once, then cancelRAF.
248
+
249
+ return () => {
250
+ cancelAnimationFrame(raf);
251
+ el.removeEventListener("mousemove", onMove);
252
+ el.removeEventListener("mouseleave", onLeave);
253
+ };
254
+ }, [size]);
255
+
256
+ return (
257
+ <canvas
258
+ ref={ref}
259
+ style={{ width: size, height: size, cursor: "crosshair" }}
260
+ aria-hidden="true"
261
+ />
262
+ );
263
+ }