Gravity Simulator banner

Gravity Simulator

20 devlogs
15h 22m 44s

Gravity Simulator is a browser-based 2D physics playground built with vanilla HTML, CSS, and JavaScript. You can spawn cube and sphere bodies, tune gravity and size, run or step the simulation, drag objects directly on canvas, and inspect real-tim…

Gravity Simulator is a browser-based 2D physics playground built with vanilla HTML, CSS, and JavaScript. You can spawn cube and sphere bodies, tune gravity and size, run or step the simulation, drag objects directly on canvas, and inspect real-time status metrics.

This project uses AI

Gemini for README.md, Claude for debugging and GitHub Copilot for code completion

Demo Repository

Loading README...

anup34343

Shipped this project!

I built a browser-based gravity simulator that let users create cube and sphere objects, tune gravity and time scale, drag objects on a canvas, and watch collisions, trails, and velocity vectors update in real time. I organized the app around a state-driven simulationStore in script.js, which kept rendering, physics, and UI controls synchronized through requestAnimationFrame and reusable object patching. The hardest part was keeping the interaction model responsive while preserving stable physics, so I added broad-phase collision checks, NaN recovery, object limits, and keyboard and touch support to keep the simulation usable as it grew. I also tightened the interface in index.html and styles.css with clear labels, a help modal, accessibility attributes, and a responsive layout so the canvas and side controls worked across screen sizes. The project ended up as a focused physics playground with a cleaner architecture, stronger UX, and room for save/load and preset features next.

anup34343

I rewrote README.md into a complete project guide that described the Gravity Simulator architecture, controls, and runtime behavior in plain technical language. I organized the document around the actual implementation in index.html, styles.css, and script.js so the setup, toolbar actions, simulation settings, object workflows, and keyboard shortcuts matched the code paths users interact with. I documented the canvas-based requestAnimationFrame loop, the state-driven simulationStore pattern, and the object model built around createPhysicsObject, applyObjectSourcePatch, and advanceSimulationByDelta because those functions define how physics and UI stay synchronized. I also called out the performance safeguards, accessibility features, and rendering passes so contributors could understand why the app uses broad-phase collision pruning, touch-friendly canvas handling, and optional overlays like trails and velocity vectors. I kept the README focused on direct usage, architecture, and development notes so it could serve as a practical entry point for contributors without exposing unfinished roadmap details.

Attachment
0
anup34343

I added accessibility and UX refinements across index.html, styles.css, and script.js so the simulator worked cleanly with keyboard, touch, and assistive technologies. In index.html, I introduced a Help button, a canvas interaction hint, ARIA descriptions on the gravity and time-scale controls, and a dialog-based onboarding modal that explained selection, dragging, duplication, deletion, and help shortcuts. In script.js, I wired handleGlobalKeyboardShortcuts to the window and canvas so N cycled object selection, arrow keys moved the active object, Delete removed it, Ctrl+D duplicated it, and H opened the modal through openHelpModal and closeHelpModal. I kept the drag flow pointer-driven for mobile by preserving the existing pointerdown, pointermove, and pointerup handlers and adding pointerleave cleanup so touch drags released predictably. I also tightened showToast messages and validation branches so each error told the user exactly how to recover, such as selecting an object first, entering a positive size, or adding an object before using bulk edit. In styles.css, I added the help modal surface, backdrop, canvas hint treatment, and larger touch targets on small screens because the interface needed clearer affordances without changing the simulation layout.

Attachment
0
anup34343

I implemented performance and stability safeguards in script.js by adding MAX_OBJECT_COUNT, LOW_FPS_THRESHOLD, and cooldown constants that governed warning frequency and object limits. I enforced hard caps in handleAddObject and handleDuplicateSelectedObject, where I blocked inserts past the limit and surfaced immediate feedback through showToast to keep the simulation from degrading under uncontrolled growth. I optimized collision cost in resolveObjectCollisions by replacing unconditional pair checks with a sweep-and-prune broad phase that sorted x-axis bounds into broadPhaseEntries and skipped non-overlapping pairs before narrow-phase impulse resolution. I hardened numeric integrity in advanceSimulationByDelta with sanitizeObjectForSimulation and toFiniteNumber, where I clamped invalid position, velocity, acceleration, gravity, and size values and recovered broken objects to safe defaults. I reduced unnecessary render work by calling drawCurrentCanvasFrame inside simulationTick and step execution paths instead of repeatedly running full canvas reinitialization, while I kept resize handling in initSimulationCanvas for dimension changes only. I extended startFpsTracker and updateToolbarStateLabel so the requestAnimationFrame loop computed overload state from FPS and object pressure, persisted overloadWarningActive in the store, and displayed throttled stability warnings without spamming the UI.

Attachment
0
anup34343

I updated the canvas renderer in script.js by editing drawCanvasPlaceholder(ctx, width, height) to remove the boxed border treatment and restore the small-cell grid background. I drew the grid with the Canvas 2D API by setting ctx.strokeStyle to “#e2e8f0”, fixing ctx.lineWidth at 1, and iterating both axes with const step = 24 to generate uniform square cells. I kept the render pipeline ordered as background fill, grid pass, optional trail pass via drawObjectTrails, object pass via drawSimulationObjects, and optional vector pass via drawObjectVelocityVectors so overlays stayed legible and deterministic. I used this structure because the app already followed a state-driven pattern where drawCanvasPlaceholder composed visual layers from appState flags instead of branching across multiple renderer functions. I preserved the existing velocity vector toggle behavior by leaving the appState.velocityVectorsEnabled check and drawObjectVelocityVectors call intact after the object pass. I chose to restore the older fine-grid style because it gave spatial references without introducing the heavier boxed framing that visually competed with object selection highlights and motion cues.

Attachment
0
anup34343

I implemented two runtime simulation switches in index.html by adding collisionToggleInput and trailsToggleInput to the simulationSettingsForm fieldset so users could control contact resolution and path rendering from the same control surface as gravity and time scale. I wired both controls in script.js with handleCollisionToggleChange(event) and handleTrailsToggleChange(event), then connected their change listeners near the existing control bindings so they participated in the same event-driven state flow. I gated collision resolution in advanceSimulationByDelta(deltaSeconds) by conditionally calling resolveObjectCollisions(boundedObjects) only when appState.collisionsEnabled remained true, which let me disable object-to-object impulses without altering gravity integration or world-bound responses. I implemented trail state through trailHistory, createTrailHistorySnapshot(objects, previousTrailHistory), and ensureTrailHistoryForObjects(objects), where I appended position samples per object id, applied a movement threshold, and capped memory with TRAIL_MAX_POINTS. I rendered paths in drawObjectTrails(ctx, objects) and invoked it from drawCanvasPlaceholder(ctx, width, height) before drawSimulationObjects(ctx, appState.objects), which preserved object visibility and overlay highlighting while showing motion history underneath. I synchronized lifecycle behavior by updating or clearing trailHistory during handleAddObject(), handleDuplicateSelectedObject(), handleDeleteSelectedObject(), handleReset(), and toggle transitions so trail data stayed consistent with object arrays and never referenced removed entities.

Attachment
0
anup34343

I extended the simulation settings in index.html by adding gravitySliderInput and gravityControlValue alongside the existing gravityInput so users could control global gravity through both numeric and range interfaces. I implemented synchronization logic in script.js inside handleGravityInputChange(event), where I parsed the incoming value, clamped it to the 0-30 range, wrote it back into both controls, and updated the on-panel readout text to keep UI state deterministic. I propagated the selected gravity to every active object by mapping appState.objects through applyObjectSourcePatch(object, { gravity: clampedValue }) and committing the result through simulationStore.update, which kept state mutations centralized in the existing store pattern. I also updated the integration path in advanceSimulationByDelta(deltaSeconds) so each integratedObject carried explicit acceleration values by writing ax and ay into object.acceleration before velocity and position updates. I computed ay as base acceleration plus per-object gravity, then I reused that value for velocity integration to keep force application and persisted object state aligned in one pass. I chose this structure because the project already relied on DOM id bindings, event-driven handlers, and immutable-style store updates, so the gravity controls and acceleration tracking fit the existing architecture without introducing parallel physics code paths.

Attachment
0
anup34343

I added a time-scale control to index.html by inserting the range input timeScaleInput and the live readout timeScaleValue inside simulationSettingsForm so users could change simulation speed without leaving the main settings flow. I wired the control in script.js through handleTimeScaleInputChange(event), where I parsed numeric input, clamped values to the 0.25-3 range, updated simulationStore state with timeScale, and synchronized the rendered speed text immediately. I integrated the new state into simulationTick(timestamp) by multiplying rawDeltaSeconds with appState.timeScale before clamping deltaSeconds, which changed how quickly advanceSimulationByDelta(deltaSeconds) progressed object motion while preserving stability limits. I updated updateToolbarStateLabel() to append the active speed multiplier so runtime state reporting matched internal timing behavior and gave direct feedback during start, pause, and step flows. I fixed slider overflow in styles.css by adding box-sizing: border-box to shared control inputs and defining a dedicated .control-form input[type=“range”] rule that removed text-input padding and borders for proper track sizing inside the fieldset grid. I kept the implementation consistent with the project’s existing pattern of DOM id bindings, event-driven state updates, and requestAnimationFrame-based loop control so the new speed feature fit the current architecture without introducing parallel timing paths.

Attachment
0
anup34343

I moved the selected-object editor from the canvas layer into the sidebar by relocating the selectedObjectEditor fieldset in index.html so it renders immediately after simulationSettingsForm and before createObjectForm inside the control-form flow. I kept the interaction model stable in script.js by preserving the same DOM id bindings and continuing to drive visibility through openSelectedObjectEditor() and closeSelectedObjectEditor(), which toggle the hidden attribute based on selection events from handleCanvasPointerDown() and clearSelectedObjectState(). I retained panel synchronization through updateSelectedObjectPanel(), where I still mapped selectedTypeValue, selectedIdValue, selectedSizeValue, and selectedGravityValue from appState.selectedObjectId and mirrored editable size values with setInputValueIfNotFocused(). I simplified styles.css by deleting the obsolete .sim-stage__overlay rule set and related responsive overrides, because the editor no longer required absolute positioning, pointer-event isolation, or overlay-specific sizing rules. I kept the sidebar layout consistent by relying on the existing control-panel and control-form grid patterns, which let the moved fieldset inherit spacing, summary card styling, and action button alignment without introducing new component-specific CSS branches. I restored and validated the root design tokens in the :root block so spacing, typography, panel widths, and color variables remained available after the refactor and prevented cascade-level parse issues.

Attachment
0
anup34343

I reworked the simulation viewport in styles.css by setting .sim-stage__canvas to width: min(100%, 1024px) with aspect-ratio: 1 / 1, then I constrained vertical growth with max-height so the 1024 target still fit inside smaller screens. I kept the layout responsive by widening –max-content-width for desktop while preserving mobile behavior in the existing media query path, which let the canvas occupy more usable space without clipping the control panel. I removed the selectedApplyBtn control from index.html and deleted the matching button bindings in script.js, so the selected-object editor no longer depended on an explicit apply click. I kept mutation flow in applySelectedObjectChanges and triggered it through scheduleLiveSelectedObjectApply on selected size, density, and volume input events, which maintained immediate updates while preserving the existing validation guards. I retained selectedDuplicateBtn and selectedDeleteBtn actions in the editor and continued to gate them through updateSelectedObjectPanel based on selection state, which kept object lifecycle controls available only when a valid target existed. I chose this structure because the project already centered state updates in the simulation store and redraw path, so removing the manual apply control reduced UI friction without introducing a second editing pipeline.

Attachment
0
anup34343

I implemented full selected-object editing in index.html by expanding the Edit Selected Object fieldset with selectedGravityInput, selectedWeightInput, selectedMassInput, selectedSizeInput, selectedDensityInput, selectedVolumeInput, and selectedApplyBtn. I wired those controls in script.js and funneled updates through applySelectedObjectChanges, where I parsed numeric input with parseSelectedInputNumber and validated domain constraints before mutating state. I mapped derived edits back to source-of-truth fields by converting volume to size with calculateSizeFromVolume, weight to mass with weight divided by gravity, and density to mass with density multiplied by volume, then I committed every change through applyObjectSourcePatch. I kept panel values synchronized with updateSelectedObjectPanel and used setInputValueIfNotFocused so requestAnimationFrame-driven updates and selection changes refreshed the UI without overwriting active typing. I enabled selectedApplyBtn only when a selection existed and I updated the objects array immutably in the simulation store before redrawing with drawCurrentCanvasFrame and updating status metrics. I chose this pattern because the project already centralized physics consistency in applyObjectSourcePatch, so I reused that contract to prevent circular derived-field writes while still allowing direct user editing of gravity, weight, mass, size, density, and volume.

Attachment
0
anup34343

I built a live selected-object details panel in index.html by adding a control-form__summary block with fields for type, id, mass, size, weight, gravity, density, and volume, plus readonly inputs for selectedGravityInput, selectedDensityInput, and selectedVolumeInput. I wired these elements in script.js with DOM bindings and implemented getSelectedObject, formatSelectedNumber, and updateSelectedObjectPanel to map appState.selectedObjectId to the current object and render consistent numeric output. I called updateSelectedObjectPanel from updateStatusBar so every selection change, physics tick, and drag update pushed fresh values into the edit panel without adding a separate render pipeline. I kept the panel fields readonly and disabled the apply button to separate display state from mutation state, which avoided conflicting write paths before the full edit workflow lands. I preserved the existing pointer-event selection flow in handleCanvasPointerDown and findObjectAtPoint, so the summary switched immediately when I picked a cube or sphere on the canvas. I chose this store-driven synchronization pattern because the project already used a single appState object and update cycle, and that let the panel stay accurate while objects moved under requestAnimationFrame and pointer-driven drag logic.

Attachment
0
anup34343

I tightened the canvas drag pipeline in script.js by separating pointer capture from full canvas redraws and by adding a lightweight drawCurrentCanvasFrame path. I wired handleCanvasPointerDown, handleCanvasPointerMove, and handleCanvasPointerRelease to track dragOffset, lastDragPoint, and dragVelocity in the simulation store, which let me keep the selected object under the pointer while preserving release momentum. I skipped the active object inside advanceSimulationByDelta so the physics loop no longer fought the drag gesture, and I smoothed the throw vector with interpolated pointer velocity instead of raw frame-to-frame spikes. I also corrected findObjectAtPoint to use shape-specific hit testing so cube selection matched the rendered square bounds instead of a circular proxy. In styles.css, I kept touch-action: none on .sim-stage__canvas so mobile browsers would not intercept the pointer stream, and I reused the existing canvas render path rather than adding a separate interaction layer because the app already treated the canvas as the single source of visual truth.

Attachment
0
anup34343

I made the canvas interaction functional by wiring the create form in index.html to the simulation runtime in script.js so pressing the Add Object button immediately appended a new physics body into store state and rendered it on the stage. I implemented create-form parsing in validateCreateObjectForm(), where I accepted default mass and size values when fields were blank, rejected invalid ranges such as non-positive mass or zero volume, and returned normalized numeric input for object construction. I connected placement logic through handleAddObject(), which computed effective mass and size from optional density and volume inputs, then chose a spawn position with either getCenterSpawnPosition() or getRandomSpawnPositionForSize() based on the random toggle state. I added drawSimulationObjects(ctx, objects) and called it from drawCanvasPlaceholder() so newly created cube and sphere bodies became visible in the existing canvas paint cycle without waiting for extra rendering infrastructure. I kept calculations stable by reusing createPhysicsObject() and the source-of-truth patch pipeline, which preserved derived value consistency while objects entered the simulation loop. I used this structure because it separated validation, spawn selection, object construction, and rendering into focused functions that I could extend safely when I add richer creation controls and per-object editing behavior.

Attachment
0
anup34343

I formalized the physics update contract in script.js by defining OBJECT_SOURCE_OF_TRUTH_FIELDS and OBJECT_DERIVED_FIELDS, then I enforced those boundaries in applyObjectSourcePatch(object, patch). I treated type, size, mass, and gravity as canonical inputs and blocked direct writes to derived fields, so the engine no longer accepted ad hoc updates to weight, density, or volume that could create circular state transitions. I centralized derivation in recalculateDependentValues(object), where I computed volume with calculateVolume(type, size), weight with calculateWeight(mass, gravity), and density with calculateDensity(mass, volume) on every normalized object pass. I updated object construction in createPhysicsObject(overrides) to run through the same source-patch pipeline, which guaranteed that new objects and edited objects followed identical normalization rules. I routed gravity edits through handleGravityInputChange(event) with applyObjectSourcePatch, so changing one source property automatically propagated consistent dependent recalculations across all objects in store state. I chose this source-first architecture because it eliminated circular writes, made derivation deterministic, and kept every physics recalculation path explicit and auditable.

Attachment
0
anup34343

I implemented the core runtime in script.js by wiring a dedicated requestAnimationFrame loop through startSimulationLoop(), simulationTick(timestamp), and stopSimulationLoop(), and I connected those paths to handleStart(), handlePause(), and handleReset(). I computed frame delta inside simulationTick from lastSimulationTimestamp, clamped it for stability, and passed it to advanceSimulationByDelta(deltaSeconds) so motion updates ran on elapsed time instead of frame count. I expanded world-state tracking with worldWidth and worldHeight, updated those values in initSimulationCanvas(), and enforced boundary behavior in applyWorldBoundsAndFloorCollision() with horizontal wall bounces, ceiling response, and floor damping via restitution and friction factors. I added pairwise object interaction in resolveObjectCollisions(objects) by checking overlap with getObjectCollisionRadius(), separating interpenetrating bodies along the collision normal, and applying a simple impulse response based on inverse mass and relative velocity. I preserved locked-body behavior by zeroing inverse mass for locked objects so movable bodies resolved against fixed ones without shifting anchored state. I chose this structure because the simulation loop, delta integration, boundary constraints, and collision solver now live in explicit, composable functions that I can evolve independently as I add richer forces and rendering.

Attachment
0
anup34343

I built the physics object foundation in script.js by defining a PhysicsObject typedef and implementing createPhysicsObject(overrides) to normalize every runtime field before objects entered simulation state. I constrained shape values through SHAPE_TYPES, generated stable ids with generateObjectId(), and enforced numeric guards for size, mass, and volume so invalid values could not propagate into motion math. I implemented calculateVolume(type, size), calculateWeight(mass, gravity), and calculateDensity(mass, volume) to derive core physical properties from a single construction path instead of scattering formulas across handlers. I expanded appState with an objects array and kept objectCount synchronized inside updateStatusBar() so UI metrics stayed data-driven as object collections changed. I created objectModelTemplate = createPhysicsObject() at bootstrap to verify the constructor path and keep a ready instance shape for upcoming add-object flows. I chose this factory-plus-helper pattern because it centralized validation, preserved deterministic defaults, and made future systems like collision updates and per-object editing consume the same consistent model contract.

Attachment
0
anup34343

I implemented a dedicated validation messaging layer by adding a live #toastArea container in index.html with aria-live and aria-atomic so feedback surfaced immediately without interrupting the main workflow. I built the toast presentation in styles.css with .toast-area, .toast, and variant classes (.toast--error, .toast--success, .toast--info), and I used the existing token system for spacing, radius, and responsive placement to keep the component consistent with the rest of the UI. I added a reusable showToast(message, variant) function in script.js that created the message node, assigned semantic role values, mounted it into the toast area, and removed it after a timed lifecycle. I stored timer state in appState.toastTimer and cleared previous timers before scheduling a new one so consecutive validation events replaced stale messages instead of stacking uncontrolled overlays. I connected validation to runtime input handling in handleGravityInputChange(event) and triggered an error toast when parsing failed, which provided immediate user feedback at the exact point of invalid entry. I also emitted an initial info toast after bootstrapping to verify the notification pipeline and confirm that event-driven messaging worked alongside the toolbar and status update loop.

Attachment
0
anup34343

I added a compact simulation status bar in index.html with #fpsValue, #objectCountValue, #gravityValue, and #selectedObjectValue so the workspace could surface runtime metrics without crowding the canvas or control panel. I styled the strip in styles.css with the existing token set and reused the same card treatment as the toolbar so the interface stayed visually consistent across the layout. I extended script.js with appState.objectCount, appState.currentGravity, appState.selectedObject, and appState.fps, then centralized DOM sync inside updateStatusBar() to keep the UI in lockstep with state changes. I built startFpsTracker() on top of window.requestAnimationFrame() and a one-second rolling window so the display reported a coarse but stable frame rate instead of flickering on every paint. I tied the gravity field to handleGravityInputChange() and reused handleReset() to clear selection and refresh metrics, which kept the status bar honest after toolbar actions changed simulation state. I kept the implementation event-driven and state-backed because the project still lacked a full physics loop, and this pattern let me expose meaningful runtime feedback now without committing to a heavier simulation architecture too early.

Attachment
0
anup34343

I added a simulation action toolbar in index.html by creating a section.sim-toolbar with #startBtn, #pauseBtn, #resetBtn, #stepBtn, and a live #simStateLabel element that exposed runtime state in the UI. I styled the toolbar in styles.css with the existing token system by reusing variables like --toolbar-height, --radius-sm, and --primary, and I used a BEM-style class pattern (.sim-toolbar__btn, .sim-toolbar__state) to keep the component isolated from the canvas and form styles. I implemented control logic in script.js through handleStart(), handlePause(), handleReset(), and handleStep(), and each handler updated appState.isRunning or appState.stepCount so the UI reflected explicit state transitions. I centralized view synchronization in updateToolbarButtons() and updateToolbarStateLabel() so button disable states and active styling stayed consistent after every action. I kept canvas behavior deterministic by calling initSimulationCanvas() from reset and step actions, which reused resizeCanvasToContainer() and drawCanvasPlaceholder() to redraw without introducing a full physics loop yet. I chose this event-driven pattern with a shared appState object because it separated DOM events, state mutation, and rendering updates, which made later integration with requestAnimationFrame and physics systems straightforward.

Attachment
0
anup34343

I built a two-column simulator workspace in index.html by introducing a .workspace container that paired the #simCanvas stage with an aside.control-panel for object creation and selected-object editing controls. I implemented responsive behavior in styles.css using CSS Grid, panel-size tokens like --panel-width-md, and breakpoints that collapsed the layout to a single column on smaller viewports while preserving canvas usability with explicit min-height rules. I wired the canvas runtime in script.js through initSimulationCanvas(), resizeCanvasToContainer(), and drawCanvasPlaceholder(), where I scaled the backing store with window.devicePixelRatio, synced dimensions to #canvasSizeLabel, and rendered a grid placeholder so the stage showed deterministic output before physics logic landed. I structured form controls in index.html with fieldset groups and labeled inputs so I could map shape, mass, size, gravity, density, and volume fields directly to future state updates without refactoring markup later. I removed prism support everywhere by deleting the Prism option from the shape selector in index.html. I chose this token-driven and function-focused pattern to keep layout concerns in CSS, drawing concerns in dedicated JavaScript functions.

Attachment
0