- New /bigscreen-3d route: React Three Fiber 3D campus with buildings, PV panels, heat pumps, meters, and sensors — all procedural geometry - Interactive: hover highlight, click to select, camera fly-in to device detail views (PV inverter, heat pump, meter, heat meter, sensor) - Real-time data: 15s polling for overview, 5s for selected device - Energy flow particles along PV→Building, Grid→Building, Building→HP paths - HUD overlay with date/clock, bottom metrics bar, device list panel - New /bigscreen route: 2D dashboard with energy flow diagram, charts - New /devices route: device management page - Vite config: optimizeDeps.force for R3F dep consistency - Data backfill script for testing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
|
|
interface Props {
|
|
value: number;
|
|
duration?: number;
|
|
decimals?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export default function AnimatedNumber({ value, duration = 1500, decimals = 0, className }: Props) {
|
|
const [display, setDisplay] = useState(0);
|
|
const rafRef = useRef<number>(0);
|
|
const startRef = useRef<number>(0);
|
|
const fromRef = useRef<number>(0);
|
|
|
|
useEffect(() => {
|
|
fromRef.current = display;
|
|
startRef.current = performance.now();
|
|
|
|
const animate = (now: number) => {
|
|
const elapsed = now - startRef.current;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
// ease-out cubic
|
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
const current = fromRef.current + (value - fromRef.current) * eased;
|
|
setDisplay(current);
|
|
if (progress < 1) {
|
|
rafRef.current = requestAnimationFrame(animate);
|
|
}
|
|
};
|
|
|
|
rafRef.current = requestAnimationFrame(animate);
|
|
return () => cancelAnimationFrame(rafRef.current);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [value, duration]);
|
|
|
|
return <span className={className}>{display.toFixed(decimals)}</span>;
|
|
}
|