### The wheels on the bus go... ðŸšŒ

...round and round.

Yet another turtle that makes use of the 'Draw different frames.' option when exporting to an animated gif. It will only animate if the 'time' variable is set to 0 or 1 exactly.

If you think: "I saw this somewhere...", that's right: reddit.com/r/generat…/rotating_rotations/

const time = 1; //min=0 max=1 step=.001
const n = 9; //min=2 max=20 step=1
const maxR = 90; //min=50 max=100 step=1
const fillOdds = 1; //min=0 max=1 step=1 (No, Yes)
const clockwise = 1; //min=0 max=1 step=1 (No, Yes)
const offset = .25; //min=0 max=1 step=.01
const print = -1; //min=-1 max=1 step=2 (White on black, Black on white)
const displacement = 0; //min=0 max=3 step=1 (Always full: 1, sin(time*Ï€), 1 - sin(time*Ï€), 1 - |cos(time*Ï€)|)
const rotation = 1; //min=-15 max=15 step=1

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

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

const rPerCircle = maxR / n;

function getCenters(t) {
const centers = [];
for(let i = 0; i < n; i++) {
const thisR = i==0?0:rPerCircle;
centers.push([thisR * Math.cos((offset + (i+1)*t)*2*Math.PI),thisR * Math.sin((offset + (i+1)*t)*2*Math.PI)]);
}
return centers;
}
let lastT = -1;
let centers;
// The walk function will be called until it returns false.
function walk(i, gifGeneratingT) {
const t = Math.abs((clockwise == 1? 0:1) - (time == 1 || time == 0? gifGeneratingT: time));

if(lastT != t) {
lastT = t;
centers = getCenters(t);
polygons = new Polygons();
}

let d = (() => {
switch(displacement) {
case 1:
return Math.sin(t*Math.PI);
case 2:
return 1 - Math.sin(t*Math.PI);
case 3:
return 1 - Math.abs(Math.cos(t*Math.PI));
default:
return 1;
}
})()

const thisCenter = V.scale(centers.filter((e, idx) => idx <= n-i).reduce((a, c) => V.add(a, c), [0,0]), d);

const rotationMatrix = V.rot2d(time * rotation * Math.PI * 2);

const p = polygons.create();
if(fillOdds == 1 && i % 2 + (n + 1) % 2 == 1) p.addHatching(1, .15);
polygons.draw(turtle, p);

return i < n;
}

function init() {
///////////////////////////////////////////////////////
// Vector functions - Created by Jurgen Westerhof 2024
///////////////////////////////////////////////////////
class Vector {
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 equals(a,b)   { return !a.some((e, i) => e != b[i]); }
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)); }

static clamp(a,min,max) { return a.map((e,i) => Math.min(Math.max(e, min[i]), max[i])) };
static rotateClamp(a,min,max) { return a.map((e,i) => { const d = max[i]-min[i]; if(d == 0) return min[i]; while(e < min[i]) { e+=d; } while(e > max[i]) { e-=d; } return e; }); }
}
this.V = Vector;

class Intersection2D {
//a-start, a-direction, b-start, b-direction
//returns false on no intersection or [[intersection:x,y], scalar a-direction, scalar b-direction
static info(as, ad, bs, bd) { const d = V.sub(bs, as), det = -V.det([bd, ad]); if(det === 0) return false; const res = [V.det([d, bd]) / det, V.det([d, ad]) / det]; return [V.add(as, V.scale(ad, res[0])), ...res]; }
static ray(a, b, c, d) { return this.info(a, b, c, d); }
static segment(a,b,c,d, inclusiveStart = true, inclusiveEnd = true) { const i = this.info(a, V.sub(b, a), c, V.sub(d, c)); return i === false? false: ( (inclusiveStart? 0<=i[1] && 0<=i[2]: 0<i[1] && 0<i[2]) && (inclusiveEnd?   i[1]<=1 && i[2]<=1: i[1]<1 && i[2]<1) )?i[0]:false;}
static tour(tour, segmentStart, segmentDirection) { return tour.map((e, i, a) => [i, this.info(e, V.sub(a[(i+1)%a.length], e), segmentStart, segmentDirection)]).filter(e => e[1] !== false && 0 <= e[1][1] && e[1][1] <= 1).filter(e => 0 <= e[1][2]).map(e => ({position: e[1][0],tourIndex: e[0],tourSegmentPortion: e[1][1],segmentPortion: e[1][2],}));}
static inside(tour, pt) { return tour.map((e,i,a) => this.segment(e, a[(i+1)%a.length], pt, [Number.MAX_SAFE_INTEGER, 0], true, false)).filter(e => e !== false).length % 2 == 1; }
}
this.Intersection = Intersection2D;

class PathTools {
static bezier(p1, cp1, cp2, p2, steps = null) {steps = (steps === null? Math.max(3, (V.len(V.sub(cp1, p1)) + V.len(V.sub(cp2, cp1)) + V.len(V.sub(p2, cp2))) | 0): steps) - 1; return Array.from({length: steps + 1}).map((v, i, a, f = i/steps) => [[V.lerp(p1, cp1, f),V.lerp(cp1, cp2, f),V.lerp(cp2, p2, f)]].map(v => V.lerp(V.lerp(v[0], v[1], f), V.lerp(v[1], v[2], f), f))[0]);}
// https://stackoverflow.com/questions/18655135/divide-bezier-curve-into-two-equal-halves#18681336
static splitBezier(p1, cp1, cp2, p2, t=.5) {const e = V.lerp(p1, cp1, t);const f = V.lerp(cp1, cp2, t);const g = V.lerp(cp2, p2, t);const h = V.lerp(e, f, t);const j = V.lerp(f, g, t);const k = V.lerp(h, j, t);return [[p1, e, h, k], [k, j, g, p2]];}
static circle(r){return this.circular(r,Math.max(12, r*2*Math.PI|0));}
static arc(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(); }
static draw(turtle, path) {path.forEach((pt, i) => turtle[i==0?'jump':'goto'](pt));}
static drawTour(turtle, path) {this.draw(turtle, path.concat([path[0]]));}
static drawPoint(turtle, pt, r = .1) {this.drawTour(turtle, this.circle(r).map(e => V.add(e, pt)));}
}

this.PT = PathTools;

class Complex {
static sub(a,b)     { return V.sub(a,b); }
static scale(a,s)   { return V.scale(a,s); }
static mult(a,b)    { return [a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]]; }
static sqrt(a)      { return [[Math.hypot(...a)**.5, Math.atan2(...a.reverse()) / 2]].map(ra => [ra[0]*Math.cos(ra[1]), ra[0]*Math.sin(ra[1])]).pop(); }
}
this.C = Complex;

class Numbers {
static approx(a,b,p)        { return Math.abs(a-b) < (p === undefined? .001: p); }
static clamp(a, min, max)   { return Math.min(Math.max(a, min), max); }
static rotateClamp(a, min, max) {
if(min == max) return min;
while (a < min) { a+=(max-min); }
while (a > max) { a-=(max-min); }
return a;
}
}
this.N = Numbers;

class Matrix {
static bayer(order) { return [...Array(1<<order)].map((_,y,a) => { const g = (k=order,x)=>k--&&4*g(k,x)|2*(x>>k)+3*(y>>k&1)&3; return a.map(g); }); }
static rotate(m) { return m[0].map((e, i) => m.map(r => r[i]).reverse()); }
static rotateCCW(m) { return m[0].map((e, i) => m.map(r => r[r.length-1-i])); }
static add(a,b) { return a.map((e, c) => e.map((e, r) => a[c][r] + b[c][r])); }
static sub(a,b) { return a.map((e, c) => e.map((e, r) => a[c][r] - b[c][r])); }
static multiply(a,b) { return Array.from({length: b.length}, (e,resCol) => Array.from({length: a[0].length}, (e,resRow) => b[resCol].reduce((acc, c, bRow) => acc + a[bRow][resRow] * b[resCol][bRow], 0)));}
static scale(a,s) { return a.map((e, c) => e.map((e, r) => a[c][r] * s)); }
static random(c,r,fillFn = Math.random) { return Array.from({length: c}, (e,i) => Array.from({length: r}, e => fillFn(c, r))); }
static identity(d) { return Array.from({length: d}, (e,c) => Array.from({length: d}, (e, r) => c==r?1:0 )); }
static log(m, name, logFn = console.log) { if(name != undefined) logFn(name); if(m === undefined || (typeof m == 'object' && (m[0] === undefined || m[0][0] === undefined))) { return logFn(`Failed to log matrix:`, m); } logFn(m[0].map((e,r) => m.map((e,c) => m[c][r]).join(', ')).join('\n')); }
static invert(m) { let _A = m.map(col => col.map(cell => cell));/*clone matrix*/let temp;const N = _A.length;const E = Array.from({length: N}, (e,i) => Array.from({length: _A[0].length}, (e,j) => i==j?1:0));for (let k = 0; k < N; k++) {temp = _A[k][k];for (let j = 0; j < N; j++) {_A[k][j] /= temp;E[k][j] /= temp;}for (let i = k + 1; i < N; i++) {temp = _A[i][k];for (let j = 0; j < N; j++) {_A[i][j] -= _A[k][j] * temp;E[i][j] -= E[k][j] * temp;}}}for (let k = N - 1; k > 0; k--) {for (let i = k - 1; i >= 0; i--) {temp = _A[i][k];for (let j = 0; j < N; j++) {_A[i][j] -= _A[k][j] * temp;E[i][j] -= E[k][j] * temp;}}}return E; }
static determinant(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.determinant(m.slice(1).map(c => c.filter((_,j) => i != j))), 0)}
static flip(m) { return Array.from({length: m[0].length}, (_, r) => Array.from({length: m.length}, (e, c) => m[c][r])); }
static sum(m) { return m.reduce((a, c) => a + c.reduce((aa, cc) => aa + cc, 0), 0); }
}
this.M = Matrix;

class Algorithms {
static nthTriangular(n) { return ((n * n) + n) / 2; }
}
this.A = Algorithms;
}

////////////////////////////////////////////////////////////////
// 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);
//