A Pen by Andreas Borgen on CodePen.
Created
January 31, 2021 18:35
-
-
Save Sphinxxxx/99c0596f783fd9d75773598fdbdd231f to your computer and use it in GitHub Desktop.
Low-poly image generator
This file contains hidden or 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
<script>console.clear();</script> | |
<script type="text/javascript" src="https://unpkg.com/vue@2"></script> | |
<script type="text/javascript" src="https://unpkg.com/[email protected]/build/dat.gui.js"></script> | |
<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/inspirit/jsfeat@master/build/jsfeat.js"></script> | |
<script> | |
/* | |
tuple by Ry-♦ | |
https://stackoverflow.com/a/21839292/1869660 | |
*/ | |
window.tuple = (function(){"use strict";let map=new Map();function tuple(){let current=map;let args=Object.freeze(Array.from(arguments));for(let item of args){if(current.has(item)){current=current.get(item);}else{let next=new Map();current.set(item,next);current=next;}}if(!current._myVal){current._myVal=args;}return current._myVal;}return tuple;})(); | |
/* | |
(c) 2017, Vladimir Agafonkin | |
Simplify.js, a high-performance JS polyline simplification library | |
mourner.github.io/simplify-js | |
*/ | |
window.simplify = (function(){"use strict";function n(n,r){var t=n[0]-r[0],u=n[1]-r[1];return t*t+u*u}function r(n,r,t){var u=r[0],i=r[1],f=t[0]-u,o=t[1]-i;if(0!==f||0!==o){var e=((n[0]-u)*f+(n[1]-i)*o)/(f*f+o*o);e>1?(u=t[0],i=t[1]):e>0&&(u+=f*e,i+=o*e)}return f=n[0]-u,o=n[1]-i,f*f+o*o}function t(r,t){for(var u,i=r[0],f=[i],o=1,e=r.length;e>o;o++)u=r[o],n(u,i)>t&&(f.push(u),i=u);return i!==u&&f.push(u),f}function u(n,t,i,f,o){for(var e,v=f,a=t+1;i>a;a++){var c=r(n[a],n[t],n[i]);c>v&&(e=a,v=c)}v>f&&(e-t>1&&u(n,t,e,f,o),o.push(n[e]),i-e>1&&u(n,e,i,f,o))}function i(n,r){var t=n.length-1,i=[n[0]];return u(n,0,t,r,i),i.push(n[t]),i}function f(n,r,u){if(n.length<=2)return n;var f=void 0!==r?r*r:1;return n=u?n:t(n,f),n=i(n,f)}return f})(); | |
/* | |
https://github.com/mikolalysenko/cdt2d | |
A robust 2D constrained Delaunay triangulation library written in JavaScript | |
Copyright (c) 2015 Mikola Lysenko. The MIT License (MIT) | |
*/ | |
window.cdt2d = (function(){"use strict";function r(r,n,t,e,a){for(var u=a+1;a>=e;){var o=e+a>>>1,i=r[o],s=void 0!==t?t(i,n):i-n;s>=0?(u=o,a=o-1):e=o+1}return u}function n(r,n,t,e,a){for(var u=a+1;a>=e;){var o=e+a>>>1,i=r[o],s=void 0!==t?t(i,n):i-n;s>0?(u=o,a=o-1):e=o+1}return u}function t(r,n,t,e,a){for(var u=e-1;a>=e;){var o=e+a>>>1,i=r[o],s=void 0!==t?t(i,n):i-n;0>s?(u=o,e=o+1):a=o-1}return u}function e(r,n,t,e,a){for(var u=e-1;a>=e;){var o=e+a>>>1,i=r[o],s=void 0!==t?t(i,n):i-n;0>=s?(u=o,e=o+1):a=o-1}return u}function a(r,n,t,e,a){for(;a>=e;){var u=e+a>>>1,o=r[u],i=void 0!==t?t(o,n):o-n;if(0===i)return u;0>=i?e=u+1:a=u-1}return-1}function u(r,n,t,e,a,u){return"function"==typeof t?u(r,n,t,void 0===e?0:0|e,void 0===a?r.length-1:0|a):u(r,n,void 0,void 0===t?0:0|t,void 0===e?r.length-1:0|e)}function o(r){var n={exports:{}};return r(n,n.exports),n.exports}function i(r,n,t){var e=r*n,a=K*r,u=a-r,o=a-u,i=r-o,s=K*n,f=s-n,h=s-f,v=n-h,l=e-o*h,c=l-i*h,p=c-o*v,g=i*v-p;return t?(t[0]=g,t[1]=e,t):[g,e]}function s(r,n){var t=r+n,e=t-r,a=t-e,u=n-e,o=r-a,i=o+u;return i?[i,t]:[t]}function f(r,n){var t=0|r.length,e=0|n.length;if(1===t&&1===e)return s(r[0],n[0]);var a,u,o=t+e,i=Array(o),f=0,h=0,v=0,l=Math.abs,c=r[h],p=l(c),g=n[v],d=l(g);d>p?(u=c,h+=1,t>h&&(c=r[h],p=l(c))):(u=g,v+=1,e>v&&(g=n[v],d=l(g))),t>h&&d>p||v>=e?(a=c,h+=1,t>h&&(c=r[h],p=l(c))):(a=g,v+=1,e>v&&(g=n[v],d=l(g)));for(var y,b,m,w,x,A=a+u,M=A-a,I=u-M,j=I,T=A;t>h&&e>v;)d>p?(a=c,h+=1,t>h&&(c=r[h],p=l(c))):(a=g,v+=1,e>v&&(g=n[v],d=l(g))),u=j,A=a+u,M=A-a,I=u-M,I&&(i[f++]=I),y=T+A,b=y-T,m=y-b,w=A-b,x=T-m,j=x+w,T=y;for(;t>h;)a=c,u=j,A=a+u,M=A-a,I=u-M,I&&(i[f++]=I),y=T+A,b=y-T,m=y-b,w=A-b,x=T-m,j=x+w,T=y,h+=1,t>h&&(c=r[h]);for(;e>v;)a=g,u=j,A=a+u,M=A-a,I=u-M,I&&(i[f++]=I),y=T+A,b=y-T,m=y-b,w=A-b,x=T-m,j=x+w,T=y,v+=1,e>v&&(g=n[v]);return j&&(i[f++]=j),T&&(i[f++]=T),f||(i[f++]=0),i.length=f,i}function h(r,n,t){var e=r+n,a=e-r,u=e-a,o=n-a,i=r-u;return t?(t[0]=i+o,t[1]=e,t):[i+o,e]}function v(r,n){var t=r.length;if(1===t){var e=J(r[0],n);return e[0]?e:[e[1]]}var a=Array(2*t),u=[.1,.1],o=[.1,.1],i=0;J(r[0],n,u),u[0]&&(a[i++]=u[0]);for(var s=1;t>s;++s){J(r[s],n,o);var f=u[1];N(f,o[0],u),u[0]&&(a[i++]=u[0]);var h=o[1],v=u[1],l=h+v,c=l-h,p=v-c;u[1]=l,p&&(a[i++]=p)}return u[1]&&(a[i++]=u[1]),0===i&&(a[i++]=0),a.length=i,a}function l(r,n){var t=r+n,e=t-r,a=t-e,u=n-e,o=r-a,i=o+u;return i?[i,t]:[t]}function c(r,n){var t=0|r.length,e=0|n.length;if(1===t&&1===e)return l(r[0],-n[0]);var a,u,o=t+e,i=Array(o),s=0,f=0,h=0,v=Math.abs,c=r[f],p=v(c),g=-n[h],d=v(g);d>p?(u=c,f+=1,t>f&&(c=r[f],p=v(c))):(u=g,h+=1,e>h&&(g=-n[h],d=v(g))),t>f&&d>p||h>=e?(a=c,f+=1,t>f&&(c=r[f],p=v(c))):(a=g,h+=1,e>h&&(g=-n[h],d=v(g)));for(var y,b,m,w,x,A=a+u,M=A-a,I=u-M,j=I,T=A;t>f&&e>h;)d>p?(a=c,f+=1,t>f&&(c=r[f],p=v(c))):(a=g,h+=1,e>h&&(g=-n[h],d=v(g))),u=j,A=a+u,M=A-a,I=u-M,I&&(i[s++]=I),y=T+A,b=y-T,m=y-b,w=A-b,x=T-m,j=x+w,T=y;for(;t>f;)a=c,u=j,A=a+u,M=A-a,I=u-M,I&&(i[s++]=I),y=T+A,b=y-T,m=y-b,w=A-b,x=T-m,j=x+w,T=y,f+=1,t>f&&(c=r[f]);for(;e>h;)a=g,u=j,A=a+u,M=A-a,I=u-M,I&&(i[s++]=I),y=T+A,b=y-T,m=y-b,w=A-b,x=T-m,j=x+w,T=y,h+=1,e>h&&(g=-n[h]);return j&&(i[s++]=j),T&&(i[s++]=T),s||(i[s++]=0),i.length=s,i}function p(r,n,t,e,a){this.a=r,this.b=n,this.idx=t,this.lowerIds=e,this.upperIds=a}function g(r,n,t,e){this.a=r,this.b=n,this.type=t,this.idx=e}function d(r,n){var t=r.a[0]-n.a[0]||r.a[1]-n.a[1]||r.type-n.type;return t?t:r.type!==V&&(t=U(r.a,r.b,n.b))?t:r.idx-n.idx}function y(r,n){return U(r.a,r.b,n)}function b(r,n,t,e,a){for(var u=H.lt(n,e,y),o=H.gt(n,e,y),i=u;o>i;++i){for(var s=n[i],f=s.lowerIds,h=f.length;h>1&&U(t[f[h-2]],t[f[h-1]],e)>0;)r.push([f[h-1],f[h-2],a]),h-=1;f.length=h,f.push(a);for(var v=s.upperIds,h=v.length;h>1&&U(t[v[h-2]],t[v[h-1]],e)<0;)r.push([v[h-2],v[h-1],a]),h-=1;v.length=h,v.push(a)}}function m(r,n){var t;return(t=r.a[0]<n.a[0]?U(r.a,r.b,n.a):U(n.b,n.a,r.a))?t:(t=n.b[0]<r.b[0]?U(r.a,r.b,n.b):U(n.b,n.a,r.b),t||r.idx-n.idx)}function w(r,n,t){var e=H.le(r,t,m),a=r[e],u=a.upperIds,o=u[u.length-1];a.upperIds=[o],r.splice(e+1,0,new p(t.a,t.b,t.idx,[o],u))}function x(r,n,t){var e=t.a;t.a=t.b,t.b=e;var a=H.eq(r,t,m),u=r[a],o=r[a-1];o.upperIds=u.upperIds,r.splice(a,1)}function A(r,n){for(var t=r.length,e=n.length,a=[],u=0;t>u;++u)a.push(new g(r[u],null,V,u));for(var u=0;e>u;++u){var o=n[u],i=r[o[0]],s=r[o[1]];i[0]<s[0]?a.push(new g(i,s,X,u),new g(s,i,W,u)):i[0]>s[0]&&a.push(new g(s,i,X,u),new g(i,s,W,u))}a.sort(d);for(var f=a[0].a[0]-(1+Math.abs(a[0].a[0]))*Math.pow(2,-52),h=[new p([f,1],[f,0],-1,[],[],[],[])],v=[],u=0,l=a.length;l>u;++u){var c=a[u],y=c.type;y===V?b(v,h,r,c.a,c.idx):y===X?w(h,r,c):x(h,r,c)}return v}function M(r,n){this.stars=r,this.edges=n}function I(r,n,t){for(var e=1,a=r.length;a>e;e+=2)if(r[e-1]===n&&r[e]===t)return r[e-1]=r[a-2],r[e]=r[a-1],void(r.length=a-2)}function j(r,n){for(var t=Array(r),e=0;r>e;++e)t[e]=[];return new M(t,n)}function T(r,n,t,e,a,u){var o=n.opposite(e,a);if(!(0>o)){if(e>a){var i=e;e=a,a=i,i=u,u=o,o=i}n.isConstraint(e,a)||rr(r[e],r[a],r[u],r[o])<0&&t.push(e,a)}}function q(r,n){for(var t=[],e=r.length,a=n.stars,u=0;e>u;++u)for(var o=a[u],i=1;i<o.length;i+=2){var s=o[i];if(!(u>s||n.isConstraint(u,s))){for(var f=o[i-1],h=-1,v=1;v<o.length;v+=2)if(o[v-1]===s){h=o[v];break}0>h||rr(r[u],r[s],r[f],r[h])<0&&t.push(u,s)}}for(;t.length>0;){for(var s=t.pop(),u=t.pop(),f=-1,h=-1,o=a[u],l=1;l<o.length;l+=2){var c=o[l-1],p=o[l];c===s?h=p:p===s&&(f=c)}0>f||0>h||rr(r[u],r[s],r[f],r[h])>=0||(n.flip(u,s),T(r,n,t,f,u,h),T(r,n,t,u,h,f),T(r,n,t,h,s,f),T(r,n,t,s,f,h))}}function C(r,n,t,e,a,u,o){this.cells=r,this.neighbor=n,this.flags=e,this.constraint=t,this.active=a,this.next=u,this.boundary=o}function F(r,n){return r[0]-n[0]||r[1]-n[1]||r[2]-n[2]}function S(r,n){for(var t=r.cells(),e=t.length,a=0;e>a;++a){var u=t[a],o=u[0],i=u[1],s=u[2];s>i?o>i&&(u[0]=i,u[1]=s,u[2]=o):o>s&&(u[0]=s,u[1]=o,u[2]=i)}t.sort(F);for(var f=Array(e),a=0;a<f.length;++a)f[a]=0;var h=[],v=[],l=Array(3*e),c=Array(3*e),p=null;n&&(p=[]);for(var g=new C(t,l,c,f,h,v,p),a=0;e>a;++a)for(var u=t[a],d=0;3>d;++d){var o=u[d],i=u[(d+1)%3],y=l[3*a+d]=g.locate(i,o,r.opposite(i,o)),b=c[3*a+d]=r.isConstraint(o,i);0>y&&(b?v.push(a):(h.push(a),f[a]=1),n&&p.push([i,o,-1]))}return g}function O(r,n,t){for(var e=0,a=0;a<r.length;++a)n[a]===t&&(r[e++]=r[a]);return r.length=e,r}function k(r,n,t){var e=S(r,t);if(0===n)return t?e.cells.concat(e.boundary):e.cells;for(var a=1,u=e.active,o=e.next,i=e.flags,s=e.cells,f=e.constraint,h=e.neighbor;u.length>0||o.length>0;){for(;u.length>0;){var v=u.pop();if(i[v]!==-a){i[v]=a,s[v];for(var l=0;3>l;++l){var c=h[3*v+l];c>=0&&0===i[c]&&(f[3*v+l]?o.push(c):(u.push(c),i[c]=a))}}}var p=o;o=u,u=p,o.length=0,a=-a}var g=O(s,i,n);return t?g.concat(e.boundary):g}function E(r){return[Math.min(r[0],r[1]),Math.max(r[0],r[1])]}function z(r,n){return r[0]-n[0]||r[1]-n[1]}function B(r){return r.map(E).sort(z)}function D(r,n,t){return n in r?r[n]:t}function G(r,n,t){Array.isArray(n)?(t=t||{},n=n||[]):(t=n||{},n=[]);var e=!!D(t,"delaunay",!0),a=!!D(t,"interior",!0),u=!!D(t,"exterior",!0),o=!!D(t,"infinity",!1);if(!a&&!u||0===r.length)return[];var i=Y(r,n);if(e||a!==u||o){for(var s=Z(r.length,B(n)),f=0;f<i.length;++f){var h=i[f];s.addTriangle(h[0],h[1],h[2])}return e&&nr(r,s),u?a?o?tr(s,0,o):s.cells():tr(s,1,o):tr(s,-1)}return i}var H={ge:function(n,t,e,a,o){return u(n,t,e,a,o,r)},gt:function(r,t,e,a,o){return u(r,t,e,a,o,n)},lt:function(r,n,e,a,o){return u(r,n,e,a,o,t)},le:function(r,n,t,a,o){return u(r,n,t,a,o,e)},eq:function(r,n,t,e,o){return u(r,n,t,e,o,a)}},J=i,K=+(Math.pow(2,27)+1),L=f,N=h,P=v,Q=c,R=o(function(r){function n(r,n){for(var t=Array(r.length-1),e=1;e<r.length;++e)for(var a=t[e-1]=Array(r.length-1),u=0,o=0;u<r.length;++u)u!==n&&(a[o++]=r[e][u]);return t}function t(r){for(var n=Array(r),t=0;r>t;++t){n[t]=Array(r);for(var e=0;r>e;++e)n[t][e]="m"+e+"["+(r-t-1)+"]"}return n}function e(r){return 1&r?"-":""}function a(r){if(1===r.length)return r[0];if(2===r.length)return"sum("+r[0]+","+r[1]+")";var n=r.length>>1;return"sum("+a(r.slice(0,n))+","+a(r.slice(n))+")"}function u(r){if(2===r.length)return["sum(prod("+r[0][0]+","+r[1][1]+"),prod(-"+r[0][1]+","+r[1][0]+"))"];for(var t=[],o=0;o<r.length;++o)t.push("scale("+a(u(n(r,o)))+","+e(o)+r[0][o]+")");return t}function o(r){for(var e=[],o=[],i=t(r),s=[],f=0;r>f;++f)0===(1&f)?e.push.apply(e,u(n(i,f))):o.push.apply(o,u(n(i,f))),s.push("m"+f);var h=a(e),v=a(o),l="orientation"+r+"Exact",c="function "+l+"("+s.join()+"){var p="+h+",n="+v+",d=sub(p,n);return d[d.length-1];};return "+l,p=Function("sum","prod","scale","sub",c);return p(L,J,P,Q)}function i(r){var n=g[r.length];return n||(n=g[r.length]=o(r.length)),n.apply(void 0,r)}function s(){for(;g.length<=f;)g.push(o(g.length));for(var n=[],t=["slow"],e=0;f>=e;++e)n.push("a"+e),t.push("o"+e);for(var a=["function getOrientation(",n.join(),"){switch(arguments.length){case 0:case 1:return 0;"],e=2;f>=e;++e)a.push("case ",e,":return o",e,"(",n.slice(0,e).join(),");");a.push("}var s=new Array(arguments.length);for(var i=0;i<arguments.length;++i){s[i]=arguments[i]};return slow(s);}return getOrientation"),t.push(a.join(""));var u=Function.apply(void 0,t);r.exports=u.apply(void 0,[i].concat(g));for(var e=0;f>=e;++e)r.exports[e]=g[e]}var f=5,h=1.1102230246251565e-16,v=(3+16*h)*h,l=(7+56*h)*h,c=o(3),p=o(4),g=[function(){return 0},function(){return 0},function(r,n){return n[0]-r[0]},function(r,n,t){var e,a=(r[1]-t[1])*(n[0]-t[0]),u=(r[0]-t[0])*(n[1]-t[1]),o=a-u;if(a>0){if(0>=u)return o;e=a+u}else{if(!(0>a))return o;if(u>=0)return o;e=-(a+u)}var i=v*e;return o>=i||-i>=o?o:c(r,n,t)},function(r,n,t,e){var a=r[0]-e[0],u=n[0]-e[0],o=t[0]-e[0],i=r[1]-e[1],s=n[1]-e[1],f=t[1]-e[1],h=r[2]-e[2],v=n[2]-e[2],c=t[2]-e[2],g=u*f,d=o*s,y=o*i,b=a*f,m=a*s,w=u*i,x=h*(g-d)+v*(y-b)+c*(m-w),A=(Math.abs(g)+Math.abs(d))*Math.abs(h)+(Math.abs(y)+Math.abs(b))*Math.abs(v)+(Math.abs(m)+Math.abs(w))*Math.abs(c),M=l*A;return x>M||-x>M?x:p(r,n,t,e)}];s()}),U=R[3],V=0,W=1,X=2,Y=A,Z=j,$=M.prototype;$.isConstraint=function(){function r(r,n){return r[0]-n[0]||r[1]-n[1]}var n=[0,0];return function(t,e){return n[0]=Math.min(t,e),n[1]=Math.max(t,e),H.eq(this.edges,n,r)>=0}}(),$.removeTriangle=function(r,n,t){var e=this.stars;I(e[r],n,t),I(e[n],t,r),I(e[t],r,n)},$.addTriangle=function(r,n,t){var e=this.stars;e[r].push(n,t),e[n].push(t,r),e[t].push(r,n)},$.opposite=function(r,n){for(var t=this.stars[n],e=1,a=t.length;a>e;e+=2)if(t[e]===r)return t[e-1];return-1},$.flip=function(r,n){var t=this.opposite(r,n),e=this.opposite(n,r);this.removeTriangle(r,n,t),this.removeTriangle(n,r,e),this.addTriangle(r,e,t),this.addTriangle(n,t,e)},$.edges=function(){for(var r=this.stars,n=[],t=0,e=r.length;e>t;++t)for(var a=r[t],u=0,o=a.length;o>u;u+=2)n.push([a[u],a[u+1]]);return n},$.cells=function(){for(var r=this.stars,n=[],t=0,e=r.length;e>t;++t)for(var a=r[t],u=0,o=a.length;o>u;u+=2){var i=a[u],s=a[u+1];t<Math.min(i,s)&&n.push([t,i,s])}return n};var _=o(function(r){function n(r,n){for(var t=Array(r.length-1),e=1;e<r.length;++e)for(var a=t[e-1]=Array(r.length-1),u=0,o=0;u<r.length;++u)u!==n&&(a[o++]=r[e][u]);return t}function t(r){for(var n=Array(r),t=0;r>t;++t){n[t]=Array(r);for(var e=0;r>e;++e)n[t][e]="m"+e+"["+(r-t-2)+"]"}return n}function e(r){if(1===r.length)return r[0];if(2===r.length)return"sum("+r[0]+","+r[1]+")";var n=r.length>>1;return"sum("+e(r.slice(0,n))+","+e(r.slice(n))+")"}function a(r,n){if("m"===r.charAt(0)){if("w"===n.charAt(0)){var t=r.split("[");return"w"+n.substr(1)+"m"+t[0].substr(1)}return"prod("+r+","+n+")"}return a(n,r)}function u(r){return r&!0?"-":""}function o(r){if(2===r.length)return["diff("+a(r[0][0],r[1][1])+","+a(r[1][0],r[0][1])+")"];for(var t=[],i=0;i<r.length;++i)t.push("scale("+e(o(n(r,i)))+","+u(i)+r[0][i]+")");return t}function i(r,n){for(var t=[],a=0;n-2>a;++a)t.push("prod(m"+r+"["+a+"],m"+r+"["+a+"])");return e(t)}function s(r){for(var a=[],u=[],s=t(r),f=0;r>f;++f)s[0][f]="1",s[r-1][f]="w"+f;for(var f=0;r>f;++f)0===(1&f)?a.push.apply(a,o(n(s,f))):u.push.apply(u,o(n(s,f)));for(var h=e(a),v=e(u),l="exactInSphere"+r,c=[],f=0;r>f;++f)c.push("m"+f);for(var p=["function ",l,"(",c.join(),"){"],f=0;r>f;++f){p.push("var w",f,"=",i(f,r),";");for(var g=0;r>g;++g)g!==f&&p.push("var w",f,"m",g,"=scale(w",f,",m",g,"[0]);")}p.push("var p=",h,",n=",v,",d=diff(p,n);return d[d.length-1];}return ",l);var d=Function("sum","diff","prod","scale",p.join(""));return d(L,Q,J,P)}function f(){return 0}function h(){return 0}function v(){return 0}function l(r){var n=g[r.length];return n||(n=g[r.length]=s(r.length)),n.apply(void 0,r)}function c(){for(;g.length<=p;)g.push(s(g.length));for(var n=[],t=["slow"],e=0;p>=e;++e)n.push("a"+e),t.push("o"+e);for(var a=["function testInSphere(",n.join(),"){switch(arguments.length){case 0:case 1:return 0;"],e=2;p>=e;++e)a.push("case ",e,":return o",e,"(",n.slice(0,e).join(),");");a.push("}var s=new Array(arguments.length);for(var i=0;i<arguments.length;++i){s[i]=arguments[i]};return slow(s);}return testInSphere"),t.push(a.join(""));var u=Function.apply(void 0,t);r.exports=u.apply(void 0,[l].concat(g));for(var e=0;p>=e;++e)r.exports[e]=g[e]}var p=6,g=[f,h,v];c()}),rr=_[4],nr=q,tr=k,er=C.prototype;er.locate=function(){var r=[0,0,0];return function(n,t,e){var a=n,u=t,o=e;return e>t?n>t&&(a=t,u=e,o=n):n>e&&(a=e,u=n,o=t),0>a?-1:(r[0]=a,r[1]=u,r[2]=o,H.eq(this.cells,r,F))}}();var ar=G;return ar})(); | |
</script> | |
<main id="app"> | |
<input type="file" id="sourceImg" accept="image/*" /> | |
<div id="basic"> | |
<canvas id="source" v-bind="size"></canvas> | |
<svg id="low-poly" v-bind="size" xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="currentColor" stroke-width=".5"> | |
<polygon v-for="tri in triangles" :points="tri" :color="pickColor(tri)" /> | |
</svg> | |
<span class="credit">Example hoto by <a href="https://unsplash.com/@n0plex?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Daniil Lebedev</a> on <a href="https://unsplash.com/@n0plex?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span> | |
</div> | |
<details id="advanced"> | |
<summary>Advanced</summary> | |
<div> | |
<div id="controls"></div> | |
<canvas id="edges" v-bind="size"></canvas> | |
<svg id="lines" v-bind="size"> | |
<g id="delaunay"> | |
<polygon v-for="tri in triangles" :points="tri" /> | |
</g> | |
<g id="polys"> | |
<polyline v-for="(line, i) in polylines" :points="line" :style="{ stroke: debugColor(i) }" /> | |
</g> | |
</svg> | |
</div> | |
</details> | |
</main> |
This file contains hidden or 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() { | |
"use strict"; | |
function log() { | |
const now = new Date(), | |
time = now.toLocaleTimeString() + '.' + now.getMilliseconds().toString().padEnd(3, '0'); | |
console.log.apply(console, [time].concat(Array.from(arguments))); | |
} | |
function debugColor(i) { | |
//https://coolors.co/ffbe0b-fb5607-ff006e-8338ec-3a86ff-79ff4d | |
//https://learnui.design/tools/data-color-picker.html#divergent | |
const colors = [ | |
[0xff, 0x00, 0x00], | |
[0xff, 0xff, 0x00], | |
[0x00, 0xff, 0x00], | |
[0x00, 0xff, 0xff], | |
[0x00, 0x00, 0xff], | |
[0xff, 0x00, 0xff], | |
]; | |
const color = colors[i % colors.length]; | |
return color; | |
} | |
const _state = { | |
options: { | |
zoom: 1, | |
canny: { | |
blur_radius: 4, //5, | |
low_threshold: 1, | |
high_threshold: 80, //70, | |
}, | |
tracing: { | |
minLength: 10, | |
lineTolerance: 4, //7, | |
useDelaunay: true, | |
} | |
}, | |
image: { | |
width: 0, | |
height: 0, | |
traced: [], | |
}, | |
}; | |
let _img, _w, _h, _ctx, _ctxSource, _sourceData, _bitmap; | |
function setImage(img) { | |
_img = img; | |
_w = img.width; | |
_h = img.height; | |
const measure = Math.max(_w, _h); | |
if(measure > 600) { | |
const shrink = 600/measure; | |
_w = Math.round(_w * shrink); | |
_h = Math.round(_h * shrink); | |
} | |
_state.image.width = _w; | |
_state.image.height = _h; | |
_ctx = document.querySelector('#edges').getContext("2d"); | |
_ctxSource = document.querySelector('#source').getContext("2d"); | |
_bitmap = new jsfeat.matrix_t(_w, _h, jsfeat.U8C1_t); | |
} | |
function render() { | |
_ctxSource.drawImage(_img, 0, 0, _w, _h); | |
_sourceData = _ctxSource.getImageData(0, 0, _w, _h); | |
const ctx = _ctx, | |
canny = _state.options.canny; | |
ctx.drawImage(_img, 0, 0, _w, _h); | |
var imageData = ctx.getImageData(0, 0, _w, _h); | |
log("grayscale"); | |
jsfeat.imgproc.grayscale(imageData.data, _w, _h, _bitmap); | |
var r = canny.blur_radius | 0; | |
var kernel_size = (r + 1) << 1; | |
log("gauss blur", r, kernel_size); | |
jsfeat.imgproc.gaussian_blur(_bitmap, _bitmap, kernel_size, 0); | |
log("canny edge"); | |
jsfeat.imgproc.canny( | |
_bitmap, | |
_bitmap, | |
canny.low_threshold, | |
canny.high_threshold | |
); | |
// render result back to canvas | |
log("render", _bitmap); | |
const data_u32 = new Uint32Array(imageData.data.buffer); | |
function setPixel(i, r, g, b) { | |
const alpha = 0xff << 24; | |
data_u32[i] = alpha | (b << 16)| (g << 8) | r; | |
} | |
let i = _bitmap.cols * _bitmap.rows, | |
pix = 0; | |
while (--i >= 0) { | |
pix = _bitmap.data[i] ? 160 : 0; | |
setPixel(i, pix, pix, pix); | |
} | |
ctx.putImageData(imageData, 0, 0); | |
log('trace lines'); | |
const grid = []; | |
i = 0; | |
for(let y = 0; y < _h; y++) { | |
const row = _bitmap.data.slice(i, i + _w); | |
grid.push(row); | |
i += _w; | |
} | |
//console.log(grid); | |
const traces = []; | |
for(let y = 0; y < _h; y++) { | |
for(let x = 0; x < _w; x++) { | |
const px = grid[y][x]; | |
if(px) { | |
traces.push.apply(traces, traceFrom(x, y)); | |
} | |
} | |
} | |
log('traced lines', traces.length); //.reduce((sum, x) => sum + x.length, 0)); | |
const tracing = _state.options.tracing; | |
const importantLines = []; | |
traces.forEach(trace => { | |
if(trace.length < tracing.minLength) { return; } | |
//Mark pixels: | |
const [r, g, b] = debugColor(importantLines.length); | |
trace.forEach(([x, y]) => setPixel(y * _w + x, r, g, b)); | |
importantLines.push(trace); | |
}); | |
ctx.putImageData(imageData, 0, 0); | |
_state.image.traced = importantLines; | |
log('drawn lines', importantLines.length); | |
function traceFrom(x_1, y_1) { | |
//const lines = []; | |
const branches = []; | |
function checkPixel(x, y) { | |
return grid[y] && grid[y][x]; | |
} | |
function findNext(x, y, findAll) { | |
const options = []; | |
//Check up/down/left/right first, and only diagonals if we don't find anything. | |
//This will let us trace "staircases" as one line, and not a series or intersections. | |
if(checkPixel(x, y - 1)) { options.push(tuple(x, y - 1)); } | |
if(checkPixel(x, y + 1)) { options.push(tuple(x, y + 1)); } | |
if(checkPixel(x - 1, y)) { options.push(tuple(x - 1, y)); } | |
if(checkPixel(x + 1, y)) { options.push(tuple(x + 1, y)); } | |
if(options.length && !findAll) { return options; } | |
if(checkPixel(x - 1, y - 1)) { options.push(tuple(x - 1, y - 1)); } | |
if(checkPixel(x + 1, y - 1)) { options.push(tuple(x + 1, y - 1)); } | |
if(checkPixel(x - 1, y + 1)) { options.push(tuple(x - 1, y + 1)); } | |
if(checkPixel(x + 1, y + 1)) { options.push(tuple(x + 1, y + 1)); } | |
return options; | |
} | |
function collect(a, b, branch) { | |
branch.push(tuple(a, b)); | |
grid[b][a] = 0; | |
} | |
function addBranch(startX, startY) { | |
const branch = []; | |
collect(startX, startY, branch); | |
branches.push(branch); | |
return branch; | |
} | |
//Start by initing our main branch | |
addBranch(x_1, y_1); | |
for(let i = 0; i < branches.length; i++) { | |
const branch = branches[i]; | |
let [x0, y0] = branch[0], | |
[x, y] = branch[branch.length - 1]; | |
let nexts, pass = 1, endOfLine = false; | |
while(true) { | |
nexts = findNext(x, y); | |
//If we reach an intersection: | |
if((nexts.length > 1) && (branch.length > 1)) { | |
//This may just be a 1px branch/noise, or a small section where the edge is 2px wide. | |
//To test that, we look at all the pixels we can reach from the first step of a new branch. | |
//If there are no more pixels than we can reach from our current position, | |
//it's just noise and not an actual branch. | |
const currentReach = findNext(x, y, true); | |
let maxReach = -1, maxBranch, toDelete = []; | |
nexts = nexts.filter(([xx, yy]) => { | |
const branchReach = findNext(xx, yy, true), | |
branchHasNewPixels = branchReach.some(px => !currentReach.includes(px)); | |
if(branchReach.length > maxReach) { | |
maxReach = branchReach.length; | |
maxBranch = tuple(xx, yy); | |
} | |
//If we decide this is just noise, clear the pixel so it's not picked up by a later traceFrom(). | |
//Do that after this loop is finished, or else it will interfere with the other branches chance at a `maxReach`: | |
if(!branchHasNewPixels) { toDelete.push(tuple(xx, yy)); } | |
return branchHasNewPixels; | |
}); | |
toDelete.forEach(([xx, yy]) => grid[yy][xx] = 0); | |
//If we filtered away all the branches, keep the one with more pixels. | |
//This lets us go around noisy corners or finish noisy line ends: | |
if(nexts.length === 0) { nexts = [maxBranch]; } | |
//This is a real intersection of branches: | |
if(nexts.length > 1) { | |
nexts.forEach(coord => { | |
const otherBranch = addBranch(x, y); | |
collect(coord[0], coord[1], otherBranch); | |
}); | |
endOfLine = true; | |
} | |
} | |
//Keep following the current branch: | |
if(nexts.length && !endOfLine) { | |
[x, y] = nexts[0]; | |
collect(x, y, branch); | |
} | |
else { | |
endOfLine = true; | |
} | |
if(endOfLine) { | |
if(pass === 1) { | |
//Reverse and trace in the other direction from our starting point; | |
branch.reverse(); | |
x = x0; | |
y = y0; | |
pass = 2; | |
endOfLine = false; | |
} | |
else { | |
break; | |
} | |
} | |
} | |
} | |
return branches; | |
} | |
} | |
function load() { | |
(function controls() { | |
const canny = new dat.GUI({ autoPlace: false }); | |
//canny.add(_state.options, "zoom", 1, 6).step(1).onChange(x => document.querySelector('#result').style.zoom = x); | |
const cannyHeader = canny.addFolder('Canny edge detection'); | |
cannyHeader.open(); | |
cannyHeader.add(_state.options.canny, "blur_radius", 0, 10).step(1).onChange(render); | |
cannyHeader.add(_state.options.canny, "low_threshold", 1, 200).step(1).onChange(render); | |
cannyHeader.add(_state.options.canny, "high_threshold", 1, 200).step(1).onChange(render); | |
const tracing = new dat.GUI({ autoPlace: false }); | |
const tracingHeader = tracing.addFolder('Line tracing'); | |
tracingHeader.open(); | |
tracingHeader.add(_state.options.tracing, "minLength", 0, 50).step(1).onChange(render); | |
tracingHeader.add(_state.options.tracing, "lineTolerance", 0, 10).step(1).onChange(render); | |
tracingHeader.add(_state.options.tracing, "useDelaunay").onChange(render); | |
document.querySelector('#controls').insertAdjacentElement('afterbegin', tracing.domElement); | |
document.querySelector('#controls').insertAdjacentElement('afterbegin', canny.domElement); | |
})(); | |
const img = new Image(); | |
img.onload = e => { | |
setImage(img); | |
//Changing a canvas' size (which is done by Vue) clears its content, | |
//so make sure we render() *after* the size has changed: | |
setTimeout(render, 0); | |
} | |
document.querySelector('#sourceImg').onchange = function(e) { | |
var url = URL.createObjectURL(this.files[0]); | |
img.src = url; | |
}; | |
img.crossOrigin = "anonymous"; | |
img.src = 'https://images.unsplash.com/photo-1589221158826-aed6c80c3f15?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80'; | |
} | |
new Vue({ | |
el: '#app', | |
data: _state, | |
computed: { | |
size() { | |
if(this.image.width) { | |
return { width: this.image.width, height: this.image.height }; | |
} | |
}, | |
polylines() { | |
return this.image.traced.map(trace => { | |
const tracing = this.options.tracing; | |
return simplify(trace, tracing.lineTolerance, true); | |
}); | |
}, | |
triangles() { | |
if(!this.polylines.length) { return []; } | |
const points = new Set(), edges = []; | |
this.polylines.forEach(poly => { | |
let p1 = tuple(...poly[0]), p2; | |
points.add(p1); | |
for(let i = 1; i < poly.length; i++) { | |
p2 = tuple(...poly[i]); | |
points.add(p2); | |
edges.push([p1, p2]); | |
p1 = p2; | |
} | |
}); | |
//Constrained Delaunay triangulation: | |
const pointsArr = Array.from(points).concat([ | |
[ 0, 0], | |
[_w, 0], | |
[_w, _h], | |
[ 0, _h] | |
]); | |
const edgeIndexes = edges.map(edge => edge.map(p => pointsArr.indexOf(p))); | |
//console.log('cdt 2', JSON.stringify(pointsArr), JSON.stringify(edgeIndexes)); | |
let delaunay; | |
try { | |
delaunay = cdt2d(pointsArr, edgeIndexes, { delaunay: this.options.tracing.useDelaunay }); | |
} | |
catch(ex) { | |
//"TypeError: Cannot read property 'upperIds' of undefined" | |
delaunay = []; | |
} | |
const triangles = delaunay.map(tri => tri.map(i => pointsArr[i])); | |
//console.log('cdt 3', delaunay, triangles); | |
return triangles; | |
}, | |
}, | |
methods: { | |
debugColor(i) { | |
return `rgb(${debugColor(i)})`; | |
}, | |
pickColor(triangle) { | |
const [x, y] = this.centroid(triangle).map(Math.round), | |
i = 4 * (y * _w + x); | |
const data = _sourceData.data, | |
r = data[i], | |
g = data[i + 1], | |
b = data[i + 2]; | |
return `rgb(${[r, g, b]})`; | |
}, | |
centroid(triangle) { | |
let x = 0, y = 0; | |
triangle.forEach(point => { | |
x += point[0]; | |
y += point[1]; | |
}); | |
return [x/3, y/3]; | |
} | |
}, | |
mounted() { | |
load(); | |
}, | |
}) | |
})(); |
This file contains hidden or 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
body { | |
font-family: Georgia, sans-serif; | |
input, button { | |
font: inherit | |
} | |
} | |
#basic { | |
margin: .5em 0; | |
.credit { | |
display: block; | |
} | |
} | |
#advanced { | |
image-rendering: pixelated; | |
summary { | |
background: orange; | |
padding: .5em 1em; | |
} | |
#lines { | |
fill: none; | |
#delaunay { | |
fill: black; | |
stroke: gray; | |
} | |
#polys { | |
stroke-width: 2; | |
stroke-linecap: square; | |
} | |
} | |
.dg.main { | |
display: inline-block; | |
.close-button { | |
display: none; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment