/* imagetracer.js version 1.2.6 Simple raster image tracer and vectorizer written in JavaScript. andras@jankovics.net */ /* The Unlicense / PUBLIC DOMAIN This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to http://unlicense.org/ */ (function(){ 'use strict'; function ImageTracer(){ var _this = this; this.versionnumber = '1.2.6', //////////////////////////////////////////////////////////// // // API // //////////////////////////////////////////////////////////// // Loading an image from a URL, tracing when loaded, // then executing callback with the scaled svg string as argument this.imageToSVG = function( url, callback, options ){ options = _this.checkoptions(options); // loading image, tracing and callback _this.loadImage( url, function(canvas){ callback( _this.imagedataToSVG( _this.getImgdata(canvas), options ) ); }, options ); },// End of imageToSVG() // Tracing imagedata, then returning the scaled svg string this.imagedataToSVG = function( imgd, options ){ options = _this.checkoptions(options); // tracing imagedata var td = _this.imagedataToTracedata( imgd, options ); // returning SVG string return _this.getsvgstring(td, options); },// End of imagedataToSVG() // Loading an image from a URL, tracing when loaded, // then executing callback with tracedata as argument this.imageToTracedata = function( url, callback, options ){ options = _this.checkoptions(options); // loading image, tracing and callback _this.loadImage( url, function(canvas){ callback( _this.imagedataToTracedata( _this.getImgdata(canvas), options ) ); }, options ); },// End of imageToTracedata() // Tracing imagedata, then returning tracedata (layers with paths, palette, image size) this.imagedataToTracedata = function( imgd, options ){ options = _this.checkoptions(options); // 1. Color quantization var ii = _this.colorquantization( imgd, options ); if(options.layering === 0){// Sequential layering // create tracedata object var tracedata = { layers : [], palette : ii.palette, width : ii.array[0].length-2, height : ii.array.length-2 }; // Loop to trace each color layer for(var colornum=0; colornum pathscan -> internodes -> batchtracepaths var tracedlayer = _this.batchtracepaths( _this.internodes( _this.pathscan( _this.layeringstep( ii, colornum ), options.pathomit ), options ), options.ltres, options.qtres ); // adding traced layer tracedata.layers.push(tracedlayer); }// End of color loop }else{// Parallel layering // 2. Layer separation and edge detection var ls = _this.layering( ii ); // Optional edge node visualization if(options.layercontainerid){ _this.drawLayers( ls, _this.specpalette, options.scale, options.layercontainerid ); } // 3. Batch pathscan var bps = _this.batchpathscan( ls, options.pathomit ); // 4. Batch interpollation var bis = _this.batchinternodes( bps, options ); // 5. Batch tracing and creating tracedata object var tracedata = { layers : _this.batchtracelayers( bis, options.ltres, options.qtres ), palette : ii.palette, width : imgd.width, height : imgd.height }; }// End of parallel layering // return tracedata return tracedata; },// End of imagedataToTracedata() this.optionpresets = { 'default': { // Tracing corsenabled : false, ltres : 1, qtres : 1, pathomit : 8, rightangleenhance : true, // Color quantization colorsampling : 2, numberofcolors : 16, mincolorratio : 0, colorquantcycles : 3, // Layering method layering : 0, // SVG rendering strokewidth : 1, linefilter : false, scale : 1, roundcoords : 1, viewbox : false, desc : false, lcpr : 0, qcpr : 0, // Blur blurradius : 0, blurdelta : 20 }, 'posterized1': { colorsampling:0, numberofcolors:2 }, 'posterized2': { numberofcolors:4, blurradius:5 }, 'curvy': { ltres:0.01, linefilter:true, rightangleenhance:false }, 'sharp': { qtres:0.01, linefilter:false }, 'detailed': { pathomit:0, roundcoords:2, ltres:0.5, qtres:0.5, numberofcolors:64 }, 'smoothed': { blurradius:5, blurdelta: 64 }, 'grayscale': { colorsampling:0, colorquantcycles:1, numberofcolors:7 }, 'fixedpalette': { colorsampling:0, colorquantcycles:1, numberofcolors:27 }, 'randomsampling1': { colorsampling:1, numberofcolors:8 }, 'randomsampling2': { colorsampling:1, numberofcolors:64 }, 'artistic1': { colorsampling:0, colorquantcycles:1, pathomit:0, blurradius:5, blurdelta: 64, ltres:0.01, linefilter:true, numberofcolors:16, strokewidth:2 }, 'artistic2': { qtres:0.01, colorsampling:0, colorquantcycles:1, numberofcolors:4, strokewidth:0 }, 'artistic3': { qtres:10, ltres:10, numberofcolors:8 }, 'artistic4': { qtres:10, ltres:10, numberofcolors:64, blurradius:5, blurdelta: 256, strokewidth:2 }, 'posterized3': { ltres: 1, qtres: 1, pathomit: 20, rightangleenhance: true, colorsampling: 0, numberofcolors: 3, mincolorratio: 0, colorquantcycles: 3, blurradius: 3, blurdelta: 20, strokewidth: 0, linefilter: false, roundcoords: 1, pal: [ { r: 0, g: 0, b: 100, a: 255 }, { r: 255, g: 255, b: 255, a: 255 } ] } },// End of optionpresets // creating options object, setting defaults for missing values this.checkoptions = function(options){ options = options || {}; // Option preset if(typeof options === 'string'){ options = options.toLowerCase(); if( _this.optionpresets[options] ){ options = _this.optionpresets[options]; }else{ options = {}; } } // Defaults var ok = Object.keys(_this.optionpresets['default']); for(var k=0; k return options; },// End of checkoptions() //////////////////////////////////////////////////////////// // // Vectorizing functions // //////////////////////////////////////////////////////////// // 1. Color quantization // Using a form of k-means clustering repeatead options.colorquantcycles times. http://en.wikipedia.org/wiki/Color_quantization this.colorquantization = function( imgd, options ){ var arr = [], idx=0, cd,cdl,ci, paletteacc = [], pixelnum = imgd.width * imgd.height, i, j, k, cnt, palette; // imgd.data must be RGBA, not just RGB if( imgd.data.length < pixelnum * 4 ){ var newimgddata = new Uint8ClampedArray(pixelnum * 4); for(var pxcnt = 0; pxcnt < pixelnum ; pxcnt++){ newimgddata[pxcnt*4 ] = imgd.data[pxcnt*3 ]; newimgddata[pxcnt*4+1] = imgd.data[pxcnt*3+1]; newimgddata[pxcnt*4+2] = imgd.data[pxcnt*3+2]; newimgddata[pxcnt*4+3] = 255; } imgd.data = newimgddata; }// End of RGBA imgd.data check // Filling arr (color index array) with -1 for( j=0; j 0 ){ imgd = _this.blur( imgd, options.blurradius, options.blurdelta ); } // Repeat clustering step options.colorquantcycles times for( cnt=0; cnt < options.colorquantcycles; cnt++ ){ // Average colors from the second iteration if(cnt>0){ // averaging paletteacc for palette for( k=0; k < palette.length; k++ ){ // averaging if( paletteacc[k].n > 0 ){ palette[k] = { r: Math.floor( paletteacc[k].r / paletteacc[k].n ), g: Math.floor( paletteacc[k].g / paletteacc[k].n ), b: Math.floor( paletteacc[k].b / paletteacc[k].n ), a: Math.floor( paletteacc[k].a / paletteacc[k].n ) }; } // Randomizing a color, if there are too few pixels and there will be a new cycle if( ( paletteacc[k].n/pixelnum < options.mincolorratio ) && ( cnt < options.colorquantcycles-1 ) ){ palette[k] = { r: Math.floor(Math.random()*255), g: Math.floor(Math.random()*255), b: Math.floor(Math.random()*255), a: Math.floor(Math.random()*255) }; } }// End of palette loop }// End of Average colors from the second iteration // Reseting palette accumulator for averaging for( i=0; i < palette.length; i++ ){ paletteacc[i] = { r:0, g:0, b:0, a:0, n:0 }; } // loop through all pixels for( j=0; j < imgd.height; j++ ){ for( i=0; i < imgd.width; i++ ){ // pixel index idx = (j*imgd.width+i)*4; // find closest color from palette by measuring (rectilinear) color distance between this pixel and all palette colors ci=0; cdl = 1024; // 4 * 256 is the maximum RGBA distance for( k=0; k p.y) !== (pa[j].y > p.y)) && (p.x < (pa[j].x - pa[i].x) * (p.y - pa[i].y) / (pa[j].y - pa[i].y) + pa[i].x) ) ? !isin : isin; } return isin; }, // Lookup tables for pathscan // pathscan_combined_lookup[ arr[py][px] ][ dir ] = [nextarrpypx, nextdir, deltapx, deltapy]; this.pathscan_combined_lookup = [ [[-1,-1,-1,-1], [-1,-1,-1,-1], [-1,-1,-1,-1], [-1,-1,-1,-1]],// arr[py][px]===0 is invalid [[ 0, 1, 0,-1], [-1,-1,-1,-1], [-1,-1,-1,-1], [ 0, 2,-1, 0]], [[-1,-1,-1,-1], [-1,-1,-1,-1], [ 0, 1, 0,-1], [ 0, 0, 1, 0]], [[ 0, 0, 1, 0], [-1,-1,-1,-1], [ 0, 2,-1, 0], [-1,-1,-1,-1]], [[-1,-1,-1,-1], [ 0, 0, 1, 0], [ 0, 3, 0, 1], [-1,-1,-1,-1]], [[13, 3, 0, 1], [13, 2,-1, 0], [ 7, 1, 0,-1], [ 7, 0, 1, 0]], [[-1,-1,-1,-1], [ 0, 1, 0,-1], [-1,-1,-1,-1], [ 0, 3, 0, 1]], [[ 0, 3, 0, 1], [ 0, 2,-1, 0], [-1,-1,-1,-1], [-1,-1,-1,-1]], [[ 0, 3, 0, 1], [ 0, 2,-1, 0], [-1,-1,-1,-1], [-1,-1,-1,-1]], [[-1,-1,-1,-1], [ 0, 1, 0,-1], [-1,-1,-1,-1], [ 0, 3, 0, 1]], [[11, 1, 0,-1], [14, 0, 1, 0], [14, 3, 0, 1], [11, 2,-1, 0]], [[-1,-1,-1,-1], [ 0, 0, 1, 0], [ 0, 3, 0, 1], [-1,-1,-1,-1]], [[ 0, 0, 1, 0], [-1,-1,-1,-1], [ 0, 2,-1, 0], [-1,-1,-1,-1]], [[-1,-1,-1,-1], [-1,-1,-1,-1], [ 0, 1, 0,-1], [ 0, 0, 1, 0]], [[ 0, 1, 0,-1], [-1,-1,-1,-1], [-1,-1,-1,-1], [ 0, 2,-1, 0]], [[-1,-1,-1,-1], [-1,-1,-1,-1], [-1,-1,-1,-1], [-1,-1,-1,-1]]// arr[py][px]===15 is invalid ], // 3. Walking through an edge node array, discarding edge node types 0 and 15 and creating paths from the rest. // Walk directions (dir): 0 > ; 1 ^ ; 2 < ; 3 v this.pathscan = function( arr, pathomit ){ var paths=[], pacnt=0, pcnt=0, px=0, py=0, w = arr[0].length, h = arr.length, dir=0, pathfinished=true, holepath=false, lookuprow; for(var j=0; j paths[pacnt].boundingbox[2] ){ paths[pacnt].boundingbox[2] = px-1; } if( (py-1) < paths[pacnt].boundingbox[1] ){ paths[pacnt].boundingbox[1] = py-1; } if( (py-1) > paths[pacnt].boundingbox[3] ){ paths[pacnt].boundingbox[3] = py-1; } // Next: look up the replacement, direction and coordinate changes = clear this cell, turn if required, walk forward lookuprow = _this.pathscan_combined_lookup[ arr[py][px] ][ dir ]; arr[py][px] = lookuprow[0]; dir = lookuprow[1]; px += lookuprow[2]; py += lookuprow[3]; // Close path if( (px-1 === paths[pacnt].points[0].x ) && ( py-1 === paths[pacnt].points[0].y ) ){ pathfinished = true; // Discarding paths shorter than pathomit if( paths[pacnt].points.length < pathomit ){ paths.pop(); }else{ paths[pacnt].isholepath = holepath ? true : false; // Finding the parent shape for this hole if(holepath){ var parentidx = 0, parentbbox = [-1,-1,w+1,h+1]; for(var parentcnt=0; parentcnt < pacnt; parentcnt++){ if( (!paths[parentcnt].isholepath) && _this.boundingboxincludes( paths[parentcnt].boundingbox , paths[pacnt].boundingbox ) && _this.boundingboxincludes( parentbbox , paths[parentcnt].boundingbox ) && _this.pointinpoly( paths[pacnt].points[0], paths[parentcnt].points ) ){ parentidx = parentcnt; parentbbox = paths[parentcnt].boundingbox; } } paths[parentidx].holechildren.push( pacnt ); }// End of holepath parent finding pacnt++; } }// End of Close path pcnt++; }// End of Path points loop }// End of Follow path }// End of i loop }// End of j loop return paths; },// End of pathscan() this.boundingboxincludes = function( parentbbox, childbbox ){ return ( ( parentbbox[0] < childbbox[0] ) && ( parentbbox[1] < childbbox[1] ) && ( parentbbox[2] > childbbox[2] ) && ( parentbbox[3] > childbbox[3] ) ); },// End of boundingboxincludes() // 3. Batch pathscan this.batchpathscan = function( layers, pathomit ){ var bpaths = []; for(var k in layers){ if(!layers.hasOwnProperty(k)){ continue; } bpaths[k] = _this.pathscan( layers[k], pathomit ); } return bpaths; }, // 4. interpollating between path points for nodes with 8 directions ( East, SouthEast, S, SW, W, NW, N, NE ) this.internodes = function( paths, options ){ var ins = [], palen=0, nextidx=0, nextidx2=0, previdx=0, previdx2=0, pacnt, pcnt; // paths loop for(pacnt=0; pacnt 0){ ins[pacnt].points[ ins[pacnt].points.length-1 ].linesegment = _this.getdirection( ins[pacnt].points[ ins[pacnt].points.length-1 ].x, ins[pacnt].points[ ins[pacnt].points.length-1 ].y, paths[pacnt].points[pcnt].x, paths[pacnt].points[pcnt].y ); } // This corner point ins[pacnt].points.push({ x : paths[pacnt].points[pcnt].x, y : paths[pacnt].points[pcnt].y, linesegment : _this.getdirection( paths[pacnt].points[pcnt].x, paths[pacnt].points[pcnt].y, (( paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x ) /2), (( paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y ) /2) ) }); }// End of right angle enhance // interpolate between two path points ins[pacnt].points.push({ x : (( paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x ) /2), y : (( paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y ) /2), linesegment : _this.getdirection( (( paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x ) /2), (( paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y ) /2), (( paths[pacnt].points[nextidx].x + paths[pacnt].points[nextidx2].x ) /2), (( paths[pacnt].points[nextidx].y + paths[pacnt].points[nextidx2].y ) /2) ) }); }// End of pathpoints loop }// End of paths loop return ins; },// End of internodes() this.testrightangle = function( path, idx1, idx2, idx3, idx4, idx5 ){ return ( (( path.points[idx3].x === path.points[idx1].x) && ( path.points[idx3].x === path.points[idx2].x) && ( path.points[idx3].y === path.points[idx4].y) && ( path.points[idx3].y === path.points[idx5].y) ) || (( path.points[idx3].y === path.points[idx1].y) && ( path.points[idx3].y === path.points[idx2].y) && ( path.points[idx3].x === path.points[idx4].x) && ( path.points[idx3].x === path.points[idx5].x) ) ); },// End of testrightangle() this.getdirection = function( x1, y1, x2, y2 ){ var val = 8; if(x1 < x2){ if (y1 < y2){ val = 1; }// SouthEast else if(y1 > y2){ val = 7; }// NE else { val = 0; }// E }else if(x1 > x2){ if (y1 < y2){ val = 3; }// SW else if(y1 > y2){ val = 5; }// NW else { val = 4; }// W }else{ if (y1 < y2){ val = 2; }// S else if(y1 > y2){ val = 6; }// N else { val = 8; }// center, this should not happen } return val; },// End of getdirection() // 4. Batch interpollation this.batchinternodes = function( bpaths, options ){ var binternodes = []; for (var k in bpaths) { if(!bpaths.hasOwnProperty(k)){ continue; } binternodes[k] = _this.internodes(bpaths[k], options); } return binternodes; }, // 5. tracepath() : recursively trying to fit straight and quadratic spline segments on the 8 direction internode path // 5.1. Find sequences of points with only 2 segment types // 5.2. Fit a straight line on the sequence // 5.3. If the straight line fails (distance error > ltres), find the point with the biggest error // 5.4. Fit a quadratic spline through errorpoint (project this to get controlpoint), then measure errors on every point in the sequence // 5.5. If the spline fails (distance error > qtres), find the point with the biggest error, set splitpoint = fitting point // 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences this.tracepath = function( path, ltres, qtres ){ var pcnt=0, segtype1, segtype2, seqend, smp = {}; smp.segments = []; smp.boundingbox = path.boundingbox; smp.holechildren = path.holechildren; smp.isholepath = path.isholepath; while(pcnt < path.points.length){ // 5.1. Find sequences of points with only 2 segment types segtype1 = path.points[pcnt].linesegment; segtype2 = -1; seqend=pcnt+1; while( ((path.points[seqend].linesegment === segtype1) || (path.points[seqend].linesegment === segtype2) || (segtype2 === -1)) && (seqend < path.points.length-1) ){ if((path.points[seqend].linesegment!==segtype1) && (segtype2===-1)){ segtype2 = path.points[seqend].linesegment; } seqend++; } if(seqend === path.points.length-1){ seqend = 0; } // 5.2. - 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences smp.segments = smp.segments.concat( _this.fitseq(path, ltres, qtres, pcnt, seqend) ); // forward pcnt; if(seqend>0){ pcnt = seqend; }else{ pcnt = path.points.length; } }// End of pcnt loop return smp; },// End of tracepath() // 5.2. - 5.6. recursively fitting a straight or quadratic line segment on this sequence of path nodes, // called from tracepath() this.fitseq = function( path, ltres, qtres, seqstart, seqend ){ // return if invalid seqend if( (seqend>path.points.length) || (seqend<0) ){ return []; } // variables var errorpoint=seqstart, errorval=0, curvepass=true, px, py, dist2; var tl = (seqend-seqstart); if(tl<0){ tl += path.points.length; } var vx = (path.points[seqend].x-path.points[seqstart].x) / tl, vy = (path.points[seqend].y-path.points[seqstart].y) / tl; // 5.2. Fit a straight line on the sequence var pcnt = (seqstart+1) % path.points.length, pl; while(pcnt != seqend){ pl = pcnt-seqstart; if(pl<0){ pl += path.points.length; } px = path.points[seqstart].x + vx * pl; py = path.points[seqstart].y + vy * pl; dist2 = (path.points[pcnt].x-px)*(path.points[pcnt].x-px) + (path.points[pcnt].y-py)*(path.points[pcnt].y-py); if(dist2>ltres){curvepass=false;} if(dist2>errorval){ errorpoint=pcnt; errorval=dist2; } pcnt = (pcnt+1)%path.points.length; } // return straight line if fits if(curvepass){ return [{ type:'L', x1:path.points[seqstart].x, y1:path.points[seqstart].y, x2:path.points[seqend].x, y2:path.points[seqend].y }]; } // 5.3. If the straight line fails (distance error>ltres), find the point with the biggest error var fitpoint = errorpoint; curvepass = true; errorval = 0; // 5.4. Fit a quadratic spline through this point, measure errors on every point in the sequence // helpers and projecting to get control point var t=(fitpoint-seqstart)/tl, t1=(1-t)*(1-t), t2=2*(1-t)*t, t3=t*t; var cpx = (t1*path.points[seqstart].x + t3*path.points[seqend].x - path.points[fitpoint].x)/-t2 , cpy = (t1*path.points[seqstart].y + t3*path.points[seqend].y - path.points[fitpoint].y)/-t2 ; // Check every point pcnt = seqstart+1; while(pcnt != seqend){ t=(pcnt-seqstart)/tl; t1=(1-t)*(1-t); t2=2*(1-t)*t; t3=t*t; px = t1 * path.points[seqstart].x + t2 * cpx + t3 * path.points[seqend].x; py = t1 * path.points[seqstart].y + t2 * cpy + t3 * path.points[seqend].y; dist2 = (path.points[pcnt].x-px)*(path.points[pcnt].x-px) + (path.points[pcnt].y-py)*(path.points[pcnt].y-py); if(dist2>qtres){curvepass=false;} if(dist2>errorval){ errorpoint=pcnt; errorval=dist2; } pcnt = (pcnt+1)%path.points.length; } // return spline if fits if(curvepass){ return [{ type:'Q', x1:path.points[seqstart].x, y1:path.points[seqstart].y, x2:cpx, y2:cpy, x3:path.points[seqend].x, y3:path.points[seqend].y }]; } // 5.5. If the spline fails (distance error>qtres), find the point with the biggest error var splitpoint = fitpoint; // Earlier: Math.floor((fitpoint + errorpoint)/2); // 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences return _this.fitseq( path, ltres, qtres, seqstart, splitpoint ).concat( _this.fitseq( path, ltres, qtres, splitpoint, seqend ) ); },// End of fitseq() // 5. Batch tracing paths this.batchtracepaths = function(internodepaths,ltres,qtres){ var btracedpaths = []; for(var k in internodepaths){ if(!internodepaths.hasOwnProperty(k)){ continue; } btracedpaths.push( _this.tracepath(internodepaths[k],ltres,qtres) ); } return btracedpaths; }, // 5. Batch tracing layers this.batchtracelayers = function(binternodes, ltres, qtres){ var btbis = []; for(var k in binternodes){ if(!binternodes.hasOwnProperty(k)){ continue; } btbis[k] = _this.batchtracepaths(binternodes[k], ltres, qtres); } return btbis; }, //////////////////////////////////////////////////////////// // // SVG Drawing functions // //////////////////////////////////////////////////////////// // Rounding to given decimals https://stackoverflow.com/questions/11832914/round-to-at-most-2-decimal-places-in-javascript this.roundtodec = function(val,places){ return +val.toFixed(places); }, // Getting SVG path element string from a traced path this.svgpathstring = function( tracedata, lnum, pathnum, options ){ var layer = tracedata.layers[lnum], smp = layer[pathnum], str='', pcnt; // Line filter if(options.linefilter && (smp.segments.length < 3)){ return str; } // Starting path element, desc contains layer and path number str = ''; // Rendering control points if(options.lcpr || options.qcpr){ for(pcnt=0; pcnt'; str += ''; str += ''; str += ''; } if( (!smp.segments[pcnt].hasOwnProperty('x3')) && options.lcpr){ str += ''; } } // Hole children control points for( var hcnt=0; hcnt < smp.holechildren.length; hcnt++){ var hsmp = layer[ smp.holechildren[hcnt] ]; for(pcnt=0; pcnt'; str += ''; str += ''; str += ''; } if( (!hsmp.segments[pcnt].hasOwnProperty('x3')) && options.lcpr){ str += ''; } } } }// End of Rendering control points return str; },// End of svgpathstring() // Converting tracedata to an SVG string this.getsvgstring = function( tracedata, options ){ options = _this.checkoptions(options); var w = tracedata.width * options.scale, h = tracedata.height * options.scale; // SVG start var svgstr = ''; // Drawing: Layers and Paths loops for(var lcnt=0; lcnt < tracedata.layers.length; lcnt++){ for(var pcnt=0; pcnt < tracedata.layers[lcnt].length; pcnt++){ // Adding SVG string if( !tracedata.layers[lcnt][pcnt].isholepath ){ svgstr += _this.svgpathstring( tracedata, lcnt, pcnt, options ); } }// End of paths loop }// End of layers loop // SVG End svgstr+=''; return svgstr; },// End of getsvgstring() // Comparator for numeric Array.sort this.compareNumbers = function(a,b){ return a - b; }, // Convert color object to rgba string this.torgbastr = function(c){ return 'rgba('+c.r+','+c.g+','+c.b+','+c.a+')'; }, // Convert color object to SVG color string this.tosvgcolorstr = function(c, options){ return 'fill="rgb('+c.r+','+c.g+','+c.b+')" stroke="rgb('+c.r+','+c.g+','+c.b+')" stroke-width="'+options.strokewidth+'" opacity="'+c.a/255.0+'" '; }, // Helper function: Appending an element to a container from an svgstring this.appendSVGString = function(svgstr,parentid){ var div; if(parentid){ div = document.getElementById(parentid); if(!div){ div = document.createElement('div'); div.id = parentid; document.body.appendChild(div); } }else{ div = document.createElement('div'); document.body.appendChild(div); } div.innerHTML += svgstr; }, //////////////////////////////////////////////////////////// // // Canvas functions // //////////////////////////////////////////////////////////// // Gaussian kernels for blur this.gks = [ [0.27901,0.44198,0.27901], [0.135336,0.228569,0.272192,0.228569,0.135336], [0.086776,0.136394,0.178908,0.195843,0.178908,0.136394,0.086776], [0.063327,0.093095,0.122589,0.144599,0.152781,0.144599,0.122589,0.093095,0.063327], [0.049692,0.069304,0.089767,0.107988,0.120651,0.125194,0.120651,0.107988,0.089767,0.069304,0.049692] ], // Selective Gaussian blur for preprocessing this.blur = function(imgd,radius,delta){ var i,j,k,d,idx,racc,gacc,bacc,aacc,wacc; // new ImageData var imgd2 = { width:imgd.width, height:imgd.height, data:[] }; // radius and delta limits, this kernel radius = Math.floor(radius); if(radius<1){ return imgd; } if(radius>5){ radius = 5; } delta = Math.abs( delta ); if(delta>1024){ delta = 1024; } var thisgk = _this.gks[radius-1]; // loop through all pixels, horizontal blur for( j=0; j < imgd.height; j++ ){ for( i=0; i < imgd.width; i++ ){ racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0; // gauss kernel loop for( k = -radius; k < radius+1; k++){ // add weighted color values if( (i+k > 0) && (i+k < imgd.width) ){ idx = (j*imgd.width+i+k)*4; racc += imgd.data[idx ] * thisgk[k+radius]; gacc += imgd.data[idx+1] * thisgk[k+radius]; bacc += imgd.data[idx+2] * thisgk[k+radius]; aacc += imgd.data[idx+3] * thisgk[k+radius]; wacc += thisgk[k+radius]; } } // The new pixel idx = (j*imgd.width+i)*4; imgd2.data[idx ] = Math.floor(racc / wacc); imgd2.data[idx+1] = Math.floor(gacc / wacc); imgd2.data[idx+2] = Math.floor(bacc / wacc); imgd2.data[idx+3] = Math.floor(aacc / wacc); }// End of width loop }// End of horizontal blur // copying the half blurred imgd2 var himgd = new Uint8ClampedArray(imgd2.data); // loop through all pixels, vertical blur for( j=0; j < imgd.height; j++ ){ for( i=0; i < imgd.width; i++ ){ racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0; // gauss kernel loop for( k = -radius; k < radius+1; k++){ // add weighted color values if( (j+k > 0) && (j+k < imgd.height) ){ idx = ((j+k)*imgd.width+i)*4; racc += himgd[idx ] * thisgk[k+radius]; gacc += himgd[idx+1] * thisgk[k+radius]; bacc += himgd[idx+2] * thisgk[k+radius]; aacc += himgd[idx+3] * thisgk[k+radius]; wacc += thisgk[k+radius]; } } // The new pixel idx = (j*imgd.width+i)*4; imgd2.data[idx ] = Math.floor(racc / wacc); imgd2.data[idx+1] = Math.floor(gacc / wacc); imgd2.data[idx+2] = Math.floor(bacc / wacc); imgd2.data[idx+3] = Math.floor(aacc / wacc); }// End of width loop }// End of vertical blur // Selective blur: loop through all pixels for( j=0; j < imgd.height; j++ ){ for( i=0; i < imgd.width; i++ ){ idx = (j*imgd.width+i)*4; // d is the difference between the blurred and the original pixel d = Math.abs(imgd2.data[idx ] - imgd.data[idx ]) + Math.abs(imgd2.data[idx+1] - imgd.data[idx+1]) + Math.abs(imgd2.data[idx+2] - imgd.data[idx+2]) + Math.abs(imgd2.data[idx+3] - imgd.data[idx+3]); // selective blur: if d>delta, put the original pixel back if(d>delta){ imgd2.data[idx ] = imgd.data[idx ]; imgd2.data[idx+1] = imgd.data[idx+1]; imgd2.data[idx+2] = imgd.data[idx+2]; imgd2.data[idx+3] = imgd.data[idx+3]; } } }// End of Selective blur return imgd2; },// End of blur() // Helper function: loading an image from a URL, then executing callback with canvas as argument this.loadImage = function(url,callback,options){ var img = new Image(); if(options && options.corsenabled){ img.crossOrigin = 'Anonymous'; } img.onload = function(){ var canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; var context = canvas.getContext('2d'); context.drawImage(img,0,0); callback(canvas); }; img.src = url; }, // Helper function: getting ImageData from a canvas this.getImgdata = function(canvas){ var context = canvas.getContext('2d'); return context.getImageData(0,0,canvas.width,canvas.height); }, // Special palette to use with drawlayers() this.specpalette = [ {r:0,g:0,b:0,a:255}, {r:128,g:128,b:128,a:255}, {r:0,g:0,b:128,a:255}, {r:64,g:64,b:128,a:255}, {r:192,g:192,b:192,a:255}, {r:255,g:255,b:255,a:255}, {r:128,g:128,b:192,a:255}, {r:0,g:0,b:192,a:255}, {r:128,g:0,b:0,a:255}, {r:128,g:64,b:64,a:255}, {r:128,g:0,b:128,a:255}, {r:168,g:168,b:168,a:255}, {r:192,g:128,b:128,a:255}, {r:192,g:0,b:0,a:255}, {r:255,g:255,b:255,a:255}, {r:0,g:128,b:0,a:255} ], // Helper function: Drawing all edge node layers into a container this.drawLayers = function(layers,palette,scale,parentid){ scale = scale||1; var w,h,i,j,k; // Preparing container var div; if(parentid){ div = document.getElementById(parentid); if(!div){ div = document.createElement('div'); div.id = parentid; document.body.appendChild(div); } }else{ div = document.createElement('div'); document.body.appendChild(div); } // Layers loop for (k in layers) { if(!layers.hasOwnProperty(k)){ continue; } // width, height w=layers[k][0].length; h=layers[k].length; // Creating new canvas for every layer var canvas = document.createElement('canvas'); canvas.width=w*scale; canvas.height=h*scale; var context = canvas.getContext('2d'); // Drawing for(j=0; j