🤝 How to Contribute
Add your own simulation, fix a bug, or write an article. The full project runs on static files — no build tools, no npm required.
Performance
- ≥ 30 FPS on integrated GPU (Intel UHD 620)
- Use InstancedMesh for > 50 identical objects
- requestAnimationFrame loop (no setInterval)
- devicePixelRatio scaling for HiDPI displays
Responsive
- Touch events: pinch, drag on mobile
- Canvas resizes on window resize
- Controls readable at 360px viewport width
- Minimum touch target 44×44 px
Accessible
-
aria-labelorrole="img"on canvas - All sliders have
aria-valuemin/max/now - Keyboard-navigable controls
- Simulation info button (algorithm description)
SEO
-
Unique
<title>andmeta description -
og:title,og:description,og:image canonicallink andhreflang-
JSON-LD
SoftwareApplication+BreadcrumbList
-
Create the folder
Add a new folder in the project root with the slug of your simulation (lowercase, hyphens). Copy the template file as your starting point:
-
Folder name = simulation ID, e.g.
double-pendulum/ - Single file:
double-pendulum/index.html - No subdirectories, no external JS files, all code inline
-
Folder name = simulation ID, e.g.
-
Use the HTML template
Start from the minimal template below. It includes all required meta tags, the shared navbar/footer, and a canvas with correct ARIA attributes.
HTML<!DOCTYPE html> <html lang="uk" data-theme="dark"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="canonical" href="https://www.mysimulator.uk/your-sim/"> <title>Your Simulation — 3D Browser Simulations</title> <meta name="description" content="Short description (≤ 160 chars)."> <meta property="og:title" content="Your Simulation — 3D Browser Simulations"> <meta property="og:description" content="Short description."> <meta property="og:image" content="https://www.mysimulator.uk/preview/your-sim.jpg"> <meta property="og:url" content="https://www.mysimulator.uk/your-sim/"> <meta property="og:type" content="website"> <meta name="twitter:card" content="summary_large_image"> <link rel="preconnect" href="https://cdn.jsdelivr.net"> <link rel="dns-prefetch" href="https://cdnjs.cloudflare.com"> <link rel="stylesheet" href="../../shared/theme.css"> <link rel="stylesheet" href="../../shared/components.css"> </head> <body> <a href="#main-content" class="skip-link">Skip to content</a> <header id="navbar-root" data-base="../../" aria-label="Main navigation"></header> <main id="main-content"> <canvas id="canvas" role="img" aria-label="Interactive Your Simulation — use mouse to interact"> </canvas> <!-- HUD injected by initSimHUD() --> </main> <footer id="footer-root" aria-label="Site footer"></footer> <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script> <script src="../../shared/components.js"></script> <script> /* ── Simulation config for HUD ── */ window.__SIM_INFO__ = { title: 'Your Simulation', category: 'physics', categoryPath: '../categories/physics/', description: 'What this simulation shows and the algorithm behind it.', controls: 'Mouse: drag to rotate | Scroll: zoom | R: reset', tags: ['Three.js', 'Your-tag'], relatedIds: ['fluid', 'nbody'] }; /* ── Three.js setup ── */ const canvas = document.getElementById('canvas'); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(window.innerWidth, window.innerHeight); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 1000); camera.position.set(0, 0, 5); /* ── Your simulation code ── */ // TODO: physics, geometry, animate() function animate() { requestAnimationFrame(animate); // TODO: update physics renderer.render(scene, camera); } animate(); /* ── Resize ── */ window.addEventListener('resize', () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); }); /* ── Init HUD (FPS, back btn, fullscreen, info modal) ── */ if (typeof initSimHUD === 'function') initSimHUD(window.__SIM_INFO__); </script> </body> </html> -
Register in simulations.json
Add an entry to
shared/data/simulations.json. All fields are required exceptthumbnailandarticleId:JSON{ "id": "your-sim", "title": "Your Simulation Name", "titleUk": "Ваша Симуляція", "emoji": "🔬", "category": "physics", "description": "One sentence that explains what this simulates.", "tags": ["Three.js", "GLSL", "Your-Algorithm"], "difficulty": 3, "status": "ready", "featured": false, "dateAdded": "2026-03-14", "thumbnail": "/preview/your-sim.jpg", "controls": "Mouse: drag | Scroll: zoom | R: reset", "algorithms": ["Algorithm Name"], "articleId": null, "fps": 60 }-
difficulty: 1 (trivial) → 5 (research-level) -
status:"ready"|"wip"|"planned" -
category: one ofspace physics nature life chaos math algorithms geometry chemistry sound
-
-
Rebuild the search index
After adding to
simulations.json, regenerate the search index so your simulation appears in the global search:Shellnode shared/scripts/build-search-index.jsThis reads
simulations.json,articles.json, andcategories.jsonand generatesshared/data/search-index.json. -
Test locally
Open the project via a local HTTP server (not
file://— Service Worker requires HTTP):Shell# Python (built-in) python -m http.server 8080 # Node.js (npx, no install needed) npx serve . # VS Code: use "Live Server" extension- Check FPS counter — aim for ≥ 30 FPS on integrated GPU
- Test on mobile viewport (Chrome DevTools → responsive mode)
- Tab through all controls — every element must be reachable
- Check the info modal (ⓘ button in HUD) shows correct text
Ideas for new simulations
| Name | Category | Difficulty | Algorithm |
|---|---|---|---|
| Double Pendulum | Physics | Easy | Lagrangian mechanics, RK4 |
| Electric Field Lines | Physics | Easy | Coulomb's law, streamlines |
| Voronoi Diagram | Math | Easy | Fortune's algorithm |
| Mandelbrot Set | Math | Medium | GLSL fragment shader escape time |
| Heat Diffusion | Physics | Medium | Finite differences, Crank-Nicolson |
| Quantum Harmonic Oscillator | Physics | Medium | Schrödinger wavefunction |
| Reaction-Diffusion (Gray-Scott) | Chemistry | Medium | PDE on GPU texture ping-pong |
| Cloth Tear Simulation | Physics | Hard | Position-based dynamics, mesh splitting |
| SPH + Rigid Body Coupling | Physics | Hard | SPH + Cannon-es coupling |
| Ray Marching Fractals | Math | Hard | GLSL signed distance functions |
📜 Code of Conduct
This project is open and welcoming to everyone. By participating you agree to follow these guidelines:
✅ Do
- Be respectful and constructive in all discussions
- Welcome newcomers regardless of skill level
- Provide clear, reproducible bug reports
- Keep pull requests focused on a single feature or fix
- Write self-documenting code with meaningful variable names
- Credit external algorithms and papers in code comments
🚫 Don't
- Submit simulations with external tracking or analytics
- Copy closed-source shader code without a compatible licence
- Add build tools, bundlers, or npm dependencies
-
Introduce breaking changes to
shared/components.jswithout discussion - Use offensive, hateful, or discriminatory language
Violations may result in your contribution being declined or your access removed. For major concerns, open a GitHub Issue.
🌟 Contributors
Thank you to everyone who has helped build this project — simulations, bug reports, articles and design feedback are all valued equally.
Built and contributed WebGL simulations to the library
Wrote the maths and physics explainers in
/content/
Filed detailed issues that led to real improvements
Helped bring the Ukrainian version to life
Want to see your name here?
Follow the steps above
and open a pull request. All contributors are listed in
CONTRIBUTING.md.
Ready to build something?
The project lives in a flat folder structure — no build step, no dependencies. Clone it, add a folder, and open it in a browser.