I have a code that creates an interactable interpolated fractal pattern under this explanation:

The main existing line is of rank 1. Under rank 1 line there will be 3 lines on rank 2 following these conditions:
The first line's first point is tied to the leftmost vertex of the rank 1 line. This line's other point is defined as such:
x: 1/3 of way across rank 1 line's x (toward the middle)
y: 1/2 of way across rank 1 line's y (toward the middle)

The second line's first point is tied to the rightmost vertex of the rank 1 line. This line's other point is defined as such:
x: 1/3 of way across rank 1 line's x (toward the middle)
y: 1/2 of way across rank 1 line's y (toward the middle)

The line in the middle joins the leftmost and rightmost lines.


Currently the code is very slow and unoptimized, slowing drastically when multiple ranks are added.

Zooming, panning and movement of the pattern is all very slow.

Please analyze this code and implement the best optimizations that address these faults.


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Three.js Draggable Line with Fractal Pattern</title>
  <style>
    body { margin: 0; }
    canvas { display: block; }
  </style>
</head>
<body>
  <script type="module">

import * as THREE from './three.module.js';

// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);

// Camera setup
const aspectRatio = window.innerWidth / window.innerHeight;
const cameraSize = 200;
const camera = new THREE.OrthographicCamera(
  -cameraSize * aspectRatio,
  cameraSize * aspectRatio,
  cameraSize,
  -cameraSize,
  1,
  1000
);
camera.position.set(0, 0, 500);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Zoom logic (based on mouse wheel and key modifiers)
const zoomSpeed = 0.8;

function onWheel(event) {
  event.preventDefault();

  const rect = renderer.domElement.getBoundingClientRect();
  const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
  const mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;

  const mouseWorld = new THREE.Vector3(mouseX, mouseY, 0).unproject(camera);
  const zoomFactor = 1 - event.deltaY * 0.001 * zoomSpeed;

  const ctrlPressed = event.ctrlKey;
  const shiftPressed = event.shiftKey;

  if (ctrlPressed) {
    camera.left = (camera.left - mouseWorld.x) * zoomFactor + mouseWorld.x;
    camera.right = (camera.right - mouseWorld.x) * zoomFactor + mouseWorld.x;
  } else if (shiftPressed) {
    camera.top = (camera.top - mouseWorld.y) * zoomFactor + mouseWorld.y;
    camera.bottom = (camera.bottom - mouseWorld.y) * zoomFactor + mouseWorld.y;
  } else {
    camera.left = (camera.left - mouseWorld.x) * zoomFactor + mouseWorld.x;
    camera.right = (camera.right - mouseWorld.x) * zoomFactor + mouseWorld.x;
    camera.top = (camera.top - mouseWorld.y) * zoomFactor + mouseWorld.y;
    camera.bottom = (camera.bottom - mouseWorld.y) * zoomFactor + mouseWorld.y;
  }

  camera.updateProjectionMatrix();
}

// Prevent default browser behavior for wheel scrolling with Control or Shift key
window.addEventListener('wheel', onWheel, { passive: false });

// Prevent default context menu on right-click
window.addEventListener('contextmenu', (event) => {
  event.preventDefault();
});

// Render loop
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

// Handle window resize
function onWindowResize() {
  const aspect = window.innerWidth / window.innerHeight;
  camera.left = -cameraSize * aspect;
  camera.right = cameraSize * aspect;
  camera.top = cameraSize;
  camera.bottom = -cameraSize;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onWindowResize);

// Start animation
animate();

// Panning functionality
let isPanning = false;
let isDraggingEndpoint = false;
let selectedEndpointIndex = null;
const previousMousePosition = new THREE.Vector2();

// Raycaster for detecting line interaction
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// Interactive main line code
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
const lineGeometry = new THREE.BufferGeometry().setFromPoints([
  new THREE.Vector3(-50, 0, 0),
  new THREE.Vector3(50, 0, 0)
]);
const line = new THREE.Line(lineGeometry, lineMaterial);
line.frustumCulled = false;
scene.add(line);

// Fractal lines
const lines = []; // Array to hold fractal lines
const maxRanks = 10; // Parameter to control number of ranks

// Recursive function to create fractal lines
function createFractalLines(start, end, currentRank) {
  if (currentRank > maxRanks) return;

  const deltaX = end.x - start.x;
  const deltaY = end.y - start.y;

  // Calculate intermediate points
  const x0_2 = start.x + deltaX * (1 / 3);
  const y0_2 = start.y + deltaY * (1 / 2);

  const x1_2 = end.x - deltaX * (1 / 3);
  const y1_2 = end.y - deltaY * (1 / 2);

  const z = start.z; // Assuming all lines are on the same z-plane

  // Create three new lines for the next rank
  const newLines = [
    new THREE.Line(
      new THREE.BufferGeometry().setFromPoints([start, new THREE.Vector3(x0_2, y0_2, z)]),
      new THREE.LineBasicMaterial({ color: 0xffffff })
    ),
    new THREE.Line(
      new THREE.BufferGeometry().setFromPoints([end, new THREE.Vector3(x1_2, y1_2, z)]),
      new THREE.LineBasicMaterial({ color: 0xffffff })
    ),
    new THREE.Line(
      new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(x0_2, y0_2, z), new THREE.Vector3(x1_2, y1_2, z)]),
      new THREE.LineBasicMaterial({ color: 0xffffff })
    )
  ];

  // Add new lines to the scene and lines array
  newLines.forEach(line => {
    line.frustumCulled = false;
    scene.add(line);
    lines.push(line);
  });

  // Recursively create further ranks for each new line
  newLines.forEach(line => {
    const positions = line.geometry.attributes.position.array;
    const lineStart = new THREE.Vector3(positions[0], positions[1], positions[2]);
    const lineEnd = new THREE.Vector3(positions[3], positions[4], positions[5]);
    createFractalLines(lineStart, lineEnd, currentRank + 1);
  });
}

// Function to update fractal lines
function updateFractalLines() {
  // Remove existing fractal lines from the scene
  lines.forEach(line => {
    scene.remove(line);
    line.geometry.dispose();
    line.material.dispose();
  });
  lines.length = 0; // Clear the lines array

  // Get the updated main line positions
  const positions = lineGeometry.attributes.position.array;
  const lineStart = new THREE.Vector3(positions[0], positions[1], positions[2]);
  const lineEnd = new THREE.Vector3(positions[3], positions[4], positions[5]);

  // Generate new fractal lines starting from the main line
  createFractalLines(lineStart, lineEnd, 2); // Start from rank 2
}

// Initialize fractal lines
updateFractalLines();

// Set the distance for grabbing the line (in world units)
let distanceForGrab = 2;

// Set the raycaster's line precision
raycaster.params.Line.threshold = distanceForGrab;

// Predefine vectors to avoid excessive allocations
const mouseWorld = new THREE.Vector3();
const deltaWorld = new THREE.Vector3();
const currentMouse = new THREE.Vector2();
const previousMouseWorld = new THREE.Vector3();

// Mouse down event
function onMouseDown(event) {
  if (event.button === 2) {
    isPanning = true;
    previousMousePosition.set(event.clientX, event.clientY);
  } else if (event.button === 0) {
    const rect = renderer.domElement.getBoundingClientRect();
    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = - ((event.clientY - rect.top) / rect.height) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObject(line, true);

    if (intersects.length > 0) {
      const positions = lineGeometry.attributes.position.array;
      const lineStart = new THREE.Vector3(positions[0], positions[1], positions[2]);
      const lineEnd = new THREE.Vector3(positions[3], positions[4], positions[5]);

      mouseWorld.copy(intersects[0].point);

      const distanceToStart = mouseWorld.distanceTo(lineStart);
      const distanceToEnd = mouseWorld.distanceTo(lineEnd);

      selectedEndpointIndex = (distanceToStart < distanceToEnd) ? 0 : 1;

      isDraggingEndpoint = true;
      previousMousePosition.set(mouse.x, mouse.y);
    }
  }
}

// Mouse move event
function onMouseMove(event) {
  if (isPanning) {
    const deltaX = event.clientX - previousMousePosition.x;
    const deltaY = event.clientY - previousMousePosition.y;

    const zoomFactorX = camera.right - camera.left;
    const zoomFactorY = camera.top - camera.bottom;

    const panX = (deltaX / window.innerWidth) * zoomFactorX;
    const panY = (deltaY / window.innerHeight) * zoomFactorY;

    camera.left -= panX;
    camera.right -= panX;
    camera.top += panY;
    camera.bottom += panY;
    camera.updateProjectionMatrix();

    previousMousePosition.set(event.clientX, event.clientY);
  } else if (isDraggingEndpoint) {
    const rect = renderer.domElement.getBoundingClientRect();
    currentMouse.set(
      ((event.clientX - rect.left) / rect.width) * 2 - 1,
      - ((event.clientY - rect.top) / rect.height) * 2 + 1
    );

    previousMouseWorld.set(previousMousePosition.x, previousMousePosition.y, 0).unproject(camera);
    mouseWorld.set(currentMouse.x, currentMouse.y, 0).unproject(camera);

    deltaWorld.subVectors(mouseWorld, previousMouseWorld);

    const positions = lineGeometry.attributes.position.array;
    const index = selectedEndpointIndex * 3;

    positions[index] += deltaWorld.x;
    positions[index + 1] += deltaWorld.y;

    // Update main line
    lineGeometry.attributes.position.needsUpdate = true;
    lineGeometry.computeBoundingSphere();

    previousMousePosition.copy(currentMouse);

    // Update fractal lines after changing the main line
    updateFractalLines();
  }
}

// Mouse up event
function onMouseUp(event) {
  if (event.button === 2) {
    isPanning = false;
  } else if (event.button === 0) {
    isDraggingEndpoint = false;
  }
}

// Add event listeners for mouse actions
window.addEventListener('mousedown', onMouseDown);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);

  </script>
</body>
</html>