// created by florian berger (flockaroo) - 2018 // License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. // derived from "moebius gears" https://www.shaderoo.org/?shader=3GQsG3 // by (sloppily) sorting polys in z-direction (center only) // and then using reinder's occlusion magic from "Cubic space division #2" Canvas.setpenopacity(1.); // Global code will be evaluated once. const turtle = new Turtle(); const polygonList = []; const quads = []; const PI2 = Math.PI*2.0; const GEAR_NUM = 9; const TOOTH_NUM = 15; const GEAR_W = .25; const GEAR_H = .2; const GEAR_INNER = .65; const MOEB_R = 2.; const TSPEED = 1.; const RSPEED = 1.; const iTime = 0.011; // hmm, some occlusion errors occur on some angles in iTime=0 function mcos(x) { return Math.cos(x); } function msin(x) { return Math.sin(x); } function cos2(x) { return [Math.cos(x[0]),Math.cos(x[1])]; } function sin2(x) { return [Math.sin(x[0]),Math.sin(x[1])]; } function SC(x) { return [Math.sin(x),Math.cos(x)]; } function add3(a,b) { return [a[0]+b[0],a[1]+b[1],a[2]+b[2]]; } function sub3(a,b) { return [a[0]-b[0],a[1]-b[1],a[2]-b[2]]; } function dot3(a,b) { return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; } function scale3(a,b) { return [a[0]*b,a[1]*b,a[2]*b]; } function mymix(a,b,f) { return a*(1.0-f)+b*f; } function length3(a) { return Math.sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]); } function normalize3(a) { return scale3(a,1.0/length3(a)); } function cross(a,b) { return [ a[1]*b[2]-b[1]*a[2], a[2]*b[0]-b[2]*a[0], a[0]*b[1]-b[0]*a[1] ]; } function inverseQuat(q) { //return vec4(-q.xyz,q.w)/length(q); // if already normalized this is enough return [-q[0],-q[1],-q[2],q[3]]; } function multQuat(a,b) { //return vec4(cross(a.xyz,b.xyz) + a.xyz*b.w + b.xyz*a.w, a.w*b.w - dot(a.xyz,b.xyz)); var v=add3(add3(cross(a,b), scale3(a,b[3])), scale3(b,a[3])); var w=a[3]*b[3]-dot3(a,b); return [v[0],v[1],v[2],w]; } function transformVecByQuat( v, q ) { //return v + 2.0 * cross( q.xyz, cross( q.xyz, v ) + q.w*v ); return add3(v, scale3(cross( q, add3(cross( q, v ) , scale3(v,q[3]) )) ,2.0)); } function axAng2Quat(ax, ang) { //return vec4(normalize(ax),1)*sin(vec2(ang*.5)+vec2(0,PI2*.25)).xxxy; var s=sin2([ang*0.5,ang*0.5+PI2*0.25]); var nax=normalize3(ax); return [nax[0]*s[0],nax[1]*s[0],nax[2]*s[0],s[1]]; } const GEAR_SEG_NUM = 60; const GEAR_TRI_NUM = (GEAR_SEG_NUM*8); const inuAll=[ [1,0,1], [1,1,1], [0,0,1], [1,1,1], [0,1,1], [0,0,1], [0,0,1], [0,1,1], [0,0,0], [0,1,1], [0,1,0], [0,0,0], [0,0,0], [0,1,0], [1,0,0], [0,1,0], [1,1,0], [1,0,0], [1,0,0], [1,1,0], [1,0,1], [1,1,0], [1,1,1], [1,0,1] ]; function gearPos(idx) { var idx24=idx%(8*3); var ang0=PI2/(GEAR_SEG_NUM)*Math.floor(idx/24); var ang1=PI2/(GEAR_SEG_NUM)*Math.floor(idx/24+1); var ri = GEAR_INNER; var rin = GEAR_INNER; var ro = 1.0+GEAR_H*.5*Math.cos(ang0*(TOOTH_NUM)); var ron = 1.0+GEAR_H*.5*Math.cos(ang1*(TOOTH_NUM)); var w = GEAR_W; var quadIdx=Math.floor(idx24/6); var inu=inuAll[idx24]; var r = mymix(mymix(ro,ron,inu[1]),mymix(ri,rin,inu[1]),inu[0]); var ang = mymix(ang0,ang1,inu[1]); var z = mymix(-w,w,inu[2]); return [msin(ang)*r,mcos(ang)*r,z]; } function gearsTri(idx) { var gear=Math.floor(idx/(GEAR_TRI_NUM*3)); var gidx=Math.floor(idx%(GEAR_TRI_NUM*3)); var dang=PI2/(GEAR_NUM-.5); var R=MOEB_R; var ang=gear*dang; if(ang>PI2*2.) ang-=PI2*2.; if(ang<0.0) ang+=PI2*2.; var s=(Math.floor(ang/dang+.01)%2)*2.-1.; var r=R*Math.tan(dang*.5); // radial offset of gears is different depending if lying or upright //R*(1+tan(dang*.5)*GEAR_W*.5) in upright case //R/cos(dang*.5) in lying case var pos=scale3([Math.cos(ang),Math.cos(ang+PI2*.25),0], mymix(R*(1.+Math.tan(dang*.5)*GEAR_W*.5), R/Math.cos(dang*.5), -Math.cos(ang*.5+iTime*TSPEED)*.5+.5 )*1.02); var quat=[0,.707107,0,.707107]; quat=multQuat(quat,axAng2Quat([1,0,0],2.*Math.sin(iTime*1.5*RSPEED)*s)); quat=multQuat(quat,axAng2Quat([0,1,0],ang*.25+iTime*.5*TSPEED)); quat=multQuat(quat,axAng2Quat([0,0,1],ang)); if(gear>GEAR_NUM*2) return [0,0,0]; return add3(pos,transformVecByQuat(scale3(gearPos(gidx),r),inverseQuat(quat))); } function rotX(ph,v) { return [ v[0],v[1]*mcos(ph)+v[2]*msin(ph), v[2]*mcos(ph)-v[1]*msin(ph) ]; } function project(p) { p[2]+=180; return [p[0]/p[2]*180.,p[1]/p[2]*180.,p[2]]; } function insertQuad(p0,p1,p2,p3) { var z = p0[2]+p1[2]+p2[2]+p3[2]; var idx=0; for(idx=0;idx<quads.length && quads[idx+8]<z;idx+=9); // hmm, why is the one below not working... !? //for(var i=0;i<quads.length;i+=9) { // if(quads[i+8]>z) { idx=i; break; } //} quads.splice(idx, 0, p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], p3[0], p3[1], z); } function walk(i) { var num = (2*GEAR_NUM-1)*15*16; if(i==0){ for(let j=0;j<num;j++) { var p0=gearsTri(j*6); var p1=gearsTri(j*6+1); var p2=gearsTri(j*6+2); var p3=gearsTri(j*6+4); p0=scale3(p0,28.0); p1=scale3(p1,28.0); p2=scale3(p2,28.0); p3=scale3(p3,28.0); p0=rotX(1.,p0); p1=rotX(1.,p1); p2=rotX(1.,p2); p3=rotX(1.,p3); p0=project(p0); p1=project(p1); p2=project(p2); p3=project(p3); if(cross(sub3(p1,p0),sub3(p2,p0))[2]<0.0) { insertQuad(p0,p2,p3,p1); } } } var p0=[quads[i*9+0],quads[i*9+1]]; var p1=[quads[i*9+2],quads[i*9+3]]; var p2=[quads[i*9+4],quads[i*9+5]]; var p3=[quads[i*9+6],quads[i*9+7]]; const p = new Polygon(); p.cp.push([p0[0], p0[1]]); p.cp.push([p1[0], p1[1]]); p.cp.push([p2[0], p2[1]]); p.cp.push([p3[0], p3[1]]); p.addOutline(0); drawPolygon(turtle, p); /*turtle.penup(p0); turtle.goto(p0); turtle.pendown(p0); turtle.goto(p1); turtle.goto(p2); turtle.goto(p3); turtle.goto(p0);*/ return i <= num/1.8; } //////////////////////////// // reinder's occlusion code parts from "Cubic space division #2" //////////////////////////// function drawPolygon(turtle, p) { let vis = true; for (let j=0; j<polygonList.length; j++) { if(!p.boolean(polygonList[j])) { vis = false; break; } } if (vis) { p.draw(turtle, 0); polygonList.push(p); } } // polygon functions function LineSegment(p1, p2) { this.p1 = p1; this.p2 = p2; } function Polygon() { this.cp = []; // clip path: array of [x,y] pairs this.dp = []; // 2d line to draw: array of linesegments } Polygon.prototype.addOutline = function(s=0) { for (let i=s, l=this.cp.length; i<l; i++) { this.dp.push(new LineSegment(this.cp[i], this.cp[(i+1)%l])); } } Polygon.prototype.createPoly = function(x,y,c,r,a) { this.cp = []; for (let i=0; i<c; i++) { this.cp.push( [x + Math.sin(i*Math.PI*2/c+a) * r, y + Math.cos(i*Math.PI*2/c+a) * r] ); } } Polygon.prototype.draw = function(t, inp=0) { if (this.dp.length ==0) { return; } for (let i=0, l=this.dp.length; i<l; i++) { const d = this.dp[i]; if (!vec2_equal(d.p1, t.pos())) { t.penup(); t.goto([d.p1[0]+inp*(Math.random()-.5), d.p1[1]+inp*(Math.random()-.5)]); t.pendown(); } t.goto([d.p2[0]+inp*(Math.random()-.5), d.p2[1]+inp*(Math.random()-.5)]); } } Polygon.prototype.inside = function(p) { // find number of i ntersection points from p to far away // if even your outside const p1 = [0.1, -1000]; let int = 0; for (let i=0, l=this.cp.length; i<l; i++) { if (vec2_find_segment_intersect(p, p1, this.cp[i], this.cp[(i+1)%l])) { int ++; } } return int & 1; } Polygon.prototype.boolean = function(p, diff = true) { // very naive polygon diff algorithm - made this up myself const ndp = []; for (let i=0, l=this.dp.length; i<l; i++) { const ls = this.dp[i]; // find all intersections with clip path const int = []; for (let j=0, cl=p.cp.length; j<cl; j++) { const pint = vec2_find_segment_intersect(ls.p1,ls.p2,p.cp[j],p.cp[(j+1)%cl]); if (pint) { int.push(pint); } } if (int.length == 0) { // 0 intersections, inside or outside? if (diff == !p.inside(ls.p1)) { ndp.push(ls); } } else { int.push(ls.p1); int.push(ls.p2); // order intersection points on line ls.p1 to ls.p2 const cmp = [ls.p2[0]-ls.p1[0], ls.p2[1]-ls.p1[1]]; int.sort( (a,b) => { const db = vec2_dot([b[0]-ls.p1[0], b[1]-ls.p1[1]], cmp); const da = vec2_dot([a[0]-ls.p1[0], a[1]-ls.p1[1]], cmp); return da - db; }); for (let j=0; j<int.length-1; j++) { if (!vec2_equal(int[j], int[j+1])) { if (diff == !p.inside([(int[j][0]+int[j+1][0])/2,(int[j][1]+int[j+1][1])/2])) { ndp.push(new LineSegment(int[j], int[j+1])); } } } } } this.dp = ndp; return this.dp.length > 0; } // vec functions const vec2_equal = (a,b) => vec2_dist_sqr(a,b) < 0.01; const vec2_dot = (a, b) => a[0]*b[0]+a[1]*b[1]; const vec2_dist_sqr = (a, b) => (a[0]-b[0])*(a[0]-b[0]) + (a[1]-b[1])*(a[1]-b[1]); //port of http://paulbourke.net/geometry/pointlineplane/Helpers.cs function vec2_find_segment_intersect(l1p1, l1p2, l2p1, l2p2) { const d = (l2p2[1] - l2p1[1]) * (l1p2[0] - l1p1[0]) - (l2p2[0] - l2p1[0]) * (l1p2[1] - l1p1[1]); const n_a = (l2p2[0] - l2p1[0]) * (l1p1[1] - l2p1[1]) - (l2p2[1] - l2p1[1]) * (l1p1[0] - l2p1[0]); const n_b = (l1p2[0] - l1p1[0]) * (l1p1[1] - l2p1[1]) - (l1p2[1] - l1p1[1]) * (l1p1[0] - l2p1[0]); if (d == 0) { return false; } const ua = n_a / d; const ub = n_b / d; if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) { return [l1p1[0] + (ua * (l1p2[0] - l1p1[0])), l1p1[1] + (ua * (l1p2[1] - l1p1[1])) ]; } return false; }