Bouncy lines ๐Ÿฆ˜

A burst of lines get spawned in a circle. They move in a direction and bounce off the sides and other lines for a few times.

I vibe coded this turtle ๐Ÿ˜

Log in to post a comment.

const circleRadius = 75; // min=20 max=80 step=5 Circle size for spawn area
const numLines = 600; // min=50 max=1000 step=1 Number of bouncing lines
const maxBounces = 25; // min=1 max=50 step=1 Maximum bounces before stopping
const angleSnap = 8; // min=1 max=16 step=1 Number of angle divisions (1=no snap, 8=45ยฐ increments)
const startAngleRandom = 0.2; // min=0 max=2 step=0.01
const moveDistance = 4; // min=1 max=5 step=0.1 Movement speed per step
const areaSize = 90; // min=70 max=120 step=10 Canvas area size
const maxIterations = 100000;

const lines = [];
const allSegments = [];

function lineSegmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
    var a_dx = x2 - x1;
    var a_dy = y2 - y1;
    var b_dx = x4 - x3;
    var b_dy = y4 - y3;
    var denominator = (-b_dx * a_dy + a_dx * b_dy);
    
    if (Math.abs(denominator) < 1e-10) return false; // Lines are parallel
    
    var s = (-a_dy * (x1 - x3) + a_dx * (y1 - y3)) / denominator;
    var t = (+b_dx * (y1 - y3) - b_dy * (x1 - x3)) / denominator;
    
    if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
        let intersectionX = x1 + t * a_dx;
        let intersectionY = y1 + t * a_dy;
        
        // Calculate reflection angle
        let wallDx = x4 - x3;
        let wallDy = y4 - y3;
        let wallLength = Math.sqrt(wallDx * wallDx + wallDy * wallDy);
        
        if (wallLength > 0) {
            // Normal vector to the wall (perpendicular)
            let nx = -wallDy / wallLength;
            let ny = wallDx / wallLength;
            
            // Incoming velocity
            let vx = a_dx;
            let vy = a_dy;
            
            // Reflect velocity: v' = v - 2(vยทn)n
            let dot = 2 * (vx * nx + vy * ny);
            let reflectedVx = vx - dot * nx;
            let reflectedVy = vy - dot * ny;
            
            let reflectedAngle = Math.atan2(reflectedVy, reflectedVx);
            
            return {
                x: intersectionX,
                y: intersectionY,
                angle: reflectedAngle
            };
        }
    }
    return false;
}

class BouncingLine {
    constructor(x, y, angle, isStatic = false) {
        this.turtle = new Turtle();
        this.x = x;
        this.y = y;
        this.baseAngle = angle;
        this.angle = angle;
        this.bounceCount = 0;
        this.isStatic = isStatic;
        this.active = !isStatic;
        this.segments = [];
        
        this.turtle.jump(x, y);
    }
    
    addSegment(x1, y1, x2, y2) {
        let segment = {x1, y1, x2, y2, line: this};
        this.segments.push(segment);
        allSegments.push(segment);
        if (!this.isStatic) 
            this.turtle.goto(x2, y2);
    }
    
    move() {
        if (!this.active) return;
        if (this.x < -areaSize || this.y < -areaSize || this.x > areaSize || this.y > areaSize) {
            this.active = false;
            return;
        }
        
        let startX = this.x;
        let startY = this.y;
        
        let endX = this.x + Math.cos(this.angle) * moveDistance;
        let endY = this.y + Math.sin(this.angle) * moveDistance;
        
        let closestIntersection = null;
        let closestDistance = Infinity;
        
        for (let segment of allSegments) {
            // avoid self-intersection
            if (segment.line === this && segment === this.segments.at(-1)) continue;
    
            let intersection = lineSegmentsIntersect(
                startX, startY, endX, endY,
                segment.x1, segment.y1, segment.x2, segment.y2
            );
            
            if (intersection) {
                let distance = Math.sqrt(
                    (intersection.x - startX) ** 2 + 
                    (intersection.y - startY) ** 2
                );
                
                if (distance < closestDistance && distance > 0.01) {
                    closestDistance = distance;
                    closestIntersection = intersection;
                }
            }
        }
        
        if (closestIntersection) {
            // on hit something - draw to intersection point and bounce
            this.addSegment(startX, startY, closestIntersection.x, closestIntersection.y);
            
            this.x = closestIntersection.x;
            this.y = closestIntersection.y;
            
            this.angle = snapAngle(closestIntersection.angle + this.baseAngle, angleSnap) - this.baseAngle + (0.15 + Math.random() * 0.3);
            this.baseAngle = snapAngle(this.baseAngle, angleSnap);
            
            this.bounceCount++;
            if (this.bounceCount >= maxBounces) {
                this.active = false;
            }
        } else {
            // No collision - move normally
            this.addSegment(startX, startY, endX, endY);
            this.x = endX;
            this.y = endY;
        }
    }
}

function snapAngle(angle, angleSnap = 0) {
    if (angleSnap > 1) {
        const snapIncrement = (Math.PI * 2) / angleSnap;
        return Math.round(angle / snapIncrement) * snapIncrement;
    }
    return angle;
}

function createBoundaries() {
    let boundary = new BouncingLine(-areaSize, -areaSize, 0, true);
    boundary.addSegment(-areaSize, -areaSize, areaSize, -areaSize);
    boundary.addSegment(areaSize, -areaSize, areaSize, areaSize);
    boundary.addSegment(areaSize, areaSize, -areaSize, areaSize);
    boundary.addSegment(-areaSize, areaSize, -areaSize, -areaSize);
    lines.push(boundary);
}

function addLine(i, circlePos) {
    let angle = (Math.PI * 2 * i) / numLines + Math.random() * 0.5;
    let radius = (Math.random()**0.5) * circleRadius;
    
    let x = circlePos[0] + Math.cos(angle) * radius;
    let y = circlePos[1] + Math.sin(angle) * radius;
    
    x = Math.max(-areaSize + 5, Math.min(areaSize - 5, x));
    y = Math.max(-areaSize + 5, Math.min(areaSize - 5, y));
    
    let direction = (i / numLines) * Math.PI * 2;
    direction = snapAngle(direction, angleSnap) + (-startAngleRandom + Math.random() * startAngleRandom);
    
    lines.push(new BouncingLine(x, y, direction));
}

const circlePos = [
    -areaSize/2 + Math.random() * areaSize, 
    -areaSize/2 + Math.random() * areaSize
];

/*
// Draw spawn circle (visual reference)
const circleTurtle = new Turtle();
circleTurtle.jump(circlePos[0] , circlePos[1] - circleRadius);
circleTurtle.circle(circleRadius);
*/
createBoundaries();

function walk(i) {
    let c = 20;
    while (c-- > 0 && lines.length < numLines) {
        addLine(lines.length, circlePos);
    }
        
    let activeCount = 0;
    for (let line of lines) {
        if (line.active) {
            line.move();
            activeCount++;
        }
    }
    
    return activeCount > 0 && i < maxIterations;
}