A fly's perspective on Turtletoy, using their arthropod eyes, facetted lens or compound lens. It results in an image (depending on the shape of the facets, square in this case) that has repeated segments of the scene each with slightly different perspective, kinda like a Kaleidoscope 🫶
Log in to post a comment.
// Forked from "Fly's eye lens 🪰" by Jurgen // https://turtletoy.net/turtle/cb3ddc7f57 const gridSize = 4; //min=1 max=50 step=1 const projectionMargin = 2.3; //min=0 max=10 step=.1 const segmentCapture = .12; // min=.01 max=1 step=.01 const captureWidth = 164; //min=10 max=200 step=1 // You can find the Turtle API reference here: https://turtletoy.net/syntax Canvas.setpenopacity(1); const GRID = { TOP: 1, RIGHT: 2, BOTTOM: 4, LEFT: 8 }; // Global code will be evaluated once. turtlelib_init(); const turtle = new ObservableTurtle(); const sls = FlyEyeFactory( turtle, gridSize,// gridSize, [projectionMargin-100, projectionMargin-100], //gridTopLeft, [100-projectionMargin, 100-projectionMargin], //gridBottomRight, projectionMargin, //gridMargin, [-captureWidth/2, -captureWidth/2], //sampleTopLeft, [captureWidth/2, captureWidth/2], //sampleBottomRight, segmentCapture//sampleTileSizeFraction ); turtle.penup(); turtle.goto(-50,-20); turtle.pendown(); const angle = 147; // min=65 max = 180 step = 1 // The walk function will be called until it returns false. function walk(i) { turtle.forward(100); turtle.right(angle); return i < 400; } function FlyEyeFactory(turtle, gridSize, gridTopLeft, gridBottomRight, gridMargin, sampleTopLeft, sampleBottomRight, sampleTileSizeFraction) { const sampleWidth = sampleBottomRight[0] - sampleTopLeft[0]; const sampleHeight = sampleBottomRight[1] - sampleTopLeft[1]; const tileWidth = sampleWidth * sampleTileSizeFraction; const tileHeight = sampleHeight * sampleTileSizeFraction; const sampleCenterTopLeft = V.add(sampleTopLeft, [tileWidth/2, tileHeight/2]); const columnIncrement = gridSize == 1? 0: (sampleWidth - tileWidth) / (gridSize - 1); const rowIncrement = gridSize == 1? 0: (sampleHeight - tileHeight) / (gridSize - 1); const gridWidth = gridBottomRight[0] - gridTopLeft[0]; const gridHeight = gridBottomRight[1] - gridTopLeft[1]; const gridTileWidth = (gridWidth - gridMargin * Math.max(1, gridSize - 1)) / gridSize; const gridTileHeight = (gridHeight - gridMargin * Math.max(1, gridSize - 1)) / gridSize; const flyEyeSegments = []; for(let c = 0; c < gridSize; c++) { for(let r = 0; r < gridSize; r++) { flyEyeSegments.push( new SquareLens( turtle, [ V.add(sampleTopLeft, [columnIncrement * c, rowIncrement * r]), V.add( V.add(sampleTopLeft, [columnIncrement * c, rowIncrement * r]), [tileWidth, tileHeight] ) ], [ V.add(gridTopLeft, [c * (gridTileWidth + gridMargin), r * (gridTileHeight + gridMargin)]), V.add( V.add(gridTopLeft, [c * (gridTileWidth + gridMargin), r * (gridTileHeight + gridMargin)]), [gridTileWidth, gridTileHeight] ) ], (c == 0? GRID.LEFT: 0) | (r == 0? GRID.TOP: 0) | (c == gridSize - 1? GRID.RIGHT: 0) | (r == gridSize - 1? GRID.BOTTOM: 0) ) ); } } return flyEyeSegments; } function ObservableTurtle(x, y) { class ObservableTurtle extends Turtle { constructor(x, y) { super(x, y); this.observers = []; } goto(x, y) { const isDown = this.isdown(); const from = this.pos(); this.up(); super.goto(x, y); if(isDown) { this.notifyObservers(from, this.pos()); this.down(); } } registerObserver(o) { return this.observers.push(o) - 1; } notifyObservers(...data) { this.observers.forEach(o => o.update(...data)) } } return new ObservableTurtle(x, y); } function SquareLens(turtleToObserve, lookAt, projectTo, edge = 0, projector = null) { class SquareLens { constructor(turtleToObserve, lookAt, projectTo, bottomOrRight, projector) { this.turtleToObserve = turtleToObserve; this.turtleToObserve.registerObserver(this); this.edge = edge; this.projector = projector == null? new Turtle(): projector; this.lookAt = lookAt; this.projectTo = projectTo; this.midLookAt = V.add(this.lookAt[0], V.scale(V.sub(...this.lookAt.map(l => l).reverse()), .5)); this.midProjectTo = V.add(this.projectTo[0], V.scale(V.sub(...this.projectTo.map(l => l).reverse()), .5)); this.lookAtBox = [[0, 0], [1, 0], [1, 1], [0, 1]].map(e => e.map((w, i) => this.lookAt[e[i]][i])); this.lookAtBoxEdges = this.lookAtBox.map((e, i, a) => [e, V.sub(a[(i+1)%a.length], e)]); this.ratios = lookAt[0].map((e, i, a) => (projectTo[1][i] - projectTo[0][i])/(lookAt[1][i] - lookAt[0][i])); } update(...data) { let from = data[0]; let to = data[1]; const intersections = this.lookAtBoxEdges.map(edge => Intersection.info(...edge, data[0], V.sub(to, from))).filter(int => int !== false && (0 <= int[1] && int[1] < 1 && 0 <= int[2] && int[2] <= 1)); const insiders = data.map(pt => Intersection.inside(this.lookAtBox, pt)); const collinear = (() => { //check if segment from-to overlaps with border (collinear) if(from[0] == to[0]) { //vertical line //make the line go bottom let fromY = Math.min(from[1], to[1]); let toY = Math.max(from[1], to[1]); if(fromY < this.lookAt[0][1] && toY < this.lookAt[0][1] || fromY > this.lookAt[1][1] && toY > this.lookAt[1][1]) { return false; } if(from[0] != this.lookAt[0][0] && from[0] != this.lookAt[1][0]) { return false; } if(from[0] == this.lookAt[1][0] &&//right lookat (this.edge & GRID.RIGHT) == 0) { //if it's not the right lens return false; //skip collinear } if(fromY <= this.lookAt[0][1]) { from = [from[0], this.lookAt[0][1]]; } if(lookAt[1][1] <= toY) { to = [from[0], this.lookAt[1][1]]; } return true; } else if (from[1] == to[1]) { //horizontal line //make the line go right let fromX = Math.min(from[0], to[0]); let toX = Math.max(from[0], to[0]); if(fromX < this.lookAt[0][0] && toX < this.lookAt[0][0] || fromX > this.lookAt[1][0] && toX > this.lookAt[1][0]) { return false; } if(from[1] != this.lookAt[0][1] && from[1] != this.lookAt[1][1]) { return false; } if(from[1] == this.lookAt[1][1] &&//bottom lookat (this.edge & GRID.BOTTOM) == 0) { //if it's not the bottom lens return false; //skip collinear } if(fromX <= this.lookAt[0][0]) { from = [this.lookAt[0][0], from[1]]; } if(lookAt[1][0] <= toX) { to = [this.lookAt[1][0], from[1]]; } return true; } return false; })(); if(!collinear && !insiders.every(i => i)) { //if not both inside if(intersections.length == 0) { //and not intersections return; //bail } intersections.sort((a, b) => a[2] < b[2]? -1: 1); if(insiders.every(i => !i)) { //both outside from = intersections[0][0]; to = intersections[intersections.length - 1][0]; } else { //one inside if(insiders[0]) { to = intersections[intersections.length - 1][0]; } else { from = intersections[0][0]; } } } this.projector.jump(V.add(V.mul(V.sub(from, this.midLookAt), this.ratios), this.midProjectTo)); this.projector.goto(V.add(V.mul(V.sub(to, this.midLookAt), this.ratios), this.midProjectTo)); } } return new SquareLens(turtleToObserve, lookAt, projectTo, edge, projector); } // 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_ns_c5f8fa95ed_Jurgen_Intersection(); } // 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 // Turtlelib Jurgen Intersection v 4 - start - {"id":"c5f8fa95ed","package":"Jurgen","name":"Intersection","version":"4"} function turtlelib_ns_c5f8fa95ed_Jurgen_Intersection() { /////////////////////////////////////////////////////////////// // Intersection functions - Created by Jurgen Westerhof 2024 // /////////////////////////////////////////////////////////////// class Intersection { //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; } static circles(centerA, radiusA, centerB, radiusB) {const result = {intersect_count: 0,intersect_occurs: true,one_is_in_other: false,are_equal: false,point_1: [null, null],point_2: [null, null],};const dx = centerB[0] - centerA[0];const dy = centerB[1] - centerA[1];const dist = Math.hypot(dy, dx);if (dist > radiusA + radiusB) {result.intersect_occurs = false;}if (dist < Math.abs(radiusA - radiusB) && !N.approx(dist, Math.abs(radiusA - radiusB))) {result.intersect_occurs = false;result.one_is_in_other = true;}if (V.approx(centerA, centerB) && radiusA === radiusB) {result.are_equal = true;}if (result.intersect_occurs) {const centroid = (radiusA**2 - radiusB**2 + dist * dist) / (2.0 * dist);const x2 = centerA[0] + (dx * centroid) / dist;const y2 = centerA[1] + (dy * centroid) / dist;const prec = 10000;const h = (Math.round(radiusA**2 * prec)/prec - Math.round(centroid**2 * prec)/prec)**.5;const rx = -dy * (h / dist);const ry = dx * (h / dist);result.point_1 = [x2 + rx, y2 + ry];result.point_2 = [x2 - rx, y2 - ry];if (result.are_equal) {result.intersect_count = Infinity;} else if (V.equals(result.point_1, result.point_2)) {result.intersect_count = 1;} else {result.intersect_count = 2;}}return result;} } this.Intersection = Intersection; } // Turtlelib Jurgen Intersection v 4 - end