Files
havox/projects/tsp/main.js
John Gatward 3cb8d5a14e tsp
2026-03-18 15:24:08 +00:00

246 lines
6.4 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();