/* global React */
const { useEffect, useRef } = React;
/* ==========================================================
HERO SCENE — layered CSS/SVG matte
========================================================== */
// Distant ridge / mesa silhouette
function HeroRidge() {
return (
);
}
// Buildings flanking the street — flat ochre planes with hard shadows
function HeroBuildings({ parallaxY = 0 }) {
return (
);
}
// Receding street plane
function HeroStreet() {
return (
);
}
// Tiny opponent silhouette in the distance
function HeroOpponent() {
return (
);
}
/* ==========================================================
HUD REVOLVER — single-action, leather grip, brass frame
========================================================== */
function HudRevolver({ hammerTwitch }) {
return (
);
}
/* ==========================================================
DUST PARTICLES — canvas
========================================================== */
function DustCanvas({ scrollSpeed, reduced }) {
const ref = useRef(null);
const rafRef = useRef(0);
const speedRef = useRef(scrollSpeed);
useEffect(() => { speedRef.current = scrollSpeed; }, [scrollSpeed]);
useEffect(() => {
if (reduced) return;
const canvas = ref.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
let w = canvas.width = canvas.offsetWidth * window.devicePixelRatio;
let h = canvas.height = canvas.offsetHeight * window.devicePixelRatio;
const dpr = window.devicePixelRatio;
const N = 80;
const particles = Array.from({ length: N }, () => ({
x: Math.random() * w,
y: Math.random() * h,
r: (0.5 + Math.random() * 1.8) * dpr,
vx: (0.15 + Math.random() * 0.4) * dpr,
vy: (Math.random() - 0.5) * 0.05 * dpr,
a: 0.15 + Math.random() * 0.45,
}));
const onResize = () => {
w = canvas.width = canvas.offsetWidth * window.devicePixelRatio;
h = canvas.height = canvas.offsetHeight * window.devicePixelRatio;
};
window.addEventListener("resize", onResize);
const tick = () => {
ctx.clearRect(0, 0, w, h);
const mult = 1 + speedRef.current * 2.5;
for (const p of particles) {
p.x += p.vx * mult;
p.y += p.vy;
if (p.x > w + 10) { p.x = -10; p.y = Math.random() * h; }
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(236, 226, 200, ${p.a})`;
ctx.fill();
}
rafRef.current = requestAnimationFrame(tick);
};
const start = () => {
if (rafRef.current) return; // already running
tick();
};
const stop = () => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
rafRef.current = 0;
}
};
// Resume when page becomes visible after backgrounding
const onVisibility = () => {
if (document.visibilityState === "visible") start();
else stop();
};
document.addEventListener("visibilitychange", onVisibility);
// Resume / pause when canvas enters / leaves viewport
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) start();
else stop();
}
},
{ threshold: 0 }
);
observer.observe(canvas);
start();
return () => {
stop();
window.removeEventListener("resize", onResize);
document.removeEventListener("visibilitychange", onVisibility);
observer.disconnect();
};
}, [reduced]);
return ;
}
/* ==========================================================
PILLAR ICONS
========================================================== */
function IconRevolver() {
return (
);
}
function IconStar() {
return (
);
}
function IconShield() {
return (
);
}
/* ==========================================================
ROSTER SILHOUETTES — flat black blobs with signature shapes
========================================================== */
// Riley — long duster + weathered hat
function SilRiley() {
return (
);
}
// Doc — wire-rim spectacles glint + neat vest, narrower silhouette
function SilDoc() {
return (
);
}
// Cassidy — slim gambler/gunslinger: pinch-front hat, frock coat, cigar glint, hip holster
function SilCassidy() {
return (
);
}
// Black-Eye — tall, beaded leather + fringe vest + single feather
function SilBlackEye() {
return (
);
}
/* ==========================================================
Export to window
========================================================== */
Object.assign(window, {
HeroRidge, HeroBuildings, HeroStreet, HeroOpponent,
HudRevolver, DustCanvas,
IconRevolver, IconStar, IconShield,
SilRiley, SilDoc, SilCassidy, SilBlackEye,
});