Reusable physics panels
Full-page experience · Open playground
Move and resize two independent simulations without resetting their bodies or changing their local physics.
Try it
Section titled “Try it”Click a ball, then resize or move the panels while it bounces.
What to learn
Section titled “What to learn”- Reuse one physics component in two independently owned local worlds.
- Move and resize a viewport while preserving body identity, velocity and local collision geometry.
Integration boundaries
Section titled “Integration boundaries”- space=local makes simulation coordinates relative to the PhysicsWorld2D boundary.
- Outer transforms change presentation; named commands change physical motion.
- Use toPhysicsPoint2D to convert root-logical pointer coordinates before local queries or dragging.
This is a Pibbl Canvas example. Its mount owns its runtime resources; call its controller’s dispose method when removing it. The preview starts automatically unless reduced motion is enabled; Stop disposes the mount. Preserve the source’s logical canvas size and aspect ratio. Native HTML is not captured by Canvas filters.
Related APIs
Section titled “Related APIs”- PhysicsWorld2D
- Group
- dynamicBody2D
- staticBody2D
- collider2D
- pibblPhysicsBody2D
- applyImpulse2D
- useSignal
Complete source
Section titled “Complete source”Host setup for this source: use a canvas with width 720 and height 420, compile with jsx: "react-jsx" and jsxImportSource: "@pibbl/core", import its default mount, and call const controller = mount(canvas) after attaching the canvas. Call controller.dispose() before removing it.
physics-panels.example.tsx
Section titled “physics-panels.example.tsx”import { Circle, Group, Rectangle, Text, pibbl, useSignal, type PibblController,} from "@pibbl/core";import { PhysicsWorld2D, applyImpulse2D, collider2D, pibblPhysicsBody2D, dynamicBody2D, staticBody2D,} from "@pibbl/core/physics/2d";
function Ball({ x, color }: { x: number; color: string }) { const body = pibblPhysicsBody2D(); return ( <Group physics={dynamicBody2D({ handle: body, collider: false, mass: 1 })} style={{ translateX: x, translateY: 30 }} > <Circle physics={collider2D({ restitution: 0.25, friction: 0.5 })} style={{ radius: 13, fill: color, stroke: "#17211d", strokeWidth: 2, cursor: "pointer", }} onPointerDown={() => applyImpulse2D(body, [20, -180])} /> </Group> );}
// Each instance owns a local simulation. Parent transforms only change its view.function SimulationPanel({ color }: { color: string }) { return ( <PhysicsWorld2D space="local" gravity={[0, 500]}> <Rectangle pointerEvents="none" style={{ width: 270, height: 210, fill: "#ffffff", stroke: "#17211d", strokeWidth: 3, }} /> <Rectangle physics={staticBody2D()} style={{ left: 10, top: 185, width: 250, height: 15, fill: "#334155" }} /> <Rectangle physics={staticBody2D()} pointerEvents="none" style={{ left: 0, top: 0, width: 10, height: 200, fill: "#334155" }} /> <Rectangle physics={staticBody2D()} pointerEvents="none" style={{ left: 260, top: 0, width: 10, height: 200, fill: "#334155" }} /> {[65, 120, 175].map((x) => ( <Ball key={x} x={x} color={color} /> ))} </PhysicsWorld2D> );}
function PhysicsPanels() { const resized = useSignal(false); const moved = useSignal(false); return ( <> <Rectangle style={{ width: 720, height: 420, fill: "#eff6ff" }} /> <Text pointerEvents="none" style={{ left: 360, top: 38, textAlign: "center", font: "900 24px Arial", fill: "#17211d", }} > TWO VIEWS. TWO WORLDS. </Text> <Text pointerEvents="none" style={{ left: 360, top: 63, textAlign: "center", font: "14px Arial", fill: "#475569", }} > Click a ball. Move or resize its panel while it bounces. </Text> <Group style={{ translateX: moved.get() ? 90 : 45, translateY: 90 }}> <Group style={{ scaleX: resized.get() ? 0.8 : 1, scaleY: resized.get() ? 0.8 : 1, }} > <SimulationPanel color="#2563eb" /> </Group> </Group> <Group style={{ translateX: 385, translateY: moved.get() ? 112 : 90 }}> <SimulationPanel color="#ec4899" /> </Group> <Rectangle style={{ left: 115, top: 344, width: 220, height: 44, fill: "#17211d", cursor: "pointer", }} onClick={() => resized.update((value) => !value)} /> <Text pointerEvents="none" style={{ left: 225, top: 371, textAlign: "center", font: "700 15px Arial", fill: "#ffffff", }} > Resize blue panel </Text> <Rectangle style={{ left: 385, top: 344, width: 220, height: 44, fill: "#17211d", cursor: "pointer", }} onClick={() => moved.update((value) => !value)} /> <Text pointerEvents="none" style={{ left: 495, top: 371, textAlign: "center", font: "700 15px Arial", fill: "#ffffff", }} > Move panels </Text> </> );}
export default function mount(canvas: HTMLCanvasElement): PibblController { return pibbl(canvas, <PhysicsPanels />);}Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.