Files
La-Fabrik/src/components/ebike/EbikeSpeedometer.tsx
T
Tom Boullay 396e7e4ff0
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
feat(ebike): add speedometer
2026-05-31 11:36:19 +02:00

91 lines
2.5 KiB
TypeScript

import { useEffect, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { useTexture } from "@react-three/drei";
import * as THREE from "three";
const SPEEDOMETER_DIAL_TEXTURE = "/assets/world/gps/cadran.png";
const SPEEDOMETER_NEEDLE_TEXTURE = "/assets/world/gps/fleche.png";
const SPEEDOMETER_MIN_ANGLE = Math.PI / 2;
const SPEEDOMETER_MAX_ANGLE = -Math.PI / 2;
const SPEEDOMETER_RENDER_ORDER = 10_000;
interface EbikeSpeedometerProps {
width?: number;
height?: number;
}
export function EbikeSpeedometer({
width = 0.9,
height = 0.5,
}: EbikeSpeedometerProps): React.JSX.Element {
const needleGroupRef = useRef<THREE.Group>(null);
const speedFactorRef = useRef(0);
const [dialTexture, needleTexture] = useTexture([
SPEEDOMETER_DIAL_TEXTURE,
SPEEDOMETER_NEEDLE_TEXTURE,
]) as [THREE.Texture, THREE.Texture];
const needleWidth = width * 0.68;
const needleHeight = needleWidth / 2;
useEffect(() => {
[dialTexture, needleTexture].forEach((texture) => {
texture.colorSpace = THREE.SRGBColorSpace;
texture.needsUpdate = true;
});
}, [dialTexture, needleTexture]);
useFrame((_, delta) => {
const targetSpeedFactor = THREE.MathUtils.clamp(
window.ebikeSpeedFactor ?? 0,
0,
1,
);
speedFactorRef.current = THREE.MathUtils.lerp(
speedFactorRef.current,
targetSpeedFactor,
Math.min(1, delta * 10),
);
if (needleGroupRef.current) {
needleGroupRef.current.rotation.z = THREE.MathUtils.lerp(
SPEEDOMETER_MIN_ANGLE,
SPEEDOMETER_MAX_ANGLE,
speedFactorRef.current,
);
}
});
return (
<group renderOrder={SPEEDOMETER_RENDER_ORDER}>
<mesh renderOrder={SPEEDOMETER_RENDER_ORDER}>
<planeGeometry args={[width, height]} />
<meshBasicMaterial
map={dialTexture}
transparent
depthTest={false}
depthWrite={false}
toneMapped={false}
side={THREE.DoubleSide}
/>
</mesh>
<group ref={needleGroupRef} position={[0, -height * 0.38, 0.002]}>
<mesh
position={[0, needleHeight / 2, 0]}
renderOrder={SPEEDOMETER_RENDER_ORDER + 1}
>
<planeGeometry args={[needleWidth, needleHeight]} />
<meshBasicMaterial
map={needleTexture}
transparent
depthTest={false}
depthWrite={false}
toneMapped={false}
side={THREE.DoubleSide}
/>
</mesh>
</group>
</group>
);
}