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