Contents in this wiki are for entertainment purposes only
This is not fiction ∞ this is psience of mind

P5js Visual Torus Knot

From Catcliffe Development
Revision as of 09:58, 18 April 2025 by XenoEngineer (talk | contribs) (Created page with "<pre style="margin-left:3em; font:normal 14px terminal;">/* * Torus Knot Phase Array Visualizer – Enhanced Version * * This sketch draws an array of half-knots derived from a standard torus knot * parametrization using the correct roles of p and q. A background torus can be * toggled on/off to put the visualization in context. * * Improvements: * - Added line thickness control * - Added ability to control how much of the knot is rendered (knotFraction) * - A...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
/*
 * Torus Knot Phase Array Visualizer – Enhanced Version
 *
 * This sketch draws an array of half-knots derived from a standard torus knot
 * parametrization using the correct roles of p and q. A background torus can be
 * toggled on/off to put the visualization in context.
 * 
 * Improvements:
 * - Added line thickness control
 * - Added ability to control how much of the knot is rendered (knotFraction)
 * - Added interactive controls via sliders
 * - Improved performance handling
 * - Added option to show/hide the background torus
 */

const PHI = (1 + Math.sqrt(5)) / 2;

const CONFIG = {
  // Torus profile
  R: Math.pow(PHI, 4),            // Major radius (set by Φ⁴)
  r: Math.pow(PHI, 4) - 1,        // Tube radius (balanced proportion)
  scale: 25,                      // Scaling factor for a human-viewable size

  // Knot Parameters – The Actors
  p: 13,                          // Primary winding number
  q: 8,                           // Secondary winding number
  steps: 2400,                    // Resolution for sampling the curve
  lineWeight: 2,                  // Line thickness

  // Rendering Control
  knotFraction: 1.0,              // Fraction of knot to render (0 to 1)

  // Phase Configuration – The Choreography
  phases: 3,                      // How many phase copies to display

  // Visual Guides
  showTorus: false,               // Whether to show the background torus
  halfKnotColors: {
    first: '#FF3366',             // Color for the first half-knot
    second: '#3366FF',            // Color for the second half-knot
    terminals: '#44FF00'          // Terminal points accent
  }
};

// UI Elements
let pSlider, qSlider, phasesSlider, knotFractionSlider, lineWeightSlider, torusCheckbox;
let pInput, qInput, phasesInput, knotFractionInput, lineWeightInput;

// Torus knot parametrization:
// For t in [0, 1], the curve is traced by p rotations around and q rotations along the tube.
function torusKnot(t, p, q) {
  const theta = 2 * Math.PI * p * t;  // p full rotations (longitudinal)
  const phi   = 2 * Math.PI * q * t;  // q rotations (meridional)
  const x = (CONFIG.R + CONFIG.r * Math.cos(phi)) * Math.cos(theta);
  const y = (CONFIG.R + CONFIG.r * Math.cos(phi)) * Math.sin(theta);
  const z = CONFIG.r * Math.sin(phi);
  return { x: x * CONFIG.scale, y: y * CONFIG.scale, z: z * CONFIG.scale };
}

// Generate a half-knot segment with an additional phase rotation.
function drawHalfKnot(phase, isFirstHalf) {
  const start = isFirstHalf ? 0   : 0.5;
  const end   = isFirstHalf ? 0.5 : 1;
  
  // Apply knotFraction to scale the end point
  const scaledEnd = start + (end - start) * CONFIG.knotFraction;
  
  const phaseOffset = (phase / CONFIG.phases) * 2 * Math.PI;
  
  let points = [];
  
  // Calculate the number of steps needed for the fraction of the knot
  const stepsNeeded = isFirstHalf ? 
    Math.floor((CONFIG.steps / 2) * CONFIG.knotFraction) : 
    Math.floor((CONFIG.steps / 2) * CONFIG.knotFraction);
  
  for (let i = 0; i <= stepsNeeded; i++) {
    const t_local = start + (scaledEnd - start) * (i / stepsNeeded);
    const point = torusKnot(t_local, CONFIG.p, CONFIG.q);
    // Rotate the point in the XY plane to distribute half-knots around the circle.
    const rotated = {
      x: point.x * Math.cos(phaseOffset) - point.y * Math.sin(phaseOffset),
      y: point.x * Math.sin(phaseOffset) + point.y * Math.cos(phaseOffset),
      z: point.z
    };
    points.push(rotated);
  }
  return points;
}

// Draw the curve using p5.js vertex commands.
function drawPath(points, col) {
  stroke(col);
  strokeWeight(CONFIG.lineWeight);
  noFill();
  beginShape();
  for (let pt of points) {
    vertex(pt.x, pt.y, pt.z);
  }
  endShape();
}

// Mark the starting and ending positions of a half-knot.
function markTerminals(start, end) {
  push();
  fill(CONFIG.halfKnotColors.terminals);
  noStroke();
  // Small spheres at the terminal points.
  push();
  translate(start.x, start.y, start.z);
  sphere(4);
  pop();
  
  // Only draw end sphere if we're showing the full knot
  if (CONFIG.knotFraction > 0.99) {
    push();
    translate(end.x, end.y, end.z);
    sphere(4);
    pop();
  }
  pop();
}

// Draw all phase copies of the two half-knots and mark their endpoints.
function visualize() {
  for (let phase = 0; phase < CONFIG.phases; phase++) {
    const firstHalf = drawHalfKnot(phase, true);
    if (firstHalf.length > 0) {
      drawPath(firstHalf, CONFIG.halfKnotColors.first);
      
      // Mark starting point
      markTerminals(firstHalf[0], firstHalf[firstHalf.length - 1]);
    }
    
    const secondHalf = drawHalfKnot(phase, false);
    if (secondHalf.length > 0) {
      drawPath(secondHalf, CONFIG.halfKnotColors.second);
      
      // Mark terminal points
      markTerminals(secondHalf[0], secondHalf[secondHalf.length - 1]);
    }
  }
}

function setupUI() {
  const uiY = 20;
  const spacing = 40;
  const labelWidth = 100;
  const sliderWidth = 120;
  const inputWidth = 50;
  const padding = 10;
  
  // Create sliders and text fields for interactive control
  // p value
  createP('p value:').position(10, uiY).style('color', 'white');
  pSlider = createSlider(1, 20, CONFIG.p, 0.1);
  pSlider.position(labelWidth, uiY);
  pSlider.style('width', sliderWidth + 'px');
  pSlider.input(() => { pInput.value(pSlider.value()); });
  
  pInput = createInput(CONFIG.p.toString());
  pInput.position(labelWidth + sliderWidth + padding, uiY);
  pInput.style('width', inputWidth + 'px');
  pInput.input(() => { 
    const val = parseFloat(pInput.value());
    if (!isNaN(val) && val >= 1 && val <= 20) {
      pSlider.value(val);
    }
  });
  
  // q value
  createP('q value:').position(10, uiY + spacing).style('color', 'white');
  qSlider = createSlider(1, 20, CONFIG.q, 0.1);
  qSlider.position(labelWidth, uiY + spacing);
  qSlider.style('width', sliderWidth + 'px');
  qSlider.input(() => { qInput.value(qSlider.value()); });
  
  qInput = createInput(CONFIG.q.toString());
  qInput.position(labelWidth + sliderWidth + padding, uiY + spacing);
  qInput.style('width', inputWidth + 'px');
  qInput.input(() => { 
    const val = parseFloat(qInput.value());
    if (!isNaN(val) && val >= 1 && val <= 20) {
      qSlider.value(val);
    }
  });
  
  // Phases
  createP('Phases:').position(10, uiY + spacing * 2).style('color', 'white');
  phasesSlider = createSlider(1, 8, CONFIG.phases, 1);
  phasesSlider.position(labelWidth, uiY + spacing * 2);
  phasesSlider.style('width', sliderWidth + 'px');
  phasesSlider.input(() => { phasesInput.value(phasesSlider.value()); });
  
  phasesInput = createInput(CONFIG.phases.toString());
  phasesInput.position(labelWidth + sliderWidth + padding, uiY + spacing * 2);
  phasesInput.style('width', inputWidth + 'px');
  phasesInput.input(() => { 
    const val = parseInt(phasesInput.value());
    if (!isNaN(val) && val >= 1 && val <= 8) {
      phasesSlider.value(val);
    }
  });
  
  // Knot Fraction
  createP('Knot Fraction:').position(10, uiY + spacing * 3).style('color', 'white');
  knotFractionSlider = createSlider(0.01, 1, CONFIG.knotFraction, 0.01);
  knotFractionSlider.position(labelWidth, uiY + spacing * 3);
  knotFractionSlider.style('width', sliderWidth + 'px');
  knotFractionSlider.input(() => { knotFractionInput.value(knotFractionSlider.value()); });
  
  knotFractionInput = createInput(CONFIG.knotFraction.toString());
  knotFractionInput.position(labelWidth + sliderWidth + padding, uiY + spacing * 3);
  knotFractionInput.style('width', inputWidth + 'px');
  knotFractionInput.input(() => { 
    const val = parseFloat(knotFractionInput.value());
    if (!isNaN(val) && val >= 0.01 && val <= 1) {
      knotFractionSlider.value(val);
    }
  });
  
  // Line Weight
  createP('Line Weight:').position(10, uiY + spacing * 4).style('color', 'white');
  lineWeightSlider = createSlider(1, 5, CONFIG.lineWeight, 0.1);
  lineWeightSlider.position(labelWidth, uiY + spacing * 4);
  lineWeightSlider.style('width', sliderWidth + 'px');
  lineWeightSlider.input(() => { lineWeightInput.value(lineWeightSlider.value()); });
  
  lineWeightInput = createInput(CONFIG.lineWeight.toString());
  lineWeightInput.position(labelWidth + sliderWidth + padding, uiY + spacing * 4);
  lineWeightInput.style('width', inputWidth + 'px');
  lineWeightInput.input(() => { 
    const val = parseFloat(lineWeightInput.value());
    if (!isNaN(val) && val >= 1 && val <= 5) {
      lineWeightSlider.value(val);
    }
  });
  
  // Create checkbox for showing/hiding the torus
  createP('Show Torus:').position(10, uiY + spacing * 5).style('color', 'white');
  torusCheckbox = createCheckbox('', CONFIG.showTorus);
  torusCheckbox.position(labelWidth, uiY + spacing * 5);
  torusCheckbox.style('color', 'white');
}

function updateConfigFromUI() {
  // Get values from sliders (which are synced with input fields)
  CONFIG.p = parseFloat(pSlider.value());
  CONFIG.q = parseFloat(qSlider.value());
  CONFIG.phases = parseInt(phasesSlider.value());
  CONFIG.knotFraction = parseFloat(knotFractionSlider.value());
  CONFIG.lineWeight = parseFloat(lineWeightSlider.value());
  CONFIG.showTorus = torusCheckbox.checked();
  
  // Update input fields to show current values (in case they were changed by slider)
  pInput.value(CONFIG.p);
  qInput.value(CONFIG.q);
  phasesInput.value(CONFIG.phases);
  knotFractionInput.value(CONFIG.knotFraction);
  lineWeightInput.value(CONFIG.lineWeight);
}

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  setupUI();
}

function draw() {
  background(0);
  updateConfigFromUI();
  
  // Display current p:q ratio with precise values
  push();
  textSize(16);
  fill(255);
  text(`Torus Knot (${CONFIG.p.toFixed(2)}:${CONFIG.q.toFixed(2)}) - ${(CONFIG.knotFraction * 100).toFixed(1)}% rendered`, -width/2 + 10, -height/2 + 20);
  pop();
  
  // Position the 3D view in the center-right of the screen
  translate(100, 0, 0);
  
  orbitControl(); // Allows interactive rotation with mouse input.
  
  // A subtle continuous rotation of the entire scene.
  rotateY(frameCount * 0.00015);
  
  // Render a background torus if enabled
  if (CONFIG.showTorus) {
    push();
    noFill();
    stroke(55, 45, 55);
    strokeWeight(0.5);
    // Adjust the parameters to match our CONFIG scale.
    // Note: p5.js torus() expects: torus(radius, tubeRadius, detailX, detailY)
    torus(CONFIG.R * CONFIG.scale, CONFIG.r * CONFIG.scale, 24, 12);
    pop();
  }
  
  // Draw the torus knot phase array.
  visualize();
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}