now what about actual snapping functionality? I don’t want to use grid as to be fully permanently snapped all around, I want grid to be determinant measurement element, on which you can extend wireframe but in reality it will not be actually attached to grid but appear in that unattached position, it should allow only to extend for both vertices and new edges created. need to rework that “addEdge” button to actually handle all the functionality, to not permanently snap new vertex+edge to grid but to permanently snap it to the actual wireframe construct forma. also, need to be sure that the grid is in constant and extendable (full canvas size) static position and not affected by anything, except as the instrument of pure positioning.: <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Visual Matrix Constructor</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
    }
    canvas {
      display: block;
    }
    .vmc-menu {
      display: none;
      position: absolute;
      background-color: rgba(0,0,0,0.1);
      border-radius: 5px;
      padding: 10px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      z-index:1;
    }
    .vmc-menu label {
      display: block;
      margin-bottom: 10px;
    }
    .red-dot {
      position: absolute;
      width: 5px;
      height: 5px;
      background-color: red;
      border-radius: 50%;
      pointer-events: none;
      color:red;
      z-index:0;

    }
    .green-dot {
      position: absolute;
      width: 5px;
      height: 5px;
      background-color: lime;
      border-radius: 50%;
      pointer-events: none;
      z-index:0;
    }
  </style>
</head>
<body>
  <div class="vmc-menu" id="vmc-menu">
    <label>Vertex X: <input type="number" id="vmc-vertex-x" step="0.1"></label>
    <label>Vertex Y: <input type="number" id="vmc-vertex-y" step="0.1"></label>
    <label>Vertex Z: <input type="number" id="vmc-vertex-z" step="0.1"></label>
    <button id="add-edge">Add Edge</button>
    <button id="remove-edge">Remove Edge</button>
  </div>
  <div class="red-dot" id="red-dot"></div>
    <div class="green-dot” id=“green-dot"></div>
  <script>
  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 greenDotContainer = document.createElement('div');
    document.body.appendChild(greenDotContainer);
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) {
        const {x, y} = snapToGrid(window.event.clientX, window.event.clientY);
        const newVertexX = (x - canvas.width / 2) / (canvas.height * scale) + offsetX;
        const newVertexY = (y - canvas.height / 2) / (canvas.height * scale) + offsetY;
        const newVertexZ = 0;
        vertices.push([newVertexX.toPrecision(4), newVertexY.toPrecision(4), newVertexZ]);
        startNewEdgeIndex = vertices.length - 1;
      } else {
        edges.push([startNewEdgeIndex, bestIndex]);
        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();

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


// Grid related variables
    const gridSpacing = 20; // Spacing between grid lines (in px)
    const gridPoints = []; // Array to store grid points
    let displayGrid = true; // A flag to toggle grid display on and off
    function roundToGrid(value) {
      return Math.round(value / gridSpacing) * gridSpacing;
    }
    
    function snapToGrid(x, y) {
      return {
        x: roundToGrid(x - canvas.getBoundingClientRect().left),
        y: roundToGrid(y - canvas.getBoundingClientRect().top)
      };
    }
    for (let i = 0; i < canvas.width; i += gridSpacing) {
      for (let j = 0; j < canvas.height; j += gridSpacing) {
        gridPoints.push({ x: i, y: j });
        const dot = document.createElement('div');
        dot.className = 'green-dot';
        dot.style.left = (i - 2) + 'px';
        dot.style.top = (j - 2) + 'px';
        dot.style.display = displayGrid ? '' : 'none';
        greenDotContainer.appendChild(dot);
      }
    }

    // Renders grid lines
    function renderGrid() {
      if (!displayGrid) return;
      ctx.strokeStyle = '#0F0';
      ctx.lineWidth = 1;
      for (let i = 0; i <= canvas.width; i += gridSpacing) {
        ctx.beginPath();
        ctx.moveTo(i, 0);
        ctx.lineTo(i, canvas.height);
        ctx.stroke();
      }
      for (let i = 0; i <= canvas.height; i += gridSpacing) {
        ctx.beginPath();
        ctx.moveTo(0, i);
        ctx.lineTo(canvas.width, i);
        ctx.stroke();
      }
    }

    // Adds a button to toggle the grid display
    const gridToggleButton = document.createElement('button');
    gridToggleButton.innerText = 'Toggle Grid';
    vmcMenu.insertBefore(gridToggleButton, document.getElementById('add-edge'));

    gridToggleButton.addEventListener('click', () => {
      displayGrid = !displayGrid;
      if (displayGrid) {
        greenDotContainer.style.display = 'block';
      } else {
        greenDotContainer.style.display = 'none';
      }
    });
renderGrid();
requestAnimationFrame(render);

window.addEventListener("resize", () => {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
});
  </script>
</body>
</html>