Lines in Tornado

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