A bale demonstration 🐢🐢🐢🐢🐢🐢
Log in to post a comment.
const display = 0; //min=0 max=2 step=1 (Screen friendly, Plotter friendly - all, Plotter friendly - no obscured)
const mode = 3;//min=0 max=12 step=1 (1 Spiral, 2 Sprirals, 3 Spirals, 1 Double Spiral, 2 Double Sprirals, 3 Double Spirals, 1 Latitude, 2 Latitudes, 3 Latitudes, 1 Longitudes, 2 Longitudes, 3 Longitudes, Latitude & Longitude)
const spiralRotations = 10; //min=1 max=50 step=1
const latLongResolution = 18; //min=1 max=50 step=1
const shades = 10; //min=2 max=16 step=2 Fewer shades equals faster drawing
const perspective1000 = 3; //min=0 max=10 step=1
const yaw = 30; //min=0 max=360 step=1
const pitch = 60; //min=0 max=360 step=1
const roll = 60; //min=0 max=360 step=1
const radius = 80;
const perspective = perspective1000/1000;
// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(display == 0? 1/shades: 1);
// Global code will be evaluated once.
turtlelib_init();
const bale = display == 0? new Bale(1/shades): new Turtle();
const max = spiralRotations * 4 * Math.PI;
const ptss = ((mode) => {
const rots = [V.rot3d(0, 0, 0), V.rot3d(Math.PI/2, 0, 0), V.rot3d(0, 0, Math.PI/2)];
switch(mode) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
const spirals = mode % 3;
const mirror = mode > 2? 1: 2;
const mod = mirror == 1? 2: 1;
const halfSpiralPts = Array.from({length: radius * max | 0}).map((e, i, a) => {
return [
radius * Math.cos(max/mod * i / a.length) * Math.sin(mirror*Math.PI * i/a.length),
radius * Math.cos(mirror*Math.PI * i / a.length),
radius * Math.sin(max/mod * i / a.length) * Math.sin(mirror*Math.PI * i/a.length),
];
});
const rotHalfY = V.rot3d(0, Math.PI, 0);
const spiralPts = (mode < 3)? halfSpiralPts: [...halfSpiralPts, ...halfSpiralPts.map(pt => V.trans(rotHalfY, pt)).reverse()];
return rots.filter((e, i) => i <= spirals % 3).map(rot => spiralPts.map(pt => V.trans(rot, pt)));
default:
const circle = Array.from({length: radius * 2 * Math.PI | 0}).map((e, i, a) => {
return [0, radius * Math.cos(2 * Math.PI * i / a.length), radius * Math.sin(2 * Math.PI * i / a.length)]
});
const longitudeLines = [];
for(let i = 0; i < latLongResolution; i++) {
const rot = V.rot3d(0, Math.PI * i / latLongResolution, 0, 0);
longitudeLines.push(circle.map(pt => V.trans(rot, pt)));
}
const halfLatitudeLines = [];
for(let i = latLongResolution; i > 0; i--) {
const rho = (Math.PI/2) * i / latLongResolution;
const r = radius * Math.sin(rho);
halfLatitudeLines.push(Array.from({length: 2 * Math.PI * r | 0}).map((e, i, a) => {
return [
r * Math.cos(2 * Math.PI * i / a.length),
radius * Math.cos(rho),
r * Math.sin(2 * Math.PI * i / a.length)
];
}));
}
const latitudeLines = [...halfLatitudeLines.map(e => e).reverse(), ...halfLatitudeLines.filter((e,i) => i > 0).map(e => e.map(e => [e[0], -e[1], e[2]]))];
switch(mode - 6) {
case 0:
case 1:
case 2:
return rots.filter((e, i) => i <= mode - 6).flatMap(rot => latitudeLines.map(e => e.map(pt => V.trans(rot, pt))));
case 3:
case 4:
case 5:
return rots.filter((e, i) => i <= mode - 9).flatMap(rot => longitudeLines.map(e => e.map(pt => V.trans(rot, pt))));
default:
return [...longitudeLines, ...latitudeLines];
}
}
})(mode);
const maxVisibleZ = findMaxVisibleZ(radius, perspective);
const rot = V.rot3d(Math.PI*yaw/180,Math.PI*pitch/180,Math.PI*roll/180);
// The walk function will be called until it returns false.
let drawing = false;
let key = 0;
let offset = 0;
function walk(i) {
const pts = ptss[key];
const index = i - offset;
if(index == 0) draw('jump', pts[pts.length - 1]);
draw('goto', pts[index]);
if(index == pts.length - 1) {
key++;
offset += pts.length;
}
return key < ptss.length;
}
function draw(fn, pto) {
const pt = V.trans(rot, pto);
if(display == 2 && pt[2] < maxVisibleZ) {
drawing = false;
return;
}
if(display == 0) bale.setOpacity(.2 + .8 * (pt[2] / (2*radius) + .5));
bale[drawing || display < 2? fn: 'jump'](project2d(pt));
drawing = true;
}
function project2d(vec3) {
return V.add([vec3[0], vec3[1]], V.scale([vec3[0], vec3[1]], vec3[2] * perspective));
}
////////////////////////////////////////////////////////////////
// Bale utility code 2.0 - Created by Jurgen Westerhof 2025
// https://turtletoy.net/turtle/7b8c486a30
// (Ab)using the opacity, usage:
// Canvas.setpenopacity(sceneOpacity);
// const bale = new Bale(sceneOpacity);
// Then use bale wherever you would use a turtle object to 'draw'
// in 'opacity' x (i.e Polygon hatching with a bale object and
// .25 interspacing), or:
// bale.setOpacity(.5);
// bale.jump(0,0);
// bale.goto(40,0);
////////////////////////////////////////////////////////////////
function Bale(sceneOpacity, turtleClass = Turtle) {
class Turtle {
#useTurtleCount = 0;
#turtles = [];
#sceneOpacity = 0;
constructor(sceneOpacity, turtleClass) {
this.#sceneOpacity = sceneOpacity;
this.#turtles = Array.from({length: Math.max(1, this.getTurtleCountForOpacity(1))}, (e,i) => new turtleClass());
this.#useTurtleCount = this.#turtles.length;
}
setOpacity(opacity) {
if(opacity < 0 || 1 < opacity) {
throw new Error(`Out of bounds opacity. 0 <= opacity <= 1, got ${opacity}`);
}
this.#useTurtleCount = this.getTurtleCountForOpacity(opacity);
}
getProp(prop) {
return this.#turtles[0][prop];
}
invokeProp(action, a, b, c, d, e, f, g) {
return this.#turtles.reduce((acc, turtle, i) => turtle[action == 'goto' && this.#useTurtleCount <= i? 'jump': action](a, b, c, d, e, f, g), 0);
}
getTurtleCountForOpacity(targetOpacity) {
if (targetOpacity <= 0) return 0;
if (targetOpacity >= .5 && this.#turtles.length == 1) return 1;
return Math.round(Math.log(1 - Math.min(.99, targetOpacity)) / Math.log(1 - this.#sceneOpacity));
}
}
const TurtleProxy = {
get(target, prop, receiver) {
switch(prop) {
case 'setOpacity':
return (opacity) => target.setOpacity(opacity);
case 'clone':
throw new Error('Cloning not implemented');
case 'position': //for faster processing, known functions
case 'pos': //of a turtle that don't change the
case 'xcor': //the state of a turtle but just tell
case 'x': //the state of a turtle, there's no need
case 'ycor': //to iterate over the whole bale but
case 'y': //only return the value from the first
case 'heading': //turtle in the bale. That's where these
case 'h': //9 cases are an exception to the rest
case 'isdown': //of a turtle's functions
return target.getProp(prop);
default:
return (a, b, c, d, e, f, g) => target.invokeProp(prop, a, b, c, d, e, f, g);
}
},
};
return new Proxy(new Turtle(sceneOpacity, turtleClass), TurtleProxy);
}
function findMaxVisibleZ(r, p) {
// Newton-Raphson to solve: -sin(θ)(1 + r * p * sin(θ)) + r * p * cos²(θ) = 0
function f(theta) {
const sin = Math.sin(theta);
const cos = Math.cos(theta);
return -sin * (1 + r * p * sin) + r * p * cos * cos;
}
function df(theta) {
const sin = Math.sin(theta);
const cos = Math.cos(theta);
const d_sin = Math.cos(theta);
const d_cos = -Math.sin(theta);
// Derivative of the function above (computed by hand or symbolic tool)
return -Math.cos(theta) * (1 + 2 * r * p * sin) - 2 * r * p * sin * cos;
}
// Initial guess (mid-front hemisphere)
let theta = 0;
let i = 0;
while (i < 100) {
const y = f(theta);
const dy = df(theta);
if (Math.abs(dy) < 1e-6) break; // avoid division by zero
const nextTheta = theta - y / dy;
if (Math.abs(nextTheta - theta) < 1e-8) break;
theta = nextTheta;
i++;
}
// Now get z from theta
const z = r * Math.sin(theta);
return z;
}
// Below is automatically maintained by Turtlelib 1.0
// Changes below this comment might interfere with its correct functioning.
function turtlelib_init() {
turtlelib_ns_c6665b0e9b_Jurgen_Vector_Math();
}
// Turtlelib Jurgen Vector Math v 4 - start - {"id":"c6665b0e9b","package":"Jurgen","name":"Vector Math","version":"4"}
function turtlelib_ns_c6665b0e9b_Jurgen_Vector_Math() {
/////////////////////////////////////////////////////////
// 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;
}
// Turtlelib Jurgen Vector Math v 4 - end