Files
havox/projects/flocking/script.js
2026-03-31 22:19:53 +01:00

246 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import init, { SimulationWasm } from './pkg/flocking.js';
const BOIDS_PER_1920X1080 = 250;
const MIN_INITIAL_BOIDS = 100;
const MAX_INITIAL_BOIDS = 1000;
const BOID_STEP = 10;
let wasm = null;
let sim = null;
let canvas = null;
let ctx = null;
let controls = null;
let controlsToggle = null;
let controlsResizeObserver = null;
let frameCount = 0;
let lastFpsUpdate = Date.now();
let displayWidth = 1;
let displayHeight = 1;
let resizeAnimationFrame = 0;
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function getInitialBoidCount(width, height) {
const areaScale = (width * height) / (1920 * 1080);
const targetCount = Math.round(BOIDS_PER_1920X1080 * areaScale);
const clampedCount = clamp(targetCount, MIN_INITIAL_BOIDS, MAX_INITIAL_BOIDS);
return Math.round(clampedCount / BOID_STEP) * BOID_STEP;
}
function syncBoidCountToViewport(width, height) {
const targetCount = getInitialBoidCount(width, height);
let currentCount = sim.get_boid_count();
while (currentCount < targetCount) {
sim.add_boid();
currentCount = sim.get_boid_count();
}
while (currentCount > targetCount) {
sim.remove_boid();
currentCount = sim.get_boid_count();
}
updateBoidCount();
}
function updateControlsCollapsedState(isCollapsed) {
if (controls === null || controlsToggle === null) {
return;
}
controls.classList.toggle('collapsed', isCollapsed);
controls.setAttribute('aria-expanded', String(!isCollapsed));
controlsToggle.setAttribute('aria-expanded', String(!isCollapsed));
controlsToggle.setAttribute(
'aria-label',
isCollapsed ? 'Expand controls' : 'Collapse controls'
);
controlsToggle.textContent = isCollapsed ? '' : '';
}
function sizeCanvasToViewport() {
const dpr = window.devicePixelRatio || 1;
const nextDisplayWidth = Math.max(1, Math.floor(canvas.clientWidth));
const nextDisplayHeight = Math.max(1, Math.floor(canvas.clientHeight));
const nextBufferWidth = Math.max(1, Math.round(nextDisplayWidth * dpr));
const nextBufferHeight = Math.max(1, Math.round(nextDisplayHeight * dpr));
if (canvas.width !== nextBufferWidth || canvas.height !== nextBufferHeight) {
canvas.width = nextBufferWidth;
canvas.height = nextBufferHeight;
}
displayWidth = nextDisplayWidth;
displayHeight = nextDisplayHeight;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
document.getElementById('canvasSize').textContent =
`${displayWidth}x${displayHeight}`;
return { width: displayWidth, height: displayHeight };
}
function scheduleResize() {
if (resizeAnimationFrame !== 0) {
return;
}
resizeAnimationFrame = window.requestAnimationFrame(() => {
resizeAnimationFrame = 0;
const { width, height } = sizeCanvasToViewport();
if (sim !== null) {
sim.resize(width, height);
syncBoidCountToViewport(width, height);
}
});
}
function setupResizeObserver() {
controlsResizeObserver = new ResizeObserver(() => {
scheduleResize();
});
controlsResizeObserver.observe(document.getElementById('container'));
controlsResizeObserver.observe(controls);
}
async function run() {
wasm = await init();
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
controls = document.getElementById('controls');
controlsToggle = document.getElementById('controlsToggle');
updateControlsCollapsedState(false);
const { width, height } = sizeCanvasToViewport();
sim = new SimulationWasm(width, height, getInitialBoidCount(width, height));
setupControls();
setupResizeObserver();
updateBoidCount();
window.addEventListener('resize', scheduleResize);
animate();
}
function setupControls() {
const alignSlider = document.getElementById('alignSlider');
const cohesionSlider = document.getElementById('cohesionSlider');
const separationSlider = document.getElementById('separationSlider');
const addBtn = document.getElementById('addBtn');
const removeBtn = document.getElementById('removeBtn');
controlsToggle.addEventListener('click', () => {
const isCollapsed = !controls.classList.contains('collapsed');
updateControlsCollapsedState(isCollapsed);
scheduleResize();
});
alignSlider.addEventListener('input', (e) => {
const value = Number.parseFloat(e.target.value);
sim.set_align_mult(value);
document.getElementById('alignValue').textContent = value.toFixed(2);
});
cohesionSlider.addEventListener('input', (e) => {
const value = Number.parseFloat(e.target.value);
sim.set_cohesion_mult(value);
document.getElementById('cohesionValue').textContent = value.toFixed(2);
});
separationSlider.addEventListener('input', (e) => {
const value = Number.parseFloat(e.target.value);
sim.set_separation_mult(value);
document.getElementById('separationValue').textContent = value.toFixed(2);
});
addBtn.addEventListener('click', () => {
sim.add_boid();
updateBoidCount();
});
removeBtn.addEventListener('click', () => {
sim.remove_boid();
updateBoidCount();
});
}
function updateBoidCount() {
document.getElementById('boidCount').textContent = sim.get_boid_count();
}
function getBoidView() {
const ptr = sim.boid_buffer_ptr();
const len = sim.boid_buffer_len();
return new Float32Array(wasm.memory.buffer, ptr, len);
}
function animate() {
ctx.fillStyle = '#121212';
ctx.fillRect(0, 0, displayWidth, displayHeight);
sim.step();
const boidData = getBoidView();
const stride = sim.boid_buffer_stride();
for (let i = 0; i < boidData.length; i += stride) {
const x = boidData[i];
const y = boidData[i + 1];
const vx = boidData[i + 2];
const vy = boidData[i + 3];
const r = boidData[i + 4];
const g = boidData[i + 5];
const b = boidData[i + 6];
const a = boidData[i + 7];
const speedSq = vx * vx + vy * vy;
let dx;
let dy;
if (speedSq > 1e-6) {
const invSpeed = 1 / Math.sqrt(speedSq);
dx = vx * invSpeed;
dy = vy * invSpeed;
} else {
dx = 0;
dy = -1;
}
const size = 5;
const headMult = 3;
const px = -dy;
const py = dx;
const tipX = x + dx * size * headMult;
const tipY = y + dy * size * headMult;
const baseX = x - dx * size;
const baseY = y - dy * size;
const leftX = baseX + px * size;
const leftY = baseY + py * size;
const rightX = baseX - px * size;
const rightY = baseY - py * size;
ctx.strokeStyle = `rgba(${r},${g},${b},${a / 255})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(tipX, tipY);
ctx.lineTo(leftX, leftY);
ctx.lineTo(rightX, rightY);
ctx.closePath();
ctx.stroke();
}
frameCount += 1;
const now = Date.now();
if (now - lastFpsUpdate >= 1000) {
document.getElementById('fps').textContent = frameCount;
frameCount = 0;
lastFpsUpdate = now;
}
requestAnimationFrame(animate);
}
await run();