import { StreamPhysicsFrame } from "semiotic/physics"
StreamPhysicsFrame is the low-level frame behind Semiotic physics charts. Use it when the chart wrappers are too specific and you need to declare bodies, colliders, sensors, observation events, semantic regions, and custom foreground or background graphics directly.
Make the physical rules readable
Start with the quantity and the process that changes it. Specify what one body represents, what each container holds, and what a crossing means. The apparatus should let a reader predict what happens next.
- Draw the rules that the simulation uses. Derive visible walls and ramps from collider geometry. Show sensors as permeable regions. A solid lid blocks every body; a selective gate needs a visible mechanism that explains who can pass.
- Give each clock a meaning. Source time determines when an event arrives or a rule changes. Travel time illustrates that decision. Pause source time during a teaching step, or explicitly coordinate both clocks. A snapshot with already closed bins places accepted history beneath their lids; a chronological demonstration admits the bodies before closing those lids.
- Count what the label names. Current occupancy, historical arrivals, and completed work answer different questions. Count bodies in flight separately from bodies inside a container. For an unweighted scene with no departures, arrivals equal container occupancy plus bodies in flight. Account explicitly for removed bodies, queued arrivals, or weighted units when the model uses them.
- Check the drawing against an independent ledger. Test actual body positions and crossings against expected outcomes. A correct source total does not prove that the bodies reached the right destinations. Use the same quantity definitions in motion, reduced motion, accessible text, and static output.
The Watermarks closure sequence demonstrates these rules with two events, one closing lid, and the far-left bin for late arrivals. Its displayed equation reads physical occupancy from the shared EventDrop layout.
Quick Start
Provide a physics config, a list ofinitialSpawns, and optional graphics that explain the physical structure. Hover the bodies for tooltips or tab into the frame to move through the semantic regions.
import { StreamPhysicsFrame } from "semiotic/physics" const frameProps = { /* --- Size --- */ size: [620,360], /* --- Customize --- */ title: "Bounded physics scene", foregroundGraphics: ({ size }) => <svg>{/* visual bounds and ramps */}</svg>, /* --- Interact --- */ enableHover: true, /* --- Other --- */ summary: "Six bodies fall through two ramps into a bounded settling region.", description: "A low-level StreamPhysicsFrame scene with circular body spawns, segment colliders, bounds, hover tooltips, semantic regions, and a data table.", config: { kernel: { gravity: { x: 0, y: 680 }, restitution: 0.42 }, colliders: [...boundsColliders, ...rampColliders], observation: { chartId: "stream-physics-frame-docs", chartType: "StreamPhysicsFrame" } }, initialSpawns: [ { id: "sample-1", x: 112, y: 52, vx: 70, shape: { type: "circle", radius: 11 }, datum: { label: "A" } }, // ...more queued bodies ], bodyStyle: (body) => ({ fill: body.datum?.color || "#4e79a7", stroke: "#111827", strokeWidth: 1 }), semanticItems: [ { id: "feed", label: "Input feed", x: 190, y: 56, shape: "rect", width: 220, height: 48 }, { id: "ramps", label: "Ramp system", x: 310, y: 178, shape: "rect", width: 430, height: 150 }, // ...more semantic regions ], accessibleTable: true, hoverRadius: 18 } export default () => { return <StreamPhysicsFrame {...frameProps} /> }
Sensors and Observation
Sensors are colliders with sensor: true. They do not block bodies, but they emit proximity observations. This example changes each each packet state after it passes through the green sensor region.
JSX
import { useMemo, useState } from "react" import { StreamPhysicsFrame } from "semiotic/physics" function SensorScene() { const [detected, setDetected] = useState(new Set()) const config = useMemo(() => ({ kernel: { gravity: { x: 0, y: 620 } }, colliders: [ ...boundsColliders, { id: "inspection-sensor", sensor: true, shape: { type: "aabb", x: 310, y: 204, width: 430, height: 54 } }, ], observation: { chartId: "sensor-docs", chartType: "StreamPhysicsFrame", sensors: { "inspection-sensor": { binId: "inspection", enterType: "physics-proximity-enter", exitType: "physics-proximity-exit", }, }, onObservation: (event) => { if (event.type !== "physics-proximity-enter" || !event.bodyId) return setDetected((previous) => { const next = new Set(previous) next.add(event.bodyId) return next }) }, }, }), []) return ( <StreamPhysicsFrame config={config} initialSpawns={packets} foregroundGraphics={sensorOverlay} semanticItems={sensorSemanticItems} bodyStyle={(body) => ({ fill: detected.has(body.id) ? "#10b981" : "#4e79a7", stroke: detected.has(body.id) ? "#065f46" : "#1f2937", })} enableHover /> ) }
Imperative Control
The frame ref exposes the physics control surface directly. Use this for application-level controls, custom stream ingestion, snapshot/restore, one-off impulses, or deterministic settle probes.
Time-based controllers and continuous forces use simulated fixed-step time. They run at every step boundary, even when one browser frame or reduced-motion settle advances many steps. A controller receivesctx.dt = result.steps * fixedDt. A zero-step call can synchronize the frame, but it applies no continuous force and consumes no capacity work.
Reduced motion preserves the current queue's arrival schedule, then allowsconfig.settleStepLimit steps for settling. Explicitsettle(maxSteps) calls still bound the entire run. A continuous process can still have unfinished work when that budget ends. Use the process ledger or a declared time horizon to report completion; sleeping bodies alone do not establish it. Authored onTick callbacks and controllers use synchronous execution, where they can change the world before the next step.
createCapacityQueueController records one semantic job visit per physical entry. Its observations distinguish queued,processed, blocked, and abandoned with stable jobId, visitId, queue timing, and work fields. Snapshot ages, utilization, throughput, and pressure use the same simulated clock; metricRevision provides a callback-safe reporting cadence.
JSX
const frameRef = useRef(null) frameRef.current?.push({ id: "next-particle", x: 120, y: 40, vx: 80, shape: { type: "circle", radius: 10 }, datum: { label: "next" }, }) frameRef.current?.applyImpulse("next-particle", 120, -40) const result = frameRef.current?.step(1 / 30) const fixedDt = frameRef.current?.snapshot().config.fixedDt const simulatedDt = (result?.steps ?? 0) * fixedDt frameRef.current?.step(0) // synchronization only: simulated dt is zero const settledSteps = frameRef.current?.settle(240) const bodies = frameRef.current?.getData() const snapshot = frameRef.current?.snapshot()
Keep a data coordinate exact
A body can declare fixedPosition: { x: 120 } to move vertically while x stays exactly 120. Use { y: 80 } for horizontal motion, or set both coordinates to anchor the body. Fixed coordinates take precedence over forces, impulses, and contacts and survive queued spawns, snapshots, and worker execution in the built-in engine. Custom engine adapters must honor this body field themselves.CollisionSwarmChart uses it to keep quantitative x values exact; its vertical spacing is not another data variable.
Props
| Prop | Type | Required | Default | Description |
|---|
config | object | — | — | Physics pipeline configuration: kernel options, colliders, body budget, observation hooks, sediment, timing, and engine adapter. |
initialSpawns | array | — | [] | Initial queued bodies. Each spawn has id, x, y, optional velocity, shape, datum, spawnAt, and optional springs. |
initialSpawnPacing | object | — | — | Controls spawn timing: immediate, arrival time, or { ratePerSec } pacing. |
bodyStyle | function | — | — | Canvas style object or function for each live body. Receives the body and selected/simulation context. |
selectedBodyStyle | function | — | — | Style patch applied when selection marks a body active. |
selection | object | — | — | Selection predicate for styling matching bodies. |
backgroundGraphics | function | — | — | SVG/React graphics rendered behind the canvas. |
foregroundGraphics | function | — | — | SVG/React graphics rendered above the canvas, useful for pegs, bins, sensors, lanes, and guides. |
semanticItems | array | — | [] | Keyboard-navigable semantic structures such as bins, routes, sensor regions, or bounded areas. |
accessibleTable | boolean | — | true | Expose semanticItems as the accessible data table. |
description | string | — | — | Longer accessible description for the frame. |
summary | string | — | — | Short screen-reader summary of the physics scene. |
title | string | — | — | Accessible chart title. |
enableHover | boolean | — | true | Enable body hit testing and tooltips. |
hoverRadius | number | — | 16 | Pointer hit-test radius in pixels. |
tooltipContent | function | — | — | Custom tooltip renderer for PhysicsHoverData. |
onBodyHover | function | — | — | Called with the hovered body and hover payload, or null when hover clears. |
onBodyPointerDown | function | — | — | Pointer-down callback with the nearest body, if any. |
onSemanticItemFocus | function | — | — | Called when keyboard navigation focuses a semantic item. |
onSemanticItemActivate | function | — | — | Called when Enter or Space activates the focused semantic item. |
onTick | function | — | — | Admission/fixed-step callback with physics result and imperative controls. steps is 0 on admission and 1 after each simulation step, including reduced-motion and imperative settles. Requires sync execution. |
onSimulationExecutionChange | function | — | — | Reports whether the frame is running sync or worker execution and why. |
simulationExecution | string | — | "auto" | "auto", "sync", or "worker". Auto uses the worker when the config is cloneable and body counts justify it. |
workerBodyThreshold | number | — | — | Minimum body count for automatic worker execution. |
paused | boolean | — | false | Pause the simulation loop. |
suspendWhenHidden | boolean | — | true | Pause work while the document is hidden. |
size | [number, number] | — | [640, 360] | Frame dimensions. |
responsiveWidth | boolean | — | false | Resize to container width. |
responsiveHeight | boolean | — | false | Resize to container height. |