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