tsp
This commit is contained in:
245
projects/tsp/main.js
Normal file
245
projects/tsp/main.js
Normal file
@@ -0,0 +1,245 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user