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