Spring is coming 🌻

Petals clip each other (they all overlap)

Modified the (on Turtletoy) commonly used Polygons utility code to be able to have polygons added to the scene at the start of the rendering to be clipped by polygons that were added at the end of the rendering.

Spring is coming 🌻 (variation)
Spring is coming 🌻 (variation)
Spring is coming 🌻 (variation)

Not minified Polygon Clipping utility code:
Polygon Clipping utility code

Log in to post a comment.

const petals = 30; //min=1 max=50 step=1
const innerRadius = 20; //min=2 max=30 step=.5
const outerRadius = 2; //min=2 max=30 step=.5
const petalInnerLength = 15; //min=0 max=30 step=.5
const petalOffset = 50; //min=0 max=50 step=.5
const coreRadius = 50; //min=0 max=50 step=.5
const hatchDistance = 2; //min=.3 max=4 step=.1

const petalLength = petalInnerLength + outerRadius + innerRadius;

// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(1);

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

const petalPath = getPetalPath(innerRadius, outerRadius, petalLength);

// The walk function will be called until it returns false.
function walk(i) {
    if(i == 0) {
        const p = polygons.create();
        p.addPoints(...Array.from({length: (coreRadius * 2 * Math.PI) | 0}).map((e, i, a, f = i * 2 * Math.PI / a.length) => [coreRadius * Math.sin(f),coreRadius * Math.cos(f)]));
        p.addOutline();
        p.addHatching(1, hatchDistance/2);
        p.addHatching(-1, hatchDistance/2);
        polygons.draw(turtle, p);

        polygons.startDeferSession();
    }
    const angle = i * 2 * Math.PI / petals;
    const rot = V.rot2d(angle);

    if(i == (petals / 2 | 0)) polygons.stopDeferring();
    
    const p = polygons.create();
    p.addPoints(...petalPath.map(pt => V.trans(rot, V.add(pt, [petalOffset, 0]))));
    p.addOutline();
    p.addHatching(angle + Math.PI / 2 + .5, hatchDistance)
    polygons.draw(turtle, p);

    if(i < petals - 1) {
        return true;
    }
    
    polygons.finalizeDeferSession(turtle);
    return false;
}

function getPetalPath(cr, or, l) {
    const center = [[0,0], cr];
    const end = [[l, 0], or];
    const tps = getCirclesTangentPoints(...center, ...end);
    const startAngle = V.angle(tps[0][1]);
    const range = V.angle(tps[0][0]) - startAngle;
    
    const startAngle2 = V.angle(V.sub(tps[1][0], end[0]));
    const range2 = (V.angle(V.sub(tps[1][1], end[0])) + 2*Math.PI) - startAngle2;
    return Array.from({length: range * center[1] | 0}).map((e,i,a,f=i*range/(a.length - 1)) => { return [
        center[1] * Math.cos(startAngle + f),
        center[1] * Math.sin(startAngle + f)
    ]}).concat(
        Array.from({length: range2 * end[1] | 0}).map((e,i,a,f=i*range2/(a.length - 1)) => { return V.add(end[0], [
            end[1] * Math.cos(startAngle2 + f),
            end[1] * Math.sin(startAngle2 + f)
        ])})
    );
}

////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// (Polygon binning by Lionel Lemarie 2021) https://turtletoy.net/turtle/95f33bd383
// (Delegated Hatching by Jurgen Westerhof 2024) https://turtletoy.net/turtle/d068ad6040
// (Deferred Polygon Drawing by Jurgen Westerhof 2024) https://turtletoy.net/turtle/6f3d2bc0b5
// https://turtletoy.net/turtle/a5befa1f8d
//
// const polygons = new Polygons();
// const p = polygons.create();
// polygons.draw(turtle, p);
// polygons.list();
// polygons.startDeferSession();
// polygons.stopDeferring();
// polygons.finalizeDeferSession(turtle);
//
// p.addPoints(...[[x,y],]);
// p.addSegments(...[[x,y],]);
// p.addOutline();
// p.addHatching(angle, distance); OR p.addHatching(HatchObject); where HatchObject has a method 'hatch(PolygonClass, thisPolygonInstance)'
// p.inside([x,y]);
// p.boolean(polygon, diff = true);
// p.segment_intersect([x,y], [x,y], [x,y], [x,y]);
////////////////////////////////////////////////////////////////
function Polygons(){const t=[],s=25,e=Array.from({length:s**2},t=>[]),n=class{constructor(){this.cp=[],this.dp=[],this.aabb=[]}addPoints(...t){let s=1e5,e=-1e5,n=1e5,h=-1e5;(this.cp=[...this.cp,...t]).forEach(t=>{s=Math.min(s,t[0]),e=Math.max(e,t[0]),n=Math.min(n,t[1]),h=Math.max(h,t[1])}),this.aabb=[s,n,e,h]}addSegments(...t){t.forEach(t=>this.dp.push(t))}addOutline(){for(let t=0,s=this.cp.length;t<s;t++)this.dp.push(this.cp[t],this.cp[(t+1)%s])}draw(t){for(let s=0,e=this.dp.length;s<e;s+=2)t.jump(this.dp[s]),t.goto(this.dp[s+1])}addHatching(t, s) {if(typeof t == 'object') return t.hatch(n, this);const e=new n;e.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);const h=Math.sin(t)*s,o=Math.cos(t)*s,a=200*Math.sin(t),i=200*Math.cos(t);for(let t=.5;t<150/s;t++) {e.dp.push([h*t+i,o*t-a],[h*t-i,o*t+a]);e.dp.push([-h*t+i,-o*t-a],[-h*t-i,-o*t+a]);}e.boolean(this,!1);this.dp=[...this.dp,...e.dp]}inside(t){let s=0;for(let e=0,n=this.cp.length;e<n;e++)this.segment_intersect(t,[.1,-1e3],this.cp[e],this.cp[(e+1)%n])&&s++;return 1&s}boolean(t,s=!0){const e=[];for(let n=0,h=this.dp.length;n<h;n+=2){const h=this.dp[n],o=this.dp[n+1],a=[];for(let s=0,e=t.cp.length;s<e;s++){const n=this.segment_intersect(h,o,t.cp[s],t.cp[(s+1)%e]);!1!==n&&a.push(n)}if(0===a.length)s===!t.inside(h)&&e.push(h,o);else{a.push(h,o);const n=o[0]-h[0],i=o[1]-h[1];a.sort((t,s)=>(t[0]-h[0])*n+(t[1]-h[1])*i-(s[0]-h[0])*n-(s[1]-h[1])*i);for(let n=0;n<a.length-1;n++)(a[n][0]-a[n+1][0])**2+(a[n][1]-a[n+1][1])**2>=.001&&s===!t.inside([(a[n][0]+a[n+1][0])/2,(a[n][1]+a[n+1][1])/2])&&e.push(a[n],a[n+1])}}return(this.dp=e).length>0}segment_intersect(t,s,e,n){const h=(n[1]-e[1])*(s[0]-t[0])-(n[0]-e[0])*(s[1]-t[1]);if(0===h)return!1;const o=((n[0]-e[0])*(t[1]-e[1])-(n[1]-e[1])*(t[0]-e[0]))/h,a=((s[0]-t[0])*(t[1]-e[1])-(s[1]-t[1])*(t[0]-e[0]))/h;return o>=0&&o<=1&&a>=0&&a<=1&&[t[0]+o*(s[0]-t[0]),t[1]+o*(s[1]-t[1])]}};const y=function(n,j=[]){const h={},o=200/s;for(var a=0;a<s;a++){const c=a*o-100,r=[0,c,200,c+o];if(!(n[3]<r[1]||n[1]>r[3]))for(var i=0;i<s;i++){const c=i*o-100;r[0]=c,r[2]=c+o,n[0]>r[2]||n[2]<r[0]||e[i+a*s].forEach(s=>{const e=t[s];n[3]<e.aabb[1]||n[1]>e.aabb[3]||n[0]>e.aabb[2]||n[2]<e.aabb[0]||j.includes(s)||(h[s]=1)})}}return Array.from(Object.keys(h),s=>t[s])};return{list:()=>t,create:()=>new n,draw:(n,h,o=!0)=>{rpl=y(h.aabb, this.dei === undefined? []: Array.from({length: t.length - this.dei}).map((e, i) => this.dsi + i));for(let t=0;t<rpl.length&&h.boolean(rpl[t]);t++);const td=n.isdown();if(this.dsi!==undefined&&this.dei===undefined)n.pu();h.draw(n),o&&function(n){t.push(n);const h=t.length-1,o=200/s;e.forEach((t,e)=>{const a=e%s*o-100,i=(e/s|0)*o-100,c=[a,i,a+o,i+o];c[3]<n.aabb[1]||c[1]>n.aabb[3]||c[0]>n.aabb[2]||c[2]<n.aabb[0]||t.push(h)})}(h);if(td)n.pd();},startDeferSession:()=>{if(this.dei!==undefined)throw new Error('Finalize deferring before starting new session');this.dsi=t.length;},stopDeferring:()=>{if(this.dsi === undefined)throw new Error('Start deferring before stopping');this.dei=t.length;},finalizeDeferSession:(n)=>{if(this.dei===undefined)throw new Error('Stop deferring before finalizing');for(let i=this.dsi;i<this.dei;i++) {rpl = y(t[i].aabb,Array.from({length:this.dei-this.dsi+1}).map((e,j)=>i+j));for(let j=0;j<rpl.length&&t[i].boolean(rpl[j]);j++);t[i].draw(n);}this.dsi=undefined;this.dei=undefined;}}}





////// Some math code that's beside the point of this turtle:
function getCirclesTangentPoints(c1_center, c1_radius, c2_center, c2_radius, internal = false) {
    let middle_circle = [V.scale(V.sub(c1_center, c2_center), .5)].map(hwp => [V.add(c2_center, hwp), V.len(hwp)]).pop();
    
    if(!internal && c1_radius == c2_radius) {
        let target = V.sub(c2_center, c1_center);
        let scaledTarget = V.scale(target, c1_radius/V.len(target));
        let partResult = [
            V.add(c1_center, V.trans(V.rot2d(Math.PI/2), scaledTarget)),
            V.add(c1_center, V.trans(V.rot2d(Math.PI/-2), scaledTarget))
        ];
        return [
            partResult,
            partResult.map(pt => V.add(pt, target))
        ]
    }

    let swap = !internal && c2_radius > c1_radius;
    
    if(swap) {
        let t = [[...c1_center], c1_radius];
        c1_center = c2_center;
        c1_radius = c2_radius;
        c2_center = t[0];
        c2_radius = t[1];
    }
    
    let internal_waypoints = intersectCircles2(c1_center, c1_radius + (internal?c2_radius:-c2_radius), ...middle_circle);

    if(internal_waypoints.length == 0) return [];
    
    let result = [
        [ // circle 1, point 1 and 2
            circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints[0], c1_center)),
            circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints[1], c1_center))
        ],
        [ // circle 2, point 1 and 2
            circlePointAtDirection2(c2_center, c2_radius, internal? V.sub(c1_center, internal_waypoints[0]): V.sub(internal_waypoints[0], c1_center)),
            circlePointAtDirection2(c2_center, c2_radius, internal? V.sub(c1_center, internal_waypoints[1]): V.sub(internal_waypoints[1], c1_center))
        ]
    ];
    
    return swap? [[result[1][1],result[1][0]],[result[0][1],result[0][0]]]: result;
}
function intersectCircles2(p1, r1, p2, r2) {
    let d = ((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)**.5;
    if (!(Math.abs(r1 - r2) <= d && d <= r1 + r2)) {
        return []; // no intersection, to far apart or total overlap
    }
    
    const calc = (left, right) => [[(left[0] + right[0]) / 2 + (r1**2 - r2**2) * (right[0] - left[0]) / (2 * d**2), (2 * (r1**2 + r2**2) / d**2 - ((r1**2 - r2**2)**2) / d**4 - 1)**.5 * (right[1] - left[1]) / 2]].map(t => [t[0] + t[1], t[0] - t[1]]).pop();
    
    let xs = calc(p1, p2);
    let ys = calc([...p1].reverse(), [...p2].reverse()).reverse();
    
    return [[xs[0], ys[0]], [xs[1], ys[1]]];
}
function circlePointAtDirection2(circle_center, radius, direction) { return V.add(circle_center, V.scale(direction, radius/V.len(direction))); }


////////////////////////////////////////////////////////////////
// Vector Math utility code. Created by Jurgen Westerhof 2024
// https://turtletoy.net/turtle/a424302aa8
////////////////////////////////////////////////////////////////
function init() {
    class V {
        static add  (a,b) { return a.map((v,i)=>v+b[i]); }
        static sub  (a,b) { return a.map((v,i)=>v-b[i]); }
        static mul  (a,b) { return a.map((v,i)=>v*b[i]); }
        static div  (a,b) { return a.map((v,i)=>v/b[i]); }
        static scale(a,s) { return a.map(v=>v*s); }
    
        static det(m)                { return m.length == 1? m[0][0]: m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0]: m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.det(m.slice(1).map(c => c.filter((_,j) => i != j))),0); }
        static angle(a)              { return Math.PI - Math.atan2(a[1], -a[0]); } //compatible with turtletoy heading
        static rot2d(angle)          { return [[Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]]; }
        static rot3d(yaw,pitch,roll) { return [[Math.cos(yaw)*Math.cos(pitch), Math.cos(yaw)*Math.sin(pitch)*Math.sin(roll)-Math.sin(yaw)*Math.cos(roll), Math.cos(yaw)*Math.sin(pitch)*Math.cos(roll)+Math.sin(yaw)*Math.sin(roll)],[Math.sin(yaw)*Math.cos(pitch), Math.sin(yaw)*Math.sin(pitch)*Math.sin(roll)+Math.cos(yaw)*Math.cos(roll), Math.sin(yaw)*Math.sin(pitch)*Math.cos(roll)-Math.cos(yaw)*Math.sin(roll)],[-Math.sin(pitch), Math.cos(pitch)*Math.sin(roll), Math.cos(pitch)*Math.cos(roll)]]; }
        static trans(matrix,a)       { return a.map((v,i) => a.reduce((acc, cur, ci) => acc + cur * matrix[ci][i], 0)); }
        //Mirror vector a in a ray through [0,0] with direction mirror
        static mirror2d(a,mirror)    { return [Math.atan2(...mirror)].map(angle => this.trans(this.rot2d(angle), this.mul([-1,1], this.trans(this.rot2d(-angle), a)))).pop(); }

        static approx(a,b,p) { return this.len(this.sub(a,b)) < (p === undefined? .001: p); }
        static norm  (a)     { return this.scale(a,1/this.len(a)); }
        static len   (a)     { return Math.hypot(...a); }
        static lenSq (a)     { return a.reduce((a,c)=>a+c**2,0); }
        static lerp  (a,b,t) { return a.map((v, i) => v*(1-t) + b[i]*t); }
        static dist  (a,b)   { return Math.hypot(...this.sub(a,b)); }
        
        static dot  (a,b)   { return a.reduce((a,c,i) => a+c*b[i], 0); }
        static cross(...ab) { return ab[0].map((e, i) => ab.map(v => v.filter((ee, ii) => ii != i))).map((m,i) => (i%2==0?-1:1)*this.det(m)); }
    }
    this.V = V;
}