145 lines
4.2 KiB
JavaScript
145 lines
4.2 KiB
JavaScript
// HeroZone — the reserved interactive hero. For now a constrained placeholder:
|
|
// a chamfer-clipped panel of warm chiaroscuro whose light source drifts toward
|
|
// the pointer (the one signature motion moment), with the ink stroke parallaxing
|
|
// and the watermark beneath. A toggle reveals the zone spec + safe area so any
|
|
// future interactive element inherits the same frame.
|
|
const ASSET = '../../assets';
|
|
|
|
function HeroZone() {
|
|
const [p, setP] = React.useState({ x: 0.62, y: 0.28 });
|
|
const [showSpec, setShowSpec] = React.useState(false);
|
|
const ref = React.useRef(null);
|
|
const reduce = React.useRef(
|
|
typeof matchMedia !== 'undefined' &&
|
|
matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
);
|
|
|
|
function onMove(e) {
|
|
if (reduce.current) return;
|
|
const r = ref.current.getBoundingClientRect();
|
|
setP({
|
|
x: Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)),
|
|
y: Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)),
|
|
});
|
|
}
|
|
function onLeave() {
|
|
setP({ x: 0.62, y: 0.28 });
|
|
}
|
|
|
|
// light follows pointer; ink drifts opposite for depth (short travel).
|
|
const lx = 30 + p.x * 50;
|
|
const ly = 8 + p.y * 40;
|
|
const inkShift = `translate(${(0.5 - p.x) * 26}px, ${(0.5 - p.y) * 16}px)`;
|
|
|
|
const hz = {
|
|
frame: {
|
|
position: 'relative',
|
|
width: '100%',
|
|
aspectRatio: '4 / 5',
|
|
borderRadius: 'var(--radius-cutout)',
|
|
background: `radial-gradient(120% 95% at ${lx}% ${ly}%, #C9B299 0%, #8A6A47 30%, #4A3826 60%, #221E18 92%)`,
|
|
transition: 'background 700ms var(--ease-out)',
|
|
overflow: 'hidden',
|
|
cursor: 'crosshair',
|
|
boxShadow: 'var(--shadow-2)',
|
|
},
|
|
accent: {
|
|
position: 'absolute',
|
|
right: '9%',
|
|
top: '12%',
|
|
width: '76px',
|
|
opacity: 0.16,
|
|
filter: 'invert(1)',
|
|
transform: inkShift,
|
|
transition: 'transform 900ms var(--ease-out)',
|
|
pointerEvents: 'none',
|
|
},
|
|
grain: {
|
|
position: 'absolute',
|
|
inset: 0,
|
|
backgroundImage: `url(${ASSET}/texture-grain.svg)`,
|
|
backgroundSize: '220px',
|
|
opacity: 0.12,
|
|
mixBlendMode: 'overlay',
|
|
pointerEvents: 'none',
|
|
},
|
|
watermark: {
|
|
position: 'absolute',
|
|
left: '8%',
|
|
bottom: '8%',
|
|
width: '128px',
|
|
opacity: 0.1,
|
|
filter: 'invert(1)',
|
|
pointerEvents: 'none',
|
|
},
|
|
toggle: {
|
|
position: 'absolute',
|
|
right: '8%',
|
|
bottom: '7%',
|
|
fontFamily: 'var(--font-mono)',
|
|
fontSize: '10px',
|
|
letterSpacing: '0.12em',
|
|
textTransform: 'uppercase',
|
|
color: 'rgba(239,231,218,0.55)',
|
|
background: 'transparent',
|
|
border: '1px solid rgba(239,231,218,0.22)',
|
|
borderRadius: '2px',
|
|
padding: '5px 9px',
|
|
cursor: 'pointer',
|
|
zIndex: 3,
|
|
},
|
|
safe: {
|
|
position: 'absolute',
|
|
inset: '9% 9% 9% 9%',
|
|
border: '1px dashed rgba(239,231,218,0.4)',
|
|
pointerEvents: 'none',
|
|
display: showSpec ? 'block' : 'none',
|
|
},
|
|
safeLabel: {
|
|
position: 'absolute',
|
|
top: '-9px',
|
|
left: '10px',
|
|
background: '#221E18',
|
|
padding: '0 6px',
|
|
fontFamily: 'var(--font-mono)',
|
|
fontSize: '9px',
|
|
letterSpacing: '0.1em',
|
|
color: 'rgba(239,231,218,0.7)',
|
|
},
|
|
placeholderTag: {
|
|
position: 'absolute',
|
|
left: '7%',
|
|
top: '8%',
|
|
fontFamily: 'var(--font-mono)',
|
|
fontSize: '10px',
|
|
letterSpacing: '0.14em',
|
|
textTransform: 'uppercase',
|
|
color: 'rgba(239,231,218,0.45)',
|
|
display: showSpec ? 'block' : 'none',
|
|
},
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
style={hz.frame}
|
|
onMouseMove={onMove}
|
|
onMouseLeave={onLeave}
|
|
>
|
|
<img src={`${ASSET}/accent-ringdots.svg`} style={hz.accent} alt="" />
|
|
<div style={hz.grain} />
|
|
<span style={hz.placeholderTag}>Hero zone — interactive · TBD</span>
|
|
<div style={hz.safe}>
|
|
<span style={hz.safeLabel}>SAFE AREA · 1:1.25 · pad 9%</span>
|
|
</div>
|
|
<img src={`${ASSET}/mark-rings.svg`} style={hz.watermark} alt="" />
|
|
<button
|
|
style={hz.toggle}
|
|
onClick={() => setShowSpec((s) => !s)}
|
|
>
|
|
{showSpec ? 'Hide spec' : 'Zone spec'}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
Object.assign(window, { HeroZone });
|