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

273 lines
6.6 KiB
JavaScript

import { initWasm } from './wasm.js';
import { setupCanvas, resizeCanvas } from './canvas.js';
import { createShaderProgram } from './shaders.js';
import { draw } from './tsp-gl.js';
import { initPoints } from './utils.js';
const DEFAULT_POINT_COUNT = 36;
const MIN_POINTS = 4;
const MAX_POINTS = 96;
const RADIUS_SIZE = 0.006;
function clampPointCount(value) {
if (Number.isNaN(value)) {
return DEFAULT_POINT_COUNT;
}
return Math.max(MIN_POINTS, Math.min(MAX_POINTS, value));
}
function createPointFragmentShader(pointCount) {
return `
precision mediump float;
uniform vec2 u_resolution;
uniform vec2 u_centers[${pointCount}];
void main() {
vec2 position = gl_FragCoord.xy / u_resolution;
float aspect = u_resolution.x / u_resolution.y;
vec2 scaledPosition = vec2(position.x * aspect, position.y);
float radius = ${RADIUS_SIZE};
for (int i = 0; i < ${pointCount}; i++) {
vec2 center = u_centers[i];
vec2 scaledCenter = vec2(center.x * aspect, center.y);
float distance = length(scaledPosition - scaledCenter) * (1.0 / aspect);
if (distance <= radius) {
gl_FragColor = vec4(0.5, center, 1.0);
return;
}
}
gl_FragColor = vec4(0.1, 0.1, 0.1, 1.0);
}
`;
}
function createLineFragmentShader() {
return `
precision mediump float;
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
`;
}
async function run() {
const canvas = document.getElementById('tsp-canvas');
const pointCountInput = document.getElementById('pointCountInput');
const applyPointCountButton = document.getElementById(
'applyPointCountButton'
);
const pointCountStatus = document.getElementById('pointCountStatus');
if (
!canvas ||
!pointCountInput ||
!applyPointCountButton ||
!pointCountStatus
) {
return;
}
const gl = canvas.getContext('webgl2');
if (!gl) {
pointCountStatus.textContent = 'WebGL2 unavailable in this browser.';
return;
}
gl.clearColor(0.1, 0.1, 0.1, 1);
const { initialisePoints, getPointOrder, memory } = await initWasm();
const vertexCode = `
attribute vec2 a_position;
void main(void) {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
setupCanvas(gl, canvas);
const positionBuffer = gl.createBuffer();
const lineBuffer = gl.createBuffer();
if (!positionBuffer || !lineBuffer) {
pointCountStatus.textContent = 'Unable to initialise WebGL buffers.';
return;
}
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = [-1, -1, 3, -1, -1, 3];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
const state = {
scene: null,
draggingIndex: -1,
};
function updateStatus(pointCount) {
pointCountStatus.textContent = `${pointCount} point${pointCount === 1 ? '' : 's'}`;
}
function recalculateRoute(scene) {
const pointOrderPtr = getPointOrder(scene.pointsPtr, scene.pointCount);
scene.pointOrder = Array.from(
new Uint32Array(memory.buffer, pointOrderPtr, scene.pointCount)
);
}
function syncPointsToMemory(scene) {
scene.pointArray = new Float32Array(
memory.buffer,
scene.pointsPtr,
scene.pointCount * 2
);
for (let i = 0; i < scene.points.length; i++) {
scene.pointArray[i * 2] = scene.points[i].cx;
scene.pointArray[i * 2 + 1] = scene.points[i].cy;
}
}
function initialiseScene(rawPointCount) {
const pointCount = clampPointCount(rawPointCount);
if (state.scene) {
gl.deleteProgram(state.scene.program);
gl.deleteProgram(state.scene.lineProgram);
}
const pointsPtr = initialisePoints(pointCount, Date.now());
const pointArray = new Float32Array(
memory.buffer,
pointsPtr,
pointCount * 2
);
const points = initPoints(pointArray);
const program = createShaderProgram(
gl,
vertexCode,
createPointFragmentShader(pointCount)
);
const lineProgram = createShaderProgram(
gl,
vertexCode,
createLineFragmentShader()
);
state.scene = {
pointCount,
pointsPtr,
pointArray,
points,
pointOrder: [],
program,
lineProgram,
};
recalculateRoute(state.scene);
resizeCanvas(gl, canvas, program);
updateStatus(pointCount);
pointCountInput.value = String(pointCount);
}
canvas.addEventListener('mousedown', (e) => {
if (!state.scene) {
return;
}
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
const y = 1 - ((e.clientY - rect.top) / rect.height) * 2;
for (let i = 0; i < state.scene.points.length; i++) {
const point = state.scene.points[i];
const dx = 0.5 * x + 0.5 - point.cx;
const dy = 0.5 * y + 0.5 - point.cy;
const distance = Math.hypot(dx, dy);
if (distance <= RADIUS_SIZE) {
state.draggingIndex = i;
break;
}
}
});
canvas.addEventListener('mousemove', (e) => {
if (!state.scene || state.draggingIndex === -1) {
return;
}
const rect = canvas.getBoundingClientRect();
state.scene.points[state.draggingIndex].cx =
(e.clientX - rect.left) / rect.width;
state.scene.points[state.draggingIndex].cy =
1 - (e.clientY - rect.top) / rect.height;
syncPointsToMemory(state.scene);
recalculateRoute(state.scene);
});
function stopDragging() {
state.draggingIndex = -1;
}
canvas.addEventListener('mouseup', stopDragging);
canvas.addEventListener('mouseleave', stopDragging);
function applyPointCount() {
const nextPointCount = clampPointCount(
Number.parseInt(pointCountInput.value, 10)
);
initialiseScene(nextPointCount);
}
applyPointCountButton.addEventListener('click', applyPointCount);
pointCountInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
applyPointCount();
}
});
pointCountInput.addEventListener('blur', () => {
pointCountInput.value = String(
clampPointCount(Number.parseInt(pointCountInput.value, 10))
);
});
window.addEventListener('resize', () => {
if (!state.scene) {
return;
}
resizeCanvas(gl, canvas, state.scene.program);
});
initialiseScene(DEFAULT_POINT_COUNT);
function render() {
if (state.scene) {
draw(
gl,
state.scene.points,
state.scene.program,
state.scene.lineProgram,
positionBuffer,
lineBuffer,
state.scene.pointOrder,
1
);
}
requestAnimationFrame(render);
}
render();
}
await run();