New Simulation Template
Every simulation on mysimulator.uk follows the same skeleton: a
self-contained folder with an index.html, a Three.js
renderer bootstrap, a resize-safe render loop, and a metadata entry
that wires it into the homepage grid, search index, and article
links. This reference is the canonical scaffold to copy when
starting a new simulation from scratch.
1. File & Folder Layout
Simulations live at the site root, one folder per simulation
id, matching the pattern used by existing entries such
as /fluid/index.html and
/galaxy/index.html. A new simulation called, say,
vortex should look like this:
/vortex/
index.html // self-contained: markup + inline <style> + inline <script type="module">
thumb.jpg // optional 1200×630 preview used for og:image / grid card
Simulations do not get their own build step — the
site has no bundler. Import Three.js from the shared CDN import map
declared in shared/components.js, exactly like the
existing simulations, so the module resolves the same way on every
page.
id (vortex, not
Vortex-Field). It becomes the folder name, the JSON
id, and part of the canonical URL — keep all three in
sync.
2. HTML Head Boilerplate
Copy this head block verbatim and fill in the title, description, and canonical path. Every simulation page needs a canonical URL, an OG image, and a viewport meta tag tuned for full-bleed WebGL canvases:
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="canonical" href="https://www.mysimulator.uk/vortex/" />
<title>Vortex Field — 3D Simulations</title>
<meta name="description" content="..." />
<meta property="og:image" content="https://www.mysimulator.uk/vortex/thumb.jpg" />
<style>
html, body { margin: 0; overflow: hidden; background: #000; }
#canvas-holder { position: fixed; inset: 0; }
</style>
</head>
If the simulation also has a companion write-up under
content/articles/, cross-link it via
articleId in the metadata entry (see
section 7) instead of duplicating the
explanation on the simulation page itself.
3. Renderer Bootstrap
Every simulation initializes the same three pieces — scene, camera, renderer — before touching any simulation-specific logic. Keep the renderer options identical across simulations so performance behaviour stays predictable:
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.02);
const camera = new THREE.PerspectiveCamera(
60, innerWidth / innerHeight, 0.1, 2000
);
camera.position.set(0, 8, 24);
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance",
});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // cap DPR — see section 8
document.getElementById("canvas-holder").appendChild(renderer.domElement);
const controls = new THREE.OrbitControls
? new OrbitControls(camera, renderer.domElement)
: null;
if (controls) { controls.enableDamping = true; controls.dampingFactor = 0.06; }
"high-performance". On hybrid-GPU laptops this steers
the browser toward the discrete GPU, which matters for
particle-heavy simulations.
4. Render Loop & Resize Handling
Use a single requestAnimationFrame loop driven by a
delta-time clock so the simulation runs at the same physical speed
regardless of frame rate, and always guard against
zero-size/backgrounded tabs:
const clock = new THREE.Clock();
let running = true;
function animate() {
if (!running) return;
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05); // clamp huge dt after tab-switch
updateSimulation(dt);
controls?.update();
renderer.render(scene, camera);
}
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
document.addEventListener("visibilitychange", () => {
if (document.hidden) { clock.stop(); }
else { clock.start(); }
});
animate();
5. Controls & Interaction
The homepage card shows a short controls string (e.g.
"Click+drag to splash", "Drag to rotate") — this
must match what the simulation actually does. Common interaction
patterns already used across the site:
-
Orbit camera:
OrbitControlswith damping for passive/ambient simulations (galaxies, orbits, landscapes). - Pointer force field: raycast the pointer onto a ground plane and apply an attraction/repulsion impulse to nearby particles — used by fluid, boids, and particle-swarm sims.
-
Keyboard toggles: reserve number keys
1–4for switching visual presets/color modes; reserveSpacefor pause/resume.
Always expose a visible on-canvas hint (a small bottom-left overlay, faded to ~60% opacity) describing the primary control, since most visitors never open dev tools or read the article.
6. Cleanup & Disposal
Simulations are loaded as standalone pages, not as SPA routes, so
full teardown is rarely required — but instanced geometries,
render targets, and GPU compute textures used by particle
simulations are large enough to matter on
pagehide, and are required if the simulation ever
gets embedded in an iframe/preview later:
addEventListener("pagehide", () => {
scene.traverse((obj) => {
obj.geometry?.dispose();
if (Array.isArray(obj.material)) obj.material.forEach((m) => m.dispose());
else obj.material?.dispose();
});
renderer.dispose();
});
7. simulations.json Schema
Registering a simulation is what makes it appear on the homepage
grid, in the search index, and in category filters. Add one entry
to shared/data/simulations.json (English and
Ukrainian title/description live side by side in the same object —
there is no separate localized JSON file for this):
{
"id": "vortex",
"title": "Vortex Field",
"titleUk": "Вихрове Поле",
"emoji": "🌀",
"category": "physics",
"path": "vortex/index.html",
"description": "...",
"descriptionUk": "...",
"tags": ["Three.js", "GPGPU", "Curl Noise"],
"difficulty": 3,
"status": "ready",
"featured": false,
"dateAdded": "2026-07-03",
"articleId": null,
"fps": 60,
"controls": "Click+drag to splash",
"tech": "3D"
}
Field notes:
-
categorymust be one of the values already listed inshared/data/categories.json(physics,space,nature,algorithms, …) — introducing a new category needs a separate change to that file and is out of scope for a single simulation submission. -
difficultyis an integer 1–5 used for sorting and the difficulty badge; base it on required prior knowledge, not code length. -
statusis"ready"once the sim is deployed and stable; use"beta"if it is published but still shaky on low-end GPUs. -
articleIdlinks to a matching slug undercontent/articles/when a deep-dive write-up exists; otherwise leave itnull.
8. Performance Budget
The site targets 60 fps on a mid-range laptop GPU and a usable frame rate on mobile. Apply these budgets when choosing particle counts and shader complexity:
Detect low-end devices with a simple heuristic (rendered frame time
over the first ~2 seconds, or navigator.hardwareConcurrency
combined with viewport width) and drop to a reduced particle count
preset rather than shipping two separate code paths.
9. Accessibility & Mobile
-
Respect
prefers-reduced-motion: if set, either pause continuous camera auto-rotation or reduce simulation speed by ~70%, rather than disabling the simulation outright. -
Support both mouse and touch for the primary interaction —
pointer events (
pointerdown/pointermove) cover both without duplicating listeners. -
The canvas itself has no text alternative; make sure the sim's
homepage card entry (
title/descriptionin the JSON) fully describes what the simulation shows, since that text is what screen readers and search engines see. - Never trap keyboard focus on the canvas — visitors should still be able to tab to the shared navbar/footer links.
10. Submission Checklist
Before opening a pull request for a new simulation, confirm:
- ☐ Folder +
index.htmlcreated at the site root - ☐ Canonical URL, title, description, and OG image set
-
☐ Renderer uses
powerPreference: "high-performance"and a capped pixel ratio - ☐ Resize and
visibilitychangehandlers wired up -
☐ On-canvas control hint matches the
controlsstring in the metadata entry -
☐ Entry added to
shared/data/simulations.jsonwith bothtitle/titleUkanddescription/descriptionUkfilled in - ☐ Runs at target frame rate on a throttled/low-end profile in DevTools, and on a real mobile device if possible
- ☐ Respects
prefers-reduced-motion -
☐ (Optional) companion article added under
content/articles/and cross-linked viaarticleId