@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,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* stipple-morph.template.tsx — Halftone dot cloud that morphs into a letter/shape on hover.
|
|
4
|
+
*
|
|
5
|
+
* USAGE:
|
|
6
|
+
* 1. Copy and rename (e.g. StippleMorph.tsx, HeroHalftone.tsx)
|
|
7
|
+
* 2. Pass `text` prop to set the target letter/word (default "j")
|
|
8
|
+
* 3. Adjust COLS × ROWS for dot density
|
|
9
|
+
* 4. The morph is driven by React synthetic onMouseMove/onMouseLeave — no addEventListener needed
|
|
10
|
+
*
|
|
11
|
+
* RENDERING: HTML spans (SSR-safe). Grid A built at module level; Grid B from canvas in useEffect.
|
|
12
|
+
* INTERACTION: hover canvas → cloud resolves into letter; leave → drifts back
|
|
13
|
+
* THEMING: --accent-strong CSS var for dot colour
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useRef } from "react";
|
|
16
|
+
|
|
17
|
+
const COLS = 24;
|
|
18
|
+
const ROWS = 18;
|
|
19
|
+
const SCAN_PERIOD = 480;
|
|
20
|
+
const MORPH_SPEED = 0.045; // lerp speed; higher = snappier
|
|
21
|
+
|
|
22
|
+
function srand(x: number, y: number): number {
|
|
23
|
+
return (Math.abs(Math.sin(x * 127.1 + y * 311.7) * 43758.5453)) % 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function charFor(v: number): string {
|
|
27
|
+
if (v > 0.68) return "●";
|
|
28
|
+
if (v > 0.35) return "•";
|
|
29
|
+
return "·";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Grid A: Gaussian cloud — module-level, SSR-safe ───────────────────────────
|
|
33
|
+
const GRID_A: number[][] = Array.from({ length: ROWS }, (_, yi) =>
|
|
34
|
+
Array.from({ length: COLS }, (_, xi) => {
|
|
35
|
+
const nx = (xi / (COLS - 1)) * 2 - 1;
|
|
36
|
+
const ny = (yi / (ROWS - 1)) * 2 - 1;
|
|
37
|
+
const dist = Math.sqrt(nx * nx + ny * ny);
|
|
38
|
+
const base = Math.exp(-(dist * dist) / (2 * 0.28 * 0.28));
|
|
39
|
+
return Math.max(0, Math.min(1, base + srand(xi * 53, yi * 97) * 0.12 - 0.06));
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Per-cell breathing params
|
|
44
|
+
const B_PHASE = Array.from({ length: ROWS }, (_, yi) =>
|
|
45
|
+
Array.from({ length: COLS }, (_, xi) => srand(xi * 71, yi * 113) * Math.PI * 2)
|
|
46
|
+
);
|
|
47
|
+
const B_SPEED = Array.from({ length: ROWS }, (_, yi) =>
|
|
48
|
+
Array.from({ length: COLS }, (_, xi) => 0.008 + srand(xi, yi) * 0.006)
|
|
49
|
+
);
|
|
50
|
+
const B_AMP = Array.from({ length: ROWS }, (_, yi) =>
|
|
51
|
+
Array.from({ length: COLS }, (_, xi) => 0.04 + srand(xi + 50, yi) * 0.10)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// ── Grid B: sample canvas text → density map ──────────────────────────────────
|
|
55
|
+
//
|
|
56
|
+
// REPLACE `text` param or call with any string. Single chars work best.
|
|
57
|
+
// For multi-char (e.g. "JS"), reduce font size:
|
|
58
|
+
// const fontSize = Math.floor(Math.min(ROWS*S*0.8, COLS*S*0.35));
|
|
59
|
+
//
|
|
60
|
+
function buildLetterGrid(text: string): number[][] {
|
|
61
|
+
const S = 8;
|
|
62
|
+
const oc = document.createElement("canvas");
|
|
63
|
+
oc.width = COLS * S;
|
|
64
|
+
oc.height = ROWS * S;
|
|
65
|
+
const ctx = oc.getContext("2d")!;
|
|
66
|
+
const fontSize = Math.floor(Math.min(ROWS * S * 0.88, COLS * S * 0.55));
|
|
67
|
+
ctx.fillStyle = "#fff";
|
|
68
|
+
ctx.font = `bold ${fontSize}px sans-serif`;
|
|
69
|
+
ctx.textAlign = "center";
|
|
70
|
+
ctx.textBaseline = "middle";
|
|
71
|
+
ctx.fillText(text, (COLS * S) / 2, (ROWS * S) / 2);
|
|
72
|
+
const { data } = ctx.getImageData(0, 0, COLS * S, ROWS * S);
|
|
73
|
+
return Array.from({ length: ROWS }, (_, yi) =>
|
|
74
|
+
Array.from({ length: COLS }, (_, xi) => {
|
|
75
|
+
let sum = 0;
|
|
76
|
+
for (let dy = 0; dy < S; dy++)
|
|
77
|
+
for (let dx = 0; dx < S; dx++)
|
|
78
|
+
sum += data[((yi * S + dy) * COLS * S + (xi * S + dx)) * 4] / 255;
|
|
79
|
+
return Math.min(1, (sum / (S * S)) * 2.2);
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Component ──────────────────────────────────────────────────────────────────
|
|
85
|
+
export function StippleMorphArt({ text = "j", className = "" }: { text?: string; className?: string }) {
|
|
86
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
87
|
+
// Refs for cross-closure state — never stale in the RAF loop
|
|
88
|
+
const active = useRef(false);
|
|
89
|
+
const morphRef = useRef(0);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const container = containerRef.current;
|
|
93
|
+
if (!container) return;
|
|
94
|
+
|
|
95
|
+
const GRID_B = buildLetterGrid(text);
|
|
96
|
+
const spans = Array.from(container.querySelectorAll<HTMLElement>("span[data-xi]"));
|
|
97
|
+
|
|
98
|
+
let raf: number;
|
|
99
|
+
let frame = 0;
|
|
100
|
+
|
|
101
|
+
const tick = () => {
|
|
102
|
+
// Lerp morphStrength toward 0 (idle) or 1 (letter)
|
|
103
|
+
const m = morphRef.current + ((active.current ? 1 : 0) - morphRef.current) * MORPH_SPEED;
|
|
104
|
+
morphRef.current = m;
|
|
105
|
+
|
|
106
|
+
const cx = COLS / 2, cy = ROWS / 2;
|
|
107
|
+
const maxR = Math.sqrt(cx * cx + cy * cy);
|
|
108
|
+
const waveR = ((frame % SCAN_PERIOD) / SCAN_PERIOD) * (maxR + 1);
|
|
109
|
+
|
|
110
|
+
spans.forEach(el => {
|
|
111
|
+
const xi = +el.dataset.xi!;
|
|
112
|
+
const yi = +el.dataset.yi!;
|
|
113
|
+
const vA = GRID_A[yi][xi];
|
|
114
|
+
const vB = GRID_B[yi][xi];
|
|
115
|
+
|
|
116
|
+
const breathe = Math.sin(frame * B_SPEED[yi][xi] + B_PHASE[yi][xi]) * B_AMP[yi][xi] * (1 - m * 0.85);
|
|
117
|
+
|
|
118
|
+
const dist = Math.sqrt((xi - cx) ** 2 + (yi - cy) ** 2);
|
|
119
|
+
const diff = dist - waveR;
|
|
120
|
+
const wave = (Math.abs(diff) < 1.6 ? 0.38 : diff < 0 && diff > -3.5 ? 0.11 : 0) * (1 - m);
|
|
121
|
+
|
|
122
|
+
const v = Math.max(0, Math.min(1, vA * (1 - m) + vB * m + breathe + wave));
|
|
123
|
+
el.style.opacity = v.toFixed(3);
|
|
124
|
+
const c = charFor(v);
|
|
125
|
+
if (el.textContent !== c) el.textContent = c;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
frame++;
|
|
129
|
+
raf = requestAnimationFrame(tick);
|
|
130
|
+
};
|
|
131
|
+
tick();
|
|
132
|
+
return () => cancelAnimationFrame(raf);
|
|
133
|
+
}, [text]);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
// React synthetic events on the container — reliable across all React modes
|
|
137
|
+
<div
|
|
138
|
+
ref={containerRef}
|
|
139
|
+
className={className}
|
|
140
|
+
style={{ userSelect: "none", lineHeight: 1.15, cursor: "crosshair" }}
|
|
141
|
+
onMouseMove={() => { active.current = true; }}
|
|
142
|
+
onMouseLeave={() => { active.current = false; }}
|
|
143
|
+
aria-hidden="true"
|
|
144
|
+
>
|
|
145
|
+
<div style={{ textAlign: "center", fontSize: "1.1rem", marginBottom: "0.3em", color: "rgb(var(--accent-strong))", opacity: 0.82 }}>
|
|
146
|
+
✦
|
|
147
|
+
</div>
|
|
148
|
+
{Array.from({ length: ROWS }, (_, yi) => (
|
|
149
|
+
<div key={yi} style={{ display: "flex", justifyContent: "center", gap: "0.18em" }}>
|
|
150
|
+
{Array.from({ length: COLS }, (_, xi) => {
|
|
151
|
+
const v = GRID_A[yi][xi];
|
|
152
|
+
return (
|
|
153
|
+
<span
|
|
154
|
+
key={xi}
|
|
155
|
+
data-xi={xi}
|
|
156
|
+
data-yi={yi}
|
|
157
|
+
style={{ fontSize: "0.68rem", color: "rgb(var(--accent-strong))", opacity: v, transition: "none" }}
|
|
158
|
+
>
|
|
159
|
+
{charFor(v)}
|
|
160
|
+
</span>
|
|
161
|
+
);
|
|
162
|
+
})}
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* stipple.template.tsx — Animated Unicode halftone dot component template.
|
|
4
|
+
*
|
|
5
|
+
* USAGE:
|
|
6
|
+
* 1. Copy and rename (e.g. StipplePortrait.tsx, HeroStipple.tsx)
|
|
7
|
+
* 2. Adjust COLS × ROWS to control dot density
|
|
8
|
+
* 3. Replace the GRID density function with your own if desired
|
|
9
|
+
* 4. The scan wave and breathing animation work automatically
|
|
10
|
+
*
|
|
11
|
+
* RENDERING: Pure HTML/CSS spans — no canvas. Works with SSR.
|
|
12
|
+
* THEMING: Uses --accent-strong and --accent-soft CSS vars.
|
|
13
|
+
*
|
|
14
|
+
* DEPENDENCIES: none
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useRef } from "react";
|
|
17
|
+
|
|
18
|
+
// ── Grid parameters ────────────────────────────────────────────────────────────
|
|
19
|
+
const COLS = 24; // horizontal dot count
|
|
20
|
+
const ROWS = 18; // vertical dot count
|
|
21
|
+
const SCAN_PERIOD = 480; // frames per scan wave cycle
|
|
22
|
+
|
|
23
|
+
// ── Dot character selection ────────────────────────────────────────────────────
|
|
24
|
+
function charFor(v: number): string {
|
|
25
|
+
if (v > 0.68) return "●";
|
|
26
|
+
if (v > 0.35) return "•";
|
|
27
|
+
return "·";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Deterministic pseudo-random (SSR-safe, no hydration mismatch) ──────────────
|
|
31
|
+
function srand(x: number, y: number): number {
|
|
32
|
+
return (Math.abs(Math.sin(x * 127.1 + y * 311.7) * 43758.5453)) % 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Pre-compute density grid at module level (stable across re-renders) ────────
|
|
36
|
+
//
|
|
37
|
+
// REPLACE THIS with your own density function.
|
|
38
|
+
// Current: Gaussian falloff from centre — denser at middle, dispersing at edges.
|
|
39
|
+
//
|
|
40
|
+
// `v` is in [0, 1]: 1 = densest (●), 0 = emptiest (·)
|
|
41
|
+
//
|
|
42
|
+
const GRID: number[][] = Array.from({ length: ROWS }, (_, yi) =>
|
|
43
|
+
Array.from({ length: COLS }, (_, xi) => {
|
|
44
|
+
// Normalised distance from centre [0, 1]
|
|
45
|
+
const nx = (xi / (COLS - 1)) * 2 - 1;
|
|
46
|
+
const ny = (yi / (ROWS - 1)) * 2 - 1;
|
|
47
|
+
const dist = Math.sqrt(nx * nx + ny * ny);
|
|
48
|
+
|
|
49
|
+
// Gaussian envelope
|
|
50
|
+
const base = Math.exp(-(dist * dist) / (2 * 0.28 * 0.28));
|
|
51
|
+
|
|
52
|
+
// Jitter so dots don't look perfectly smooth
|
|
53
|
+
const jitter = srand(xi * 53, yi * 97) * 0.12 - 0.06;
|
|
54
|
+
|
|
55
|
+
return Math.max(0, Math.min(1, base + jitter));
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// ── Animation state per dot ────────────────────────────────────────────────────
|
|
60
|
+
interface DotAnim {
|
|
61
|
+
el: HTMLElement;
|
|
62
|
+
v: number; // density value [0, 1]
|
|
63
|
+
x: number; // column index
|
|
64
|
+
y: number; // row index
|
|
65
|
+
phase: number; // breathing phase offset
|
|
66
|
+
speed: number; // breathing speed
|
|
67
|
+
amp: number; // breathing amplitude
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Component ──────────────────────────────────────────────────────────────────
|
|
71
|
+
//
|
|
72
|
+
// Renders COLS×ROWS dots as animated spans.
|
|
73
|
+
// The scan wave sweeps outward from centre periodically.
|
|
74
|
+
// Each dot breathes (opacity oscillation) independently.
|
|
75
|
+
//
|
|
76
|
+
export function StippleArt({ className = "" }: { className?: string }) {
|
|
77
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const container = containerRef.current;
|
|
81
|
+
if (!container) return;
|
|
82
|
+
|
|
83
|
+
// Reduced-motion: render static dots, skip animation
|
|
84
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
85
|
+
|
|
86
|
+
// Collect all dot spans with their attributes
|
|
87
|
+
const spans = container.querySelectorAll<HTMLElement>("span[data-v]");
|
|
88
|
+
const MAX_DOT_R = Math.sqrt(COLS * COLS + ROWS * ROWS) / 2;
|
|
89
|
+
|
|
90
|
+
const dots: DotAnim[] = Array.from(spans).map(el => ({
|
|
91
|
+
el,
|
|
92
|
+
v: parseFloat(el.dataset.v ?? "0"),
|
|
93
|
+
x: parseFloat(el.dataset.x ?? "0"),
|
|
94
|
+
y: parseFloat(el.dataset.y ?? "0"),
|
|
95
|
+
phase: srand(parseFloat(el.dataset.x ?? "0") * 71, parseFloat(el.dataset.y ?? "0") * 113) * Math.PI * 2,
|
|
96
|
+
speed: 0.008 + srand(parseFloat(el.dataset.x ?? "0"), parseFloat(el.dataset.y ?? "0")) * 0.006,
|
|
97
|
+
amp: 0.04 + srand(parseFloat(el.dataset.x ?? "0") + 50, parseFloat(el.dataset.y ?? "0")) * 0.10,
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
let raf: number;
|
|
101
|
+
let frame = 0;
|
|
102
|
+
const cx = COLS / 2, cy = ROWS / 2;
|
|
103
|
+
|
|
104
|
+
const tick = () => {
|
|
105
|
+
const waveR = ((frame % SCAN_PERIOD) / SCAN_PERIOD) * (MAX_DOT_R + 1);
|
|
106
|
+
|
|
107
|
+
dots.forEach(d => {
|
|
108
|
+
const dist = Math.sqrt((d.x - cx) ** 2 + (d.y - cy) ** 2);
|
|
109
|
+
const diff = dist - waveR;
|
|
110
|
+
|
|
111
|
+
// Scan wave ring boost
|
|
112
|
+
const ringBoost = Math.abs(diff) < 1.6 ? 0.40 : 0;
|
|
113
|
+
// Trailing glow behind the wave
|
|
114
|
+
const trailBoost = diff < 0 && diff > -3.5 ? 0.12 : 0;
|
|
115
|
+
// Per-dot breathing
|
|
116
|
+
const breathe = Math.sin(frame * d.speed + d.phase) * d.amp;
|
|
117
|
+
|
|
118
|
+
const opacity = Math.max(0, Math.min(1, d.v + breathe + ringBoost + trailBoost));
|
|
119
|
+
d.el.style.opacity = opacity.toFixed(3);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
frame++;
|
|
123
|
+
raf = requestAnimationFrame(tick);
|
|
124
|
+
};
|
|
125
|
+
tick();
|
|
126
|
+
return () => cancelAnimationFrame(raf);
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
ref={containerRef}
|
|
132
|
+
className={className}
|
|
133
|
+
style={{ userSelect: "none", lineHeight: 1.15 }}
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
>
|
|
136
|
+
{/* ✦ glyph above the dot grid */}
|
|
137
|
+
<div
|
|
138
|
+
style={{
|
|
139
|
+
textAlign: "center",
|
|
140
|
+
fontSize: "1.1rem",
|
|
141
|
+
letterSpacing: "0.05em",
|
|
142
|
+
marginBottom: "0.3em",
|
|
143
|
+
color: "rgb(var(--accent-strong))",
|
|
144
|
+
opacity: 0.82,
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
✦
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Dot grid */}
|
|
151
|
+
{GRID.map((row, yi) => (
|
|
152
|
+
<div key={yi} style={{ display: "flex", justifyContent: "center", gap: "0.18em" }}>
|
|
153
|
+
{row.map((v, xi) => (
|
|
154
|
+
<span
|
|
155
|
+
key={xi}
|
|
156
|
+
data-v={v.toFixed(4)}
|
|
157
|
+
data-x={xi}
|
|
158
|
+
data-y={yi}
|
|
159
|
+
style={{
|
|
160
|
+
fontSize: "0.68rem",
|
|
161
|
+
color: v > 0.55
|
|
162
|
+
? "rgb(var(--accent-strong))"
|
|
163
|
+
: "rgb(var(--accent-soft))",
|
|
164
|
+
opacity: v,
|
|
165
|
+
transition: "none",
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
{charFor(v)}
|
|
169
|
+
</span>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* trail-ghost.template.tsx — Spring-morph nodes with phosphorescent trailing effect.
|
|
4
|
+
*
|
|
5
|
+
* USAGE:
|
|
6
|
+
* 1. Copy and rename (e.g. TrailGhost.tsx, HeroTrails.tsx)
|
|
7
|
+
* 2. Replace `buildTargets` with any shape from shape-sampler.ts
|
|
8
|
+
* 3. Tune TRAIL_DECAY (lower → shorter trails) and TRAIL_MAX (higher → longer tails)
|
|
9
|
+
* 4. Background colour is baked into the persistence rect — change "0,0,0" for light mode
|
|
10
|
+
*
|
|
11
|
+
* KEY TECHNIQUE: instead of ctx.clearRect() each frame, fill with a semi-transparent rect
|
|
12
|
+
* (rgba(0,0,0,0.10)). Old frames fade rather than vanish, creating ghostly smeared trails.
|
|
13
|
+
*
|
|
14
|
+
* INTERACTION: mousemove → nodes coalesce into letter; mouseleave → drift apart
|
|
15
|
+
* THEMING: --accent-strong (node colour), --accent-soft (connection lines)
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect, useRef } from "react";
|
|
18
|
+
|
|
19
|
+
// ── Bezier helper ─────────────────────────────────────────────────────────────
|
|
20
|
+
function cubicBezier(
|
|
21
|
+
t: number,
|
|
22
|
+
p0: [number, number], p1: [number, number],
|
|
23
|
+
p2: [number, number], p3: [number, number],
|
|
24
|
+
): [number, number] {
|
|
25
|
+
const m = 1 - t;
|
|
26
|
+
return [
|
|
27
|
+
m**3*p0[0] + 3*m**2*t*p1[0] + 3*m*t**2*p2[0] + t**3*p3[0],
|
|
28
|
+
m**3*p0[1] + 3*m**2*t*p1[1] + 3*m*t**2*p2[1] + t**3*p3[1],
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Target shape — replace or import from shape-sampler.ts ───────────────────
|
|
33
|
+
//
|
|
34
|
+
// CUSTOMISE: swap this function for any sampleText / sampleSVGPath / sampleConstellation call.
|
|
35
|
+
// Return an array of { tx, ty, spring, r, alpha, isDot } objects.
|
|
36
|
+
//
|
|
37
|
+
interface Target { tx: number; ty: number; spring: number; r: number; alpha: number; isDot: boolean }
|
|
38
|
+
|
|
39
|
+
function buildTargets(size: number): Target[] {
|
|
40
|
+
const cx = size * 0.50;
|
|
41
|
+
const stemTop = size * 0.210, stemBot = size * 0.660;
|
|
42
|
+
const targets: Target[] = [];
|
|
43
|
+
|
|
44
|
+
// Tittle (dot above stem)
|
|
45
|
+
targets.push({ tx: cx, ty: size * 0.130, spring: 0.038, r: 4.5, alpha: 1.0, isDot: true });
|
|
46
|
+
|
|
47
|
+
// Vertical stem
|
|
48
|
+
for (let i = 0; i <= 12; i++)
|
|
49
|
+
targets.push({ tx: cx, ty: stemTop + (i / 12) * (stemBot - stemTop), spring: 0.036, r: 2.0, alpha: 0.92, isDot: false });
|
|
50
|
+
|
|
51
|
+
// Hook (cubic bezier descender)
|
|
52
|
+
const P0: [number,number] = [cx, stemBot];
|
|
53
|
+
const P1: [number,number] = [cx, stemBot + size * 0.115];
|
|
54
|
+
const P2: [number,number] = [cx - size * 0.115, stemBot + size * 0.145];
|
|
55
|
+
const P3: [number,number] = [cx - size * 0.155, stemBot + size * 0.040];
|
|
56
|
+
for (let i = 1; i <= 9; i++) {
|
|
57
|
+
const [bx, by] = cubicBezier(i / 9, P0, P1, P2, P3);
|
|
58
|
+
targets.push({ tx: bx, ty: by, spring: 0.034, r: 2.0, alpha: 0.88, isDot: false });
|
|
59
|
+
}
|
|
60
|
+
return targets;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Types & theme helper ──────────────────────────────────────────────────────
|
|
64
|
+
interface Pt {
|
|
65
|
+
x: number; y: number; vx: number; vy: number;
|
|
66
|
+
tx: number; ty: number; springBase: number;
|
|
67
|
+
r: number; alpha: number; isDot: boolean; isLetter: boolean;
|
|
68
|
+
trail: { x: number; y: number; a: number }[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function themeRgb(v: string): [number, number, number] {
|
|
72
|
+
const ch = getComputedStyle(document.documentElement)
|
|
73
|
+
.getPropertyValue(v).trim().split(/\s+/).map(Number);
|
|
74
|
+
return [ch[0], ch[1], ch[2]];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
78
|
+
const LINK = 68; // connection line distance threshold (px)
|
|
79
|
+
const MAX_SPEED = 1.5;
|
|
80
|
+
const IDLE_FRICTION = 0.984; // low friction → nodes drift organically in idle
|
|
81
|
+
const ACTIVE_FRICTION = 0.88; // snappy convergence on hover
|
|
82
|
+
const DRIFT = 0.055; // random brownian nudge in idle
|
|
83
|
+
const MORPH_SPEED = 0.05; // lerp rate 0→1; higher = faster morph
|
|
84
|
+
const TRAIL_DECAY = 0.91; // per-frame alpha multiplier — lower = shorter trails (try 0.80–0.96)
|
|
85
|
+
const TRAIL_MAX = 50; // max trail history length per node
|
|
86
|
+
|
|
87
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
88
|
+
export function TrailGhostArt({ size = 300 }: { size?: number }) {
|
|
89
|
+
const ref = useRef<HTMLCanvasElement>(null);
|
|
90
|
+
// useRef for cross-RAF state — never stale in the animation loop
|
|
91
|
+
const active = useRef(false);
|
|
92
|
+
const morph = useRef(0);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const el = ref.current;
|
|
96
|
+
if (!el) return;
|
|
97
|
+
|
|
98
|
+
const ctx = el.getContext("2d")!;
|
|
99
|
+
const dpr = window.devicePixelRatio || 1;
|
|
100
|
+
el.width = size * dpr;
|
|
101
|
+
el.height = size * dpr;
|
|
102
|
+
ctx.scale(dpr, dpr);
|
|
103
|
+
|
|
104
|
+
const cx = size * 0.50, cy = size * 0.46;
|
|
105
|
+
|
|
106
|
+
// Letter nodes — start scattered, attracted to targets on hover
|
|
107
|
+
const pts: Pt[] = buildTargets(size).map(t => {
|
|
108
|
+
const angle = Math.random() * Math.PI * 2;
|
|
109
|
+
const dist = size * (0.10 + Math.random() * 0.32);
|
|
110
|
+
return {
|
|
111
|
+
x: cx + Math.cos(angle) * dist,
|
|
112
|
+
y: cy + Math.sin(angle) * dist,
|
|
113
|
+
vx: (Math.random() - 0.5) * 0.6,
|
|
114
|
+
vy: (Math.random() - 0.5) * 0.6,
|
|
115
|
+
tx: t.tx, ty: t.ty, springBase: t.spring,
|
|
116
|
+
r: t.r, alpha: t.alpha, isDot: t.isDot, isLetter: true,
|
|
117
|
+
trail: [],
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Ambient nodes — no letter target, add atmosphere
|
|
122
|
+
// CUSTOMISE: increase count, adjust alpha, starting radius
|
|
123
|
+
for (let i = 0; i < 10; i++) {
|
|
124
|
+
const angle = (i / 10) * Math.PI * 2;
|
|
125
|
+
const dist = size * (0.24 + Math.random() * 0.18);
|
|
126
|
+
pts.push({
|
|
127
|
+
x: cx + Math.cos(angle) * dist * 0.75,
|
|
128
|
+
y: cy + Math.sin(angle) * dist,
|
|
129
|
+
vx: (Math.random() - 0.5) * 0.3,
|
|
130
|
+
vy: (Math.random() - 0.5) * 0.3,
|
|
131
|
+
tx: cx, ty: cy, springBase: 0,
|
|
132
|
+
r: Math.random() * 0.9 + 0.5,
|
|
133
|
+
alpha: 0.30 + Math.random() * 0.20,
|
|
134
|
+
isDot: false, isLetter: false,
|
|
135
|
+
trail: [],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// mousemove is more reliable than mouseenter on canvas in React 18
|
|
140
|
+
const onMove = () => { active.current = true; };
|
|
141
|
+
const onLeave = () => { active.current = false; };
|
|
142
|
+
el.addEventListener("mousemove", onMove);
|
|
143
|
+
el.addEventListener("mouseleave", onLeave);
|
|
144
|
+
|
|
145
|
+
let raf: number;
|
|
146
|
+
let frame = 0;
|
|
147
|
+
|
|
148
|
+
const tick = () => {
|
|
149
|
+
// PERSISTENCE ghost — fill instead of clearRect; old pixels fade gradually
|
|
150
|
+
// CUSTOMISE: increase alpha (e.g. 0.20) for shorter trails; decrease (0.05) for longer
|
|
151
|
+
ctx.fillStyle = "rgba(0,0,0,0.10)";
|
|
152
|
+
ctx.fillRect(0, 0, size, size);
|
|
153
|
+
|
|
154
|
+
const [nr, ng, nb] = themeRgb("--accent-strong");
|
|
155
|
+
const [lr, lg, lb] = themeRgb("--accent-soft");
|
|
156
|
+
|
|
157
|
+
// Lerp morph 0 (idle) → 1 (letter formed)
|
|
158
|
+
const m = morph.current + ((active.current ? 1 : 0) - morph.current) * MORPH_SPEED;
|
|
159
|
+
morph.current = m;
|
|
160
|
+
|
|
161
|
+
// Connection lines — faint web between nearby nodes
|
|
162
|
+
for (let i = 0; i < pts.length; i++) {
|
|
163
|
+
for (let j = i + 1; j < pts.length; j++) {
|
|
164
|
+
const dx = pts[i].x - pts[j].x, dy = pts[i].y - pts[j].y;
|
|
165
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
166
|
+
if (d < LINK) {
|
|
167
|
+
ctx.beginPath();
|
|
168
|
+
ctx.moveTo(pts[i].x, pts[i].y);
|
|
169
|
+
ctx.lineTo(pts[j].x, pts[j].y);
|
|
170
|
+
ctx.strokeStyle = `rgba(${lr},${lg},${lb},${((1 - d / LINK) * 0.14).toFixed(3)})`;
|
|
171
|
+
ctx.lineWidth = 0.5;
|
|
172
|
+
ctx.stroke();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
pts.forEach(p => {
|
|
178
|
+
// 1. Record trail before moving, decay all entries, trim to TRAIL_MAX
|
|
179
|
+
p.trail.unshift({ x: p.x, y: p.y, a: p.alpha * 0.65 });
|
|
180
|
+
p.trail.forEach(t => { t.a *= TRAIL_DECAY; });
|
|
181
|
+
while (p.trail.length > TRAIL_MAX) p.trail.pop();
|
|
182
|
+
|
|
183
|
+
// 2. Draw trail — fading, shrinking circles back through history
|
|
184
|
+
p.trail.forEach((t, i) => {
|
|
185
|
+
const frac = i / TRAIL_MAX;
|
|
186
|
+
const r = Math.max(0.2, p.r * (1 - frac * 0.88));
|
|
187
|
+
ctx.beginPath();
|
|
188
|
+
ctx.arc(t.x, t.y, r, 0, Math.PI * 2);
|
|
189
|
+
ctx.fillStyle = `rgba(${nr},${ng},${nb},${Math.max(0, t.a).toFixed(3)})`;
|
|
190
|
+
ctx.fill();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// 3. Physics
|
|
194
|
+
if (p.isLetter) {
|
|
195
|
+
// Spring toward target (scales with morph)
|
|
196
|
+
p.vx += (p.tx - p.x) * p.springBase * m;
|
|
197
|
+
p.vy += (p.ty - p.y) * p.springBase * m;
|
|
198
|
+
// Brownian drift fades out as letter forms
|
|
199
|
+
p.vx += (Math.random() - 0.5) * DRIFT * (1 - m);
|
|
200
|
+
p.vy += (Math.random() - 0.5) * DRIFT * (1 - m);
|
|
201
|
+
// Gentle centre gravity in idle so nodes don't escape
|
|
202
|
+
p.vx += (cx - p.x) * 0.0009 * (1 - m);
|
|
203
|
+
p.vy += (cy - p.y) * 0.0009 * (1 - m);
|
|
204
|
+
// Friction blends from idle to active
|
|
205
|
+
const fr = IDLE_FRICTION + (ACTIVE_FRICTION - IDLE_FRICTION) * m;
|
|
206
|
+
p.vx *= fr; p.vy *= fr;
|
|
207
|
+
// Soft wall bounce in idle
|
|
208
|
+
if (m < 0.15) {
|
|
209
|
+
if (p.x < 6 || p.x > size - 6) p.vx *= -0.8;
|
|
210
|
+
if (p.y < 6 || p.y > size - 6) p.vy *= -0.8;
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
p.vx += (Math.random() - 0.5) * DRIFT * 0.5;
|
|
214
|
+
p.vy += (Math.random() - 0.5) * DRIFT * 0.5;
|
|
215
|
+
p.vx += (cx - p.x) * 0.0006;
|
|
216
|
+
p.vy += (cy - p.y) * 0.0006;
|
|
217
|
+
p.vx *= IDLE_FRICTION; p.vy *= IDLE_FRICTION;
|
|
218
|
+
if (p.x < 6 || p.x > size - 6) p.vx *= -0.8;
|
|
219
|
+
if (p.y < 6 || p.y > size - 6) p.vy *= -0.8;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const spd = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
|
|
223
|
+
if (spd > MAX_SPEED) { p.vx *= MAX_SPEED / spd; p.vy *= MAX_SPEED / spd; }
|
|
224
|
+
p.x += p.vx; p.y += p.vy;
|
|
225
|
+
|
|
226
|
+
// 4. Node head — bright dot on top of trail
|
|
227
|
+
let nodeR = p.r;
|
|
228
|
+
if (p.isDot && m > 0.15) {
|
|
229
|
+
// Pulsing glow ring for the tittle (dot above "j" stem)
|
|
230
|
+
nodeR = p.r + Math.sin(frame * 0.04) * 0.8;
|
|
231
|
+
ctx.beginPath();
|
|
232
|
+
ctx.arc(p.x, p.y, nodeR + 5 + Math.sin(frame * 0.04) * 2, 0, Math.PI * 2);
|
|
233
|
+
ctx.strokeStyle = `rgba(${nr},${ng},${nb},${(0.20 * m).toFixed(3)})`;
|
|
234
|
+
ctx.lineWidth = 1;
|
|
235
|
+
ctx.stroke();
|
|
236
|
+
}
|
|
237
|
+
ctx.beginPath();
|
|
238
|
+
ctx.arc(p.x, p.y, nodeR, 0, Math.PI * 2);
|
|
239
|
+
ctx.fillStyle = `rgba(${nr},${ng},${nb},${p.alpha.toFixed(3)})`;
|
|
240
|
+
ctx.fill();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
frame++;
|
|
244
|
+
raf = requestAnimationFrame(tick);
|
|
245
|
+
};
|
|
246
|
+
tick();
|
|
247
|
+
|
|
248
|
+
// ⚠️ prefers-reduced-motion: do NOT early-return before drawing (leaves canvas blank).
|
|
249
|
+
// Correct approach would be: snap all pts to targets, call tick() once, then cancelRAF.
|
|
250
|
+
// This component always animates — add static-snap logic here if reduced motion is required.
|
|
251
|
+
|
|
252
|
+
return () => {
|
|
253
|
+
cancelAnimationFrame(raf);
|
|
254
|
+
el.removeEventListener("mousemove", onMove);
|
|
255
|
+
el.removeEventListener("mouseleave", onLeave);
|
|
256
|
+
};
|
|
257
|
+
}, [size]);
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<canvas
|
|
261
|
+
ref={ref}
|
|
262
|
+
style={{ width: size, height: size, cursor: "crosshair" }}
|
|
263
|
+
aria-hidden="true"
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
266
|
+
}
|