- Contents in this wiki are for entertainment purposes only
P5js Visual Torus Knot
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); }