Lines are simulated to move within a swirling force field, creating a dynamic and visually pattern. Each line is influenced by the swirling forces, leading to a unique and organic arrangement of patterns in every iteration of the piece.
Feel free to adjust the parameters yourself!
Log in to post a comment.
// Online example: https://turtletoy.net/turtle/f4cfd82958 // You can find the Turtle API reference here: https://turtletoy.net/syntax // Global code will be evaluated once. // Total steps for simulation const totalSimulationSteps = 2000; // min=0 max=4000 step = 10 // Total amount of lines to draw const lineAmount = 6000; // min=1 max=15000 step = 10 // Number of lines drawn per batch const batchEachWalk = 20; // min=1 max=105 step = 5 // Opacity of the drawing const drawingOpacity = -0.2; // min=-1.0 max=1.0 step = 0.05 // Radius for circle calculations const circleRadius = 72; // min=1 max=105 step = 1 // Flag to decide if background is drawn const bDrawBackground = 0; // min=0 max=1 step = 1 Canvas.setpenopacity(drawingOpacity); // Set the opacity for drawing const turtle = new Turtle(); // Initialize the Turtle object // Utility function to draw a line function line(x1, y1, x2, y2) { turtle.penup(); turtle.goto(x1, y1); turtle.pendown(); turtle.goto(x2, y2); } // Utility function to draw a square function square(x, y, size) { let sz = size * 0.5; line(x - sz, y - sz, x + sz, y - sz); line(x + sz, y - sz, x + sz, y + sz); line(x + sz, y + sz, x - sz, y + sz); line(x - sz, y + sz, x - sz, y - sz); } // Function to create a random point outside a circle function randomPointOutsideCircle(canvasSize, r) { const halfCanvasSize = canvasSize * 0.5; while (true) { const x = randomInRange(-halfCanvasSize, halfCanvasSize); const y = randomInRange(-halfCanvasSize, halfCanvasSize); if (Math.sqrt(x * x + y * y) >= r) { return { x, y }; } } } const canvasSize = 256; // Size of the canvas const squareSize = 20; // Size of the square // Function to draw a random rectangle function drawRandomRect() { const coord = randomPointOutsideCircle(canvasSize, circleRadius * 1.2); square(coord.x, coord.y, squareSize); } const forceScale = 0.5; // Scale of the force applied // Calculate the number of walks const numWalks = lineAmount / batchEachWalk; // Define rotation centers const rotationCenters = [{ x: 0, y: 0 }]; // Particle class definition class Particle { constructor(x, y) { this.position = { x: x, y: y }; this.oldPosition = { x: x, y: y }; this.bFree = true; // Flag to determine if the particle is free } // Method to get the velocity of a particle getVelocity() { return { x: this.position.x - this.oldPosition.x, y: this.position.y - this.oldPosition.y }; } } // Ribbon class definition class Ribbon { constructor(startX, startY, numSegments, segmentLength, simulationSteps, constraintIterations) { this.particles = []; // Array to hold particles this.numSegments = numSegments; this.segmentLength = segmentLength; this.simulationSteps = simulationSteps; this.constraintIterations = constraintIterations; // Initialize particle positions let l = Math.sqrt(startX * startX + startY * startY); let dx = startX / l; let dy = startY / l; for (let i = 0; i <= numSegments; i++) { this.particles.push(new Particle(startX + i * dx * segmentLength, startY + i * dy * segmentLength)); } this.particles[0].bFree = false; // First particle is fixed } // Method to calculate external force on a particle calculateExternalForce(particle) { let force = { x: 0, y: 0 }; let closestCenter = null; let closestDistance = Infinity; // Find the closest rotation center for (let center of rotationCenters) { let dx = particle.position.x - center.x; let dy = particle.position.y - center.y; let distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestCenter = center; } } // Calculate force based on the closest center if (closestCenter) { let dx = particle.position.x - closestCenter.x; let dy = particle.position.y - closestCenter.y; force.x = -forceScale * dy; force.y = forceScale * dx; } return force; } // Verlet integration for particle movement verletIntegrate(dt) { const substepTime = 0.01; const substepTimeSqr = dt * dt; for (let particle of this.particles) { if (particle.bFree) { let tempPos = { ...particle.position }; let velocity = particle.getVelocity(); let externalForce = this.calculateExternalForce(particle); particle.position.x += velocity.x + externalForce.x * substepTimeSqr; particle.position.y += velocity.y + externalForce.y * substepTimeSqr; particle.oldPosition = tempPos; } } } // Solve distance constraints between particles solveDistanceConstraint(p1, p2, desiredDistance) { let dx = p2.position.x - p1.position.x; let dy = p2.position.y - p1.position.y; let currentDistance = Math.sqrt(dx * dx + dy * dy); let difference = desiredDistance - currentDistance; let normalizedDX = dx / currentDistance; let normalizedDY = dy / currentDistance; let offsetX = normalizedDX * difference; let offsetY = normalizedDY * difference; if (p1.bFree && p2.bFree) { p1.position.x -= offsetX * 0.5; p1.position.y -= offsetY * 0.5; p2.position.x += offsetX * 0.5; p2.position.y += offsetY * 0.5; } else if (p1.bFree) { p1.position.x -= offsetX; p1.position.y -= offsetY; } else if (p2.bFree) { p2.position.x += offsetX; p2.position.y += offsetY; } } // Solve all constraints for the ribbon solveConstraints() { for (let iterationIdx = 0; iterationIdx < this.constraintIterations; iterationIdx++) { for (let i = 0; i < this.numSegments; i++) { this.solveDistanceConstraint(this.particles[i], this.particles[i + 1], this.segmentLength); } } } // Simulate the movement of the ribbon simulate() { for (let step = 0; step < this.simulationSteps; step++) { this.verletIntegrate(0.001); this.solveConstraints(); } } } // Function to get a random point within a circle function randomPointInCircle(R) { let r = Math.sqrt(Math.random()) * R; // Uniform distribution let theta = Math.random() * 2 * Math.PI; let x = r * Math.cos(theta); let y = r * Math.sin(theta); return { x, y }; } // Create a random ribbon function randomRibbon() { let point = randomPointInCircle(circleRadius); let startX = point.x; let startY = point.y; let numSegments = Math.floor(20 * Math.random()) + 20; let segmentLength = 0.1 * Math.random() + 0.5; let simulationSteps = totalSimulationSteps; let constraintIterations = 5; return new Ribbon(startX, startY, numSegments, segmentLength, simulationSteps, constraintIterations); } // Simulate and draw each ribbon function walk(i) { for (let k = 0; k < batchEachWalk; k++) { let ribbon = randomRibbon(); ribbon.simulate(); for (let i = 0; i <= ribbon.numSegments; i++) { if (i < ribbon.numSegments) { turtle.penup(); turtle.goto(ribbon.particles[i].position.x, ribbon.particles[i].position.y); turtle.pendown(); turtle.goto(ribbon.particles[i + 1].position.x, ribbon.particles[i + 1].position.y); } } if (bDrawBackground) { drawRandomRect(); } } return i < numWalks; }