Lines in a Tornado

Lines in a swirl force field
I let the parameters go too far. The original piece is without background canvas.

Log in to post a comment.

// You can find the Turtle API reference here: https://turtletoy.net/syntax


const totalSimulationSteps = 2000; // min=0 max=4000 step = 10
const lineAmount = 6000;//  min=1 max=15000 step = 10
const batchEachWalk = 20;//  min=1 max=105 step = 5
const drawingOpacity = -0.2; // min=-1.0 max=1.0 step = 0.05
const circleRadius = 72;//  min=1 max=105 step = 1
const bDrawBackground = 0;//  min=0 max=1 step = 1

Canvas.setpenopacity(drawingOpacity);

// Global code will be evaluated once.
const turtle = new Turtle();

function line(x1,y1,x2,y2){turtle.penup();turtle.goto(x1,y1);turtle.pendown();turtle.goto(x2,y2);}
function quad(left,right,top,bottom){line(left,top,left,bottom);line(left,bottom,right,bottom);line(right,bottom,right,top);line(right,top,left,top);}
function square(x,y,size){let sz = size*0.5;quad(x-sz,x+sz,y-sz,y+sz);}
function circle(x,y,radius,extent=undefined){turtle.penup();turtle.goto(x,y);turtle.pendown();turtle.circle(radius,extent);}
// function InCircle(x,y,cx,cy,radius){let dx = x - cx;let dy = y - cy;return dx*dx + dy*dy <= radius*radius;}
// function Halton(index, base){let result = 0;let invBase = 1.0 / base;let frac = invBase;while(index>0){result += (index%base)*frac;index /= base;frac *= invBase;}return result;}
// function Halton2D(index,base1,base2,range){let HaltonX = Halton(index,base1)-0.5;let HaltonY = Halton(index,base2)-0.5;let x = HaltonX * range;let y = HaltonY * range;return [x,y];}
function randomInRange(min, max) {return Math.random() * (max - min) + min;}

function randomPointOutsideCircle(canvasSize, r) {
    const halfCanvasSize = canvasSize *0.5;
    while (true) {
        const x = randomInRange(-halfCanvasSize, halfCanvasSize);
        const y = randomInRange(-halfCanvasSize, halfCanvasSize);
        const distance = Math.sqrt(x*x + y*y);

        if (distance >= r) {
            return { x:x, y:y };
        }
    }
}

const s = 10;  // Side length of the square
const r = 4;   // Radius of the circle
const point = randomPointOutsideCircle(s, r);
console.log(point);

const canvasSize = 256;

const squareSize = 20;

function drawRandomRect( ){
    const coord = randomPointOutsideCircle(canvasSize, circleRadius*1.2);
    square(coord.x,coord.y,squareSize);
}

const forceScale = 0.5;

const numWalks = lineAmount/batchEachWalk;

const rotationCenters = [
    { x: 0, y: 0 }
];

// Define the Particle class
class Particle {
    constructor(x, y) {
        this.position = { x: x, y: y };
        this.oldPosition = { x: x, y: y };
        this.bFree = true;
    }
    
    getVelocity() {
        return {
            x: this.position.x - this.oldPosition.x,
            y: this.position.y - this.oldPosition.y
        };
    }
}

class Ribbon {
    constructor(startX, startY,  numSegments, segmentLength, simulationSteps,constraintIterations) {
        this.particles = [];
        this.numSegments = numSegments;
        this.segmentLength = segmentLength;
        this.simulationSteps = simulationSteps;
        this.constraintIterations = constraintIterations
        
        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;
    }
    
    
    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;
            
    }


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

    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;
        }
    }
    
    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() {
        for(let step = 0; step < this.simulationSteps; step++)
        {
            this.verletIntegrate(0.001)
            this.solveConstraints()
        }
    }
}

function randomPointInCircle(R) {
    let r = Math.sqrt(Math.random()) * R; // Square root ensures uniform distribution
    let theta = Math.random() * 2 * Math.PI;
    let x = r * Math.cos(theta);
    let y = r * Math.sin(theta);
    return { x, y };
}

// Randomize parameters within a legitimate range
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);
}

// draw one 'simulated' line each walk
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;
}