/* global React, window */ // ============================================================ // CAT WALKER — dimensional cat that roams the desk with a smart // wander state machine. Click it: it meows + says "meow G!". // ============================================================ let _walkAudio = null; function playMeow() { try { _walkAudio = _walkAudio || new (window.AudioContext || window.webkitAudioContext)(); const ctx = _walkAudio; if (ctx.state === "suspended") ctx.resume(); const t0 = ctx.currentTime; const mk = (mult, detune) => { const o = ctx.createOscillator(); o.type = "sawtooth"; o.detune.value = detune; o.frequency.setValueAtTime(540 * mult, t0); o.frequency.linearRampToValueAtTime(880 * mult, t0 + 0.11); o.frequency.linearRampToValueAtTime(700 * mult, t0 + 0.26); o.frequency.linearRampToValueAtTime(470 * mult, t0 + 0.46); return o; }; const o1 = mk(1, 0), o2 = mk(1, 9); const lfo = ctx.createOscillator(); lfo.frequency.value = 22; const lfoG = ctx.createGain(); lfoG.gain.value = 18; lfo.connect(lfoG); lfoG.connect(o1.frequency); const bp = ctx.createBiquadFilter(); bp.type = "bandpass"; bp.frequency.value = 1150; bp.Q.value = 3.4; const g = ctx.createGain(); g.gain.setValueAtTime(0.0001, t0); g.gain.exponentialRampToValueAtTime(0.3, t0 + 0.06); g.gain.setValueAtTime(0.28, t0 + 0.3); g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.5); o1.connect(bp); o2.connect(bp); bp.connect(g); g.connect(ctx.destination); o1.start(t0); o2.start(t0); lfo.start(t0); o1.stop(t0 + 0.52); o2.stop(t0 + 0.52); lfo.stop(t0 + 0.52); } catch (e) { /* no-op */ } } function WalkingCatSVG({ riso = false }) { const c = riso ? { a: "#F2503B", b: "#c93722", belly: "#F4ECDC", stripe: "#2BBFB3", line: "#23201d", ear: "#f5b7ac" } : { a: "#E89A4C", b: "#C26A22", belly: "#F6E7CE", stripe: "#A9521A", line: "#3a2414", ear: "#e7a98f" }; const uid = riso ? "r" : "n"; return ( ); } function CatWalker({ riso = false }) { const wrapRef = React.useRef(null); const xRef = React.useRef(null); const dirRef = React.useRef(1); const [facing, setFacing] = React.useState(1); const [walking, setWalking] = React.useState(true); const [bubble, setBubble] = React.useState(false); const bubbleT = React.useRef(null); const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const meow = () => { playMeow(); setBubble(true); clearTimeout(bubbleT.current); bubbleT.current = setTimeout(() => setBubble(false), 1900); }; React.useEffect(() => { const margin = 60; const vw = () => window.innerWidth; if (xRef.current == null) xRef.current = Math.max(margin, vw() * 0.2); if (reduce) { if (wrapRef.current) wrapRef.current.style.transform = `translateX(${xRef.current}px)`; setWalking(false); return; } let raf, last = 0, mode = "walk", pauseUntil = 0; const speed = 66; // px/s const step = (ts) => { if (!last) last = ts; const dt = Math.min(0.05, (ts - last) / 1000); last = ts; if (mode === "walk") { xRef.current += speed * dirRef.current * dt; if (xRef.current < margin) { xRef.current = margin; dirRef.current = 1; setFacing(1); } else if (xRef.current > vw() - margin) { xRef.current = vw() - margin; dirRef.current = -1; setFacing(-1); } // occasionally stop to look around (~ every 5s avg) if (Math.random() < dt / 5) { mode = "pause"; setWalking(false); pauseUntil = ts + 1500 + Math.random() * 2800; } } else { if (ts > pauseUntil) { mode = "walk"; setWalking(true); if (Math.random() < 0.5) { dirRef.current *= -1; setFacing((f) => -f); } } } if (wrapRef.current) wrapRef.current.style.transform = `translateX(${xRef.current}px)`; raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); const onResize = () => { const m = 60; if (xRef.current > vw() - m) xRef.current = vw() - m; }; window.addEventListener("resize", onResize); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", onResize); }; }, []); return (
); } window.CatWalker = CatWalker; window.WalkingCatSVG = WalkingCatSVG;