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