UNPKG

tui-image-editor

Version:
1,397 lines (1,299 loc) 41.8 kB
/* imagetracer.js version 1.2.4 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/ */ export default class ImageTracer { static tracerDefaultOption() { return { pathomit: 100, ltres: 0.1, qtres: 1, scale: 1, strokewidth: 5, viewbox: false, linefilter: true, desc: false, rightangleenhance: false, pal: [ { r: 0, g: 0, b: 0, a: 255, }, { r: 255, g: 255, b: 255, a: 255, }, ], }; } /* eslint-disable */ constructor() { this.versionnumber = '1.2.4'; this.optionpresets = { default: { corsenabled: false, ltres: 1, qtres: 1, pathomit: 8, rightangleenhance: true, colorsampling: 2, numberofcolors: 16, mincolorratio: 0, colorquantcycles: 3, layering: 0, strokewidth: 1, linefilter: false, scale: 1, roundcoords: 1, viewbox: false, desc: false, lcpr: 0, qcpr: 0, 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 }, ], }, }; this.pathscan_combined_lookup = [ [ [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], ], [ [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], ], ]; 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, ], ]; 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 }, ]; } imageToSVG(url, callback, options) { options = this.checkoptions(options); this.loadImage( url, (canvas) => { callback(this.imagedataToSVG(this.getImgdata(canvas), options)); }, options ); } imagedataToSVG(imgd, options) { options = this.checkoptions(options); const td = this.imagedataToTracedata(imgd, options); return this.getsvgstring(td, options); } imageToTracedata(url, callback, options) { options = this.checkoptions(options); this.loadImage( url, (canvas) => { callback(this.imagedataToTracedata(this.getImgdata(canvas), options)); }, options ); } imagedataToTracedata(imgd, options) { options = this.checkoptions(options); const ii = this.colorquantization(imgd, options); let tracedata; if (options.layering === 0) { tracedata = { layers: [], palette: ii.palette, width: ii.array[0].length - 2, height: ii.array.length - 2, }; for (let colornum = 0; colornum < ii.palette.length; colornum += 1) { const tracedlayer = this.batchtracepaths( this.internodes( this.pathscan(this.layeringstep(ii, colornum), options.pathomit), options ), options.ltres, options.qtres ); tracedata.layers.push(tracedlayer); } } else { const ls = this.layering(ii); if (options.layercontainerid) { this.drawLayers(ls, this.specpalette, options.scale, options.layercontainerid); } const bps = this.batchpathscan(ls, options.pathomit); const bis = this.batchinternodes(bps, options); tracedata = { layers: this.batchtracelayers(bis, options.ltres, options.qtres), palette: ii.palette, width: imgd.width, height: imgd.height, }; } return tracedata; } checkoptions(options) { options = options || {}; if (typeof options === 'string') { options = options.toLowerCase(); if (this.optionpresets[options]) { options = this.optionpresets[options]; } else { options = {}; } } const ok = Object.keys(this.optionpresets['default']); for (let k = 0; k < ok.length; k += 1) { if (!options.hasOwnProperty(ok[k])) { options[ok[k]] = this.optionpresets['default'][ok[k]]; } } return options; } colorquantization(imgd, options) { const arr = []; let idx = 0; let cd; let cdl; let ci; const paletteacc = []; const pixelnum = imgd.width * imgd.height; let i; let j; let k; let cnt; let palette; for (j = 0; j < imgd.height + 2; j += 1) { arr[j] = []; for (i = 0; i < imgd.width + 2; i += 1) { arr[j][i] = -1; } } if (options.pal) { palette = options.pal; } else if (options.colorsampling === 0) { palette = this.generatepalette(options.numberofcolors); } else if (options.colorsampling === 1) { palette = this.samplepalette(options.numberofcolors, imgd); } else { palette = this.samplepalette2(options.numberofcolors, imgd); } if (options.blurradius > 0) { imgd = this.blur(imgd, options.blurradius, options.blurdelta); } for (cnt = 0; cnt < options.colorquantcycles; cnt += 1) { if (cnt > 0) { for (k = 0; k < palette.length; k += 1) { 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), }; } 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), }; } } } for (i = 0; i < palette.length; i += 1) { paletteacc[i] = { r: 0, g: 0, b: 0, a: 0, n: 0 }; } for (j = 0; j < imgd.height; j += 1) { for (i = 0; i < imgd.width; i += 1) { idx = (j * imgd.width + i) * 4; ci = 0; cdl = 1024; for (k = 0; k < palette.length; k += 1) { cd = Math.abs(palette[k].r - imgd.data[idx]) + Math.abs(palette[k].g - imgd.data[idx + 1]) + Math.abs(palette[k].b - imgd.data[idx + 2]) + Math.abs(palette[k].a - imgd.data[idx + 3]); if (cd < cdl) { cdl = cd; ci = k; } } paletteacc[ci].r += imgd.data[idx]; paletteacc[ci].g += imgd.data[idx + 1]; paletteacc[ci].b += imgd.data[idx + 2]; paletteacc[ci].a += imgd.data[idx + 3]; paletteacc[ci].n += 1; arr[j + 1][i + 1] = ci; } } } return { array: arr, palette }; } samplepalette(numberofcolors, imgd) { let idx; const palette = []; for (let i = 0; i < numberofcolors; i += 1) { idx = Math.floor((Math.random() * imgd.data.length) / 4) * 4; palette.push({ r: imgd.data[idx], g: imgd.data[idx + 1], b: imgd.data[idx + 2], a: imgd.data[idx + 3], }); } return palette; } samplepalette2(numberofcolors, imgd) { let idx; const palette = []; const ni = Math.ceil(Math.sqrt(numberofcolors)); const nj = Math.ceil(numberofcolors / ni); const vx = imgd.width / (ni + 1); const vy = imgd.height / (nj + 1); for (let j = 0; j < nj; j += 1) { for (let i = 0; i < ni; i += 1) { if (palette.length === numberofcolors) { break; } else { idx = Math.floor((j + 1) * vy * imgd.width + (i + 1) * vx) * 4; palette.push({ r: imgd.data[idx], g: imgd.data[idx + 1], b: imgd.data[idx + 2], a: imgd.data[idx + 3], }); } } } return palette; } generatepalette(numberofcolors) { const palette = []; let rcnt; let gcnt; let bcnt; if (numberofcolors < 8) { const graystep = Math.floor(255 / (numberofcolors - 1)); for (let i = 0; i < numberofcolors; i += 1) { palette.push({ r: i * graystep, g: i * graystep, b: i * graystep, a: 255 }); } } else { const colorqnum = Math.floor(Math.pow(numberofcolors, 1 / 3)); const colorstep = Math.floor(255 / (colorqnum - 1)); const rndnum = numberofcolors - colorqnum * colorqnum * colorqnum; for (rcnt = 0; rcnt < colorqnum; rcnt += 1) { for (gcnt = 0; gcnt < colorqnum; gcnt += 1) { for (bcnt = 0; bcnt < colorqnum; bcnt += 1) { palette.push({ r: rcnt * colorstep, g: gcnt * colorstep, b: bcnt * colorstep, a: 255 }); } } } for (rcnt = 0; rcnt < rndnum; rcnt += 1) { palette.push({ r: Math.floor(Math.random() * 255), g: Math.floor(Math.random() * 255), b: Math.floor(Math.random() * 255), a: Math.floor(Math.random() * 255), }); } } return palette; } layering(ii) { const layers = []; let val = 0; const ah = ii.array.length; const aw = ii.array[0].length; let n1; let n2; let n3; let n4; let n5; let n6; let n7; let n8; let i; let j; let k; for (k = 0; k < ii.palette.length; k += 1) { layers[k] = []; for (j = 0; j < ah; j += 1) { layers[k][j] = []; for (i = 0; i < aw; i += 1) { layers[k][j][i] = 0; } } } for (j = 1; j < ah - 1; j += 1) { for (i = 1; i < aw - 1; i += 1) { val = ii.array[j][i]; n1 = ii.array[j - 1][i - 1] === val ? 1 : 0; n2 = ii.array[j - 1][i] === val ? 1 : 0; n3 = ii.array[j - 1][i + 1] === val ? 1 : 0; n4 = ii.array[j][i - 1] === val ? 1 : 0; n5 = ii.array[j][i + 1] === val ? 1 : 0; n6 = ii.array[j + 1][i - 1] === val ? 1 : 0; n7 = ii.array[j + 1][i] === val ? 1 : 0; n8 = ii.array[j + 1][i + 1] === val ? 1 : 0; layers[val][j + 1][i + 1] = 1 + n5 * 2 + n8 * 4 + n7 * 8; if (!n4) { layers[val][j + 1][i] = 0 + 2 + n7 * 4 + n6 * 8; } if (!n2) { layers[val][j][i + 1] = 0 + n3 * 2 + n5 * 4 + 8; } if (!n1) { layers[val][j][i] = 0 + n2 * 2 + 4 + n4 * 8; } } } return layers; } layeringstep(ii, cnum) { const layer = []; const ah = ii.array.length; const aw = ii.array[0].length; let i; let j; for (j = 0; j < ah; j += 1) { layer[j] = []; for (i = 0; i < aw; i += 1) { layer[j][i] = 0; } } for (j = 1; j < ah; j += 1) { for (i = 1; i < aw; i += 1) { layer[j][i] = (ii.array[j - 1][i - 1] === cnum ? 1 : 0) + (ii.array[j - 1][i] === cnum ? 2 : 0) + (ii.array[j][i - 1] === cnum ? 8 : 0) + (ii.array[j][i] === cnum ? 4 : 0); } } return layer; } pathscan(arr, pathomit) { const paths = []; let pacnt = 0; let pcnt = 0; let px = 0; let py = 0; const w = arr[0].length; const h = arr.length; let dir = 0; let pathfinished = true; let holepath = false; let lookuprow; for (let j = 0; j < h; j += 1) { for (let i = 0; i < w; i += 1) { if (arr[j][i] === 4 || arr[j][i] === 11) { px = i; py = j; paths[pacnt] = {}; paths[pacnt].points = []; paths[pacnt].boundingbox = [px, py, px, py]; paths[pacnt].holechildren = []; pathfinished = false; pcnt = 0; holepath = arr[j][i] === 11; dir = 1; while (!pathfinished) { paths[pacnt].points[pcnt] = {}; paths[pacnt].points[pcnt].x = px - 1; paths[pacnt].points[pcnt].y = py - 1; paths[pacnt].points[pcnt].t = arr[py][px]; if (px - 1 < paths[pacnt].boundingbox[0]) { paths[pacnt].boundingbox[0] = px - 1; } if (px - 1 > 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; } lookuprow = this.pathscan_combined_lookup[arr[py][px]][dir]; arr[py][px] = lookuprow[0]; dir = lookuprow[1]; px += lookuprow[2]; py += lookuprow[3]; if (px - 1 === paths[pacnt].points[0].x && py - 1 === paths[pacnt].points[0].y) { pathfinished = true; if (paths[pacnt].points.length < pathomit) { paths.pop(); } else { paths[pacnt].isholepath = !!holepath; if (holepath) { let parentidx = 0, parentbbox = [-1, -1, w + 1, h + 1]; for (let parentcnt = 0; parentcnt < pacnt; parentcnt++) { if ( !paths[parentcnt].isholepath && this.boundingboxincludes( paths[parentcnt].boundingbox, paths[pacnt].boundingbox ) && this.boundingboxincludes(parentbbox, paths[parentcnt].boundingbox) ) { parentidx = parentcnt; parentbbox = paths[parentcnt].boundingbox; } } paths[parentidx].holechildren.push(pacnt); } pacnt += 1; } } pcnt += 1; } } } } return paths; } boundingboxincludes(parentbbox, childbbox) { return ( parentbbox[0] < childbbox[0] && parentbbox[1] < childbbox[1] && parentbbox[2] > childbbox[2] && parentbbox[3] > childbbox[3] ); } batchpathscan(layers, pathomit) { const bpaths = []; for (const k in layers) { if (!layers.hasOwnProperty(k)) { continue; } bpaths[k] = this.pathscan(layers[k], pathomit); } return bpaths; } internodes(paths, options) { const ins = []; let palen = 0; let nextidx = 0; let nextidx2 = 0; let previdx = 0; let previdx2 = 0; let pacnt; let pcnt; for (pacnt = 0; pacnt < paths.length; pacnt += 1) { ins[pacnt] = {}; ins[pacnt].points = []; ins[pacnt].boundingbox = paths[pacnt].boundingbox; ins[pacnt].holechildren = paths[pacnt].holechildren; ins[pacnt].isholepath = paths[pacnt].isholepath; palen = paths[pacnt].points.length; for (pcnt = 0; pcnt < palen; pcnt += 1) { nextidx = (pcnt + 1) % palen; nextidx2 = (pcnt + 2) % palen; previdx = (pcnt - 1 + palen) % palen; previdx2 = (pcnt - 2 + palen) % palen; if ( options.rightangleenhance && this.testrightangle(paths[pacnt], previdx2, previdx, pcnt, nextidx, nextidx2) ) { if (ins[pacnt].points.length > 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 ); } 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 ), }); } 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 ), }); } } return ins; } testrightangle(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) ); } getdirection(x1, y1, x2, y2) { let val = 8; if (x1 < x2) { if (y1 < y2) { val = 1; } else if (y1 > y2) { val = 7; } else { val = 0; } } else if (x1 > x2) { if (y1 < y2) { val = 3; } else if (y1 > y2) { val = 5; } else { val = 4; } } else if (y1 < y2) { val = 2; } else if (y1 > y2) { val = 6; } else { val = 8; } return val; } batchinternodes(bpaths, options) { const binternodes = []; for (const k in bpaths) { if (!bpaths.hasOwnProperty(k)) { continue; } binternodes[k] = this.internodes(bpaths[k], options); } return binternodes; } tracepath(path, ltres, qtres) { let pcnt = 0; let segtype1; let segtype2; let seqend; const smp = {}; smp.segments = []; smp.boundingbox = path.boundingbox; smp.holechildren = path.holechildren; smp.isholepath = path.isholepath; while (pcnt < path.points.length) { 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 += 1; } if (seqend === path.points.length - 1) { seqend = 0; } smp.segments = smp.segments.concat(this.fitseq(path, ltres, qtres, pcnt, seqend)); if (seqend > 0) { pcnt = seqend; } else { pcnt = path.points.length; } } return smp; } fitseq(path, ltres, qtres, seqstart, seqend) { if (seqend > path.points.length || seqend < 0) { return []; } let errorpoint = seqstart, errorval = 0, curvepass = true, px, py, dist2; let tl = seqend - seqstart; if (tl < 0) { tl += path.points.length; } let vx = (path.points[seqend].x - path.points[seqstart].x) / tl, vy = (path.points[seqend].y - path.points[seqstart].y) / tl; let 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; } 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, }, ]; } const fitpoint = errorpoint; curvepass = true; errorval = 0; let t = (fitpoint - seqstart) / tl, t1 = (1 - t) * (1 - t), t2 = 2 * (1 - t) * t, t3 = t * t; let 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; 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; } 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, }, ]; } const splitpoint = fitpoint; return this.fitseq(path, ltres, qtres, seqstart, splitpoint).concat( this.fitseq(path, ltres, qtres, splitpoint, seqend) ); } batchtracepaths(internodepaths, ltres, qtres) { const btracedpaths = []; for (const k in internodepaths) { if (!internodepaths.hasOwnProperty(k)) { continue; } btracedpaths.push(this.tracepath(internodepaths[k], ltres, qtres)); } return btracedpaths; } batchtracelayers(binternodes, ltres, qtres) { const btbis = []; for (const k in binternodes) { if (!binternodes.hasOwnProperty(k)) { continue; } btbis[k] = this.batchtracepaths(binternodes[k], ltres, qtres); } return btbis; } roundtodec(val, places) { return Number(val.toFixed(places)); } svgpathstring(tracedata, lnum, pathnum, options) { let layer = tracedata.layers[lnum], smp = layer[pathnum], str = '', pcnt; if (options.linefilter && smp.segments.length < 3) { return str; } str = `<path ${options.desc ? `desc="l ${lnum} p ${pathnum}" ` : ''}${this.tosvgcolorstr( tracedata.palette[lnum], options )}d="`; if (options.roundcoords === -1) { str += `M ${smp.segments[0].x1 * options.scale} ${smp.segments[0].y1 * options.scale} `; for (pcnt = 0; pcnt < smp.segments.length; pcnt++) { str += `${smp.segments[pcnt].type} ${smp.segments[pcnt].x2 * options.scale} ${ smp.segments[pcnt].y2 * options.scale } `; if (smp.segments[pcnt].hasOwnProperty('x3')) { str += `${smp.segments[pcnt].x3 * options.scale} ${ smp.segments[pcnt].y3 * options.scale } `; } } str += 'Z '; } else { str += `M ${this.roundtodec( smp.segments[0].x1 * options.scale, options.roundcoords )} ${this.roundtodec(smp.segments[0].y1 * options.scale, options.roundcoords)} `; for (pcnt = 0; pcnt < smp.segments.length; pcnt++) { str += `${smp.segments[pcnt].type} ${this.roundtodec( smp.segments[pcnt].x2 * options.scale, options.roundcoords )} ${this.roundtodec(smp.segments[pcnt].y2 * options.scale, options.roundcoords)} `; if (smp.segments[pcnt].hasOwnProperty('x3')) { str += `${this.roundtodec( smp.segments[pcnt].x3 * options.scale, options.roundcoords )} ${this.roundtodec(smp.segments[pcnt].y3 * options.scale, options.roundcoords)} `; } } str += 'Z '; } for (var hcnt = 0; hcnt < smp.holechildren.length; hcnt++) { var hsmp = layer[smp.holechildren[hcnt]]; if (options.roundcoords === -1) { if (hsmp.segments[hsmp.segments.length - 1].hasOwnProperty('x3')) { str += `M ${hsmp.segments[hsmp.segments.length - 1].x3 * options.scale} ${ hsmp.segments[hsmp.segments.length - 1].y3 * options.scale } `; } else { str += `M ${hsmp.segments[hsmp.segments.length - 1].x2 * options.scale} ${ hsmp.segments[hsmp.segments.length - 1].y2 * options.scale } `; } for (pcnt = hsmp.segments.length - 1; pcnt >= 0; pcnt--) { str += `${hsmp.segments[pcnt].type} `; if (hsmp.segments[pcnt].hasOwnProperty('x3')) { str += `${hsmp.segments[pcnt].x2 * options.scale} ${ hsmp.segments[pcnt].y2 * options.scale } `; } str += `${hsmp.segments[pcnt].x1 * options.scale} ${ hsmp.segments[pcnt].y1 * options.scale } `; } } else { if (hsmp.segments[hsmp.segments.length - 1].hasOwnProperty('x3')) { str += `M ${this.roundtodec( hsmp.segments[hsmp.segments.length - 1].x3 * options.scale )} ${this.roundtodec(hsmp.segments[hsmp.segments.length - 1].y3 * options.scale)} `; } else { str += `M ${this.roundtodec( hsmp.segments[hsmp.segments.length - 1].x2 * options.scale )} ${this.roundtodec(hsmp.segments[hsmp.segments.length - 1].y2 * options.scale)} `; } for (pcnt = hsmp.segments.length - 1; pcnt >= 0; pcnt--) { str += `${hsmp.segments[pcnt].type} `; if (hsmp.segments[pcnt].hasOwnProperty('x3')) { str += `${this.roundtodec(hsmp.segments[pcnt].x2 * options.scale)} ${this.roundtodec( hsmp.segments[pcnt].y2 * options.scale )} `; } str += `${this.roundtodec(hsmp.segments[pcnt].x1 * options.scale)} ${this.roundtodec( hsmp.segments[pcnt].y1 * options.scale )} `; } } str += 'Z '; } str += '" />'; if (options.lcpr || options.qcpr) { for (pcnt = 0; pcnt < smp.segments.length; pcnt++) { if (smp.segments[pcnt].hasOwnProperty('x3') && options.qcpr) { str += `<circle cx="${smp.segments[pcnt].x2 * options.scale}" cy="${ smp.segments[pcnt].y2 * options.scale }" r="${options.qcpr}" fill="cyan" stroke-width="${ options.qcpr * 0.2 }" stroke="black" />`; str += `<circle cx="${smp.segments[pcnt].x3 * options.scale}" cy="${ smp.segments[pcnt].y3 * options.scale }" r="${options.qcpr}" fill="white" stroke-width="${ options.qcpr * 0.2 }" stroke="black" />`; str += `<line x1="${smp.segments[pcnt].x1 * options.scale}" y1="${ smp.segments[pcnt].y1 * options.scale }" x2="${smp.segments[pcnt].x2 * options.scale}" y2="${ smp.segments[pcnt].y2 * options.scale }" stroke-width="${options.qcpr * 0.2}" stroke="cyan" />`; str += `<line x1="${smp.segments[pcnt].x2 * options.scale}" y1="${ smp.segments[pcnt].y2 * options.scale }" x2="${smp.segments[pcnt].x3 * options.scale}" y2="${ smp.segments[pcnt].y3 * options.scale }" stroke-width="${options.qcpr * 0.2}" stroke="cyan" />`; } if (!smp.segments[pcnt].hasOwnProperty('x3') && options.lcpr) { str += `<circle cx="${smp.segments[pcnt].x2 * options.scale}" cy="${ smp.segments[pcnt].y2 * options.scale }" r="${options.lcpr}" fill="white" stroke-width="${ options.lcpr * 0.2 }" stroke="black" />`; } } for (var hcnt = 0; hcnt < smp.holechildren.length; hcnt++) { var hsmp = layer[smp.holechildren[hcnt]]; for (pcnt = 0; pcnt < hsmp.segments.length; pcnt++) { if (hsmp.segments[pcnt].hasOwnProperty('x3') && options.qcpr) { str += `<circle cx="${hsmp.segments[pcnt].x2 * options.scale}" cy="${ hsmp.segments[pcnt].y2 * options.scale }" r="${options.qcpr}" fill="cyan" stroke-width="${ options.qcpr * 0.2 }" stroke="black" />`; str += `<circle cx="${hsmp.segments[pcnt].x3 * options.scale}" cy="${ hsmp.segments[pcnt].y3 * options.scale }" r="${options.qcpr}" fill="white" stroke-width="${ options.qcpr * 0.2 }" stroke="black" />`; str += `<line x1="${hsmp.segments[pcnt].x1 * options.scale}" y1="${ hsmp.segments[pcnt].y1 * options.scale }" x2="${hsmp.segments[pcnt].x2 * options.scale}" y2="${ hsmp.segments[pcnt].y2 * options.scale }" stroke-width="${options.qcpr * 0.2}" stroke="cyan" />`; str += `<line x1="${hsmp.segments[pcnt].x2 * options.scale}" y1="${ hsmp.segments[pcnt].y2 * options.scale }" x2="${hsmp.segments[pcnt].x3 * options.scale}" y2="${ hsmp.segments[pcnt].y3 * options.scale }" stroke-width="${options.qcpr * 0.2}" stroke="cyan" />`; } if (!hsmp.segments[pcnt].hasOwnProperty('x3') && options.lcpr) { str += `<circle cx="${hsmp.segments[pcnt].x2 * options.scale}" cy="${ hsmp.segments[pcnt].y2 * options.scale }" r="${options.lcpr}" fill="white" stroke-width="${ options.lcpr * 0.2 }" stroke="black" />`; } } } } return str; } getsvgstring(tracedata, options) { options = this.checkoptions(options); const w = tracedata.width * options.scale; const h = tracedata.height * options.scale; let svgstr = `<svg ${ options.viewbox ? `viewBox="0 0 ${w} ${h}" ` : `width="${w}" height="${h}" ` }version="1.1" xmlns="http://www.w3.org/2000/svg" desc="Created with imagetracer.js version ${ this.versionnumber }" >`; for (let lcnt = 0; lcnt < tracedata.layers.length; lcnt += 1) { for (let pcnt = 0; pcnt < tracedata.layers[lcnt].length; pcnt += 1) { if (!tracedata.layers[lcnt][pcnt].isholepath) { svgstr += this.svgpathstring(tracedata, lcnt, pcnt, options); } } } svgstr += '</svg>'; return svgstr; } compareNumbers(a, b) { return a - b; } torgbastr(c) { return `rgba(${c.r},${c.g},${c.b},${c.a})`; } tosvgcolorstr(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}" `; } appendSVGString(svgstr, parentid) { let 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; } blur(imgd, radius, delta) { let i, j, k, d, idx, racc, gacc, bacc, aacc, wacc; const imgd2 = { width: imgd.width, height: imgd.height, data: [] }; radius = Math.floor(radius); if (radius < 1) { return imgd; } if (radius > 5) { radius = 5; } delta = Math.abs(delta); if (delta > 1024) { delta = 1024; } const thisgk = this.gks[radius - 1]; for (j = 0; j < imgd.height; j++) { for (i = 0; i < imgd.width; i++) { racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0; for (k = -radius; k < radius + 1; k++) { 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]; } } 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); } } const himgd = new Uint8ClampedArray(imgd2.data); for (j = 0; j < imgd.height; j++) { for (i = 0; i < imgd.width; i++) { racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0; for (k = -radius; k < radius + 1; k++) { 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]; } } 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); } } for (j = 0; j < imgd.height; j++) { for (i = 0; i < imgd.width; i++) { idx = (j * imgd.width + i) * 4; 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]); 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]; } } } return imgd2; } loadImage(url, callback, options) { const img = new Image(); if (options && options.corsenabled) { img.crossOrigin = 'Anonymous'; } img.src = url; img.onload = function () { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const context = canvas.getContext('2d'); context.drawImage(img, 0, 0); callback(canvas); }; } getImgdata(canvas) { const context = canvas.getContext('2d'); return context.getImageData(0, 0, canvas.width, canvas.height); } drawLayers(layers, palette, scale, parentid) { scale = scale || 1; let w, h, i, j, k; let 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); } for (k in layers) { if (!layers.hasOwnProperty(k)) { continue; } w = layers[k][0].length; h = layers[k].length; const canvas = document.createElement('canvas'); canvas.width = w * scale; canvas.height = h * scale; const context = canvas.getContext('2d'); for (j = 0; j < h; j += 1) { for (i = 0; i < w; i += 1) { context.fillStyle = this.torgbastr(palette[layers[k][j][i] % palette.length]); context.fillRect(i * scale, j * scale, scale, scale); } } div.appendChild(canvas); } } }