@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.
- package/LICENSE +21 -0
- package/README.md +150 -1
- package/bundles/antigravity-plugin/agents/code-reviewer.md +47 -0
- package/bundles/antigravity-plugin/agents/preflight.md +53 -0
- package/bundles/antigravity-plugin/guard_agy.py +338 -0
- package/bundles/antigravity-plugin/hooks.json +36 -0
- package/bundles/antigravity-plugin/mcp_config.json +8 -0
- package/bundles/antigravity-plugin/mcp_ping_server.py +55 -0
- package/bundles/antigravity-plugin/plugin.json +5 -0
- package/bundles/antigravity-plugin/session_end_agy.py +57 -0
- package/bundles/antigravity-plugin/skills/_registry.md +115 -0
- package/bundles/antigravity-plugin/skills/add-rules/SKILL.md +45 -0
- package/bundles/antigravity-plugin/skills/api-design/SKILL.md +249 -0
- package/bundles/antigravity-plugin/skills/backend-development/SKILL.md +305 -0
- package/bundles/antigravity-plugin/skills/best-practices/SKILL.md +500 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +263 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +326 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +245 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/anti-patterns.md +571 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/before-after-examples.md +1114 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/best-practices-guide.md +513 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/common-workflows.md +692 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/prompt-patterns.md +547 -0
- package/bundles/antigravity-plugin/skills/brainstorming/SKILL.md +57 -0
- package/bundles/antigravity-plugin/skills/ci-cd-pipeline/SKILL.md +315 -0
- package/bundles/antigravity-plugin/skills/code-documentation/SKILL.md +271 -0
- package/bundles/antigravity-plugin/skills/code-review/SKILL.md +122 -0
- package/bundles/antigravity-plugin/skills/codebase-audit/SKILL.md +204 -0
- package/bundles/antigravity-plugin/skills/context-curator/SKILL.md +157 -0
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +40 -0
- package/bundles/antigravity-plugin/skills/css-architecture/SKILL.md +305 -0
- package/bundles/antigravity-plugin/skills/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
- package/bundles/antigravity-plugin/skills/database-design/SKILL.md +177 -0
- package/bundles/antigravity-plugin/skills/db-diagram/SKILL.md +148 -0
- package/bundles/antigravity-plugin/skills/db-diagram/scripts/sql_to_graph.py +1212 -0
- package/bundles/antigravity-plugin/skills/digress/SKILL.md +61 -0
- package/bundles/antigravity-plugin/skills/draw-io/SKILL.md +162 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/aws-icons.md +677 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/layout-guidelines.md +142 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/troubleshooting.md +118 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/workflows.md +103 -0
- package/bundles/antigravity-plugin/skills/draw-io/scripts/convert-drawio-to-png.sh +25 -0
- package/bundles/antigravity-plugin/skills/draw-io/scripts/find_aws_icon.py +79 -0
- package/bundles/antigravity-plugin/skills/error-handling/SKILL.md +260 -0
- package/bundles/antigravity-plugin/skills/excalidraw-db/SKILL.md +38 -0
- package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +300 -0
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +198 -0
- package/bundles/antigravity-plugin/skills/frontend-design/SKILL.md +193 -0
- package/bundles/antigravity-plugin/skills/frontend-design/references/my-tech-stack.md +127 -0
- package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +195 -0
- package/bundles/antigravity-plugin/skills/git-commit/SKILL.md +285 -0
- package/bundles/antigravity-plugin/skills/golden-plan/SKILL.md +577 -0
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +100 -0
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +114 -0
- package/bundles/antigravity-plugin/skills/javascript-typescript/SKILL.md +142 -0
- package/bundles/antigravity-plugin/skills/kb/SKILL.md +60 -0
- package/bundles/antigravity-plugin/skills/mermaid-db/SKILL.md +33 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/SKILL.md +236 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/advanced-features.md +556 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/architecture-diagrams.md +192 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/c4-diagrams.md +410 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/class-diagrams.md +361 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/erd-diagrams.md +510 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/flowcharts.md +450 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/sequence-diagrams.md +394 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/troubleshooting.md +335 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/workflows.md +418 -0
- package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +68 -0
- package/bundles/antigravity-plugin/skills/mockup/SKILL.md +242 -0
- package/bundles/antigravity-plugin/skills/particle-art/SKILL.md +243 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/canvas-utils.ts +171 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/dot-field.template.tsx +203 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/flow-field.template.tsx +263 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/node-shape.template.tsx +261 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/shape-sampler.ts +281 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/stipple-morph.template.tsx +167 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/stipple.template.tsx +175 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/trail-ghost.template.tsx +266 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/usage-examples.md +320 -0
- package/bundles/antigravity-plugin/skills/playwright/API_REFERENCE.md +653 -0
- package/bundles/antigravity-plugin/skills/playwright/SKILL.md +454 -0
- package/bundles/antigravity-plugin/skills/playwright/lib/helpers.js +441 -0
- package/bundles/antigravity-plugin/skills/playwright/package.json +26 -0
- package/bundles/antigravity-plugin/skills/playwright/run.js +228 -0
- package/bundles/antigravity-plugin/skills/prd/SKILL.md +107 -0
- package/bundles/antigravity-plugin/skills/preflight/SKILL.md +435 -0
- package/bundles/antigravity-plugin/skills/python/SKILL.md +388 -0
- package/bundles/antigravity-plugin/skills/react-best-practices/SKILL.md +269 -0
- package/bundles/antigravity-plugin/skills/react-dev/README.md +404 -0
- package/bundles/antigravity-plugin/skills/react-dev/SKILL.md +459 -0
- package/bundles/antigravity-plugin/skills/react-dev/examples/generic-components.md +579 -0
- package/bundles/antigravity-plugin/skills/react-dev/examples/server-components.md +579 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/event-handlers.md +574 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/hooks.md +456 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/react-19-patterns.md +638 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/react-router.md +1002 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/tanstack-router.md +587 -0
- package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +486 -0
- package/bundles/antigravity-plugin/skills/resume/SKILL.md +36 -0
- package/bundles/antigravity-plugin/skills/security-review/SKILL.md +196 -0
- package/bundles/antigravity-plugin/skills/setup-project-ai/SKILL.md +61 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +107 -0
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +103 -0
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +102 -0
- package/bundles/antigravity-plugin/skills/skill-creator/LICENSE.txt +202 -0
- package/bundles/antigravity-plugin/skills/skill-creator/SKILL.md +491 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/analyzer.md +274 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/comparator.md +202 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/grader.md +223 -0
- package/bundles/antigravity-plugin/skills/skill-creator/assets/eval_review.html +146 -0
- package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/bundles/antigravity-plugin/skills/skill-creator/references/schemas.md +430 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/__init__.py +0 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/generate_report.py +326 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/improve_description.py +247 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/package_skill.py +136 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_eval.py +310 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_loop.py +328 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/utils.py +47 -0
- package/bundles/antigravity-plugin/skills/sql/SKILL.md +321 -0
- package/bundles/antigravity-plugin/skills/tdd/SKILL.md +37 -0
- package/bundles/antigravity-plugin/skills/tdd-workflow/SKILL.md +188 -0
- package/bundles/antigravity-plugin/skills/technical-writing/SKILL.md +286 -0
- package/bundles/antigravity-plugin/skills/test-strategy/SKILL.md +155 -0
- package/bundles/antigravity-plugin/skills/ui-brief/SKILL.md +84 -0
- package/bundles/antigravity-plugin/skills/ui-review/SKILL.md +176 -0
- package/bundles/antigravity-plugin/skills/ui-review/references/framework-fixes.md +471 -0
- package/bundles/antigravity-plugin/skills/ui-review/references/visual-checklist.md +236 -0
- package/bundles/antigravity-plugin/skills/usage-review/SKILL.md +77 -0
- package/bundles/antigravity-plugin/skills/use-model/SKILL.md +64 -0
- package/bundles/antigravity-plugin/skills/using-git-worktrees/SKILL.md +217 -0
- package/bundles/antigravity-plugin/skills/version/SKILL.md +18 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/SKILL.md +732 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/LICENSE.txt +202 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/SKILL.md +97 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/console_logging.py +35 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/scripts/with_server.py +106 -0
- package/bundles/antigravity-plugin/skills/windows-deployment/SKILL.md +880 -0
- package/bundles/antigravity-plugin/skills/writing-plans/SKILL.md +384 -0
- package/bundles/antigravity-plugin/statusline-command-agy.sh +91 -0
- package/bundles/antigravity-plugin/warm_start_agy.py +149 -0
- package/bundles/manifest.json +6 -0
- package/dist/cli.js +5363 -0
- 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
|
+
}
|