Created
October 22, 2018 08:55
-
-
Save siriux/d5acb203e6ba181a17cd9da9bfa381b6 to your computer and use it in GitHub Desktop.
GPU rasterizer simulator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This is just to simulate and idea for a gpu algorithm to render vector curves with border and dashes. | |
Some code taken and modified from Pomax: https://github.com/Pomax |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Bezier=function(t){function n(i){if(r[i])return r[i].exports;var e=r[i]={exports:{},id:i,loaded:!1};return t[i].call(e.exports,e,e.exports,n),e.loaded=!0,e.exports}var r={};return n.m=t,n.c=r,n.p="",n(0)}([function(t,n,r){"use strict";t.exports=r(1)},function(t,n,r){"use strict";var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};!function(){function n(t,n,r,i,e){"undefined"==typeof e&&(e=.5);var o=y.projectionratio(e,t),s=1-o,u={x:o*n.x+s*i.x,y:o*n.y+s*i.y},a=y.abcratio(e,t),f={x:r.x+(r.x-u.x)/a,y:r.y+(r.y-u.y)/a};return{A:f,B:r,C:u}}var e=Math.abs,o=Math.min,s=Math.max,u=Math.cos,a=Math.sin,f=Math.acos,c=Math.sqrt,h=Math.PI,x={x:0,y:0,z:0},y=r(2),p=r(3),l=function(t){var n=t&&t.forEach?t:[].slice.call(arguments),r=!1;if("object"===i(n[0])){r=n.length;var o=[];n.forEach(function(t){["x","y","z"].forEach(function(n){"undefined"!=typeof t[n]&&o.push(t[n])})}),n=o}var s=!1,u=n.length;if(r){if(r>4){if(1!==arguments.length)throw new Error("Only new Bezier(point[]) is accepted for 4th and higher order curves");s=!0}}else if(6!==u&&8!==u&&9!==u&&12!==u&&1!==arguments.length)throw new Error("Only new Bezier(point[]) is accepted for 4th and higher order curves");var a=!s&&(9===u||12===u)||t&&t[0]&&"undefined"!=typeof t[0].z;this._3d=a;for(var f=[],c=0,h=a?3:2;c<u;c+=h){var x={x:n[c],y:n[c+1]};a&&(x.z=n[c+2]),f.push(x)}this.order=f.length-1,this.points=f;var p=["x","y"];a&&p.push("z"),this.dims=p,this.dimlen=p.length,function(t){for(var n=t.order,r=t.points,i=y.align(r,{p1:r[0],p2:r[n]}),o=0;o<i.length;o++)if(e(i[o].y)>1e-4)return void(t._linear=!1);t._linear=!0}(this),this._t1=0,this._t2=1,this.update()};l.fromSVG=function(t){var n=t.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/g).map(parseFloat),r=/[cq]/.test(t);return r?(n=n.map(function(t,r){return r<2?t:t+n[r%2]}),new l(n)):new l(n)},l.quadraticFromPoints=function(t,r,i,e){if("undefined"==typeof e&&(e=.5),0===e)return new l(r,r,i);if(1===e)return new l(t,r,r);var o=n(2,t,r,i,e);return new l(t,o.A,i)},l.cubicFromPoints=function(t,r,i,e,o){"undefined"==typeof e&&(e=.5);var s=n(3,t,r,i,e);"undefined"==typeof o&&(o=y.dist(r,s.C));var u=o*(1-e)/e,a=y.dist(t,i),f=(i.x-t.x)/a,c=(i.y-t.y)/a,h=o*f,x=o*c,p=u*f,v=u*c,d={x:r.x-h,y:r.y-x},m={x:r.x+p,y:r.y+v},g=s.A,z={x:g.x+(d.x-g.x)/(1-e),y:g.y+(d.y-g.y)/(1-e)},b={x:g.x+(m.x-g.x)/e,y:g.y+(m.y-g.y)/e},_={x:t.x+(z.x-t.x)/e,y:t.y+(z.y-t.y)/e},w={x:i.x+(b.x-i.x)/(1-e),y:i.y+(b.y-i.y)/(1-e)};return new l(t,_,w,i)};var v=function(){return y};l.getUtils=v,l.prototype={getUtils:v,valueOf:function(){return this.toString()},toString:function(){return y.pointsToString(this.points)},toSVG:function(t){if(this._3d)return!1;for(var n=this.points,r=n[0].x,i=n[0].y,e=["M",r,i,2===this.order?"Q":"C"],o=1,s=n.length;o<s;o++)e.push(n[o].x),e.push(n[o].y);return e.join(" ")},update:function(){this.dpoints=[];for(var t=this.points,n=t.length,r=n-1;n>1;n--,r--){for(var i,e=[],o=0;o<r;o++)i={x:r*(t[o+1].x-t[o].x),y:r*(t[o+1].y-t[o].y)},this._3d&&(i.z=r*(t[o+1].z-t[o].z)),e.push(i);this.dpoints.push(e),t=e}this.computedirection()},computedirection:function(){var t=this.points,n=y.angle(t[0],t[this.order],t[1]);this.clockwise=n>0},length:function(){return y.length(this.derivative.bind(this))},_lut:[],getLUT:function(t){if(t=t||100,this._lut.length===t)return this._lut;this._lut=[];for(var n=0;n<=t;n++)this._lut.push(this.compute(n/t));return this._lut},on:function(t,n){n=n||5;for(var r,i=this.getLUT(),e=[],o=0,s=0;s<i.length;s++)r=i[s],y.dist(r,t)<n&&(e.push(r),o+=s/i.length);return!!e.length&&(o/=e.length)},project:function(t){var n=this.getLUT(),r=n.length-1,i=y.closest(n,t),e=i.mdist,o=i.mpos;if(0===o||o===r){var s=o/r,u=this.compute(s);return u.t=s,u.d=e,u}var a,s,f,c,h=(o-1)/r,x=(o+1)/r,p=.1/r;for(e+=1,s=h,a=s;s<x+p;s+=p)f=this.compute(s),c=y.dist(t,f),c<e&&(e=c,a=s);return f=this.compute(a),f.t=a,f.d=e,f},get:function(t){return this.compute(t)},point:function(t){return this.points[t]},compute:function(t){if(0===t)return this.points[0];if(1===t)return this.points[this.order];var n=this.points,r=1-t;if(1===this.order)return f={x:r*n[0].x+t*n[1].x,y:r*n[0].y+t*n[1].y},this._3d&&(f.z=r*n[0].z+t*n[1].z),f;if(this.order<4){var i,e,o,s=r*r,u=t*t,a=0;2===this.order?(n=[n[0],n[1],n[2],x],i=s,e=r*t*2,o=u):3===this.order&&(i=s*r,e=s*t*3,o=r*u*3,a=t*u);var f={x:i*n[0].x+e*n[1].x+o*n[2].x+a*n[3].x,y:i*n[0].y+e*n[1].y+o*n[2].y+a*n[3].y};return this._3d&&(f.z=i*n[0].z+e*n[1].z+o*n[2].z+a*n[3].z),f}for(var c=JSON.parse(JSON.stringify(this.points));c.length>1;){for(var h=0;h<c.length-1;h++)c[h]={x:c[h].x+(c[h+1].x-c[h].x)*t,y:c[h].y+(c[h+1].y-c[h].y)*t},"undefined"!=typeof c[h].z&&(c[h]=c[h].z+(c[h+1].z-c[h].z)*t);c.splice(c.length-1,1)}return c[0]},raise:function(){for(var t,n,r,i=this.points,e=[i[0]],o=i.length,t=1;t<o;t++)n=i[t],r=i[t-1],e[t]={x:(o-t)/o*n.x+t/o*r.x,y:(o-t)/o*n.y+t/o*r.y};return e[o]=i[o-1],new l(e)},derivative:function(t){var n,r,i=1-t,e=0,o=this.dpoints[0];2===this.order&&(o=[o[0],o[1],x],n=i,r=t),3===this.order&&(n=i*i,r=i*t*2,e=t*t);var s={x:n*o[0].x+r*o[1].x+e*o[2].x,y:n*o[0].y+r*o[1].y+e*o[2].y};return this._3d&&(s.z=n*o[0].z+r*o[1].z+e*o[2].z),s},inflections:function(){return y.inflections(this.points)},normal:function(t){return this._3d?this.__normal3(t):this.__normal2(t)},__normal2:function(t){var n=this.derivative(t),r=c(n.x*n.x+n.y*n.y);return{x:-n.y/r,y:n.x/r}},__normal3:function(t){var n=this.derivative(t),r=this.derivative(t+.01),i=c(n.x*n.x+n.y*n.y+n.z*n.z),e=c(r.x*r.x+r.y*r.y+r.z*r.z);n.x/=i,n.y/=i,n.z/=i,r.x/=e,r.y/=e,r.z/=e;var o={x:r.y*n.z-r.z*n.y,y:r.z*n.x-r.x*n.z,z:r.x*n.y-r.y*n.x},s=c(o.x*o.x+o.y*o.y+o.z*o.z);o.x/=s,o.y/=s,o.z/=s;var u=[o.x*o.x,o.x*o.y-o.z,o.x*o.z+o.y,o.x*o.y+o.z,o.y*o.y,o.y*o.z-o.x,o.x*o.z-o.y,o.y*o.z+o.x,o.z*o.z],a={x:u[0]*n.x+u[1]*n.y+u[2]*n.z,y:u[3]*n.x+u[4]*n.y+u[5]*n.z,z:u[6]*n.x+u[7]*n.y+u[8]*n.z};return a},hull:function(t){var n,r=this.points,i=[],e=[],o=0,s=0,u=0;for(e[o++]=r[0],e[o++]=r[1],e[o++]=r[2],3===this.order&&(e[o++]=r[3]);r.length>1;){for(i=[],s=0,u=r.length-1;s<u;s++)n=y.lerp(t,r[s],r[s+1]),e[o++]=n,i.push(n);r=i}return e},split:function(t,n){if(0===t&&n)return this.split(n).left;if(1===n)return this.split(t).right;var r=this.hull(t),i={left:new l(2===this.order?[r[0],r[3],r[5]]:[r[0],r[4],r[7],r[9]]),right:new l(2===this.order?[r[5],r[4],r[2]]:[r[9],r[8],r[6],r[3]]),span:r};if(i.left._t1=y.map(0,0,1,this._t1,this._t2),i.left._t2=y.map(t,0,1,this._t1,this._t2),i.right._t1=y.map(t,0,1,this._t1,this._t2),i.right._t2=y.map(1,0,1,this._t1,this._t2),!n)return i;n=y.map(n,t,1,0,1);var e=i.right.split(n);return e.left},extrema:function(){var t,n,r=this.dims,i={},e=[];return r.forEach(function(r){n=function(t){return t[r]},t=this.dpoints[0].map(n),i[r]=y.droots(t),3===this.order&&(t=this.dpoints[1].map(n),i[r]=i[r].concat(y.droots(t))),i[r]=i[r].filter(function(t){return t>=0&&t<=1}),e=e.concat(i[r].sort(y.numberSort))}.bind(this)),e=e.sort(y.numberSort).filter(function(t,n){return e.indexOf(t)===n}),i.values=e,i},bbox:function(){var t=this.extrema(),n={};return this.dims.forEach(function(r){n[r]=y.getminmax(this,r,t[r])}.bind(this)),n},overlaps:function(t){var n=this.bbox(),r=t.bbox();return y.bboxoverlap(n,r)},offset:function(t,n){if("undefined"!=typeof n){var r=this.get(t),i=this.normal(t),e={c:r,n:i,x:r.x+i.x*n,y:r.y+i.y*n};return this._3d&&(e.z=r.z+i.z*n),e}if(this._linear){var o=this.normal(0),s=this.points.map(function(n){var r={x:n.x+t*o.x,y:n.y+t*o.y};return n.z&&i.z&&(r.z=n.z+t*o.z),r});return[new l(s)]}var u=this.reduce();return u.map(function(n){return n.scale(t)})},simple:function(){if(3===this.order){var t=y.angle(this.points[0],this.points[3],this.points[1]),n=y.angle(this.points[0],this.points[3],this.points[2]);if(t>0&&n<0||t<0&&n>0)return!1}var r=this.normal(0),i=this.normal(1),o=r.x*i.x+r.y*i.y;this._3d&&(o+=r.z*i.z);var s=e(f(o));return s<h/3},reduce:function(){var t,n,r=0,i=0,o=.01,s=[],u=[],a=this.extrema().values;for(a.indexOf(0)===-1&&(a=[0].concat(a)),a.indexOf(1)===-1&&a.push(1),r=a[0],t=1;t<a.length;t++)i=a[t],n=this.split(r,i),n._t1=r,n._t2=i,s.push(n),r=i;return s.forEach(function(t){for(r=0,i=0;i<=1;)for(i=r+o;i<=1+o;i+=o)if(n=t.split(r,i),!n.simple()){if(i-=o,e(r-i)<o)return[];n=t.split(r,i),n._t1=y.map(r,0,1,t._t1,t._t2),n._t2=y.map(i,0,1,t._t1,t._t2),u.push(n),r=i;break}r<1&&(n=t.split(r,1),n._t1=y.map(r,0,1,t._t1,t._t2),n._t2=t._t2,u.push(n))}),u},scale:function(t){var n=this.order,r=!1;if("function"==typeof t&&(r=t),r&&2===n)return this.raise().scale(r);var i=this.clockwise,e=r?r(0):t,o=r?r(1):t,s=[this.offset(0,10),this.offset(1,10)],u=y.lli4(s[0],s[0].c,s[1],s[1].c);if(!u)throw new Error("cannot scale this curve. Try reducing it first.");var a=this.points,f=[];return[0,1].forEach(function(t){var r=f[t*n]=y.copy(a[t*n]);r.x+=(t?o:e)*s[t].n.x,r.y+=(t?o:e)*s[t].n.y}.bind(this)),r?([0,1].forEach(function(e){if(2!==this.order||!e){var o=a[e+1],s={x:o.x-u.x,y:o.y-u.y},h=r?r((e+1)/n):t;r&&!i&&(h=-h);var x=c(s.x*s.x+s.y*s.y);s.x/=x,s.y/=x,f[e+1]={x:o.x+h*s.x,y:o.y+h*s.y}}}.bind(this)),new l(f)):([0,1].forEach(function(t){if(2!==this.order||!t){var r=f[t*n],i=this.derivative(t),e={x:r.x+i.x,y:r.y+i.y};f[t+1]=y.lli4(r,e,u,a[t+1])}}.bind(this)),new l(f))},outline:function(t,n,r,i){function e(t,n,r,i,e){return function(o){var s=i/r,u=(i+e)/r,a=n-t;return y.map(o,0,1,t+s*a,t+u*a)}}n="undefined"==typeof n?t:n;var o,s=this.reduce(),u=s.length,a=[],f=[],c=0,h=this.length(),x="undefined"!=typeof r&&"undefined"!=typeof i;s.forEach(function(o){_=o.length(),x?(a.push(o.scale(e(t,r,h,c,_))),f.push(o.scale(e(-n,-i,h,c,_)))):(a.push(o.scale(t)),f.push(o.scale(-n))),c+=_}),f=f.map(function(t){return o=t.points,o[3]?t.points=[o[3],o[2],o[1],o[0]]:t.points=[o[2],o[1],o[0]],t}).reverse();var l=a[0].points[0],v=a[u-1].points[a[u-1].points.length-1],d=f[u-1].points[f[u-1].points.length-1],m=f[0].points[0],g=y.makeline(d,l),z=y.makeline(v,m),b=[g].concat(a).concat([z]).concat(f),_=b.length;return new p(b)},outlineshapes:function(t,n,r){n=n||t;for(var i=this.outline(t,n).curves,e=[],o=1,s=i.length;o<s/2;o++){var u=y.makeshape(i[o],i[s-o],r);u.startcap.virtual=o>1,u.endcap.virtual=o<s/2-1,e.push(u)}return e},intersects:function(t,n){return t?t.p1&&t.p2?this.lineIntersects(t):(t instanceof l&&(t=t.reduce()),this.curveintersects(this.reduce(),t,n)):this.selfintersects(n)},lineIntersects:function(t){var n=o(t.p1.x,t.p2.x),r=o(t.p1.y,t.p2.y),i=s(t.p1.x,t.p2.x),e=s(t.p1.y,t.p2.y),u=this;return y.roots(this.points,t).filter(function(t){var o=u.get(t);return y.between(o.x,n,i)&&y.between(o.y,r,e)})},selfintersects:function(t){var n,r,i,e,o=this.reduce(),s=o.length-2,u=[];for(n=0;n<s;n++)i=o.slice(n,n+1),e=o.slice(n+2),r=this.curveintersects(i,e,t),u=u.concat(r);return u},curveintersects:function(t,n,r){var i=[];t.forEach(function(t){n.forEach(function(n){t.overlaps(n)&&i.push({left:t,right:n})})});var e=[];return i.forEach(function(t){var n=y.pairiteration(t.left,t.right,r);n.length>0&&(e=e.concat(n))}),e},arcs:function(t){t=t||.5;var n=[];return this._iterate(t,n)},_error:function(t,n,r,i){var o=(i-r)/4,s=this.get(r+o),u=this.get(i-o),a=y.dist(t,n),f=y.dist(t,s),c=y.dist(t,u);return e(f-a)+e(c-a)},_iterate:function(t,n){var r,i=0,e=1;do{r=0,e=1;var o,s,f,c,h,x=this.get(i),p=!1,l=!1,v=e,d=1,m=0;do{l=p,c=f,v=(i+e)/2,m++,o=this.get(v),s=this.get(e),f=y.getccenter(x,o,s),f.interval={start:i,end:e};var g=this._error(f,x,i,e);if(p=g<=t,h=l&&!p,h||(d=e),p){if(e>=1){if(f.interval.end=d=1,c=f,e>1){var z={x:f.x+f.r*u(f.e),y:f.y+f.r*a(f.e)};f.e+=y.angle({x:f.x,y:f.y},z,this.get(1))}break}e+=(e-i)/2}else e=v}while(!h&&r++<100);if(r>=100)break;c=c?c:f,n.push(c),i=d}while(e<1);return n}},t.exports=l}()},function(t,n,r){"use strict";!function(){var n=Math.abs,i=Math.cos,e=Math.sin,o=Math.acos,s=Math.atan2,u=Math.sqrt,a=Math.pow,f=function(t){return t<0?-a(-t,1/3):a(t,1/3)},c=Math.PI,h=2*c,x=c/2,y=1e-6,p=Number.MAX_SAFE_INTEGER,l=Number.MIN_SAFE_INTEGER,v={Tvalues:[-.06405689286260563,.06405689286260563,-.1911188674736163,.1911188674736163,-.3150426796961634,.3150426796961634,-.4337935076260451,.4337935076260451,-.5454214713888396,.5454214713888396,-.6480936519369755,.6480936519369755,-.7401241915785544,.7401241915785544,-.820001985973903,.820001985973903,-.8864155270044011,.8864155270044011,-.9382745520027328,.9382745520027328,-.9747285559713095,.9747285559713095,-.9951872199970213,.9951872199970213],Cvalues:[.12793819534675216,.12793819534675216,.1258374563468283,.1258374563468283,.12167047292780339,.12167047292780339,.1155056680537256,.1155056680537256,.10744427011596563,.10744427011596563,.09761865210411388,.09761865210411388,.08619016153195327,.08619016153195327,.0733464814110803,.0733464814110803,.05929858491543678,.05929858491543678,.04427743881741981,.04427743881741981,.028531388628933663,.028531388628933663,.0123412297999872,.0123412297999872],arcfn:function(t,n){var r=n(t),i=r.x*r.x+r.y*r.y;return"undefined"!=typeof r.z&&(i+=r.z*r.z),u(i)},between:function(t,n,r){return n<=t&&t<=r||v.approximately(t,n)||v.approximately(t,r)},approximately:function(t,r,i){return n(t-r)<=(i||y)},length:function(t){var n,r,i=.5,e=0,o=v.Tvalues.length;for(n=0;n<o;n++)r=i*v.Tvalues[n]+i,e+=v.Cvalues[n]*v.arcfn(r,t);return i*e},map:function(t,n,r,i,e){var o=r-n,s=e-i,u=t-n,a=u/o;return i+s*a},lerp:function(t,n,r){var i={x:n.x+t*(r.x-n.x),y:n.y+t*(r.y-n.y)};return n.z&&r.z&&(i.z=n.z+t*(r.z-n.z)),i},pointToString:function(t){var n=t.x+"/"+t.y;return"undefined"!=typeof t.z&&(n+="/"+t.z),n},pointsToString:function(t){return"["+t.map(v.pointToString).join(", ")+"]"},copy:function(t){return JSON.parse(JSON.stringify(t))},angle:function(t,n,r){var i=n.x-t.x,e=n.y-t.y,o=r.x-t.x,u=r.y-t.y,a=i*u-e*o,f=i*o+e*u;return s(a,f)},round:function(t,n){var r=""+t,i=r.indexOf(".");return parseFloat(r.substring(0,i+1+n))},dist:function(t,n){var r=t.x-n.x,i=t.y-n.y;return u(r*r+i*i)},closest:function(t,n){var r,i,e=a(2,63);return t.forEach(function(t,o){i=v.dist(n,t),i<e&&(e=i,r=o)}),{mdist:e,mpos:r}},abcratio:function(t,r){if(2!==r&&3!==r)return!1;if("undefined"==typeof t)t=.5;else if(0===t||1===t)return t;var i=a(t,r)+a(1-t,r),e=i-1;return n(e/i)},projectionratio:function(t,n){if(2!==n&&3!==n)return!1;if("undefined"==typeof t)t=.5;else if(0===t||1===t)return t;var r=a(1-t,n),i=a(t,n)+r;return r/i},lli8:function(t,n,r,i,e,o,s,u){var a=(t*i-n*r)*(e-s)-(t-r)*(e*u-o*s),f=(t*i-n*r)*(o-u)-(n-i)*(e*u-o*s),c=(t-r)*(o-u)-(n-i)*(e-s);return 0!=c&&{x:a/c,y:f/c}},lli4:function(t,n,r,i){var e=t.x,o=t.y,s=n.x,u=n.y,a=r.x,f=r.y,c=i.x,h=i.y;return v.lli8(e,o,s,u,a,f,c,h)},lli:function(t,n){return v.lli4(t,t.c,n,n.c)},makeline:function(t,n){var i=r(1),e=t.x,o=t.y,s=n.x,u=n.y,a=(s-e)/3,f=(u-o)/3;return new i(e,o,e+a,o+f,e+2*a,o+2*f,s,u)},findbbox:function(t){var n=p,r=p,i=l,e=l;return t.forEach(function(t){var o=t.bbox();n>o.x.min&&(n=o.x.min),r>o.y.min&&(r=o.y.min),i<o.x.max&&(i=o.x.max),e<o.y.max&&(e=o.y.max)}),{x:{min:n,mid:(n+i)/2,max:i,size:i-n},y:{min:r,mid:(r+e)/2,max:e,size:e-r}}},shapeintersections:function(t,n,r,i,e){if(!v.bboxoverlap(n,i))return[];var o=[],s=[t.startcap,t.forward,t.back,t.endcap],u=[r.startcap,r.forward,r.back,r.endcap];return s.forEach(function(n){n.virtual||u.forEach(function(i){if(!i.virtual){var s=n.intersects(i,e);s.length>0&&(s.c1=n,s.c2=i,s.s1=t,s.s2=r,o.push(s))}})}),o},makeshape:function(t,n,r){var i=n.points.length,e=t.points.length,o=v.makeline(n.points[i-1],t.points[0]),s=v.makeline(t.points[e-1],n.points[0]),u={startcap:o,forward:t,back:n,endcap:s,bbox:v.findbbox([o,t,n,s])},a=v;return u.intersections=function(t){return a.shapeintersections(u,u.bbox,t,t.bbox,r)},u},getminmax:function(t,n,r){if(!r)return{min:0,max:0};var i,e,o=p,s=l;r.indexOf(0)===-1&&(r=[0].concat(r)),r.indexOf(1)===-1&&r.push(1);for(var u=0,a=r.length;u<a;u++)i=r[u],e=t.get(i),e[n]<o&&(o=e[n]),e[n]>s&&(s=e[n]);return{min:o,mid:(o+s)/2,max:s,size:s-o}},align:function(t,n){var r=n.p1.x,o=n.p1.y,u=-s(n.p2.y-o,n.p2.x-r),a=function(t){return{x:(t.x-r)*i(u)-(t.y-o)*e(u),y:(t.x-r)*e(u)+(t.y-o)*i(u)}};return t.map(a)},roots:function(t,n){n=n||{p1:{x:0,y:0},p2:{x:1,y:0}};var r=t.length-1,e=v.align(t,n),s=function(t){return 0<=t&&t<=1};if(2===r){var a=e[0].y,c=e[1].y,x=e[2].y,y=a-2*c+x;if(0!==y){var p=-u(c*c-a*x),l=-a+c,d=-(p+l)/y,m=-(-p+l)/y;return[d,m].filter(s)}return c!==x&&0===y?[(2*c-x)/2*(c-x)].filter(s):[]}var g,d,z,b,_,w=e[0].y,E=e[1].y,S=e[2].y,M=e[3].y,y=-w+3*E-3*S+M,a=(3*w-6*E+3*S)/y,c=(-3*w+3*E)/y,x=w/y,e=(3*c-a*a)/3,k=e/3,O=(2*a*a*a-9*a*c+27*x)/27,T=O/2,N=T*T+k*k*k;if(N<0){var j=-e/3,I=j*j*j,A=u(I),C=-O/(2*A),F=C<-1?-1:C>1?1:C,q=o(F),U=f(A),B=2*U;return z=B*i(q/3)-a/3,b=B*i((q+h)/3)-a/3,_=B*i((q+2*h)/3)-a/3,[z,b,_].filter(s)}if(0===N)return g=T<0?f(-T):-f(T),z=2*g-a/3,b=-g-a/3,[z,b].filter(s);var G=u(N);return g=f(-T+G),d=f(T+G),[g-d-a/3].filter(s)},droots:function(t){if(3===t.length){var n=t[0],r=t[1],i=t[2],e=n-2*r+i;if(0!==e){var o=-u(r*r-n*i),s=-n+r,a=-(o+s)/e,f=-(-o+s)/e;return[a,f]}return r!==i&&0===e?[(2*r-i)/(2*(r-i))]:[]}if(2===t.length){var n=t[0],r=t[1];return n!==r?[n/(n-r)]:[]}},inflections:function(t){if(t.length<4)return[];var n=v.align(t,{p1:t[0],p2:t.slice(-1)[0]}),r=n[2].x*n[1].y,i=n[3].x*n[1].y,e=n[1].x*n[2].y,o=n[3].x*n[2].y,s=18*(-3*r+2*i+3*e-o),u=18*(3*r-i-3*e),a=18*(e-r);if(v.approximately(s,0)){if(!v.approximately(u,0)){var f=-a/u;if(0<=f&&f<=1)return[f]}return[]}var c=u*u-4*s*a,h=Math.sqrt(c),o=2*s;return v.approximately(o,0)?[]:[(h-u)/o,-(u+h)/o].filter(function(t){return 0<=t&&t<=1})},bboxoverlap:function(t,r){var i,e,o,s,u,a=["x","y"],f=a.length;for(i=0;i<f;i++)if(e=a[i],o=t[e].mid,s=r[e].mid,u=(t[e].size+r[e].size)/2,n(o-s)>=u)return!1;return!0},expandbox:function(t,n){n.x.min<t.x.min&&(t.x.min=n.x.min),n.y.min<t.y.min&&(t.y.min=n.y.min),n.z&&n.z.min<t.z.min&&(t.z.min=n.z.min),n.x.max>t.x.max&&(t.x.max=n.x.max),n.y.max>t.y.max&&(t.y.max=n.y.max),n.z&&n.z.max>t.z.max&&(t.z.max=n.z.max),t.x.mid=(t.x.min+t.x.max)/2,t.y.mid=(t.y.min+t.y.max)/2,t.z&&(t.z.mid=(t.z.min+t.z.max)/2),t.x.size=t.x.max-t.x.min,t.y.size=t.y.max-t.y.min,t.z&&(t.z.size=t.z.max-t.z.min)},pairiteration:function(t,n,r){var i=t.bbox(),e=n.bbox(),o=1e5,s=r||.5;if(i.x.size+i.y.size<s&&e.x.size+e.y.size<s)return[(o*(t._t1+t._t2)/2|0)/o+"/"+(o*(n._t1+n._t2)/2|0)/o];var u=t.split(.5),a=n.split(.5),f=[{left:u.left,right:a.left},{left:u.left,right:a.right},{left:u.right,right:a.right},{left:u.right,right:a.left}];f=f.filter(function(t){return v.bboxoverlap(t.left.bbox(),t.right.bbox())});var c=[];return 0===f.length?c:(f.forEach(function(t){c=c.concat(v.pairiteration(t.left,t.right,s))}),c=c.filter(function(t,n){return c.indexOf(t)===n}))},getccenter:function(t,n,r){var o,u=n.x-t.x,a=n.y-t.y,f=r.x-n.x,c=r.y-n.y,y=u*i(x)-a*e(x),p=u*e(x)+a*i(x),l=f*i(x)-c*e(x),d=f*e(x)+c*i(x),m=(t.x+n.x)/2,g=(t.y+n.y)/2,z=(n.x+r.x)/2,b=(n.y+r.y)/2,_=m+y,w=g+p,E=z+l,S=b+d,M=v.lli8(m,g,_,w,z,b,E,S),k=v.dist(M,t),O=s(t.y-M.y,t.x-M.x),T=s(n.y-M.y,n.x-M.x),N=s(r.y-M.y,r.x-M.x);return O<N?((O>T||T>N)&&(O+=h),O>N&&(o=N,N=O,O=o)):N<T&&T<O?(o=N,N=O,O=o):N+=h,M.s=O,M.e=N,M.r=k,M},numberSort:function(t,n){return t-n}};t.exports=v}()},function(t,n,r){"use strict";!function(){var n=r(2),i=function(t){this.curves=[],this._3d=!1,t&&(this.curves=t,this._3d=this.curves[0]._3d)};i.prototype={valueOf:function(){return this.toString()},toString:function(){return"["+this.curves.map(function(t){return n.pointsToString(t.points)}).join(", ")+"]"},addCurve:function(t){this.curves.push(t),this._3d=this._3d||t._3d},length:function(){return this.curves.map(function(t){return t.length()}).reduce(function(t,n){return t+n})},curve:function(t){return this.curves[t]},bbox:function t(){for(var r=this.curves,t=r[0].bbox(),i=1;i<r.length;i++)n.expandbox(t,r[i].bbox());return t},offset:function t(n){var t=[];return this.curves.forEach(function(r){t=t.concat(r.offset(n))}),new i(t)}},t.exports=i}()}]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function createDrawElement(w, h) { | |
var cvs = document.createElement("canvas"); | |
cvs.width = w; | |
cvs.height = h; | |
var ctx = cvs.getContext("2d"); | |
var randomColors = []; | |
for(var i=0,j; i<360; i++) { | |
j = (i*47)%360; | |
randomColors.push("hsl("+j+",50%,50%)"); | |
} | |
var randomIndex = 0; | |
return { | |
getCanvas: function() { return cvs; }, | |
reset: function(curve, evt) { | |
cvs.width = cvs.width; | |
ctx.strokeStyle = "black"; | |
ctx.fillStyle = "none"; | |
if (evt && curve) { | |
curve.mouse = {x: evt.offsetX, y: evt.offsetY}; | |
} | |
randomIndex = 0; | |
}, | |
setColor: function(c) { | |
ctx.strokeStyle = c; | |
}, | |
noColor: function(c) { | |
ctx.strokeStyle = "transparent"; | |
}, | |
setRandomColor: function() { | |
randomIndex = (randomIndex+1) % randomColors.length; | |
var c = randomColors[randomIndex]; | |
ctx.strokeStyle = c; | |
}, | |
setRandomFill: function(a) { | |
randomIndex = (randomIndex+1) % randomColors.length; | |
a = (typeof a === "undefined") ? 1 : a; | |
var c = randomColors[randomIndex]; | |
c = c.replace('hsl(','hsla(').replace(')',','+a+')'); | |
ctx.fillStyle = c; | |
}, | |
setFill: function(c) { | |
ctx.fillStyle = c; | |
}, | |
noFill: function() { | |
ctx.fillStyle = "transparent"; | |
}, | |
drawSkeleton: function(curve, offset, nocoords) { | |
offset = offset || { x:0, y:0 }; | |
var pts = curve.points; | |
//ctx.strokeStyle = "lightgrey"; | |
this.drawLine(pts[0], pts[1], offset); | |
if(pts.length === 3) { this.drawLine(pts[1], pts[2], offset); } | |
else {this.drawLine(pts[2], pts[3], offset); } | |
//ctx.strokeStyle = "black"; | |
if(!nocoords) this.drawPoints(pts, offset); | |
}, | |
drawCurve: function(curve, offset) { | |
offset = offset || { x:0, y:0 }; | |
var ox = offset.x; | |
var oy = offset.y; | |
ctx.beginPath(); | |
var p = curve.points, i; | |
ctx.moveTo(p[0].x + ox, p[0].y + oy); | |
if(p.length === 3) { | |
ctx.quadraticCurveTo( | |
p[1].x + ox, p[1].y + oy, | |
p[2].x + ox, p[2].y + oy | |
); | |
} | |
if(p.length === 4) { | |
ctx.bezierCurveTo( | |
p[1].x + ox, p[1].y + oy, | |
p[2].x + ox, p[2].y + oy, | |
p[3].x + ox, p[3].y + oy | |
); | |
} | |
ctx.stroke(); | |
ctx.closePath(); | |
}, | |
drawLine: function(p1, p2, offset) { | |
offset = offset || { x:0, y:0 }; | |
var ox = offset.x; | |
var oy = offset.y; | |
ctx.beginPath(); | |
ctx.moveTo(p1.x + ox,p1.y + oy); | |
ctx.lineTo(p2.x + ox,p2.y + oy); | |
ctx.stroke(); | |
}, | |
drawPoint: function(p, offset) { | |
offset = offset || { x:0, y:0 }; | |
var ox = offset.x; | |
var oy = offset.y; | |
ctx.beginPath(); | |
ctx.arc(p.x + ox, p.y + oy, 5, 0, 2*Math.PI); | |
ctx.stroke(); | |
}, | |
drawPoints: function(points, offset) { | |
offset = offset || { x:0, y:0 }; | |
points.forEach(function(p) { | |
this.drawCircle(p, 3, offset); | |
}.bind(this)); | |
}, | |
drawArc: function(p, offset) { | |
offset = offset || { x:0, y:0 }; | |
var ox = offset.x; | |
var oy = offset.y; | |
ctx.beginPath(); | |
ctx.moveTo(p.x + ox, p.y + oy); | |
ctx.arc(p.x + ox, p.y + oy, p.r, p.s, p.e); | |
ctx.lineTo(p.x + ox, p.y + oy); | |
ctx.fill(); | |
ctx.stroke(); | |
}, | |
drawCircle: function(p, r, offset) { | |
offset = offset || { x:0, y:0 }; | |
var ox = offset.x; | |
var oy = offset.y; | |
ctx.beginPath(); | |
ctx.arc(p.x + ox, p.y + oy, r, 0, 2*Math.PI); | |
ctx.stroke(); | |
}, | |
drawbbox: function(bbox, offset) { | |
offset = offset || { x:0, y:0 }; | |
var ox = offset.x; | |
var oy = offset.y; | |
ctx.beginPath(); | |
ctx.moveTo(bbox.x.min + ox, bbox.y.min + oy); | |
ctx.lineTo(bbox.x.min + ox, bbox.y.max + oy); | |
ctx.lineTo(bbox.x.max + ox, bbox.y.max + oy); | |
ctx.lineTo(bbox.x.max + ox, bbox.y.min + oy); | |
ctx.closePath(); | |
ctx.stroke(); | |
}, | |
drawHull: function(hull, offset) { | |
ctx.beginPath(); | |
if(hull.length === 6) { | |
ctx.moveTo(hull[0].x, hull[0].y); | |
ctx.lineTo(hull[1].x, hull[1].y); | |
ctx.lineTo(hull[2].x, hull[2].y); | |
ctx.moveTo(hull[3].x, hull[3].y); | |
ctx.lineTo(hull[4].x, hull[4].y); | |
} else { | |
ctx.moveTo(hull[0].x, hull[0].y); | |
ctx.lineTo(hull[1].x, hull[1].y); | |
ctx.lineTo(hull[2].x, hull[2].y); | |
ctx.lineTo(hull[3].x, hull[3].y); | |
ctx.moveTo(hull[4].x, hull[4].y); | |
ctx.lineTo(hull[5].x, hull[5].y); | |
ctx.lineTo(hull[6].x, hull[6].y); | |
ctx.moveTo(hull[7].x, hull[7].y); | |
ctx.lineTo(hull[8].x, hull[8].y); | |
} | |
ctx.stroke(); | |
}, | |
drawShape: function(shape, offset) { | |
offset = offset || { x:0, y:0 }; | |
var order = shape.forward.points.length - 1; | |
ctx.beginPath(); | |
ctx.moveTo(offset.x + shape.startcap.points[0].x, offset.y + shape.startcap.points[0].y); | |
ctx.lineTo(offset.x + shape.startcap.points[3].x, offset.y + shape.startcap.points[3].y); | |
if(order === 3) { | |
ctx.bezierCurveTo( | |
offset.x + shape.forward.points[1].x, offset.y + shape.forward.points[1].y, | |
offset.x + shape.forward.points[2].x, offset.y + shape.forward.points[2].y, | |
offset.x + shape.forward.points[3].x, offset.y + shape.forward.points[3].y | |
); | |
} else { | |
ctx.quadraticCurveTo( | |
offset.x + shape.forward.points[1].x, offset.y + shape.forward.points[1].y, | |
offset.x + shape.forward.points[2].x, offset.y + shape.forward.points[2].y | |
); | |
} | |
ctx.lineTo(offset.x + shape.endcap.points[3].x, offset.y + shape.endcap.points[3].y); | |
if(order === 3) { | |
ctx.bezierCurveTo( | |
offset.x + shape.back.points[1].x, offset.y + shape.back.points[1].y, | |
offset.x + shape.back.points[2].x, offset.y + shape.back.points[2].y, | |
offset.x + shape.back.points[3].x, offset.y + shape.back.points[3].y | |
); | |
} else { | |
ctx.quadraticCurveTo( | |
offset.x + shape.back.points[1].x, offset.y + shape.back.points[1].y, | |
offset.x + shape.back.points[2].x, offset.y + shape.back.points[2].y | |
); | |
} | |
ctx.closePath(); | |
ctx.fill(); | |
ctx.stroke(); | |
}, | |
drawText: function(text, offset) { | |
offset = offset || { x:0, y:0 }; | |
ctx.fillText(text, offset.x, offset.y); | |
} | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en" style="height: 100%;"> | |
<head> | |
<title>Test</title> | |
<script type="text/javascript" src="bezier.js"></script> | |
<script type="text/javascript" src="draw.js"></script> | |
<script type="text/javascript" src="interaction.js"></script> | |
</head> | |
<body style="position:absolute; top:0; bottom:0; right:0; left:0;"> | |
<script type="text/javascript"> | |
var utils = Bezier.getUtils(); | |
var w = document.body.offsetWidth; | |
var h = document.body.offsetHeight; | |
// var normScreenParams = {s: 200, tx: 200, ty: h/2}; | |
var distance = 100; | |
//var curve = new Bezier(600, 50, 655, 70, 700, 50); | |
//var curve = new Bezier(600, 50, 1260, 51, 1500, 50); | |
var curve = [{x:800, y:500}, {x: 900, y:550}, {x:1000, y:500}]; | |
//var curve = new Bezier(800, 500, 860, 450, 1000, 500); | |
var pixel = {center: {x: 0, y:0}}; | |
function isOnTheRight(l0, l1, p) { | |
// Is p on the right side of the line from l0 to l1 ??? | |
return ((l1.x - l0.x)*(p.y - l0.y) - (l1.y - l0.y)*(p.x - l0.x)) < 0; | |
} | |
function isReversed(c) { | |
return isOnTheRight(c[0], c[2], c[1]); | |
} | |
function isSimple(c) { | |
//return c.simple(); | |
c = c; | |
// Standard version calculating the real angle | |
var v1 = {x: c[0].x - c[1].x, y: c[0].y - c[1].y}; | |
var v2 = {x: c[2].x - c[1].x, y: c[2].y - c[1].y}; | |
var v1l = Math.sqrt(v1.x*v1.x + v1.y*v1.y); | |
var v2l = Math.sqrt(v2.x*v2.x + v2.y*v2.y); | |
var v1n = {x: v1.x/v1l, y: v1.y/v1l}; | |
var v2n = {x: v2.x/v2l, y: v2.y/v2l}; | |
var v1v2 = v1n.x*v2n.x + v1n.y*v2n.y; // Cos of angle between vectors (Dot product) | |
//var greater90 = v1v2 < 0; // > 1/2PI == 90 | |
var greater120 = v1v2 < -0.5; // > 2/3PI == 120 | |
// Angle on control point > 90 | |
// Optimized version that test if the triangle is obtuse using pythagoras theorem | |
// https://en.wikipedia.org/wiki/Pythagorean_theorem#Converse | |
var l1 = {x: c[0].x - c[1].x, y: c[0].y - c[1].y}; | |
var l2 = {x: c[2].x - c[1].x, y: c[2].y - c[1].y}; | |
var l3 = {x: c[2].x - c[0].x, y: c[2].y - c[0].y}; | |
var m12 = l1.x*l1.x + l1.y*l1.y; | |
var m22 = l2.x*l2.x + l2.y*l2.y; | |
var m32 = l3.x*l3.x + l3.y*l3.y; | |
var obtuse = m12 + m22 < m32; | |
// Control point is near the middle (outside of extreme quaters) | |
var v = {x: (c[2].x - c[0].x) / 4, y: (c[2].y - c[0].y) / 4} | |
var q1p1 = {x: c[0].x + v.x, y: c[0].y + v.y }; | |
var q1p2 = {x: q1p1.x - v.y, y: q1p1.y + v.x }; | |
var isCenteredOnRight = isOnTheRight(q1p1, q1p2, c[1]); | |
var q2p1 = {x: c[2].x - v.x, y: c[2].y - v.y }; | |
var q2p2 = {x: q2p1.x - v.y, y: q2p1.y + v.x }; | |
var isCenteredOnLeft = !isOnTheRight(q2p1, q2p2, c[1]); | |
return greater120 && isCenteredOnRight && isCenteredOnLeft; | |
//return obtuse && isCenteredOnRight && isCenteredOnLeft; | |
} | |
function split(c, t) { | |
// TODO Avoid bezier.js | |
var s = (new Bezier(c)).split(t); | |
return {left: s.left.points, right: s.right.points}; | |
} | |
function reduce(c) { | |
// return c.reduce(); | |
if (isSimple(c)) { | |
return [c]; | |
} else { | |
//var s = c.split(0.5); | |
//return reduce(s.left).concat(reduce(s.right)); | |
function findSimpleRec(c, t) { | |
//return isSimple(c.split(t).left ? t : findSimpleRec(c, t/2); | |
var s = split(c, t); | |
if (isSimple(s.left)) { | |
return t; | |
} else { | |
return findSimpleRec(c, t/2); | |
} | |
} | |
var current = c; | |
var curves = []; | |
while (!isSimple(current)) { | |
var t = findSimpleRec(current, 0.5); | |
var s = split(current, t); | |
curves.push(s.left); | |
current = s.right; | |
} | |
curves.push(current); | |
return curves; | |
} | |
} | |
function scale(s, d) { | |
//return s.scale(d); | |
var p0 = s[0]; | |
var p1 = s[1]; | |
var p2 = s[2]; | |
var np = []; | |
// Tangents | |
var t0 = {x: p1.x - p0.x, y: p1.y - p0.y}; | |
var mt0 = Math.sqrt(t0.x*t0.x + t0.y*t0.y); | |
t0 = {x: t0.x/mt0, y: t0.y/mt0}; | |
var t1 = {x: p2.x - p1.x, y: p2.y - p1.y}; | |
var mt1 = Math.sqrt(t1.x*t1.x + t1.y*t1.y); | |
t1 = {x: t1.x/mt1, y: t1.y/mt1}; | |
// Normals | |
var n0 = {x: -t0.y, y: t0.x}; | |
var n1 = {x: -t1.y, y: t1.x}; | |
np[0] = {x: p0.x + d * n0.x, y: p0.y + d * n0.y}; | |
np[2] = {x: p2.x + d * n1.x, y: p2.y + d * n1.y}; | |
var od0 = {x: np[0].x + t0.x, y: np[0].y + t0.y}; | |
var od1 = {x: np[2].x + t1.x, y: np[2].y + t1.y}; | |
function lineIntersection(x1,y1,x2,y2,x3,y3,x4,y4) { | |
var nx=(x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4), | |
ny=(x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4), | |
d=(x1-x2)*(y3-y4)-(y1-y2)*(x3-x4); | |
if(d==0) { | |
return {x: (x2+x4)/2, y: (y2+y4)/2}; // Middle point | |
} else { | |
return {x: nx/d, y: ny/d}; | |
} | |
} | |
np[1] = lineIntersection(np[0].x, np[0].y, od0.x, od0.y, np[2].x, np[2].y, od1.x, od1.y); | |
return np; | |
} | |
function calcAreaForPx(p10_x, p10_y, p11_x, p11_y, p12_x, p12_y, | |
p20_x, p20_y, p21_x, p21_y, p22_x, p22_y, | |
pixel_x, pixel_y, | |
bezierLength, curveProportions, segmentLength, segmentOffset, | |
reversedInternalBorder) { | |
// segmentLength and segmentOffset are relative to the t parameter (1 means whole curve) | |
// Find the t values for the cut points of the curves with the line between o and px | |
// Align -> Rotate to make the line the vertical axis (only calculate x coord) | |
// Calculate t for x=0 | |
// Note: We don't normalize the line vector, as the scaled curve will cut at the same point | |
// Still, the direction of the vector is important and should point towards the curve. | |
// In practice is not needed because these cases are avoided. | |
// Note: All the curve points and the pixel use the origin point as the base. This saves calculations and parameters. | |
// We need 48 simple ops (taking MAD into account) | |
// to calculate the ts, points and normals (not including dashed) | |
// And 22 for the interseccion most common case. | |
// This is ~92 in the most common case of a simple border | |
// Align the points to the pixel vector (from o to the current pixel) | |
// Then find the t value for x = 0 | |
// Then calculate the cut points and tangents | |
var ap10 = p10_x*pixel_y - p10_y*pixel_x; | |
var ap11 = p11_x*pixel_y - p11_y*pixel_x; | |
var ap12 = p12_x*pixel_y - p12_y*pixel_x; | |
var t1 = ap10 / (ap10 - ap11 - Math.sqrt(ap11*ap11 - ap10*ap12)); | |
// TODO Maybe extract the parts not depending on t1 first to give time to the previous sqrt and / | |
var lerp11_x = p10_x + t1*(p11_x - p10_x); | |
var lerp12_x = p11_x + t1*(p12_x - p11_x); | |
var tan1_x = lerp12_x - lerp11_x; | |
var cP1_x = lerp11_x + t1*tan1_x; | |
var lerp11_y = p10_y + t1*(p11_y - p10_y); | |
var lerp12_y = p11_y + t1*(p12_y - p11_y); | |
var tan1_y = lerp12_y - lerp11_y; | |
var cP1_y = lerp11_y + t1*tan1_y; | |
/* | |
var cP1_ax = p10_x - 2*p11_x + p12_x; | |
var cP1_bx = p11_x - p10_x; | |
var cP1_ay = p10_y - 2*p11_y + p12_y; | |
var cP1_by = p11_y - p10_y; | |
var halftan1_x = cP1_ax*t1 + cP1_bx; | |
var halftan1_y = cP1_ay*t1 + cP1_by; | |
var cP1_x = (halftan1_x + cP1_bx)*t1 + p10_x; | |
var cP1_y = (halftan1_y + cP1_by)*t1 + p10_y; | |
var tan1_x = 2*halftan1_x; | |
var tan1_y = 2*halftan1_y; | |
*/ | |
var ap20 = p20_x*pixel_y - p20_y*pixel_x; | |
var ap21 = p21_x*pixel_y - p21_y*pixel_x; | |
var ap22 = p22_x*pixel_y - p22_y*pixel_x; | |
var t2 = ap20 / (ap20 - ap21 - Math.sqrt(ap21*ap21 - ap20*ap22)); | |
var lerp21_x = p20_x + t2*(p21_x - p20_x); | |
var lerp22_x = p21_x + t2*(p22_x - p21_x); | |
var tan2_x = lerp22_x - lerp21_x; | |
var cP2_x = lerp21_x + t2*tan2_x; | |
var lerp21_y = p20_y + t2*(p21_y - p20_y); | |
var lerp22_y = p21_y + t2*(p22_y - p21_y); | |
var tan2_y = lerp22_y - lerp21_y; | |
var cP2_y = lerp21_y + t2*tan2_y | |
/* | |
var cP2_ax = p20_x - 2*p21_x + p22_x; | |
var cP2_bx = p21_x - p20_x; | |
var cP2_ay = p20_y - 2*p21_y + p22_y; | |
var cP2_by = p21_y - p20_y; | |
var halftan2_x = cP2_ax*t2 + cP2_bx; | |
var halftan2_y = cP2_ay*t2 + cP2_by; | |
var cP2_x = (halftan2_x + cP2_bx)*t2 + p20_x; | |
var cP2_y = (halftan2_y + cP2_by)*t2 + p20_y; | |
var tan2_x = 2*halftan2_x; | |
var tan2_y = 2*halftan2_y; | |
*/ | |
// Intersect curves with pixel | |
// TODO Displace the tangent depending on curvature to have smaller error | |
var area1 = intersectLineAndPixel(cP1_x, cP1_y, tan1_x, tan1_y, pixel_x, pixel_y); | |
var area2 = intersectLineAndPixel(cP2_x, cP2_y, tan2_x, tan2_y, pixel_x, pixel_y); | |
// TODO Optimize: In this situation we don't need to calculate anything related to c1 | |
if (reversedInternalBorder) { | |
area1 = 0; | |
} | |
var borderArea = area2 - area1; | |
// Calculate the dashed area | |
var dashedArea = 1; | |
if (true) { | |
var tn; | |
if (reversedInternalBorder) { | |
// Special case to avoid the problems of the reversed internal border | |
tn = t2; | |
} else { | |
// Interpolate normals | |
// We don't normalize the distances or the tangents because we only care about ratio | |
// And the approximation is good enough | |
var d1 = -(tan1_x*(pixel_x - cP1_x) + tan1_y*(pixel_y - cP1_y)); // Distance from the pixel to the normal line on t1 | |
var d2 = tan2_x*(pixel_x - cP2_x) + tan2_y*(pixel_y - cP2_y); // Distance from the pixel to the normal line on t2 | |
var iFactor = d1 / (d1+d2); | |
if (iFactor < 0) { iFactor = 1; } // This is realy important, simple clamp creates many more artifacts !!! | |
// Note: There are still some artifacts due to the interpolation, but really small, so we stop here | |
// Problems seem to appear when the normals cross, but the trick above works really well | |
// var curve = new Bezier(600, 50, 655, 70, 700, 50); | |
// var distance = 600; | |
var inx = -(tan1_y*(1-iFactor) + tan2_y*iFactor); | |
var iny = tan1_x*(1-iFactor) + tan2_x*iFactor; | |
// Find cut point of the interpolated normal | |
// Interpolated normal {p1: px, p2: {x: px.x + inx, y: px.y + iny}} | |
var pn0 = (p20_x - pixel_x)*iny - (p20_y - pixel_y)*inx; | |
var pn1 = (p21_x - pixel_x)*iny - (p21_y - pixel_y)*inx; | |
var pn2 = (p22_x - pixel_x)*iny - (p22_y - pixel_y)*inx; | |
tn = pn0 / (pn0 - pn1 - Math.sqrt(pn1*pn1 - pn0*pn2)); | |
} | |
if (tn > 0 && tn < 1) { | |
// Segmented border, with cheap AA | |
// IMPORTANT !! alpha can be outside [0,1], clamp if needed !!!! | |
// Corrention to have aprox equal length segments | |
// based on a flat bezier with extremes on (0,0) and (1,0) and control on (tc,0) | |
// curveProportions is the proportion of the sides of the triangle that touch the control point | |
// NOTE: tc is constant on the whole curve, we don't need to recalculate per pixel | |
// Alternative (worse) correction based on the idea of gamma | |
// var tc = (l1 / (l0 + l1)) + 0.5; | |
// t = Math.pow(t, tc); | |
var t = (2*curveProportions + (1 - 2*curveProportions)*tn)*tn; | |
var change = ((t+segmentOffset) / segmentLength) % 2; // Position with respect to change:[0, 2) with change at 1 and 2 | |
var d1 = 1 - change; // Distance to 1, the change from black to white | |
var d2 = 2 - change; // distance to 2, the change from white to black | |
var pixelSizeInv = bezierLength*segmentLength; // With respect to d1 and d2, where a segment measures 1 | |
// This is the inverse to avoid the two divisions | |
// NOTE: We can make pixelSizeInv smaller to get blurrier edges (divide by 2) | |
dashedArea = (d1 > 0) ? d1*pixelSizeInv : 1 - d2*pixelSizeInv; // Here we get the gradients for AA if distances are closer than 1px | |
// Of they are larger, we get values greater than 1 or lower than 0 | |
// clamp | |
if (dashedArea < 0) {dashedArea = 0} | |
if (dashedArea > 1) {dashedArea = 1} | |
} | |
} | |
return borderArea * dashedArea; | |
} | |
function intersectLineAndPixel(p_x, p_y, tan_x, tan_y, pixel_x, pixel_y) { | |
// Most common case ~22 simple ops (taking into account MAD) | |
// Line defined by point p and vector tan | |
// Pixel center at px | |
// Reversed can be 1 or -1 to indicate the area we need | |
// Optimized calculations, see below for the detailed calculations | |
const sqrt2 = 1.41421356237; | |
const halfsqrt2 = 0.70710678119; // Also Sin and Cos of 45 degrees | |
// Change the origin to the pixel | |
var opx = p_x - pixel_x; | |
var opy = p_y - pixel_y; | |
// Rotate 45 degrees | |
// The halfsqrt2 has been factored out and propagated | |
var px = opx - opy; | |
var py = opx + opy; | |
var tx = tan_x - tan_y; | |
var ty = tan_x + tan_y; | |
// TODO Rotate and scale line when the object is transformed | |
// Maybe just scale point and rotate tangent ??? | |
// Aux intermediate variables | |
var abslx = Math.abs(ty); | |
var absly = Math.abs(tx); | |
var abslw = Math.abs(px*ty - py*tx); | |
// Situations and areas depending on positions of A and B | |
var area; | |
var not_parallel_x = abslx != 0; // Not parallel to x | |
var not_parallel_y = absly != 0; // Not parallel to y | |
var not_parallel = not_parallel_x && not_parallel_y; | |
var aout = abslx < abslw; // A inside the square; Alternate form ((0.5*abslw)/(abslx*halfsqrt2)) > halfsqrt2 | |
var bout = absly < abslw; // B inside the square; Alternate form ((0.5*abslw)/(absly*halfsqrt2)) > halfsqrt2 | |
// Optimized for the most common case, both out | |
if (not_parallel && aout && bout) { | |
area = 0; | |
} else { | |
// Line AB | |
var halfabslw = abslw*0.5; | |
var abx = -abslx*halfabslw*halfsqrt2; | |
var aby = -halfabslw*absly*halfsqrt2; | |
var abw = halfabslw*halfabslw; | |
// Aux intermediate variables | |
var sqrt2abw = sqrt2*abw; | |
var sqrt2abx = sqrt2*abx; | |
var sqrt2aby = sqrt2*aby; | |
var wpx = sqrt2abw + abx; | |
var wmx = sqrt2abw - abx; | |
var wpy = sqrt2abw + aby; | |
var wmy = sqrt2abw - aby; | |
var xpy = sqrt2abx + sqrt2aby; | |
var xmy = sqrt2abx - sqrt2aby; | |
if (not_parallel) { | |
var halfsqrt2xpy = halfsqrt2/xpy; // Aux | |
if (aout || bout) { // Only one out | |
area = aout ? -(wpy*wpy)/(xmy*xpy) : halfsqrt2xpy*wpx*(sqrt2*wpy/xmy + 1); | |
} else { // Both inside | |
if (abslw != 0) { | |
area = Math.abs(halfsqrt2xpy*(wpx+wpy)); | |
} else { | |
// If abslw == 0 A and B are on the center and the calculations are wrong | |
// Fortunately this case is simple, it's always half the area | |
area = 0.5; | |
} | |
} | |
} else { // Parallel | |
area = (not_parallel_y ? wpy*wpy : wmy*wmy) / (xpy*xpy); | |
} | |
} | |
// Reverse the area if needed based on the original line | |
// Calculates is the pixel is on the right side of the original line | |
if ((tan_y*opx - tan_x*opy) < 0) { | |
area = 1 - area; | |
} | |
return area; | |
/* Detailed calculations | |
// Change the origin to the pixel | |
var opx = p_x - pixel_x; | |
var opy = p_y - pixel_y; | |
// Rotate 45 degrees | |
var px = (opx - opy) * halfsqrt2; | |
var py = (opx + opy) * halfsqrt2; | |
var tx = (tan_x - tan_y) * halfsqrt2; | |
var ty = (tan_x + tan_y) * halfsqrt2; | |
// Intersect with axes in homogeneus coordinates | |
// horizontal axis: (0, 1, 0) <- from points (0,0,1);(1,0,1) | |
// vertical axis: (1, 0, 0) <- from points (0,0,1);(0,1,1) | |
var lx = -ty; | |
var ly = tx; | |
var lw = px*ty - py*tx; | |
// Horizontal (A) and Vertical (B) intersections | |
// Moved to first quadrant with absolute values | |
var ax = Math.abs(lw); | |
var ay = 0; | |
var aw = Math.abs(-lx); | |
var bx = 0; | |
var by = Math.abs(-lw); | |
var bw = Math.abs(ly); | |
// Situations depending on positions of A and B | |
var s1 = aw < 0.0001; // Horizontal line | |
var s2 = bw < 0.0001; // Vertical line | |
var s3 = (ax/aw) > halfsqrt2 && (by/bw) > halfsqrt2; // A and B are outside | |
var s4 = (ax/aw) < halfsqrt2 && (by/bw) < halfsqrt2; // A and B are inside | |
var s5 = (ax/aw) > halfsqrt2 && (by/bw) < halfsqrt2; // A ouside and B inside | |
var s6 = (ax/aw) < halfsqrt2 && (by/bw) > halfsqrt2; // A inside and B outside | |
// Create the line AB | |
var abx = -aw*by; | |
var aby = -ax*bw; | |
var abw = ax*by; | |
// Optimized line AB | |
var abslw = Math.abs(px*ty - py*tx); | |
var abx = -Math.abs(ty)*abslw; | |
var aby = -abslw*Math.abs(tx); | |
var abw = abslw*abslw; | |
// Intersect AB with 3 sides of the square (rotated 45 degres) to get the C points | |
// side 1: (-halfsqrt2, halfsqrt2, -1/2) <- from points (-halfsqrt2,0,1);(0,halfsqrt2,1) | |
// side 2: (halfsqrt2, halfsqrt2, -1/2) <- from points (0,halfsqrt2,1);(halfsqrt2,0,1) | |
// side 3: (halfsqrt2, -halfsqrt2, -1/2) <- from points (halfsqrt2,0,1);(0,-halfsqrt2,1) | |
var c1x = -halfsqrt2*abw - 0.5*aby; | |
var c1y = -halfsqrt2*abw + 0.5*abx; | |
var c1w = halfsqrt2*abx + halfsqrt2*aby; | |
var c2x = -halfsqrt2*abw - 0.5*aby; | |
var c2y = halfsqrt2*abw + 0.5*abx; | |
var c2w = halfsqrt2*abx - halfsqrt2*aby; | |
var c3x = halfsqrt2*abw - 0.5*aby; | |
var c3y = halfsqrt2*abw + 0.5*abx; | |
var c3w = -halfsqrt2*abx - halfsqrt2*aby; | |
// Calculate the lengths of the C points | |
// l1 and l2 with respect to (0,halfsqrt2) and l3 with respect to (halfsqrt2,0) | |
// Makes use of the third point in https://en.wikipedia.org/wiki/Triangle#Right_triangles | |
// Can divide by zero, but in that case the result won't be used later | |
var l1 = -(c1x/c1w)*sqrt2; | |
var l2 = (c2x/c2w)*sqrt2; | |
var l3 = -(c3y/c3w)*sqrt2; | |
// Areas | |
var t_parallel_x = (c1x/c1w)*(c1x/c1w); | |
var t_parallel_y = (c3y/c3w)*(c3y/c3w); | |
var t_x_outside = (l1*l2)/2; | |
var t_y_outside = ((1-l2)*l3)/2; | |
var t_both_inside = Math.abs(l1 + (l3 - l1)/2); | |
*/ | |
} | |
function calcDashedParameters(c, length, offset) { | |
var p0 = c[0]; | |
var p1 = c[1]; | |
var p2 = c[2]; | |
var l0 = Math.sqrt((p1.x - p0.x)*(p1.x - p0.x) + (p1.y - p0.y)*(p1.y - p0.y)); | |
var l1 = Math.sqrt((p2.x - p1.x)*(p2.x - p1.x) + (p2.y - p1.y)*(p2.y - p1.y)); | |
var l2 = Math.sqrt((p0.x - p2.x)*(p0.x - p2.x) + (p0.y - p2.y)*(p0.y - p2.y)); | |
var bezierLength = (l0 + l1 + 2*l2) / 3; // Approximate bezier length | |
var curveProportions = l0 / (l0 + l1); // Curve side proportions for the t parameter correction | |
return { | |
bezierLength: bezierLength, | |
curveProportions: curveProportions, | |
segmentLength: length / bezierLength, // With respect to t | |
segmentOffset: offset / bezierLength, // With respect to t | |
nextSegmentOffset: offset + bezierLength // Total offset in pixels | |
} | |
} | |
var api = createDrawElement(w, h); | |
var cvs = api.getCanvas(); | |
var context = cvs.getContext("2d"); | |
document.body.appendChild(cvs); | |
var handler = handleInteraction(cvs, curve); | |
document.body.addEventListener("keydown", function(evt) { | |
var delta = 1; | |
if (!!evt.shiftKey) { | |
delta = 10; | |
} | |
if (!!evt.ctrlKey) { | |
delta = 0.1; | |
} | |
if (evt.keyCode == 37) { | |
distance -= delta; | |
} | |
if (evt.keyCode == 39) { | |
distance += delta; | |
} | |
handler.onupdate(evt); | |
}); | |
function drawCircle(p, r, o, c) { | |
api.setColor(c); | |
context.lineWidth = o; | |
api.drawCircle(p, r); | |
context.lineWidth = 1; | |
} | |
handler.onupdate = function(evt) { | |
var borderFill = evt ? evt.keyCode == 70 : false; | |
api.reset(); | |
// Change the coordinate system to "normal" | |
// can break text !!!!!!! | |
// There is a change in the fix function of interection | |
context.translate(0, h); | |
context.scale(1, -1); | |
function drawCurve(c1, c2, color, dashedParameters) { | |
// Origin | |
// FIXME Lines can be parallel, use homogeneous coordinates for o ! | |
// This can be used to detect if the curve is a line on the shader | |
var o = utils.lli4(c1[0], c2[0], c1[2], c2[2]); // TODO Replace with custom function | |
// Find if the origin is between c1 and c2 using the squares of the lengths | |
var x12 = c1[0].x - c2[0].x; | |
var y12 = c1[0].y - c2[0].y; | |
var l12 = x12*x12 + y12*y12; | |
var xo2 = o.x - c2[0].x; | |
var yo2 = o.y - c2[0].y; | |
var lo2 = xo2*xo2 + yo2*yo2; | |
var reversedInternalBorder = l12 > lo2; | |
function renderTriangle(p1_x, p1_y, p2_x, p2_y, p3_x, p3_y) { | |
// Based on https://fgiesen.wordpress.com/2013/02/08/triangle-rasterization-in-practice/ | |
function orient2d(ax, ay, bx, by, cx, cy) { | |
return (bx-ax)*(cy-ay) - (by-ay)*(cx-ax); | |
} | |
var max_x = Math.ceil(Math.max(p1_x, Math.max(p2_x, p3_x))); | |
var min_x = Math.floor(Math.min(p1_x, Math.min(p2_x, p3_x))); | |
var max_y = Math.ceil(Math.max(p1_y, Math.max(p2_y, p3_y))); | |
var min_y = Math.floor(Math.min(p1_y, Math.min(p2_y, p3_y))); | |
for(var x=min_x; x<=max_x; x++) { | |
for(var y=min_y; y<=max_y; y++) { | |
var r = color[0]; var g = color[1]; var b = color[2]; var alpha = 0; | |
// TODO offset the points to have space to AA the flat parts | |
// Determine barycentric coordinates | |
var w0 = orient2d(p2_x, p2_y, p3_x, p3_y, x, y); | |
var w1 = orient2d(p3_x, p3_y, p1_x, p1_y, x, y); | |
var w2 = orient2d(p1_x, p1_y, p2_x, p2_y, x, y); | |
// If p is on or inside all edges, render pixel. | |
if ((w0 >= 0 && w1 >= 0 && w2 >= 0) || (w0 <= 0 && w1 <= 0 && w2 <= 0)){ | |
// The origin transformation saves calculations on the shader | |
// TODO Take this transformation out of the loop | |
alpha = calcAreaForPx(p10_x - o.x, p10_y - o.y, p11_x - o.x, p11_y - o.y, p12_x - o.x, p12_y - o.y, | |
p20_x - o.x, p20_y - o.y, p21_x - o.x, p21_y - o.y, p22_x - o.x, p22_y - o.y, | |
x - o.x, y - o.y, | |
dashedParameters.bezierLength, dashedParameters.curveProportions, dashedParameters.segmentLength, dashedParameters.segmentOffset, | |
reversedInternalBorder); | |
// alpha *= 0.3; | |
if (alpha < 0 || alpha > 1) { console.log(alpha);} | |
// Set the pixel color | |
var idx = (x + (h-y) * w) * 4; // Y reversed ! | |
d[idx+0] = r; //d[idx+0]*(1-alpha) + 255*r*alpha; | |
d[idx+1] = g; //d[idx+1]*(1-alpha) + 255*g*alpha; | |
d[idx+2] = b; //d[idx+2]*(1-alpha) + 255*b*alpha; | |
d[idx+3] = d[idx+3]*(1-alpha) + 255*alpha; | |
// FIXME White fill can overlap with stroke !!! | |
// Probably the only solution is to draw the fill and the stroke independently. | |
//d[idx+0] = 255*(1-alpha); d[idx+1] = 255*(1-alpha); d[idx+2] = 255*(1-alpha); // White fill (and exterior) | |
} | |
} | |
} | |
} | |
// Points of the curves with respect to the origin point | |
var p10_x = c1[0].x; | |
var p10_y = c1[0].y; | |
var p11_x = c1[1].x; | |
var p11_y = c1[1].y; | |
var p12_x = c1[2].x; | |
var p12_y = c1[2].y; | |
var p20_x = c2[0].x; | |
var p20_y = c2[0].y; | |
var p21_x = c2[1].x; | |
var p21_y = c2[1].y; | |
var p22_x = c2[2].x; | |
var p22_y = c2[2].y; | |
if (!reversedInternalBorder) { | |
renderTriangle(p10_x, p10_y, p12_x, p12_y, p20_x, p20_y); | |
renderTriangle(p20_x, p20_y, p12_x, p12_y, p22_x, p22_y); | |
renderTriangle(p20_x, p20_y, p22_x, p22_y, p21_x, p21_y); | |
} else { | |
// If the triangle is reversed we adjust the painting to the origin | |
renderTriangle(p20_x, p20_y, o.x, o.y, p22_x, p22_y); | |
renderTriangle(p20_x, p20_y, p22_x, p22_y, p21_x, p21_y); | |
} | |
return o; | |
} | |
// Buffer for the pixel colors | |
var id = context.createImageData(w,h); | |
var d = id.data; | |
// Reverse curve | |
var baseCurve = curve; | |
if (isReversed(curve)) { | |
baseCurve = [curve[2], curve[1], curve[0]]; | |
} | |
// Main Curve | |
var reduced = reduce(baseCurve); | |
// Offset Curve | |
var i = 0; | |
var origins = []; | |
var length = 10; // In pixels TODO Correct this if scaled | |
var dashOffset = 0; | |
var offsets = reduce(baseCurve).map(function(r) { | |
var s1 = scale(r,-distance/2); | |
var s2 = scale(r,distance/2); | |
var color = [0,0,0]; //[255*(i*0.4323 % 1), 255*(i*0.4323 % 1), 255*(i*0.4323 % 1)] | |
var dashedParameters = calcDashedParameters(s2, length, dashOffset); | |
dashOffset = dashedParameters.nextSegmentOffset; | |
var o = drawCurve(s1, s2, color, dashedParameters); | |
origins.push(o); | |
i += 1; | |
return [s1,s2]; | |
}); | |
context.putImageData(id, 0, 0); | |
// Draw main skeleton | |
api.setRandomColor(); | |
api.drawSkeleton(new Bezier(baseCurve)); | |
if (false) { | |
// Draw origins | |
origins.forEach(function(o){ drawCircle(o, 2, 5, "orange") }); | |
// Draw offset skeletons | |
offsets.forEach(curves => { | |
var s1 = curves[0]; | |
var s2 = curves[1]; | |
api.setRandomColor(); | |
api.drawSkeleton(new Bezier(s1)); | |
api.drawSkeleton(new Bezier(s2)); | |
}); | |
} | |
}; | |
handler.onupdate(); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function eventFixedCoords(e) { | |
e = e || window.event; | |
var target = e.target || e.srcElement, | |
rect = target.getBoundingClientRect(); | |
return {x: e.clientX - rect.left, y: rect.bottom - e.clientY}; | |
}; | |
function handleInteraction(cvs, curve) { | |
curve.mouse = false; | |
var lpts = curve; | |
var moving = false, mx = my = ox = oy = 0, cx, cy, mp = false; | |
var handler = { onupdate: function() {} }; | |
cvs.addEventListener("mousedown", function(evt) { | |
var coords = eventFixedCoords(evt); | |
mx = coords.x; | |
my = coords.y; | |
lpts.forEach(function(p) { | |
if(Math.abs(mx-p.x)<10 && Math.abs(my-p.y)<10) { | |
moving = true; | |
mp = p; | |
cx = p.x; | |
cy = p.y; | |
} | |
}); | |
}); | |
cvs.addEventListener("mousemove", function(evt) { | |
var coords = eventFixedCoords(evt); | |
var found = false; | |
if(!lpts) return; | |
lpts.forEach(function(p) { | |
var mx = coords.x; | |
var my = coords.y; | |
if(Math.abs(mx-p.x)<10 && Math.abs(my-p.y)<10) { | |
found = found || true; | |
} | |
}); | |
cvs.style.cursor = found ? "pointer" : "default"; | |
if(!moving) { | |
return handler.onupdate(evt); | |
} | |
ox = coords.x - mx; | |
oy = coords.y - my; | |
mp.x = cx + ox; | |
mp.y = cy + oy; | |
//curve.update(); | |
handler.onupdate(); | |
}); | |
cvs.addEventListener("mouseup", function(evt) { | |
if(!moving) return; | |
// console.log(curve.points.map(function(p) { return p.x+", "+p.y; }).join(", ")); | |
moving = false; | |
mp = false; | |
}); | |
cvs.addEventListener("click", function(evt) { | |
var coords = eventFixedCoords(evt); | |
var mx = coords.x; | |
var my = coords.y; | |
}); | |
return handler; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment