and how do you make “vmcMenu” appear on grid hover, to place new or add new edges or lines?: const canvas = document.createElement(‘canvas’);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
const ctx = canvas.getContext(‘2d’);
const vmcMenu = document.getElementById(‘vmc-menu’);

const GRID_SIZE = 10;
const GRID_SPACING = 0.2;
const gridLines = [];
function generateGrid(gridSize, gridSpacing) {
// Generate the grid lines along the X and Z axes.

// Loop through gridSize (both X and Z)
for (let i = 0; i <= gridSize; i += gridSpacing) {
// Add X-Axis lines
gridLines.push([
[-gridSize / 2 + i, 0, -gridSize / 2],
[-gridSize / 2 + i, 0, gridSize / 2]
]);

// Add Z-Axis lines
gridLines.push([
[-gridSize / 2, 0, -gridSize / 2 + i],
[gridSize / 2, 0, -gridSize / 2 + i]
]);
}
}
generateGrid(GRID_SIZE, GRID_SPACING);
function getClosestGridPoint(point, gridSize, gridSpacing) {
// Find the nearest grid vertex to the given point and return it.
const x = Math.round(point[0] / gridSpacing) * gridSpacing;
const y = Math.round(point[1] / gridSpacing) * gridSpacing;
const z = Math.round(point[2] / gridSpacing) * gridSpacing;

return [x, y, z];
}
const wireframeLines = [];
const vertices = [
[0, 0, 0],
[0, 1, 0],
[1, 1, 0],
[1, 0, 0],
[0, 0, 1],
[0, 1, 1],
[1, 1, 1],
[1, 0, 1],
];

const edges = [
[0, 1],
[1, 2],
[2, 3],
[3, 0],
[0, 4],
[1, 5],
[2, 6],
[3, 7],
[4, 5],
[5, 6],
[6, 7],
[7, 4],
];

const scale = 0.025;
const zoom = 1;
const offsetX = 0.5;
const offsetY = 0.5;

let angleX = 0;
let angleY = 0;
let angleZ = 0;
let bestIndex = -1;
let bestDistance = Infinity;
let startNewEdgeIndex = -1;
let isMouseDown = false;
let prevMousePos = null;

// Red Dot
const redDot = document.getElementById(‘red-dot’);

document.getElementById(‘add-edge’).addEventListener(‘click’, () => {
if (bestIndex === -1) return;
if (startNewEdgeIndex === -1) {
startNewEdgeIndex = bestIndex;
} else {
const startPoint = getClosestGridPoint(vertices[startNewEdgeIndex], GRID_SIZE, GRID_SPACING);
const endPoint = getClosestGridPoint(vertices[bestIndex], GRID_SIZE, GRID_SPACING);
wireframeLines.push([startPoint, endPoint]);
startNewEdgeIndex = -1;
}
});

// Remove Edge
document.getElementById(‘remove-edge’).addEventListener(‘click’, () => {
if (bestIndex === -1) return;
edges.forEach((edge, index) => {
if (edge.includes(bestIndex)) {
edges.splice(index, 1);
}
});
});

function rotateX(angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
return [
[1, 0, 0],
[0, c, -s],
[0, s, c],
];
}

function rotateY(angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
return [
[c, 0, s],
[0, 1, 0],
[-s, 0, c],
];
}

function rotateZ(angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
return [
[c, -s, 0],
[s, c, 0],
[0, 0, 1],
];
}

function project(vertex, scale, offsetX, offsetY, zoom) {
const [x, y, z] = vertex;
const posX = (x - offsetX) * scale;
const posY = (y - offsetY) * scale;
const posZ = z * scale;

return [
(posX * (zoom + posZ) + canvas.width / 2),
(posY * (zoom + posZ) + canvas.height / 2),
];
}

function transform(vertex, rotationMatrix) {
const [x, y, z] = vertex;
const [rowX, rowY, rowZ] = rotationMatrix;
return [
x * rowX[0] + y * rowX[1] + z * rowX[2],
x * rowY[0] + y * rowY[1] + z * rowY[2],
x * rowZ[0] + y * rowZ[1] + z * rowZ[2],
];
}

function extraterrestrialTransformation(vertex, frequency, amplitude) {
const [x, y, z] = vertex;
const cosX = (Math.cos(x * frequency) * amplitude);
const cosY = (Math.cos(y * frequency) * amplitude);
const cosZ = (Math.cos(z * frequency) * amplitude);

return [x + cosX, y + cosY, z + cosZ];
}

function getDeviation(maxDeviation) {
const t = Date.now() / 1000;
const frequency = 100 / 50;
const amplitude = maxDeviation / 10;
const deviation = Math.sin(t * frequency) * amplitude;
return deviation.toFixed(3);
}

function render() {
ctx.fillStyle = ‘#FFF’;
ctx.fillRect(0, 0, canvas.width, canvas.height);

const rotX = rotateX(angleX);
const rotY = rotateY(angleY);
const rotZ = rotateZ(angleZ);

// Extraterrestrial transformation parameters
const frequency = 1;
const amplitude = 0.8;

const transformedVertices = vertices.map(vertex => {
const extraterrestrialVertex = extraterrestrialTransformation(vertex, frequency, amplitude);
const cx = extraterrestrialVertex[0] - offsetX;
const cy = extraterrestrialVertex[1] - offsetY;
const cz = extraterrestrialVertex[2] - offsetY;

const rotated = transform(transform(transform([cx, cy, cz], rotX), rotY), rotZ);

return [
rotated[0] + offsetX,
rotated[1] + offsetY,
rotated[2] + offsetY,
];
});

const projectedVertices = transformedVertices.map(vertex => project(vertex, canvas.height * scale, offsetX, offsetY, zoom));

ctx.lineWidth = 2;
ctx.strokeStyle = ‘hsla(’ + (angleX + offsetX + angleY + offsetY) * 55 + ‘, 100%, 30%, 0.8)’;
ctx.beginPath();

for (let edge of edges) {
const [a, b] = edge;
const [x1, y1] = projectedVertices[a];
const [x2, y2] = projectedVertices[b];

const dist = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2 + (y2 - x1) ** 2 + (x2 - y1));
const angle = Math.atan2(y2 - y1, x2 - x1, x2 - y1, y2 - x1);

// Calculate control point for curved edge
const cpDist = 0.005 * dist;
const cpX = (x1 + x2) / 2 + cpDist * Math.cos(angle - Math.PI / 2) * getDeviation(0.2);
const cpY = (y1 + y2) / 2 + cpDist * Math.sin(angle - Math.PI / 2) * getDeviation(0.2);

ctx.moveTo(x1, y1, x2, y2);
ctx.quadraticCurveTo(cpX, cpY, x2, y2, x1, y1);
}

ctx.stroke();
ctx.strokeStyle = “#999”;
ctx.lineWidth = 1;
ctx.beginPath();
for (let line of gridLines) {
const [start, end] = line;
const [x1, y1] = project(start, canvas.height * scale, offsetX, offsetY, zoom);
const [x2, y2] = project(end, canvas.height * scale, offsetX, offsetY, zoom);
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
}
ctx.stroke();
ctx.strokeStyle = “#0F0”;
ctx.lineWidth = 2;
ctx.beginPath();
for (let line of wireframeLines) {
const [start, end] = line;
const [x1, y1] = project(start, canvas.height * scale, offsetX, offsetY, zoom);
const [x2, y2] = project(end, canvas.height * scale, offsetX, offsetY, zoom);
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
}
ctx.stroke();
canvas.addEventListener(‘mousedown’, (event) => {
isMouseDown = true;
prevMousePos = { x: event.clientX, y: event.clientY };
});

canvas.addEventListener(‘mouseup’, () => {
isMouseDown = false;
prevMousePos = null;
});

canvas.addEventListener(‘mousemove’, (event) => {
const mousePos = {
x: event.clientX - canvas.getBoundingClientRect().left,
y: event.clientY - canvas.getBoundingClientRect().top
};

bestIndex = -1;
bestDistance = Infinity;

projectedVertices.forEach((currVertex, index) => {
const distance = Math.hypot(
currVertex[0] - mousePos.x,
currVertex[1] - mousePos.y
);
if (distance < bestDistance) {
bestIndex = index;
bestDistance = distance;
}
});

if (bestDistance < 10 && bestIndex !== -1) {
vmcMenu.style.display = ‘block’;
vmcMenu.style.left = mousePos.x + ‘px’;
vmcMenu.style.top = mousePos.y + ‘px’;

document.getElementById(‘vmc-vertex-x’).value = vertices[bestIndex][0];
document.getElementById(‘vmc-vertex-y’).value = vertices[bestIndex][1];
document.getElementById(‘vmc-vertex-z’).value = vertices[bestIndex][2];

document.getElementById(‘vmc-vertex-x’).dataset.vertexIndex = bestIndex;
document.getElementById(‘vmc-vertex-y’).dataset.vertexIndex = bestIndex;
document.getElementById(‘vmc-vertex-z’).dataset.vertexIndex = bestIndex;

redDot.style.display = ‘block’;
redDot.style.left = projectedVertices[bestIndex][0] - 3 + ‘px’;
redDot.style.top = projectedVertices[bestIndex][1] - 3 + ‘px’;
} else {
vmcMenu.style.display = ‘none’;
redDot.style.display = ‘none’;
}

if (isMouseDown && prevMousePos) {
const deltaX = event.clientX - prevMousePos.x;
const deltaY = event.clientY - prevMousePos.y;

angleY += deltaX * 0.01;
angleX += deltaY * 0.01;

prevMousePos = { x: event.clientX, y: event.clientY };
}
});

function updateVertexValue(event, indexToUpdate) {
const newValue = parseFloat(event.target.value);
const vertexIndex = parseInt(event.target.dataset.vertexIndex);

if (!isNaN(newValue) && vertexIndex >= 0) {
vertices[vertexIndex][indexToUpdate] = newValue;
}
}

document.getElementById(‘vmc-vertex-x’).addEventListener(‘input’, (event) => {
updateVertexValue(event, 0);
});
document.getElementById(‘vmc-vertex-y’).addEventListener(‘input’, (event) => {
updateVertexValue(event, 1);
});
document.getElementById(‘vmc-vertex-z’).addEventListener(‘input’, (event) => {
updateVertexValue(event, 2);
});

angleX += +getDeviation(0.0005);
angleY += +getDeviation(0.0005);
angleZ += +getDeviation(0.0005);

requestAnimationFrame(render);
}

requestAnimationFrame(render);
window.addEventListener(“resize”, () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});