import { useEffect, useRef, useState } from "react"; import { updateWorld } from "./physics"; import { drawWorld } from "./drawing"; import { getDefaultMyWorld, type WorldState } from "./worlds"; export default function Pendulum() { const canvasRef = useRef(null); const [subStep, setSubStep] = useState(10); // number of sub-steps per frame const [constraintIterations, setConstraintIterations] = useState(1); // number of constraint iterations per sub-step // refs to hold pendulum instances and animation state so we can reset from UI const worldRef = useRef(null); const rafRef = useRef(null); const prevTimeRef = useRef(0); // Drag state const draggingRef = useRef<{ ballIdx: number | null, offset: [number, number] | null }>({ ballIdx: null, offset: null }); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const dpr = window.devicePixelRatio || 1; const resize = () => { const w = canvas.clientWidth || 600; const h = canvas.clientHeight || 400; canvas.width = Math.max(1, Math.round(w * dpr)); canvas.height = Math.max(1, Math.round(h * dpr)); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; resize(); window.addEventListener("resize", resize); if (!worldRef.current) { worldRef.current = getDefaultMyWorld(); } prevTimeRef.current = performance.now(); const animate = (time: number) => { const deltaTime = time - prevTimeRef.current; prevTimeRef.current = time; const dt = Math.min(0.1, deltaTime / 1000) / subStep; for (let i = 0; i < subStep; i++) { if (worldRef.current) { updateWorld(worldRef.current, dt, constraintIterations); } } ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); if (worldRef.current) { drawWorld(ctx, worldRef.current, worldRef.current.ballDrawers); } rafRef.current = requestAnimationFrame(animate); }; rafRef.current = requestAnimationFrame(animate); // --- Drag interaction handlers --- function getPointerPos(evt: PointerEvent) { if (!canvas) return [0, 0]; const rect = canvas.getBoundingClientRect(); return [evt.clientX - rect.left, evt.clientY - rect.top] as [number, number]; } function onPointerDown(e: PointerEvent) { if (!worldRef.current) return; const pos = getPointerPos(e); let minDist = Infinity; let closestIdx: number | null = null; worldRef.current.balls.forEach((ball, idx) => { const dx = ball.pos[0] - pos[0]; const dy = ball.pos[1] - pos[1]; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 25 && dist < minDist && !ball.isFixed) { // Only allow dragging non-fixed balls minDist = dist; closestIdx = idx; } }); if (closestIdx !== null) { draggingRef.current.ballIdx = closestIdx; draggingRef.current.offset = [ worldRef.current.balls[closestIdx].pos[0] - pos[0], worldRef.current.balls[closestIdx].pos[1] - pos[1], ]; // Pause animation while dragging if (rafRef.current) cancelAnimationFrame(rafRef.current); } } function onPointerMove(e: PointerEvent) { if (!worldRef.current) return; if (draggingRef.current.ballIdx === null) return; const pos = getPointerPos(e); const idx = draggingRef.current.ballIdx; const offset = draggingRef.current.offset || [0, 0]; // Update ball position worldRef.current.balls[idx].pos = [pos[0] + offset[0], pos[1] + offset[1]]; // Optionally update prevPos for stability worldRef.current.balls[idx].prevPos = [pos[0] + offset[0], pos[1] + offset[1]]; // Redraw if (!ctx) return; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); drawWorld(ctx, worldRef.current, worldRef.current.ballDrawers); } function onPointerUp() { if (draggingRef.current.ballIdx !== null) { draggingRef.current.ballIdx = null; draggingRef.current.offset = null; // Resume animation prevTimeRef.current = performance.now(); rafRef.current = requestAnimationFrame(animate); } } canvas.addEventListener("pointerdown", onPointerDown); window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointerup", onPointerUp); return () => { window.removeEventListener("resize", resize); if (rafRef.current) cancelAnimationFrame(rafRef.current); canvas.removeEventListener("pointerdown", onPointerDown); window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointerup", onPointerUp); }; }, [constraintIterations, subStep]); // reset handler to re-create pendulums and avoid large dt on next frame const reset = () => { worldRef.current = getDefaultMyWorld(); prevTimeRef.current = performance.now(); }; return (
); }