Ponzo 🚂

An #illusion described at en.wikipedia.org/wiki/ponzo_illusion

The showHelperLines control will toggle the drawing of lines that help you see through the illusion that the white lines seem not to have the same length caused by the railroad track in perspective that they're drawn on.

Log in to post a comment.

const showHelperLines = 0;//min=0 max=1 step=1 (No, Yes)

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

// Global code will be evaluated once.
const bales = Bales(3);
const polygons = new Polygons();

const length2 = (a) => Math.sqrt(lengthsq2(a));
const lengthsq2 = (a) => dot2(a,a);
const dot2 = (a,b) => a[0]*b[0]+a[1]*b[1];
const add2 = (a, b) => [a[0]+b[0], a[1]+b[1]];
const sub2 = (a,b) => [a[0]-b[0],a[1]-b[1]];
const normalize2 = (a) => { const length = length2(a); return scale2(a,length<0.0001?1:1/length); }
const scale2 = (a, s) => [a[0]*s, a[1]*s];
const rot2 = (a) => [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a)];
const trans2 = (m, a) => [m[0]*a[0]+m[2]*a[1], m[1]*a[0]+m[3]*a[1]];

const perspective = [0, -105];

for(let i = -1; i < 2; i+=2) {
    let p = polygons.create();
    p.addPoints([-40, i*82], [40, i*82], [40, i*84], [-40, i*84]);
    polygons.draw(bales[2], p);
    
    if(showHelperLines) {
        p = polygons.create();
        p.addPoints([i*40, -100], [i*40, 100], [i*40.5, 100],[i*40.5, -100]);
        polygons.draw(bales[2], p);
    }

    p = polygons.create();
    p.addPoints([i * 100, 200], perspective, [i * 90, 200]);
    p.addHatching(1, .15);
    polygons.draw(bales[0], p);
}

for(let i = 0, y = 0; y < 200; i++, y = (.05*i)**8) {
    const left = [-y * 100 / 220, y - 105];
    const right = trans2([-1, 0, 0, 1], left);
    
    const dir = (v, w) => scale2(normalize2(sub2(v, perspective)), w);

    const p = polygons.create();
    p.addPoints(left, right, add2(right, dir(right, y/30)), add2(left, dir(left, y/30)));
    p.addHatching(1, .15);
    polygons.draw(bales[0], p);
}

const p = polygons.create();
p.addPoints([-101, -101], [101, -101], [101, 101], [-101, 101]);
p.addHatching(1, .15);
polygons.draw(bales[1], p);

////////////////////////////////////////////////////////////////
// Bale utility code - Created by Jurgen Westerhof 2022
// https://turtletoy.net/turtle/beb59d67ae
// Abusing the opacity, usage:
//      Canvas.setpenopacity(1/paletteSize);
//      const bales = Bales(paletteSize); // Bales(count, includeFullTransparent = true, turtleClass = null)
// Then use bales[x] wherever you would use a turtle object to 'draw'
// in 'color' x (i.e Polygon hatching with a bale object and .15 interspacing)
//      bales[x].jump(0,0);
//      bales[x].goto(40,0);
////////////////////////////////////////////////////////////////
function Bale(n, turtleClass = null) {class Bale {constructor(n, turtleClass = null) { this.turtles = Array.apply(null,{length: n}).map(i => turtleClass == null? new Turtle(): new turtleClass()); }back(e)         { this.turtles.forEach(t => t.back(e)); return this; }backward(e)     { this.turtles.forEach(t => t.backward(e)); return this; }bk(e)           { this.turtles.forEach(t => t.bk(e)); return this; }fd(e)           { this.turtles.forEach(t => t.fd(e)); return this; }forward(e)      { this.turtles.forEach(t => t.forward(e)); return this; }left(e)         { this.turtles.forEach(t => t.left(e)); return this; }lt(e)           { this.turtles.forEach(t => t.lt(e)); return this; }right(e)        { this.turtles.forEach(t => t.right(e)); return this; }rt(e)           { this.turtles.forEach(t => t.rt(e)); return this; }seth(e)         { this.turtles.forEach(t => t.seth(e)); return this; }setheading(e)   { this.turtles.forEach(t => t.setheading(e)); return this; }setx(e)         { this.turtles.forEach(t => t.setx(e)); return this; }sety(e)         { this.turtles.forEach(t => t.sety(e)); return this; }setpos(x, y)        { this.turtles.forEach(t => t.setpos(x, y)); return this; }setposition(x, y)   { this.turtles.forEach(t => t.setposition(x, y)); return this; }toradians(e)    { this.turtles.forEach(t => t.toradians(e)); return this; }degrees(e)      { this.turtles.forEach(t => t.degrees(e)); return this; }goto(x, y)      { this.turtles.forEach(t => t.goto(x, y)); return this; }jmp(x, y)       { this.turtles.forEach(t => t.jmp(x, y)); return this; }jump(x, y)      { this.turtles.forEach(t => t.jump(x, y)); return this; }circle(radius, extent, steps) { this.turtles.map(t => t.circle(radius, extent, steps)); return this; }clone()         { let b = new Bale(this.turtles.length); this.turtles.forEach((t, k) => b.turtles[k] = t.clone()); return b; }h()             { return this.turtles.length == 0? null: this.turtles[0].h(); }heading()       { return this.turtles.length == 0? null: this.turtles[0].heading(); }home()          { this.turtles.forEach(t => t.home()); return this; }isdown()        { return this.turtles.length == 0? null: this.turtles[0].isdown(); }pos()           { return this.turtles.length == 0? null: this.turtles[0].pos(); }position()      { return this.turtles.length == 0? null: this.turtles[0].position(); }pd()            { this.turtles.forEach(t => t.pd()); return this; }pendown()       { this.turtles.forEach(t => t.pendown()); return this; }penup()         { this.turtles.forEach(t => t.penup()); return this; }pu()            { this.turtles.forEach(t => t.pu()); return this; }down()          { this.turtles.forEach(t => t.down()); return this; }up()            { this.turtles.forEach(t => t.up()); return this; }radians()       { this.turtles.forEach(t => t.radians()); return this; }x()             { return this.turtles.length == 0? null: this.turtles[0].x(); }xcor()          { return this.turtles.length == 0? null: this.turtles[0].xcor(); }y()             { return this.turtles.length == 0? null: this.turtles[0].y(); }ycor()          { return this.turtles.length == 0? null: this.turtles[0].ycor(); }set(key, value) { this.turtles.forEach(i => i[key] = value); return this; }get(key) { return this.turtles.length == 0? null: this.turtles[0][key]; }}return new Bale(n, turtleClass);}
function Bales(count, includeFullTransparent = true, turtleClass = null) { if(count == 1) return [new Bale(1, turtleClass)]; const getExponent = (base, target) => Math.log(target) / Math.log(base); const baleSize = count - (includeFullTransparent?1:0); const n = Array.apply(null,{length: baleSize}).map((v,k) => Math.round(getExponent(1 - 1/count, 1 - (count - k == count?.99:(baleSize - k)/baleSize)))); if(includeFullTransparent) n.push(0); return n.map(i => new Bale(i, turtleClass));}

////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// (Polygon binning by Lionel Lemarie 2021)
// https://turtletoy.net/turtle/a5befa1f8d
////////////////////////////////////////////////////////////////
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){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])]}};return{list:()=>t,create:()=>new n,draw:(n,h,o=!0)=>{reducedPolygonList=function(n){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]||(h[s]=1)})}}return Array.from(Object.keys(h),s=>t[s])}(h.aabb);for(let t=0;t<reducedPolygonList.length&&h.boolean(reducedPolygonList[t]);t++);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)}}}