fix(hex-background): replace lerp with duration-based easing
Hexagons now interpolate via smoothstep over a fixed duration instead of frame-rate-dependent lerp. Prevents height convergence to mean over time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,6 @@ import {
|
|||||||
Shape,
|
Shape,
|
||||||
ExtrudeGeometry,
|
ExtrudeGeometry,
|
||||||
MeshStandardMaterial,
|
MeshStandardMaterial,
|
||||||
MathUtils,
|
|
||||||
} from 'three';
|
} from 'three';
|
||||||
|
|
||||||
export interface HexGridProps {
|
export interface HexGridProps {
|
||||||
@@ -29,6 +28,13 @@ export interface HexGridProps {
|
|||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
/** Interval (ms) between height target changes @default 1500 */
|
/** Interval (ms) between height target changes @default 1500 */
|
||||||
animationInterval?: number;
|
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(
|
function computeHexPositions(
|
||||||
@@ -62,6 +68,7 @@ export function HexGrid({
|
|||||||
margin = 0.05,
|
margin = 0.05,
|
||||||
maxHeight = 3,
|
maxHeight = 3,
|
||||||
animationInterval = 1500,
|
animationInterval = 1500,
|
||||||
|
animationDuration = 1200,
|
||||||
}: HexGridProps) {
|
}: HexGridProps) {
|
||||||
const meshRef = useRef<InstancedMesh>(null);
|
const meshRef = useRef<InstancedMesh>(null);
|
||||||
|
|
||||||
@@ -70,11 +77,11 @@ export function HexGrid({
|
|||||||
[rows, cols, hexRadius, extrudeHeight, margin],
|
[rows, cols, hexRadius, extrudeHeight, margin],
|
||||||
);
|
);
|
||||||
|
|
||||||
const targetHeightsRef = useRef(
|
const animStateRef = useRef(
|
||||||
positions.map(() => 1 + Math.random() * maxHeight),
|
positions.map(() => {
|
||||||
);
|
const h = 1 + Math.random() * maxHeight;
|
||||||
const currentHeightsRef = useRef(
|
return {startHeight: h, targetHeight: h, startTime: 0};
|
||||||
positions.map(() => 1 + Math.random() * maxHeight),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reuse dummy object for matrix updates — 15-20% FPS improvement
|
// Reuse dummy object for matrix updates — 15-20% FPS improvement
|
||||||
@@ -101,25 +108,41 @@ export function HexGrid({
|
|||||||
// Regenerate target heights at interval
|
// Regenerate target heights at interval
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
targetHeightsRef.current = positions.map(
|
const now = performance.now();
|
||||||
() => 0.5 + Math.random() * maxHeight,
|
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);
|
}, animationInterval);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [positions, maxHeight, animationInterval]);
|
}, [positions.length, maxHeight, animationInterval, animationDuration]);
|
||||||
|
|
||||||
// Animate per frame: lerp current heights toward targets
|
// Animate per frame: time-based smoothstep easing
|
||||||
useFrame((_state, delta) => {
|
useFrame(() => {
|
||||||
if (!meshRef.current) return;
|
if (!meshRef.current) return;
|
||||||
const dummy = dummyRef.current;
|
const dummy = dummyRef.current;
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
positions.forEach(({x, z}, i) => {
|
positions.forEach(({x, z}, i) => {
|
||||||
currentHeightsRef.current[i] = MathUtils.lerp(
|
const state = animStateRef.current[i];
|
||||||
currentHeightsRef.current[i],
|
const elapsed = Math.min(
|
||||||
targetHeightsRef.current[i],
|
(now - state.startTime) / animationDuration,
|
||||||
delta * 0.5,
|
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.position.set(x, height / 2, z);
|
||||||
dummy.scale.set(1, height, 1);
|
dummy.scale.set(1, height, 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user