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