Curtain opening

Cloth physics. Set the iterations to max and export to a 10-second GIF for the full animation.

Log in to post a comment.

// LL 2021

const density = 30; // min=2 max=50 step=1
const gravity = 0.01; /// min=0 max=1 step=0.001

const offset_y = 60;
const scale = 14; /// min=1 max=50 step=1

const style = 1; // min=0 max=1 step=1 (Polygons (fast),Polygons (slow))

const string_link_count = 40;

const thickness = 4; /// min=1 max=100 step=1
const iterations = 128; // min=0 max=500 step=1

Canvas.setpenopacity(1);

const turtle = new Turtle();

var polygons = null;

var particles = [];
var things_to_draw = [];

var iterations_t = iterations;
var iteration=0;

console.clear();

class Particle {
    constructor(x, y, px, py, lock_y, floor=0) {
        this.x = x;
        this.y = y;
        this.px = px;
        this.py = py;
        this.lock_y = lock_y;
        this.floor = floor;
    }
    
    update() {
        var dx = this.x - this.px, dy = this.y - this.py;
        this.px = this.x; this.py = this.y;

        // Verlet
        const friction = 0.99;
        dx *= friction;
        dy *= friction;
        
        dy += gravity;
        
        this.x += dx;
        if (!this.lock_y) this.y += dy;
        
        if (this.y > this.floor - this.radius) {
            this.y = this.floor - this.radius;
            this.px = this.x - (this.x - this.px) * 0.5;
        }
    }
}

function drawPoints(points, shaded=false) {
    if (style == 0) {
        turtle.jump(points[points.length-1]);
        points.forEach(p=>turtle.goto(p));
    } else {
        const p1 = polygons.create();
        p1.addPoints(...points);
        if (shaded) p1.addHatching(-Math.PI/4, 2);
        p1.addOutline();
        polygons.draw(turtle, p1, true);
    }
}

class Quad {
    constructor(i1, i2, i3, i4) {
        this.indices = [i1, i2, i3, i4];
    }
    
    draw() {
        var points = [];
        this.indices.forEach(i => points.push([particles[i].x*scale, particles[i].y*scale+offset_y]));
        drawPoints(points);
    }
}

class Spring {
    constructor(index1, index2) {
        this.index1 = index1;
        this.index2 = index2;
        this.length = Math.hypot(particles[index1].x - particles[index2].x, particles[index1].y - particles[index2].y);
    }
    
    update() {
        var x1 = particles[this.index1].x;
        var y1 = particles[this.index1].y;
        var x2 = particles[this.index2].x;
        var y2 = particles[this.index2].y;
        const l = Math.max(0.01, Math.hypot(x1-x2, y1-y2));
        const diff = this.length - l;
        if (diff<0) {
            const strength = 0.1;
            particles[this.index1].x += (x1 - x2) / l * diff * strength;
            if (!particles[this.index1].lock_y) particles[this.index1].y += (y1 - y2) / l * diff * strength;
            particles[this.index2].x += (x2 - x1) / l * diff * strength;
            if (!particles[this.index2].lock_y) particles[this.index2].y += (y2 - y1) / l * diff * strength;
        }
    }
}

class Curtain {
    constructor(ox, oy, width, height) {
        this.springs = [];
        this.particles = [];
        this.quads = [];

        const first_particle = particles.length;
        
        const density_w=density, density_h=2*density;

        for (var dy=0; dy<density_h; dy++) {
            for (var dx=0; dx<density_w; dx++) {
                var x = ox + width / (density_w-1) * dx;
                var y = oy - height + height / (density_h-1) * dy;
                this.addParticle(x, y, dy==0);
            }
        }
        this.opened_x = particles[first_particle].x;
        this.closed_x = particles[first_particle+density-1].x;
        
        for (var i=0; i<density_h-1; i++) {
            for (var j=0; j<density_w-1; j++) {
                const i1 = first_particle+i*density_w+j, i2=i1+1, i3=i1+density_w, i4=i3+1;
                this.quads.push(new Quad(i1, i2, i4, i3));
                this.springs.push(new Spring(i1, i2));
                this.springs.push(new Spring(i2, i4));
                this.springs.push(new Spring(i4, i3));
                this.springs.push(new Spring(i3, i1));
            }
        }
    }
    
    addParticle(x, y, lock_y) {
        var particle = new Particle(x, y, x, y, lock_y);
        this.particles.push(particle);
        particles.push(particle);
        return particles.length - 1;
    }
    
    update(open) {
        for (var j=0; j<density; j++) {
            this.particles[j].x = this.closed_x + (this.opened_x - this.closed_x) * open * (density-1-j) / (density-1);
        }

        this.particles.forEach(p => p.update());
        for (var i=0; i<50; i++)
            this.springs.forEach(p => p.update());
    }
    
    queueDraw() {
        this.quads.forEach(q => things_to_draw.push(q));
    }
}

function drawFloor() {
    for (var layer=0; layer<thickness; layer++) {
        const tstep = .25;
        const inset = (thickness-1-layer) * tstep;
        turtle.jump(-95, offset_y+inset); turtle.goto(95, offset_y+inset);
    }
}

var curtains = [];

function walk(i, t) {
    if (i==0) {
        iterations_t = iterations * t;
        polygons = new Polygons();
        
        if (t==0 || t==1) {
            iteration=0;
            curtains = [];
            curtains.push(new Curtain(0, 0, -5, 10));
            curtains.push(new Curtain(0, 0, +5, 10));
        }
        
        for (; iteration<iterations_t; iteration++) {
            var open = (Math.cos(Math.min(iteration*0.05, Math.PI*4))+1)/2;
            curtains.forEach(c=>c.update(open));
        }
            
        curtains.forEach(c=>c.queueDraw());

        //drawFloor();        
    }
    
    if (things_to_draw.length < 1) {
        return false;
    }

    things_to_draw.pop().draw();

    return true;
}

////

function sleep(milliseconds) { start=Date.now(); while (Date.now()-start<milliseconds); }

////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// https://turtletoy.net/turtle/a5befa1f8d
////////////////////////////////////////////////////////////////

function Polygons() {
	const polygonList = [];
	const Polygon = class {
		constructor() {
			this.cp = [];       // clip path: array of [x,y] pairs
			this.dp = [];       // 2d lines [x0,y0],[x1,y1] to draw
			this.aabb = [];     // AABB bounding box
		}
		addPoints(...points) {
		    // add point to clip path and update bounding box
		    let xmin = 1e5, xmax = -1e5, ymin = 1e5, ymax = -1e5;
			(this.cp = [...this.cp, ...points]).forEach( p => {
				xmin = Math.min(xmin, p[0]), xmax = Math.max(xmax, p[0]);
				ymin = Math.min(ymin, p[1]), ymax = Math.max(ymax, p[1]);
			});
		    this.aabb = [(xmin+xmax)/2, (ymin+ymax)/2, (xmax-xmin)/2, (ymax-ymin)/2];
		}
		addSegments(...points) {
		    // add segments (each a pair of points)
		    points.forEach(p => this.dp.push(p));
		}
		addOutline() {
			for (let i = 0, l = this.cp.length; i < l; i++) {
				this.dp.push(this.cp[i], this.cp[(i + 1) % l]);
			}
		}
		draw(t) {
			for (let i = 0, l = this.dp.length; i < l; i+=2) {
				t.jump(this.dp[i]), t.goto(this.dp[i + 1]);
			}
		}
		addHatching(a, d) {
			const tp = new Polygon();
			tp.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);
			const dx = Math.sin(a) * d,   dy = Math.cos(a) * d;
			const cx = Math.sin(a) * 200, cy = Math.cos(a) * 200;
			for (let i = 0.5; i < 150 / d; i++) {
				tp.dp.push([dx * i + cy,   dy * i - cx], [dx * i - cy,   dy * i + cx]);
				tp.dp.push([-dx * i + cy, -dy * i - cx], [-dx * i - cy, -dy * i + cx]);
			}
			tp.boolean(this, false);
			this.dp = [...this.dp, ...tp.dp];
		}
		inside(p) {
			let int = 0; // find number of i ntersection points from p to far away
			for (let i = 0, l = this.cp.length; i < l; i++) {
				if (this.segment_intersect(p, [0.1, -1000], this.cp[i], this.cp[(i + 1) % l])) {
					int++;
				}
			}
			return int & 1; // if even your outside
		}
		boolean(p, diff = true) {
		    // bouding box optimization by ge1doot.
		    if (Math.abs(this.aabb[0] - p.aabb[0]) - (p.aabb[2] + this.aabb[2]) >= 0 &&
				Math.abs(this.aabb[1] - p.aabb[1]) - (p.aabb[3] + this.aabb[3]) >= 0) return this.dp.length > 0;
				
			// polygon diff algorithm (narrow phase)
			const ndp = [];
			for (let i = 0, l = this.dp.length; i < l; i+=2) {
				const ls0 = this.dp[i];
				const ls1 = this.dp[i + 1];
				// find all intersections with clip path
				const int = [];
				for (let j = 0, cl = p.cp.length; j < cl; j++) {
					const pint = this.segment_intersect(ls0, ls1, p.cp[j], p.cp[(j + 1) % cl]);
					if (pint !== false) {
						int.push(pint);
					}
				}
				if (int.length === 0) {
					// 0 intersections, inside or outside?
					if (diff === !p.inside(ls0)) {
						ndp.push(ls0, ls1);
					}
				} else {
					int.push(ls0, ls1);
					// order intersection points on line ls.p1 to ls.p2
					const cmpx = ls1[0] - ls0[0];
					const cmpy = ls1[1] - ls0[1];
					int.sort( (a,b) =>  (a[0] - ls0[0]) * cmpx + (a[1] - ls0[1]) * cmpy - 
					                    (b[0] - ls0[0]) * cmpx - (b[1] - ls0[1]) * cmpy);
					 
					for (let j = 0; j < int.length - 1; j++) {
						if ((int[j][0] - int[j+1][0])**2 + (int[j][1] - int[j+1][1])**2 >= 0.001) {
							if (diff === !p.inside([(int[j][0]+int[j+1][0])/2,(int[j][1]+int[j+1][1])/2])) {
								ndp.push(int[j], int[j+1]);
							}
						}
					}
				}
			}
			return (this.dp = ndp).length > 0;
		}
		//port of http://paulbourke.net/geometry/pointlineplane/Helpers.cs
		segment_intersect(l1p1, l1p2, l2p1, l2p2) {
			const d   = (l2p2[1] - l2p1[1]) * (l1p2[0] - l1p1[0]) - (l2p2[0] - l2p1[0]) * (l1p2[1] - l1p1[1]);
			if (d === 0) return false;
			const n_a = (l2p2[0] - l2p1[0]) * (l1p1[1] - l2p1[1]) - (l2p2[1] - l2p1[1]) * (l1p1[0] - l2p1[0]);
			const n_b = (l1p2[0] - l1p1[0]) * (l1p1[1] - l2p1[1]) - (l1p2[1] - l1p1[1]) * (l1p1[0] - l2p1[0]);
			const ua = n_a / d;
			const ub = n_b / d;
			if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
				return [l1p1[0] + ua * (l1p2[0] - l1p1[0]), l1p1[1] + ua * (l1p2[1] - l1p1[1])];
			}
			return false;
		}
	};
	return {
		list: () => polygonList,
		create: () => new Polygon(),
		draw: (turtle, p, addToVisList=true) => {
			for (let j = 0; j < polygonList.length && p.boolean(polygonList[j]); j++);
			p.draw(turtle);
			if (addToVisList) polygonList.push(p);
		}
	};
}