/* ============ shared hooks/helpers ============ */
const { useState, useEffect, useLayoutEffect, useRef, useCallback } = React;
function useInView(opts) {
const ref = useRef(null);
const [seen, setSeen] = useState(false);
useEffect(() => {
const el = ref.current; if (!el) return;
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { setSeen(true); io.disconnect(); }
}, { threshold: 0.18, ...(opts || {}) });
io.observe(el);
return () => io.disconnect();
}, []);
return [ref, seen];
}
function Reveal({ children, d = 0, cls = "", tag = "div" }) {
const [ref, seen] = useInView();
const Tag = tag;
return {children};
}
/* clip line-by-line heading */
function Lines({ items, className = "" }) {
const [ref, seen] = useInView();
return (
{items.map((t, i) => {t})}
);
}
/* ============ HERO interactive node field ============ */
function HeroCanvas() {
const canvasRef = useRef(null);
const wrapRef = useRef(null);
useEffect(() => {
const cv = canvasRef.current, wrap = wrapRef.current;
const ctx = cv.getContext("2d");
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let W = 0, H = 0, dpr = Math.min(window.devicePixelRatio || 1, 2);
let raf, t = 0;
const mouse = { x: 0.5, y: 0.5, tx: 0.5, ty: 0.5 };
// normalized node layout: a HW x SW system
const nodes = [
{ x: .12, y: .30, label: "地域 / 人", en: "PEOPLE", k: "sw" },
{ x: .30, y: .68, label: "アプリ", en: "APP", k: "sw" },
{ x: .50, y: .24, label: "センサー", en: "SENSOR", k: "hw" },
{ x: .70, y: .60, label: "AI", en: "AI", k: "sw" },
{ x: .50, y: .82, label: "ロボット", en: "ROBOT", k: "hw" },
{ x: .86, y: .32, label: "クラウド", en: "CLOUD", k: "sw" },
{ x: .40, y: .46, label: "デバイス", en: "DEVICE", k: "hw" },
];
const edges = [[0,1],[1,6],[6,2],[2,3],[3,5],[6,3],[1,4],[4,3],[0,6],[2,5]];
nodes.forEach(n => { n.ph = Math.random() * 6.28; n.ax = .012 + Math.random()*.01; n.ay = .012 + Math.random()*.01; });
// bits traveling along edges
const bits = edges.map((e, i) => ({ e, p: Math.random(), sp: .0012 + Math.random()*.0016 }));
function resize() {
W = wrap.clientWidth; H = wrap.clientHeight;
cv.width = W * dpr; cv.height = H * dpr;
cv.style.width = W + "px"; cv.style.height = H + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
resize();
const ro = new ResizeObserver(resize); ro.observe(wrap);
function pos(n) {
const px = mouse.x - .5, py = mouse.y - .5;
const fx = n.x + Math.sin(t * n.ax + n.ph) * .012 + px * (.04 + (n.k==="hw"?.02:0));
const fy = n.y + Math.cos(t * n.ay + n.ph) * .012 + py * (.04 + (n.k==="hw"?.02:0));
return { x: fx * W, y: fy * H };
}
const css = getComputedStyle(document.documentElement);
const accent = css.getPropertyValue("--accent").trim() || "#7ae800";
const ink = "#0c0c0b", grey = "#b9b6af", line = "#d2cfc8";
function draw() {
t += reduce ? 0 : 1;
mouse.x += (mouse.tx - mouse.x) * .06;
mouse.y += (mouse.ty - mouse.y) * .06;
ctx.clearRect(0, 0, W, H);
const P = nodes.map(pos);
// edges
edges.forEach((e) => {
const a = P[e[0]], b = P[e[1]];
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
ctx.strokeStyle = line; ctx.lineWidth = 1; ctx.stroke();
});
// traveling bits
bits.forEach(bt => {
if (!reduce) bt.p += bt.sp; if (bt.p > 1) bt.p -= 1;
const a = P[bt.e[0]], b = P[bt.e[1]];
const x = a.x + (b.x - a.x) * bt.p, y = a.y + (b.y - a.y) * bt.p;
ctx.beginPath(); ctx.rect(x - 2, y - 2, 4, 4);
ctx.fillStyle = accent; ctx.fill();
});
// nodes
P.forEach((p, i) => {
const n = nodes[i];
const hw = n.k === "hw";
// hit-glow near mouse
const mx = mouse.x * W, my = mouse.y * H;
const dist = Math.hypot(mx - p.x, my - p.y);
const near = Math.max(0, 1 - dist / 180);
if (hw) {
ctx.beginPath(); ctx.rect(p.x - 5, p.y - 5, 10, 10);
ctx.fillStyle = "#fff"; ctx.fill();
ctx.lineWidth = near > .1 ? 2 : 1.4; ctx.strokeStyle = near > .1 ? accent : ink; ctx.stroke();
} else {
ctx.beginPath(); ctx.arc(p.x, p.y, 5.5, 0, 6.2832);
ctx.fillStyle = ink; ctx.fill();
if (near > .1) { ctx.beginPath(); ctx.arc(p.x, p.y, 9, 0, 6.2832); ctx.strokeStyle = accent; ctx.lineWidth = 1.5; ctx.stroke(); }
}
// label
ctx.font = "600 11px 'Roboto Mono', monospace";
ctx.fillStyle = near > .1 ? ink : grey;
ctx.fillText(n.en, p.x + 12, p.y + 4);
ctx.font = "700 12px 'Hiragino Kaku Gothic ProN','Yu Gothic',sans-serif";
ctx.fillStyle = near > .1 ? ink : "#8d8a83";
ctx.fillText(n.label, p.x + 12, p.y - 9);
});
raf = requestAnimationFrame(draw);
}
draw();
function onMove(e) {
const r = wrap.getBoundingClientRect();
mouse.tx = (e.clientX - r.left) / r.width;
mouse.ty = (e.clientY - r.top) / r.height;
}
wrap.addEventListener("pointermove", onMove);
return () => { cancelAnimationFrame(raf); ro.disconnect(); wrap.removeEventListener("pointermove", onMove); };
}, []);
return ;
}
/* ============ HERO ============ */
function Hero({ go }) {
return (
);
}
Object.assign(window, { useInView, Reveal, Lines, Hero });