Ripples (code cleanup from fork)
Based on an example for @rupertxrussell by Jurgen
Ripples 003 (variation)
Log in to post a comment.
// Forked from "Ripples 004" by rupertxrussell
// https://turtletoy.net/turtle/5e4b7e6483
// Forked from "Fork: Ripples 002" by Jurgen
// https://turtletoy.net/turtle/9a9e87ac98
// Not my code, just wanted to remove unused methods and expand the Polygon function
// to be able to learn how the clipping works.
// You can find the Turtle API reference here: https://turtletoy.net/syntax
/**
* Cleaned Ripple Scene
* Uses Hidden Line Removal via the Polygons utility.
*/
Canvas.setpenopacity(1);
// --- Parameters ---
const Xspread = 75;
const Yspread = 30;
const frequency = 0.5;
const rippleCount = 8;
const numberOfRaindrops = 256;
// --- Initialization ---
const turtle = new Turtle();
const polygons = new Polygons();
// --- Main Loop ---
for (let i = 0; i < numberOfRaindrops; i++) {
const position = [
getRndInteger(-Xspread, Xspread),
getRndInteger(-Yspread, Yspread)
];
const count = getRndInteger(3, rippleCount);
const growth = getRndInteger(3, frequency);
drawRipples(position, count, growth);
}
// --- Core Functions ---
function drawRipples(position, count, radiusIncrease) {
for (let i = 1; i <= count; i++) {
const radius = i * radiusIncrease;
const p = polygons.create();
// Generate points for the circle and offset them to the ripple position
const points = circlePoints(radius).map(pt => [
pt[0] + position[0],
pt[1] + position[1]
]);
p.addPoints(...points);
p.addOutline();
// This handles the clipping against previously drawn shapes
polygons.draw(turtle, p);
}
}
function getRndInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function circlePoints(radius) {
const steps = Math.floor(radius * (2 * Math.PI) + 1);
const pts = [];
for (let i = 0; i < steps; i++) {
const theta = (i / steps) * (2 * Math.PI);
pts.push([
radius * Math.cos(theta),
radius * Math.sin(theta)
]);
}
return pts;
}
/**
* Polygon Clipping Utility
* Created by Reinder Nijhoff & Lionel Lemarie
*/
function Polygons() {
const polygonList = [];
const gridResolution = 25;
const spatialGrid = Array.from({ length: gridResolution ** 2 }, () => []);
class Polygon {
constructor() {
this.cp = []; // Control Points (the boundary)
this.dp = []; // Drawing Points (the actual lines to be drawn)
this.aabb = []; // Axis-Aligned Bounding Box [minX, minY, maxX, maxY]
}
/**
* Defines the shape's area for clipping purposes.
*/
addPoints(...points) {
let minX = 1e5, maxX = -1e5, minY = 1e5, maxY = -1e5;
this.cp = [...this.cp, ...points];
this.cp.forEach(pt => {
minX = Math.min(minX, pt[0]);
maxX = Math.max(maxX, pt[0]);
minY = Math.min(minY, pt[1]);
maxY = Math.max(maxY, pt[1]);
});
this.aabb = [minX, minY, maxX, maxY];
}
/**
* Converts the boundary points into drawable line segments.
*/
addOutline() {
for (let i = 0, len = this.cp.length; i < len; i++) {
this.dp.push(this.cp[i], this.cp[(i + 1) % len]);
}
}
/**
* Instructs the Turtle to draw the segments that survived clipping.
*/
render(turtle) {
for (let i = 0, len = this.dp.length; i < len; i += 2) {
turtle.jump(this.dp[i]);
turtle.goto(this.dp[i + 1]);
}
}
/**
* Ray-casting algorithm to check if a point is inside this polygon.
*/
isPointInside(point) {
let intersections = 0;
for (let i = 0, len = this.cp.length; i < len; i++) {
if (this.segmentIntersect(point, [0.1, -1000], this.cp[i], this.cp[(i + 1) % len])) {
intersections++;
}
}
return (intersections % 2) === 1;
}
/**
* The Clipping Engine:
* Compares this polygon's lines against another polygon (target).
*/
boolean(target) {
const newDrawingPoints = [];
for (let i = 0, len = this.dp.length; i < len; i += 2) {
const start = this.dp[i];
const end = this.dp[i + 1];
const intersections = [];
// Find all points where our line crosses the target's boundary
for (let j = 0; j < target.cp.length; j++) {
const intersect = this.segmentIntersect(start, end, target.cp[j], target.cp[(j + 1) % target.cp.length]);
if (intersect !== false) intersections.push(intersect);
}
if (intersections.length === 0) {
// If no intersections, keep the line only if it's outside the target
if (!target.isPointInside(start)) {
newDrawingPoints.push(start, end);
}
} else {
// Split the line into segments at intersection points
intersections.push(start, end);
const dx = end[0] - start[0];
const dy = end[1] - start[1];
// Sort points along the line
intersections.sort((a, b) => (a[0] - start[0]) * dx + (a[1] - start[1]) * dy - ((b[0] - start[0]) * dx + (b[1] - start[1]) * dy));
for (let k = 0; k < intersections.length - 1; k++) {
const p1 = intersections[k];
const p2 = intersections[k + 1];
const mid = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2];
// Only keep segments whose midpoint is outside the target
if (!target.isPointInside(mid)) {
newDrawingPoints.push(p1, p2);
}
}
}
}
this.dp = newDrawingPoints;
return this.dp.length > 0;
}
/**
* Standard Line-Segment Intersection math.
*/
segmentIntersect(p1, p2, p3, p4) {
const denominator = (p4[1] - p3[1]) * (p2[0] - p1[0]) - (p4[0] - p3[0]) * (p2[1] - p1[1]);
if (denominator === 0) return false;
const ua = ((p4[0] - p3[0]) * (p1[1] - p3[1]) - (p4[1] - p3[1]) * (p1[0] - p3[0])) / denominator;
const ub = ((p2[0] - p1[0]) * (p1[1] - p3[1]) - (p2[1] - p1[1]) * (p1[0] - p3[0])) / denominator;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
return [p1[0] + ua * (p2[0] - p1[0]), p1[1] + ua * (p2[1] - p1[1])];
}
return false;
}
}
return {
create: () => new Polygon(),
draw: (turtle, currentPoly, shouldStore = true) => {
// 1. Optimization: Only check polygons nearby using the spatial grid
const nearbyPolys = (function(aabb) {
const foundIndices = {};
const cellSize = 200 / gridResolution;
for (let y = 0; y < gridResolution; y++) {
const yPos = y * cellSize - 100;
if (!(aabb[3] < yPos || aabb[1] > yPos + cellSize)) {
for (let x = 0; x < gridResolution; x++) {
const xPos = x * cellSize - 100;
if (!(aabb[0] > xPos + cellSize || aabb[2] < xPos)) {
spatialGrid[x + y * gridResolution].forEach(idx => {
const other = polygonList[idx];
if (!(aabb[3] < other.aabb[1] || aabb[1] > other.aabb[3] || aabb[0] > other.aabb[2] || aabb[2] < other.aabb[0])) {
foundIndices[idx] = 1;
}
});
}
}
}
}
return Object.keys(foundIndices).map(idx => polygonList[idx]);
})(currentPoly.aabb);
// 2. Clip the current polygon against all nearby existing ones
for (let i = 0; i < nearbyPolys.length; i++) {
if (!currentPoly.boolean(nearbyPolys[i])) break;
}
// 3. Draw the resulting lines
currentPoly.render(turtle);
// 4. Store the polygon in the list and spatial grid for future clipping
if (shouldStore) {
polygonList.push(currentPoly);
const idx = polygonList.length - 1;
const cellSize = 200 / gridResolution;
spatialGrid.forEach((cell, i) => {
const x = (i % gridResolution) * cellSize - 100;
const y = Math.floor(i / gridResolution) * cellSize - 100;
if (!(x + cellSize < currentPoly.aabb[0] || x > currentPoly.aabb[2] || y + cellSize < currentPoly.aabb[1] || y > currentPoly.aabb[3])) {
cell.push(idx);
}
});
}
}
};
}