Compare commits
5 Commits
@dangerous
...
@dangerous
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cfc945960 | ||
|
|
77c6a5a3bf | ||
|
|
27824ae189 | ||
|
|
1f67ebf48a | ||
|
|
5112e5c057 |
@@ -1,5 +1,13 @@
|
||||
# @dangerousthings/hex-background
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Fixed
|
||||
- Animation no longer degrades to noise over time — replaced frame-rate-dependent lerp with duration-based smoothstep easing
|
||||
|
||||
### Added
|
||||
- `animationDuration` prop (default 1200ms) controls how long each hexagon takes to reach its target height
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dangerousthings/hex-background",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "3D hexagon grid background for the Dangerous Things design system",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
Shape,
|
||||
ExtrudeGeometry,
|
||||
MeshStandardMaterial,
|
||||
MathUtils,
|
||||
} from 'three';
|
||||
|
||||
export interface HexGridProps {
|
||||
@@ -29,6 +28,13 @@ export interface HexGridProps {
|
||||
maxHeight?: number;
|
||||
/** Interval (ms) between height target changes @default 1500 */
|
||||
animationInterval?: number;
|
||||
/** Duration (ms) for each hex to reach its target @default 1200 */
|
||||
animationDuration?: number;
|
||||
}
|
||||
|
||||
/** Hermite smoothstep: 3t² - 2t³ */
|
||||
function smoothstep(t: number): number {
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
|
||||
function computeHexPositions(
|
||||
@@ -62,6 +68,7 @@ export function HexGrid({
|
||||
margin = 0.05,
|
||||
maxHeight = 3,
|
||||
animationInterval = 1500,
|
||||
animationDuration = 1200,
|
||||
}: HexGridProps) {
|
||||
const meshRef = useRef<InstancedMesh>(null);
|
||||
|
||||
@@ -70,11 +77,11 @@ export function HexGrid({
|
||||
[rows, cols, hexRadius, extrudeHeight, margin],
|
||||
);
|
||||
|
||||
const targetHeightsRef = useRef(
|
||||
positions.map(() => 1 + Math.random() * maxHeight),
|
||||
);
|
||||
const currentHeightsRef = useRef(
|
||||
positions.map(() => 1 + Math.random() * maxHeight),
|
||||
const animStateRef = useRef(
|
||||
positions.map(() => {
|
||||
const h = 1 + Math.random() * maxHeight;
|
||||
return {startHeight: h, targetHeight: h, startTime: 0};
|
||||
}),
|
||||
);
|
||||
|
||||
// Reuse dummy object for matrix updates — 15-20% FPS improvement
|
||||
@@ -101,25 +108,41 @@ export function HexGrid({
|
||||
// Regenerate target heights at interval
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
targetHeightsRef.current = positions.map(
|
||||
() => 0.5 + Math.random() * maxHeight,
|
||||
const now = performance.now();
|
||||
animStateRef.current = animStateRef.current.map((state) => {
|
||||
// Compute where the hex currently is (same easing as useFrame)
|
||||
const elapsed = Math.min(
|
||||
(now - state.startTime) / animationDuration,
|
||||
1,
|
||||
);
|
||||
const t = smoothstep(elapsed);
|
||||
const currentHeight =
|
||||
state.startHeight + (state.targetHeight - state.startHeight) * t;
|
||||
return {
|
||||
startHeight: currentHeight,
|
||||
targetHeight: 0.5 + Math.random() * maxHeight,
|
||||
startTime: now,
|
||||
};
|
||||
});
|
||||
}, animationInterval);
|
||||
return () => clearInterval(interval);
|
||||
}, [positions, maxHeight, animationInterval]);
|
||||
}, [positions.length, maxHeight, animationInterval, animationDuration]);
|
||||
|
||||
// Animate per frame: lerp current heights toward targets
|
||||
useFrame((_state, delta) => {
|
||||
// Animate per frame: time-based smoothstep easing
|
||||
useFrame(() => {
|
||||
if (!meshRef.current) return;
|
||||
const dummy = dummyRef.current;
|
||||
const now = performance.now();
|
||||
|
||||
positions.forEach(({x, z}, i) => {
|
||||
currentHeightsRef.current[i] = MathUtils.lerp(
|
||||
currentHeightsRef.current[i],
|
||||
targetHeightsRef.current[i],
|
||||
delta * 0.5,
|
||||
const state = animStateRef.current[i];
|
||||
const elapsed = Math.min(
|
||||
(now - state.startTime) / animationDuration,
|
||||
1,
|
||||
);
|
||||
const height = currentHeightsRef.current[i];
|
||||
const t = smoothstep(elapsed);
|
||||
const height =
|
||||
state.startHeight + (state.targetHeight - state.startHeight) * t;
|
||||
|
||||
dummy.position.set(x, height / 2, z);
|
||||
dummy.scale.set(1, height, 1);
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface HexGridBackgroundProps {
|
||||
maxHeight?: number;
|
||||
/** Height target refresh interval in ms @default 1500 */
|
||||
animationInterval?: number;
|
||||
/** Duration (ms) for each hex to ease to target @default 1200 */
|
||||
animationDuration?: number;
|
||||
/** Camera orbital speed @default 0.02 */
|
||||
cameraSpeed?: number;
|
||||
/** Camera orbital radius @default 8 */
|
||||
@@ -62,6 +64,7 @@ export function HexGridBackground({
|
||||
margin = 0.05,
|
||||
maxHeight = 3,
|
||||
animationInterval = 1500,
|
||||
animationDuration = 1200,
|
||||
cameraSpeed = 0.02,
|
||||
cameraRadius = 8,
|
||||
fov = 40,
|
||||
@@ -134,6 +137,7 @@ export function HexGridBackground({
|
||||
margin={margin}
|
||||
maxHeight={maxHeight}
|
||||
animationInterval={animationInterval}
|
||||
animationDuration={animationDuration}
|
||||
/>
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dangerousthings/react",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"description": "React web components for the Dangerous Things design system",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
||||
@@ -41,6 +41,9 @@ export type DTGalleryItem =
|
||||
|
||||
interface DTGalleryProps {
|
||||
items: DTGalleryItem[];
|
||||
/** Externally-controlled active index. When this value changes, the gallery
|
||||
* jumps to the corresponding item. Thumbnail clicks still work normally. */
|
||||
activeIndex?: number;
|
||||
/** Color variant @default 'normal' */
|
||||
variant?: DTVariant;
|
||||
/** Aspect ratio (width / height) for the display area @default 1 */
|
||||
@@ -85,12 +88,20 @@ const fillStyle: CSSProperties = {
|
||||
|
||||
export function DTGallery({
|
||||
items,
|
||||
activeIndex: activeIndexProp,
|
||||
variant = 'normal',
|
||||
aspectRatio = 1,
|
||||
className,
|
||||
style,
|
||||
}: DTGalleryProps) {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [activeIndex, setActiveIndex] = useState(activeIndexProp ?? 0);
|
||||
|
||||
// Sync external activeIndex prop to internal state when it changes
|
||||
useEffect(() => {
|
||||
if (activeIndexProp != null && activeIndexProp >= 0 && activeIndexProp < items.length) {
|
||||
setActiveIndex(activeIndexProp);
|
||||
}
|
||||
}, [activeIndexProp, items.length]);
|
||||
|
||||
const hasModel = items.some((i) => i.type === 'model3d');
|
||||
useModelViewerScript(hasModel);
|
||||
|
||||
@@ -25,7 +25,7 @@ export function DTLabel({
|
||||
}: DTLabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={cx('badge', 'badge-mode', getVariantClass(variant), className)}
|
||||
className={cx('badge', 'p-3', 'badge-mode', getVariantClass(variant), className)}
|
||||
style={style}>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user