Files
dt-design-system/packages/hex-background/src/HexGridBackground.tsx
michael e74c285b70 Add storefront component migration: cards, badges, progress bars, animations, and React wrapper package
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>
2026-03-06 10:18:39 -08:00

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>
);
}