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();