/* ============ 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 (
HASHBIT Inc. SYSTEM ONLINE

Building infrastructure for people
through hardware and software.

01 — 接続されたひとつのシステム SCROLL
); } Object.assign(window, { useInView, Reveal, Lines, Hero });