Major feature migration from dt-shopify-storefront into the design system: - Card system: per-card color modes (normal/emphasis/warning/success/other), progress bars, full-bleed card body (card-body-flush), image bevel clip-paths, and visible structural borders at bevel corners - Badge system: dt-card-badge with position-aware offsets for cards vs media frames - Media frames: dt-bevel-media with inner surface fill, image clipping, and "MISSING MEDIA" placeholder for empty containers - Interactive bevel buttons: dt-btn with hover/selected states and pulse animation - Menu items: dt-menu-item with active states and level indentation - Animations: scale-in, fade-in, slide-up, pulse, ping, spin keyframes - Feature legend: CSS grid for product feature icons with state colors - Scrollbar styling for DT brand - New @dangerousthings/react package wrapping web CSS components - New @dangerousthings/tailwind-preset package - New @dangerousthings/hex-background package - Desktop showcase rewritten in React with Tailwind CSS - Mobile showcase updated with new screens (animations, filters, advanced cards) - Tokens: added mode color tokens, RGB variants, and selected-state tokens Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
142 lines
3.9 KiB
TypeScript
142 lines
3.9 KiB
TypeScript
/**
|
|
* HexGridBackground — Full-viewport hex grid background
|
|
*
|
|
* Drop-in component that renders a fixed, semi-transparent 3D hexagon grid
|
|
* behind page content. Automatically sizes grid to viewport and detects
|
|
* automated browsers to skip rendering.
|
|
*/
|
|
|
|
import {useState, useEffect, useRef, Suspense} from 'react';
|
|
import {Canvas} from '@react-three/fiber';
|
|
import {HexGrid} from './HexGrid';
|
|
import {HexCamera} from './HexCamera';
|
|
|
|
export interface HexGridBackgroundProps {
|
|
/** Canvas opacity @default 0.5 */
|
|
opacity?: number;
|
|
/** Hexagon radius @default 0.5 */
|
|
hexRadius?: number;
|
|
/** Spacing between hexagons @default 0.05 */
|
|
margin?: number;
|
|
/** Maximum animation height @default 3 */
|
|
maxHeight?: number;
|
|
/** Height target refresh interval in ms @default 1500 */
|
|
animationInterval?: number;
|
|
/** Camera orbital speed @default 0.02 */
|
|
cameraSpeed?: number;
|
|
/** Camera orbital radius @default 8 */
|
|
cameraRadius?: number;
|
|
/** Camera field of view @default 40 */
|
|
fov?: number;
|
|
}
|
|
|
|
function calculateGridDimensions(
|
|
width: number,
|
|
height: number,
|
|
cameraDistance: number,
|
|
fov: number,
|
|
hexRadius: number,
|
|
margin: number,
|
|
): {rows: number; cols: number} {
|
|
const aspect = width / height;
|
|
const fovRadians = (fov * Math.PI) / 180;
|
|
const visibleHeight = 2 * cameraDistance * Math.tan(fovRadians / 2);
|
|
const visibleWidth = visibleHeight * aspect;
|
|
|
|
const hexWidth = hexRadius * 2;
|
|
const horizontalStep = hexWidth * 0.75 + margin;
|
|
const verticalStep = hexWidth * 0.866 + margin;
|
|
|
|
const cols = Math.ceil((visibleWidth / horizontalStep) * 1.5);
|
|
const rows = Math.ceil((visibleHeight / verticalStep) * 1.5);
|
|
|
|
return {
|
|
rows: Math.min(Math.max(rows, 15), 35),
|
|
cols: Math.min(Math.max(cols, 15), 35),
|
|
};
|
|
}
|
|
|
|
export function HexGridBackground({
|
|
opacity = 0.5,
|
|
hexRadius = 0.5,
|
|
margin = 0.05,
|
|
maxHeight = 3,
|
|
animationInterval = 1500,
|
|
cameraSpeed = 0.02,
|
|
cameraRadius = 8,
|
|
fov = 40,
|
|
}: HexGridBackgroundProps) {
|
|
const [mounted, setMounted] = useState(false);
|
|
const [isAutomated, setIsAutomated] = useState(false);
|
|
const [gridDimensions, setGridDimensions] = useState({rows: 20, cols: 20});
|
|
const resizeTimeout = useRef<ReturnType<typeof setTimeout>>();
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
|
|
// Detect automated browsers (Selenium, Puppeteer, etc.)
|
|
if (typeof navigator !== 'undefined' && (navigator as any).webdriver) {
|
|
setIsAutomated(true);
|
|
return;
|
|
}
|
|
|
|
const updateDimensions = () => {
|
|
const dims = calculateGridDimensions(
|
|
window.innerWidth,
|
|
window.innerHeight,
|
|
cameraRadius,
|
|
fov,
|
|
hexRadius,
|
|
margin,
|
|
);
|
|
setGridDimensions(dims);
|
|
};
|
|
|
|
updateDimensions();
|
|
|
|
const handleResize = () => {
|
|
clearTimeout(resizeTimeout.current);
|
|
resizeTimeout.current = setTimeout(updateDimensions, 250);
|
|
};
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
return () => {
|
|
window.removeEventListener('resize', handleResize);
|
|
clearTimeout(resizeTimeout.current);
|
|
};
|
|
}, [cameraRadius, fov, hexRadius, margin]);
|
|
|
|
if (!mounted || isAutomated) return null;
|
|
|
|
return (
|
|
<Canvas
|
|
camera={{position: [5, 5, 5], fov, near: 0.5, far: 50}}
|
|
style={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100vw',
|
|
height: '100vh',
|
|
zIndex: -1,
|
|
pointerEvents: 'none',
|
|
opacity,
|
|
margin: 0,
|
|
}}>
|
|
<HexCamera speed={cameraSpeed} radius={cameraRadius} />
|
|
<ambientLight intensity={20} />
|
|
<directionalLight position={[20, 40, 20]} intensity={1.5} />
|
|
<directionalLight position={[-10, 10, -10]} intensity={0.5} />
|
|
<Suspense fallback={null}>
|
|
<HexGrid
|
|
rows={gridDimensions.rows}
|
|
cols={gridDimensions.cols}
|
|
hexRadius={hexRadius}
|
|
margin={margin}
|
|
maxHeight={maxHeight}
|
|
animationInterval={animationInterval}
|
|
/>
|
|
</Suspense>
|
|
</Canvas>
|
|
);
|
|
}
|