mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-07 19:58:24 +00:00
35c09f2854
- Store domain shown as Store.GregKilmer.com (no uppercase transform) - Cards and panels 10px corners; buttons pill-shaped - Canvas star/circuit violet switched to brand violet #6A2FE0 - Nav, store nav and footer seal at the brand's 72px minimum (nav 80px tall) - Faint seals on contact, store cards, deck boxes, order and professional Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
254 lines
8.1 KiB
TypeScript
254 lines
8.1 KiB
TypeScript
/**
|
|
* CircuitField.tsx
|
|
* ---------------------------------------------------------------------------
|
|
* The Professional page's counterpart to ConstellationField. It draws a faint
|
|
* circuit-board pattern — orthogonal traces connecting node pads, with a slow
|
|
* "signal pulse" traveling the traces — instead of stars and constellations.
|
|
*
|
|
* Crucially it carries the SAME `view-transition-name` ("sky") as the star
|
|
* fields on other pages. So when you navigate from, say, the Home hero
|
|
* (ConstellationField / AnimatedBackground) to the Professional page, the
|
|
* browser's View Transition morphs the star field into the circuit field as
|
|
* one element — constellations visibly transform into circuitry.
|
|
*
|
|
* Honors prefers-reduced-motion (renders a static board) and pauses off-screen.
|
|
*/
|
|
import { useEffect, useRef } from 'react';
|
|
|
|
interface Props {
|
|
/**
|
|
* Tailwind/className passthrough. The .astro call site includes `vt-sky` when
|
|
* this element should carry the view-transition morph. Not computed here to
|
|
* avoid a server/client hydration mismatch (see AnimatedBackground).
|
|
*/
|
|
className?: string;
|
|
}
|
|
|
|
interface Trace {
|
|
x1: number;
|
|
y1: number;
|
|
x2: number;
|
|
y2: number;
|
|
}
|
|
interface Node {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export default function CircuitField({
|
|
className = '',
|
|
}: Props) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
let raf = 0;
|
|
let width = 0;
|
|
let height = 0;
|
|
let t0 = performance.now();
|
|
|
|
let traces: Trace[] = [];
|
|
let nodes: Node[] = [];
|
|
|
|
const gold = '#c9a24a';
|
|
const violet = '#6a2fe0';
|
|
const teal = '#2bc2b7';
|
|
|
|
function build() {
|
|
if (!canvas || !ctx) return;
|
|
const rect = canvas.getBoundingClientRect();
|
|
width = rect.width;
|
|
height = rect.height;
|
|
canvas.width = Math.floor(width * dpr);
|
|
canvas.height = Math.floor(height * dpr);
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
|
|
// Build a clean, ALIGNED orthogonal circuit on a grid: pads sit exactly at
|
|
// grid intersections (no jitter) and traces connect them in straight
|
|
// right-angle lines. Reads as an orderly schematic / PCB layout.
|
|
const step = 64;
|
|
const cols = Math.ceil(width / step) + 1;
|
|
const rows = Math.ceil(height / step) + 1;
|
|
const present: boolean[][] = [];
|
|
nodes = [];
|
|
for (let r = 0; r < rows; r++) {
|
|
present[r] = [];
|
|
for (let c = 0; c < cols; c++) {
|
|
// ~62% of intersections get a pad.
|
|
present[r][c] = Math.random() < 0.62;
|
|
if (present[r][c]) {
|
|
// Snapped to the grid — no jitter, so traces land flush on pads.
|
|
nodes.push({ x: c * step, y: r * step });
|
|
}
|
|
}
|
|
}
|
|
traces = [];
|
|
for (let r = 0; r < rows; r++) {
|
|
for (let c = 0; c < cols; c++) {
|
|
if (!present[r][c]) continue;
|
|
const here = { x: c * step, y: r * step };
|
|
// Right neighbor.
|
|
if (c + 1 < cols && present[r][c + 1] && Math.random() < 0.7) {
|
|
traces.push({ x1: here.x, y1: here.y, x2: (c + 1) * step, y2: r * step });
|
|
}
|
|
// Down neighbor.
|
|
if (r + 1 < rows && present[r + 1][c] && Math.random() < 0.7) {
|
|
traces.push({ x1: here.x, y1: here.y, x2: c * step, y2: (r + 1) * step });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function frame(now: number) {
|
|
if (!ctx) return;
|
|
const dt = Math.min(0.05, (now - t0) / 1000);
|
|
t0 = now;
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
// Traces. A signal pulse brightens a moving window along each trace, with
|
|
// traces staggered so pulses are distributed across the board over time.
|
|
for (let i = 0; i < traces.length; i++) {
|
|
const t = traces[i];
|
|
// Base trace — clearly visible violet conductor line.
|
|
ctx.strokeStyle = 'rgba(43, 194, 183, 0.55)';
|
|
ctx.lineWidth = 1.4;
|
|
ctx.beginPath();
|
|
ctx.moveTo(t.x1, t.y1);
|
|
ctx.lineTo(t.x2, t.y2);
|
|
ctx.stroke();
|
|
|
|
// Pulse: a bright gold "signal" traveling the trace. position 0..1,
|
|
// looping, with traces staggered so pulses spread across the board.
|
|
const period = 6 + (i % 7);
|
|
const phase = ((now / 1000) % period) / period;
|
|
const px = t.x1 + (t.x2 - t.x1) * phase;
|
|
const py = t.y1 + (t.y2 - t.y1) * phase;
|
|
ctx.strokeStyle = gold;
|
|
ctx.globalAlpha = 0.95;
|
|
ctx.lineWidth = 2.2;
|
|
ctx.shadowBlur = 10;
|
|
ctx.shadowColor = gold;
|
|
ctx.beginPath();
|
|
ctx.moveTo(px - (t.x2 - t.x1) * 0.08, py - (t.y2 - t.y1) * 0.08);
|
|
ctx.lineTo(px, py);
|
|
ctx.stroke();
|
|
// Bright pulse head dot.
|
|
ctx.globalAlpha = 1;
|
|
ctx.fillStyle = '#fbf3dc';
|
|
ctx.beginPath();
|
|
ctx.arc(px, py, 1.8, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
// Node pads — solid teal fill with a brighter violet ring (solder pad).
|
|
for (const n of nodes) {
|
|
ctx.beginPath();
|
|
ctx.fillStyle = teal;
|
|
ctx.globalAlpha = 0.95;
|
|
ctx.arc(n.x, n.y, 2.4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.globalAlpha = 0.85;
|
|
ctx.strokeStyle = violet;
|
|
ctx.lineWidth = 1.4;
|
|
ctx.beginPath();
|
|
ctx.arc(n.x, n.y, 4.5, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
|
|
void dt;
|
|
raf = requestAnimationFrame(frame);
|
|
}
|
|
|
|
function drawStatic() {
|
|
if (!ctx) return;
|
|
ctx.clearRect(0, 0, width, height);
|
|
for (const t of traces) {
|
|
ctx.strokeStyle = 'rgba(43, 194, 183, 0.5)';
|
|
ctx.lineWidth = 1.4;
|
|
ctx.beginPath();
|
|
ctx.moveTo(t.x1, t.y1);
|
|
ctx.lineTo(t.x2, t.y2);
|
|
ctx.stroke();
|
|
}
|
|
for (const n of nodes) {
|
|
ctx.fillStyle = teal;
|
|
ctx.globalAlpha = 0.95;
|
|
ctx.beginPath();
|
|
ctx.arc(n.x, n.y, 2.4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
build();
|
|
if (reduce) {
|
|
drawStatic();
|
|
} else {
|
|
t0 = performance.now();
|
|
raf = requestAnimationFrame(frame);
|
|
}
|
|
const onResize = () => {
|
|
build();
|
|
if (reduce) drawStatic();
|
|
};
|
|
window.addEventListener('resize', onResize);
|
|
|
|
const io = new IntersectionObserver((entries) => {
|
|
if (reduce) return;
|
|
for (const e of entries) {
|
|
if (e.isIntersecting) {
|
|
if (!raf) {
|
|
t0 = performance.now();
|
|
raf = requestAnimationFrame(frame);
|
|
}
|
|
} else {
|
|
cancelAnimationFrame(raf);
|
|
raf = 0;
|
|
}
|
|
}
|
|
});
|
|
io.observe(canvas);
|
|
|
|
return () => {
|
|
cancelAnimationFrame(raf);
|
|
window.removeEventListener('resize', onResize);
|
|
io.disconnect();
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
// Root container mirrors AnimatedBackground's structure (gradient layers +
|
|
// canvas all INSIDE the named element) so the view-transition morph from a
|
|
// star hero into this circuit field is element-to-element equivalent and
|
|
// nothing flashes separately. The `vt-sky`/opacity classes are passed via
|
|
// className from the .astro call site.
|
|
<div
|
|
className={`pointer-events-none absolute inset-0 -z-10 overflow-hidden ${className}`}
|
|
aria-hidden="true"
|
|
>
|
|
{/* Base gradient — IDENTICAL colors to AnimatedBackground's night sky so
|
|
the morph between star field and circuit field is color-consistent.
|
|
No nebula radials here (the user asked to keep the circuit style only);
|
|
just the clean deep-blue sky base. */}
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
background:
|
|
'linear-gradient(180deg, #080c14 0%, #0b111c 32%, #0e1522 58%, #131c2b 82%, #1a2a3a 100%)',
|
|
}}
|
|
/>
|
|
{/* Circuit traces + nodes + signal pulses. */}
|
|
<canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />
|
|
</div>
|
|
);
|
|
}
|