Could i adapt this code to add a parameter to change how many ranks of the fractal are made? at the moment only 2 ranks are being made. 3 ranks would make the pattern on rank 3 under the rank 2 lines.



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

// Rank 2 lines
const line1_geometry = new THREE.BufferGeometry();
const line1 = new THREE.Line(line1_geometry, lineMaterial);
scene.add(line1);

const line2_geometry = new THREE.BufferGeometry();
const line2 = new THREE.Line(line2_geometry, lineMaterial);
scene.add(line2);

const line3_geometry = new THREE.BufferGeometry();
const line3 = new THREE.Line(line3_geometry, lineMaterial);
scene.add(line3);

// Function to update rank 2 lines based on the main line
function updateRank2Lines() {
  const positions = lineGeometry.attributes.position.array;

  const x0 = positions[0], y0 = positions[1], z0 = positions[2];
  const x1 = positions[3], y1 = positions[4], z1 = positions[5];

  const deltaX = x1 - x0;
  const deltaY = y1 - y0;

  // First line's second point (from left endpoint)
  const x0_2 = x0 + deltaX * (1/3);
  const y0_2 = y0 + deltaY * (1/2);

  // Second line's second point (from right endpoint)
  const x1_2 = x1 - deltaX * (1/3);
  const y1_2 = y1 - deltaY * (1/2);

  // Update line1 (from (x0, y0) to (x0_2, y0_2))
  const line1_positions = new Float32Array([x0, y0, z0, x0_2, y0_2, z0]);
  line1_geometry.setAttribute('position', new THREE.BufferAttribute(line1_positions, 3));
  line1_geometry.attributes.position.needsUpdate = true;
  line1_geometry.computeBoundingSphere();

  // Update line2 (from (x1, y1) to (x1_2, y1_2))
  const line2_positions = new Float32Array([x1, y1, z1, x1_2, y1_2, z1]);
  line2_geometry.setAttribute('position', new THREE.BufferAttribute(line2_positions, 3));
  line2_geometry.attributes.position.needsUpdate = true;
  line2_geometry.computeBoundingSphere();

  // Update line3 (connecting the two computed points)
  const line3_positions = new Float32Array([x0_2, y0_2, z0, x1_2, y1_2, z1]);
  line3_geometry.setAttribute('position', new THREE.BufferAttribute(line3_positions, 3));
  line3_geometry.attributes.position.needsUpdate = true;
  line3_geometry.computeBoundingSphere();
}

// Call the function once to initialize the rank 2 lines
updateRank2Lines();

// 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 rank 2 lines after changing the main line
    updateRank2Lines();
  }
}

// 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>