Структура файлів проєкту
Кожна симуляція розміщується у власній теці верхнього рівня. Назва теки стає шляхом в URL — пишіть її малими літерами, через дефіс, без пробілів.
index.html ← сторінка вашої симуляції
# необов'язкові доповнення:
worker.js ← Web Worker для важких обчислень
shader.vert ← зовнішній вершинний шейдер GLSL
shader.frag ← зовнішній фрагментний шейдер GLSL
Спільні ресурси (CSS, JS, іконки) розміщуються в shared/
у корені. НЕ копіюйте спільні файли до теки своєї симуляції —
посилайтеся на них через відносний шлях.
Посилання на спільні файли
Атрибут data-base на елементах navbar/footer вказує
shared/components.js, наскільки високо вгору по дереву
шукати:
<!-- корінь navbar — автоматично заповнюється components.js -->
<div id="navbar-root" data-base="../"></div>
<link rel="stylesheet" href="../shared/theme.css">
<link rel="stylesheet" href="../shared/components.css">
<!-- корінь footer -->
<div id="footer-root" data-base="../"></div>
<script src="../shared/components.js" defer></script>
data-base="../". Сторінки всередині
categories/ чи content/ використовують
data-base="../../../".
Шаблон симуляції
Почніть із цього мінімального каркаса. Він дає вам полотно на весь в'юпорт, Three.js через import map, цикл анімації та повідомлення про запасний варіант WebGL.
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Моя симуляція — 3D Simulations</title>
<meta name="description" content="Короткий опис, максимум 160 символів.">
<link rel="stylesheet" href="../shared/theme.css">
<link rel="stylesheet" href="../shared/components.css">
<style>
body { margin: 0; overflow: hidden; background: #0a0a0f; }
#canvas-wrap { position: fixed; inset: 0; }
canvas { display: block; width: 100% !important; height: 100% !important; }
</style>
<!-- Import map для Three.js -->
<script type="importmap">
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160/examples/jsm/" } }
</script>
</head>
<body>
<div id="navbar-root" data-base="../"></div>
<div id="canvas-wrap"></div>
<div id="footer-root" data-base="../"></div>
<script src="../shared/components.js" defer></script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ── Налаштування сцени ──────────────────────────────────────
const canvas = document.createElement('canvas');
document.getElementById('canvas-wrap').appendChild(canvas);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 5, 15);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// ── Освітлення ──────────────────────────────────────────────
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const sun = new THREE.DirectionalLight(0xffffff, 1.2);
sun.position.set(10, 20, 10);
scene.add(sun);
// ── Об'єкти вашої симуляції ─────────────────────────────────
const geo = new THREE.SphereGeometry(1, 32, 32);
const mat = new THREE.MeshStandardMaterial({ color: 0x6366f1 });
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
// ── Зміна розміру ───────────────────────────────────────────
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
// ── Цикл анімації ───────────────────────────────────────────
const clock = new THREE.Clock();
(function animate() {
requestAnimationFrame(animate);
const t = clock.getElapsedTime();
mesh.rotation.y = t * 0.5;
controls.update();
renderer.render(scene, camera);
})();
</script>
</body>
</html>
<head> — він має йти перед будь-яким тегом
скрипта type="module".
HUD та панель керування
Усі симуляції в цьому проєкті дотримуються однакового патерну HUD: напівпрозора панель у лівому нижньому (або правому нижньому) кутку, абсолютно позиційована поверх полотна.
Стандартна розмітка HUD
<!-- Додайте всередині <body>, після canvas-wrap -->
<div class="hud" id="hud">
<div class="hud-title">Моя симуляція</div>
<div class="hud-row">
<label class="hud-label" for="speed">Швидкість</label>
<input class="hud-range" id="speed" type="range" min="0.1" max="5" step="0.1" value="1">
</div>
<div class="hud-row">
<label class="hud-label" for="count">Частинки</label>
<input class="hud-range" id="count" type="range" min="100" max="5000" step="100" value="1000">
</div>
<button class="hud-btn" id="btn-reset">Скинути</button>
</div>
<style>
.hud {
position: fixed; bottom: 1.5rem; left: 1.5rem; z-index: 10;
background: rgba(10,10,15,0.85); backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.08); border-radius: 12px;
padding: 1rem 1.25rem; min-width: 220px; color: #e2e8f0;
}
.hud-title { font-size: 0.7rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.1em; color: #6366f1; margin-bottom: 0.85rem; }
.hud-row { display: flex; flex-direction: column; gap: 0.25rem; margin-bottom: 0.6rem; }
.hud-label { font-size: 0.72rem; color: #94a3b8; }
.hud-range { width: 100%; accent-color: #6366f1; }
.hud-btn { width: 100%; margin-top: 0.4rem; padding: 0.4rem;
border: 1px solid rgba(255,255,255,0.12); border-radius: 6px;
background: rgba(255,255,255,0.05); color: #e2e8f0;
font-size: 0.78rem; cursor: pointer; transition: background 0.15s; }
.hud-btn:hover { background: rgba(255,255,255,0.1); }
</style>
Підключення елементів керування до симуляції
// У вашому модульному скрипті, після налаштування сцени:
const speedSlider = document.getElementById('speed');
const resetBtn = document.getElementById('btn-reset');
let simSpeed = 1;
speedSlider.addEventListener('input', e => simSpeed = parseFloat(e.target.value));
resetBtn.addEventListener('click', resetSimulation);
function resetSimulation() {
// повторно ініціалізуйте частинки / геометрію тут
}
// Використовуйте simSpeed у циклі анімації:
function animate() {
requestAnimationFrame(animate);
const dt = clock.getDelta() * simSpeed;
updateParticles(dt);
renderer.render(scene, camera);
}
H, щоб
перемкнути видимість HUD. Додайте
document.addEventListener('keydown', e => { if (e.key ===
'h') hud.classList.toggle('hidden'); })
Мобільні пристрої та адаптивність
Усі симуляції мають працювати на сенсорних екранах. Три ключові напрямки: сенсорне керування орбітою, розмір HUD та масштабування продуктивності.
OrbitControls, зручні для сенсора
OrbitControls обробляє сенсорні події «з коробки» — обертання одним пальцем, масштабування щипком двома пальцями, панорамування двома пальцями. Додатковий код не потрібен.
Адаптивний HUD
/* Згортаємо HUD на дуже малих екранах */
@media (max-width: 480px) {
.hud {
left: 0.75rem; right: 0.75rem; bottom: 0.75rem;
min-width: unset;
}
/* Більші сенсорні цілі */
.hud-range { height: 24px; }
.hud-btn { padding: 0.65rem; font-size: 0.85rem; }
}
Масштабування продуктивності
// Визначаємо мобільний пристрій і зменшуємо кількість частинок / роздільність
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
const PARTICLE_COUNT = isMobile ? 2000 : 10000;
renderer.setPixelRatio(isMobile ? 1 : Math.min(devicePixelRatio, 2));
// Адаптивна якість: якщо FPS падає нижче 30, зменшуємо кількість частинок удвічі
let frameCount = 0, lastTime = performance.now();
function checkFPS() {
frameCount++;
const now = performance.now();
if (now - lastTime >= 2000) {
const fps = frameCount / 2;
if (fps < 30 && particleCount > 500) {
particleCount = Math.floor(particleCount * 0.7);
rebuildParticles();
}
frameCount = 0; lastTime = now;
}
}
Контрольний список якості
Перш ніж надсилати pull request або додавати свою симуляцію до каталогу, перевірте кожен пункт нижче:
HTML та метадані
-
Унікальний
<title>у форматі Назва — 3D Simulations -
<meta name="description">— 120–160 символів, звичайний текст -
<link rel="canonical">з повним абсолютним URL -
lang="en"(абоlang="uk"для української версії) -
Коректні
og:title,og:description,og:type
Продуктивність
- Стабільні 60 fps на пристрої середнього класу (тестуйте з обмеженням CPU 4× у Chrome DevTools)
-
Немає витоків пам'яті —
geometry.dispose()таmaterial.dispose()викликано під час очищення -
Pixel ratio обмежено до 2 (
Math.min(devicePixelRatio, 2)) - Текстури мають розміри, що є степенем двійки (64, 128, 256 … 2048)
Доступність
-
Усі інтерактивні елементи керування мають видиму рамку фокуса
(
:focus-visible) -
Підписи HUD використовують
<label for="…">абоaria-label -
Полотно має
aria-labelз описом симуляції - Кнопка паузи доступна без навігації мишею
Кросбраузерність і мобільні пристрої
- Протестовано в Chrome, Firefox, Safari (WebKit)
- Сенсорні події працюють в iOS Safari (щипок, обертання)
- Коректне повідомлення-замінник, якщо контекст WebGL недоступний
- HUD зручний при ширині в'юпорта 375px
Додавання до каталогу
Коли ваша симуляція пройде контрольний список, додайте її до
shared/data/simulations.json (якщо він існує) і
додайте картку в index.html, що вказує на нову теку.
preview/my-sim.jpg (1200×630) та описом в
один абзац. Контрольний список перевірки запуститься автоматично.
Що далі?
Продовжуйте розвивати навички за допомогою цих ресурсів