Bezier twists 🌪️

Two regularly interconnected circles where the connections have Bezier properties.

Log in to post a comment.

const innerRadius = 10; //min=0 max=50 step=1
const outerRadius = 90; //min=51 max=100 step=1
const nLines = 70; //min=1 max=200 step=1
const spin = 0; //min=0 max=360 step=1
const innerVolume = 65; //min=1 max=100 step=1
const outerVolume = 75; //min=1 max=100 step=1
const innerOffset = 90; //min=0 max=360 step=1
const outerOffset = 90; //min=0 max=360 step=1
const plotFinish = 1; //min=0 max=1 step=1 (No, Yes)
const midControl = 0; //min=0 max=1 step=1 (No, Yes)
const midRadius = 50; //min=10 max=80 step=1
const midSpin = 90; //min=0 max=360 step=1
const minInnerVolume = 10; //min=1 max=100 step=1
const minOuterVolume = 10; //min=1 max=100 step=1
const midOffset = 90; //min=0 max=360 step=1

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

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

const innerCirclePoints = circlePoints(innerRadius, 2 * Math.PI, 0, nLines);
const midCirclePoints = circlePoints(midRadius, 2*Math.PI, midSpin * Math.PI/180, nLines);
const outerCirclePoints = circlePoints(outerRadius, 2 * Math.PI, spin * Math.PI/180, nLines);

const innerRot = rot2(innerOffset * Math.PI / 180);
const midRot = rot2(midOffset * Math.PI / 180);
const outerRot = rot2(outerOffset * Math.PI / 180);

if(plotFinish == 1) {
    drawTour(turtle, circlePoints(innerRadius));
    drawTour(turtle, circlePoints(outerRadius));
}

// The walk function will be called until it returns false.
function walk(i) {
    if(midControl == 0) {
        drawPath(turtle, bezier2(
            innerCirclePoints[i],
            trans2(innerRot, scale2(norm2(innerCirclePoints[i]), innerVolume)),
            outerCirclePoints[i],
            trans2(outerRot, scale2(norm2(outerCirclePoints[i]), -outerVolume)),
            outerRadius
        ));
    } else {
        drawPath(turtle, bezier2(
            innerCirclePoints[i],
            trans2(innerRot, scale2(norm2(innerCirclePoints[i]), innerVolume)),
            midCirclePoints[i],
            trans2(midRot, scale2(norm2(midCirclePoints[i]), -minInnerVolume)),
            (outerRadius / 2) | 0
        ));
        drawPath(turtle, bezier2(
            midCirclePoints[i],
            trans2(midRot, scale2(norm2(midCirclePoints[i]), minOuterVolume)),
            outerCirclePoints[i],
            trans2(outerRot, scale2(norm2(outerCirclePoints[i]), -outerVolume)),
            (outerRadius / 2) | 0
        ));
    }
    return i < nLines - 1;
}



function approx1(a,b,delta=0.0001) { return -delta < a-b && a-b < delta }

////////////////////////////////////////////////////////////////
// 2D Vector Math utility code - Created by several Turtletoy users
////////////////////////////////////////////////////////////////
function norm2(a) { return scale2(a, 1/len2(a)); }
function add2(a, b) { return [a[0]+b[0], a[1]+b[1]]; }
function sub2(a, b) { return [a[0]-b[0], a[1]-b[1]]; }
function mul2(a, b) { return [a[0]*b[0], a[1]*b[1]]; }
function scale2(a, s) { return mul2(a, [s,s]); }
function lerp2(a,b,t) { return [a[0]*(1-t) + b[0]*t, a[1]*(1-t) + b[1]*t]; }
function lenSq2(a) { return a[0]**2+a[1]**2; }
function len2(a) { return Math.sqrt(lenSq2(a)); }
function rot2(a) { return [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a)]; }
function trans2(m, a) { return [m[0]*a[0]+m[2]*a[1], m[1]*a[0]+m[3]*a[1]]; } //Matrix(2x1) x Matrix(2x2)
function dist2(a,b) { return Math.hypot(...sub2(a,b)); }
function dot2(a,b) { return a[0]*b[0]+a[1]*b[1]; }
function cross2(a,b) { return a[0]*b[1] - a[1]*b[0]; }
function multiply2(a2x2, a) { return [(a[0]*a2x2[0])+(a[1]*a2x2[1]),(a[0]*a2x2[2])+(a[1]*a2x2[3])]; } //Matrix(2x2) x Matrix(1x2)
function intersect_info2(as, ad, bs, bd) {
    const d = [bs[0] - as[0], bs[1] - as[1]];
    const det = bd[0] * ad[1] - bd[1] * ad[0];
    if(det === 0) return false;
    const res = [(d[1] * bd[0] - d[0] * bd[1]) / det, (d[1] * ad[0] - d[0] * ad[1]) / det];
    return [...res, add2(as, scale2(ad, res[0]))];
}
function intersect_ray2(a, b, c, d) {
    const i = intersect_info2(a, b, c, d);
    return i === false? i: i[2];
}
function segment_intersect2(a,b,c,d, inclusive = true) {
    const i = intersect_info2(a, sub2(b, a), c, sub2(d, c));
    if(i === false) return false;
    const t = inclusive? 0<=i[0]&&i[0]<=1&&0<=i[1]&&i[1]<=1: 0<i[0]&&i[0]<1&&0<i[1]&&i[1]<1;
    return t?i[2]:false;
}
function approx2(a,b,delta=0.0001) { return len2(sub2(a,b)) < delta }
function eq2(a,b) { return a[0]==b[0]&&a[1]==b[1]; }
function clamp2(a, tl, br) { return [Math.max(Math.min(br[0], a[0]), tl[0]), Math.max(Math.min(br[1], a[1]), tl[1])]; }
function nearSq2(test, near, delta = .0001) {
    return near[0] - delta < test[0] && test[0] < near[0] + delta &&
           near[1] - delta < test[1] && test[1] < near[1] + delta;
}

////////////////////////////////////////////////////////////////
// Start of some path utility code - Created by Jurgen Westerhof 2023
////////////////////////////////////////////////////////////////
function circlePoints(radius, extend = 2 * Math.PI, clockWiseStart = 0, steps = null, includeLast = false) { return [steps == null? (radius*extend+1)|0: steps].map(steps => Array.from({length: steps}).map((v, i, a) => [radius * Math.cos(clockWiseStart + extend*i/(a.length-(includeLast?1:0))), radius * Math.sin(clockWiseStart + extend*i/(a.length-(includeLast?1:0)))])).pop(); }
function pts2Edges(pts) { return pts.map((v, i, a) => [v, a[(i+1)%a.length]]); }
function drawPath(turtle, pts) { return pts.forEach((pt, i) => turtle[i == 0? 'jump':'goto'](pt)); }
function drawTour(turtle, pts) { return drawPath(turtle, pts.concat([pts[0]])); }
function drawPoint(turtle, pt) { return drawTour(turtle, circlePoints(.5).map(p => add2(p, pt))); }
function isInPolygon(edges, pt) { return edges.map(edge => intersect_info2(edge[0], sub2(edge[1], edge[0]), pt, [0, 300])).filter(ii => ii !== false && 0 <= ii[0] && ii[0] <= 1 && 0 < ii[1]).length % 2 == 1; }
function bezier2(origin, relativeOriginControl, target, relativeTargetControl, steps) {
    return Array.from({length: steps + 1}, (v, idx) => {
        const t = idx/steps;
        
        const originControl = add2(origin, relativeOriginControl);
        const targetControl = add2(target, relativeTargetControl);
        
        const originDev = lerp2(origin, originControl, t);
        const secDev = lerp2(originControl, targetControl, t);
        const targetDev = lerp2(targetControl, target, t);
        
        const originPt = lerp2(originDev, secDev, t);
        const targetPt = lerp2(secDev, targetDev, t);
    
        return lerp2(originPt, targetPt, t);
    });
}
function redistributePathLinear(path, steps) {
    const segments = path.map((v, i, arr) => [v, arr[(i+1)%arr.length], len2(sub2(v, arr[(i+1)%arr.length]))]);
    const last = segments.pop();
    const length = segments.reduce((p, v) => p + v[2], 0);
    const stepLength = length/steps;

    let lengthLeft = stepLength;
    const result = [segments[0][0]];
    while(segments.length > 0) {
        const segment = segments.shift();
        const delta = lengthLeft - segment[2];
        if(delta > 0) {
            lengthLeft = delta;
            continue;
        }
        if(delta === 0) {
            result.push(segment[1]);
            lengthLeft = stepLength;
            continue;
        }
        const newPoint = lerp2(segment[0], segment[1], lengthLeft / segment[2]);
        result.push(newPoint);
        segments.unshift([newPoint, segment[1], segment[2] - lengthLeft]);
        lengthLeft = stepLength;
    }
    if(result.length < steps + 1) result.push(last[0]);
    return result;
}