// tacho.jsx — animated tachometer gauge + redline bar. Exports to window. const { useState, useEffect, useRef } = React; const REDLINE = 8000; // max scale (x1000 -> 0..8) const RED_FROM = 6500; // red zone start function angleFor(v){ return 225 + (Math.min(v,REDLINE)/REDLINE)*270; } // clockwise from north: 0->225(SW), max->495/135(SE), gap at bottom function polar(cx,cy,r,deg){ const a=(deg-90)*Math.PI/180; return [cx+r*Math.cos(a),cy+r*Math.sin(a)]; } function arcPath(cx,cy,r,d0,d1){ const [x0,y0]=polar(cx,cy,r,d0), [x1,y1]=polar(cx,cy,r,d1); const large = (d1-d0)>180?1:0; return `M ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1}`; } // count-up number hook function useCountUp(target, run, dur=1900){ const [val,setVal]=useState(0); useEffect(()=>{ if(!run){ setVal(target); return; } let raf, start; const tick=(t)=>{ if(!start)start=t; const p=Math.min((t-start)/dur,1); const e=1-Math.pow(1-p,3); setVal(Math.round(target*e)); if(p<1) raf=requestAnimationFrame(tick); }; raf=requestAnimationFrame(tick); return ()=>cancelAnimationFrame(raf); },[target,run,dur]); return val; } function Tacho({ target=7900, animate=true, size=520 }){ const cx=200, cy=205, R=168; // tick marks every 1000 const majors=[]; const labels=[]; for(let v=0; v<=REDLINE; v+=1000){ const deg=angleFor(v); const [ox,oy]=polar(cx,cy,R,deg); const [ix,iy]=polar(cx,cy,R-22,deg); const red = v>=RED_FROM; majors.push(); const [lx,ly]=polar(cx,cy,R-42,deg); labels.push({v/1000}); } // minor ticks every 500 const minors=[]; for(let v=0; v<=REDLINE; v+=250){ if(v%1000===0) continue; const deg=angleFor(v); const [ox,oy]=polar(cx,cy,R,deg); const [ix,iy]=polar(cx,cy,R-11,deg); minors.push(); } const endAngle = angleFor(target); const needleRef = useRef(null); useEffect(()=>{ const g = needleRef.current; if(!g) return; const start = angleFor(0), end = endAngle; if(!animate){ g.setAttribute('transform', `rotate(${end} ${cx} ${cy})`); return; } g.setAttribute('transform', `rotate(${start} ${cx} ${cy})`); const dur=1700, delay=300; const ease = p => 1 - Math.pow(1-p, 3); let raf, t0=null; const step = (now)=>{ if(t0===null) t0=now; const el = now - t0 - delay; if(el < 0){ raf=requestAnimationFrame(step); return; } const p = Math.min(el/dur, 1); const ang = start + (end-start)*ease(p); g.setAttribute('transform', `rotate(${ang} ${cx} ${cy})`); if(p<1) raf=requestAnimationFrame(step); }; raf=requestAnimationFrame(step); return ()=>cancelAnimationFrame(raf); },[animate, endAngle, cx, cy]); return (
{/* dial face */} {/* outer track arc */} {/* red zone arc */} {minors}{majors}{labels} {/* needle */}
Vollgas
); } // Horizontal redline bar (hero variant C + reused) function RedlineBar({ target=7200, animate=true }){ const [w,setW]=useState(0); useEffect(()=>{ if(!animate){ setW(target/REDLINE*100); return; } const id=setTimeout(()=>setW(target/REDLINE*100), 250); return ()=>clearTimeout(id); },[target,animate]); return (
{Array.from({length:8}).map((_,i)=>)}
02468 · REDLINE
); } Object.assign(window, { Tacho, RedlineBar, useCountUp, angleFor });