UNPKG

@image-tracer-ts/browser

Version:

Platform-specific bindings for image-tracer-ts. Turn images into SVG files in browsers.

1 lines 758 kB
{"version":3,"file":"image-tracer-browser.mjs","sources":["../../core/dist/index.mjs","../../../node_modules/to-data-view/index.js","../../../node_modules/@canvas/image-data/browser.js","../../../node_modules/decode-bmp/index.js","../../../node_modules/decode-ico/index.js","../../../node_modules/magic-bytes.js/dist/model/toHex.js","../../../node_modules/magic-bytes.js/dist/model/tree.js","../../../node_modules/magic-bytes.js/dist/model/pattern-tree.js","../../../node_modules/magic-bytes.js/dist/index.js","../../../node_modules/pako/lib/utils/common.js","../../../node_modules/pako/lib/zlib/trees.js","../../../node_modules/pako/lib/zlib/adler32.js","../../../node_modules/pako/lib/zlib/crc32.js","../../../node_modules/pako/lib/zlib/messages.js","../../../node_modules/pako/lib/zlib/deflate.js","../../../node_modules/pako/lib/utils/strings.js","../../../node_modules/pako/lib/zlib/zstream.js","../../../node_modules/pako/lib/deflate.js","../../../node_modules/pako/lib/zlib/inffast.js","../../../node_modules/pako/lib/zlib/inftrees.js","../../../node_modules/pako/lib/zlib/inflate.js","../../../node_modules/pako/lib/zlib/constants.js","../../../node_modules/pako/lib/zlib/gzheader.js","../../../node_modules/pako/lib/inflate.js","../../../node_modules/pako/index.js","../../../node_modules/@vivaxy/png/lib/helpers/crc32.js","../../../node_modules/@vivaxy/png/lib/helpers/color-types.js","../../../node_modules/@vivaxy/png/lib/helpers/filters.js","../../../node_modules/@vivaxy/png/lib/helpers/rescale-sample.js","../../../node_modules/@vivaxy/png/lib/helpers/channels.js","../../../node_modules/@vivaxy/png/lib/helpers/interlace.js","../../../node_modules/@vivaxy/png/lib/decode/decode-idat.js","../../../node_modules/@vivaxy/png/lib/helpers/signature.js","../../../node_modules/@vivaxy/png/lib/helpers/gamma.js","../../../node_modules/@vivaxy/png/lib/helpers/utf8.js","../../../node_modules/@vivaxy/png/lib/helpers/typed-array.js","../../../node_modules/@vivaxy/png/lib/helpers/chromaticities.js","../../../node_modules/@vivaxy/png/lib/decode/index.js","../../../node_modules/@vivaxy/png/lib/encode/encode-idat.js","../../../node_modules/@vivaxy/png/lib/encode/index.js","../../../node_modules/read-pixels/dist/read-pixels.min.js","../src/image-loader.ts","../src/image-tracer-browser.ts","../src/index.ts"],"sourcesContent":["var RgbColorData;\n(function (RgbColorData) {\n function toString(c) {\n return `RgbColor(${c.r},${c.g},${c.b})`;\n }\n RgbColorData.toString = toString;\n})(RgbColorData || (RgbColorData = {}));\nclass RgbColor {\n r;\n g;\n b;\n a;\n /**\n * If the `a` value is below this value, a color is considered invisible.\n */\n static MINIMUM_A = 13; // that is 0.05\n constructor(r = 0, g = 0, b = 0, a = 255) {\n this.r = r;\n this.g = g;\n this.b = b;\n this.a = a;\n }\n static fromRgbColorData(c) {\n return new RgbColor(c.r, c.g, c.b, c.a ?? 255);\n }\n static createRandomColor() {\n const color = new RgbColor();\n color.randomize();\n return color;\n }\n static fromPixelArray(pixelData, pixelIndex, isRgba = true) {\n const color = new RgbColor();\n color.setFromPixelArray(pixelData, pixelIndex, isRgba);\n return color;\n }\n static fromHex(hex) {\n const values = hex.substring(1).match(/.{1,2}/g)?.map(n => parseInt(n, 16));\n return RgbColor.fromPixelArray(values, 0, values.length > 3);\n }\n static buildColorAverage(counter) {\n const r = Math.floor(counter.r / counter.n);\n const g = Math.floor(counter.g / counter.n);\n const b = Math.floor(counter.b / counter.n);\n const a = Math.floor(counter.a / counter.n);\n return new RgbColor(r, g, b, a);\n }\n isInvisible() {\n return this.a < RgbColor.MINIMUM_A;\n }\n hasOpacity() {\n return this.a < 255;\n }\n setFromColorCounts(counter) {\n this.r = Math.floor(counter.r / counter.n);\n this.g = Math.floor(counter.g / counter.n);\n this.b = Math.floor(counter.b / counter.n);\n this.a = Math.floor(counter.a / counter.n);\n }\n randomize() {\n this.r = Math.floor(Math.random() * 256);\n this.g = Math.floor(Math.random() * 256);\n this.b = Math.floor(Math.random() * 256);\n this.a = Math.floor(Math.random() * 128) + 128;\n }\n setFromPixelArray(pixelData, pixelIndex, isRgba = true) {\n const pixelWidth = isRgba ? 4 : 3;\n const offset = pixelIndex * pixelWidth;\n this.r = pixelData[offset + 0];\n this.g = pixelData[offset + 1];\n this.b = pixelData[offset + 2];\n this.a = isRgba ? pixelData[offset + 3] : 255;\n }\n get [Symbol.toStringTag]() {\n return `RgbaColor(${this.r},${this.g},${this.b},${this.a})`;\n }\n calculateDistanceToPixelInArray(pixelData, pixelIndex, isRgba = true) {\n const a = isRgba ? pixelData[pixelIndex + 3] : 255;\n // In my experience, https://en.wikipedia.org/wiki/Rectilinear_distance works better than https://en.wikipedia.org/wiki/Euclidean_distance\n return Math.abs(this.r - pixelData[pixelIndex])\n + Math.abs(this.g - pixelData[pixelIndex + 1])\n + Math.abs(this.b - pixelData[pixelIndex + 2])\n + Math.abs(this.a - a);\n }\n equals(color) {\n return this.r === color.r\n && this.g === color.g\n && this.b === color.b\n && this.a === (color.a ?? 255);\n }\n toCssColor() {\n return !this.hasOpacity() ? `rgb(${this.r},${this.g},${this.b})` : `rgba(${this.r},${this.g},${this.b},${this.a})`;\n }\n toCssColorHex() {\n const int = this.toInt32();\n let hex = int.toString(16);\n const leadingZeros = (this.hasOpacity() ? 8 : 6) - hex.length;\n if (leadingZeros > 0) {\n hex = '0'.repeat(leadingZeros) + hex;\n }\n return '#' + hex.toUpperCase();\n }\n toInt32() {\n if (!this.hasOpacity()) {\n return ((this.r << 16) | (this.g << 8) | (this.b)) >>> 0; // keep unsigned\n }\n return ((this.r << 24) | (this.g << 16) | (this.b << 8) | this.a) >>> 0; // keep unsigned\n }\n}\n\nvar ColorDistanceBuffering;\n(function (ColorDistanceBuffering) {\n ColorDistanceBuffering[\"OFF\"] = \"off\";\n ColorDistanceBuffering[\"ON\"] = \"on\";\n ColorDistanceBuffering[\"REASONABLE\"] = \"reasonable\";\n})(ColorDistanceBuffering || (ColorDistanceBuffering = {}));\nclass ColorIndex {\n rows; // palette color index for each pixel in the image\n palette;\n options;\n verbose;\n constructor(imageData, options, quantizeFunction) {\n this.options = options;\n this.verbose = options.verbose ?? false;\n this.palette = this.buildPalette(imageData, quantizeFunction);\n this.verbose && console.time(' - Color Quantization');\n this.rows = this.buildColorData(imageData, this.palette);\n this.verbose && console.timeEnd(' - Color Quantization');\n }\n /**\n * @param imageData\n * @returns\n */\n buildPalette(imageData, quantizeFunction) {\n const numberOfColors = Math.max(this.options.numberOfColors, 2);\n const palette = quantizeFunction(imageData, numberOfColors);\n if (this.options.verbose) {\n console.log(`Created palette with ${palette.length} colors.`);\n }\n return palette.map(c => (c instanceof RgbColor) ? c : RgbColor.fromRgbColorData(c));\n }\n /**\n * Using a form of k-means clustering repeated options.colorClusteringCycles times. http://en.wikipedia.org/wiki/Color_quantization\n *\n *\n * @param imageData\n * @returns\n */\n buildColorData(imageData, palette) {\n let imageColorIndex;\n const numberOfCycles = Math.max(this.options.colorClusteringCycles, 1);\n for (let cycle = 1; cycle <= numberOfCycles; cycle++) {\n const isLastCycle = cycle === numberOfCycles;\n const nextImageColorIndex = this.runClusteringCycle(imageData, palette, isLastCycle);\n const isFinished = isLastCycle || (imageColorIndex && this.colorIndexesEqual(imageColorIndex, nextImageColorIndex));\n imageColorIndex = nextImageColorIndex;\n if (isFinished) {\n this.options.verbose && console.log(`Ran ${cycle} clustering cycles`);\n break;\n }\n }\n return imageColorIndex;\n }\n runClusteringCycle(imageData, palette, isLastCycle) {\n const colorIndex = this.buildImageColorIndex(imageData, palette);\n const colorCounts = this.buildColorCounts(imageData, colorIndex, palette.length);\n const numPixels = imageData.width * imageData.height;\n this.adjustPaletteToColorAverages(palette, colorCounts, numPixels, isLastCycle);\n return colorIndex;\n }\n colorIndexesEqual(i1, i2) {\n if (i1.length !== i2.length) {\n return false;\n }\n for (let rowIx = 0; rowIx < i1.length; rowIx++) {\n const row1 = i1[rowIx];\n const row2 = i2[rowIx];\n if (row1.length !== row2.length) {\n return false;\n }\n for (let colIx = 0; colIx < row1.length; colIx++) {\n if (row1[colIx] !== row2[colIx]) {\n return false;\n }\n }\n }\n return true;\n }\n adjustPaletteToColorAverages(palette, colorCounters, numPixels, isLastCycle) {\n for (let k = 0; k < palette.length; k++) {\n const counter = colorCounters[k];\n const colorBelowThreshold = this.options.minColorQuota > 0 && counter.n / numPixels < this.options.minColorQuota;\n if (colorBelowThreshold && !isLastCycle) {\n palette[k].randomize();\n }\n else if (counter.n > 0) {\n palette[k].setFromColorCounts(counter);\n }\n }\n }\n /**\n * Maps each pixel in the image to the palette index of the closest color.\n *\n * @param imageData\n * @param palette\n * @returns\n */\n buildImageColorIndex(imageData, palette) {\n const bufferingMode = this.options.colorDistanceBuffering;\n const useBuffer = bufferingMode === ColorDistanceBuffering.ON ||\n (bufferingMode === ColorDistanceBuffering.REASONABLE && palette.length >= 32);\n return useBuffer ?\n this.buildImageColorIndexBuffered(imageData, palette) :\n this.buildImageColorIndexUnbuffered(imageData, palette);\n }\n buildImageColorIndexUnbuffered(imageData, palette) {\n const imageColorIndex = this.initColorIndexArray(imageData.width, imageData.height);\n for (let h = 0; h < imageData.height; h++) {\n for (let w = 0; w < imageData.width; w++) {\n const pixelOffset = (h * imageData.width + w) * 4;\n const closestColorIx = this.findClosestPaletteColorIx(imageData, pixelOffset, palette);\n imageColorIndex[h + 1][w + 1] = closestColorIx;\n }\n }\n return imageColorIndex;\n }\n buildImageColorIndexBuffered(imageData, palette) {\n const imageColorIndex = this.initColorIndexArray(imageData.width, imageData.height);\n const closestColorMap = [];\n let skips = 0, distinctValues = 0;\n for (let h = 0; h < imageData.height; h++) {\n for (let w = 0; w < imageData.width; w++) {\n const pixelOffset = (h * imageData.width + w) * 4;\n const colorId = this.getPixelColorId(imageData, pixelOffset);\n if (closestColorMap[colorId] !== undefined) {\n skips++;\n }\n else {\n closestColorMap[colorId] = this.findClosestPaletteColorIx(imageData, pixelOffset, palette);\n distinctValues++;\n }\n imageColorIndex[h + 1][w + 1] = closestColorMap[colorId];\n }\n }\n this.verbose && console.log(`Buffered ${distinctValues} colors to skip ${skips} comparisons (`, Math.round(100 * skips / (skips + distinctValues)), '%)');\n return imageColorIndex;\n }\n getPixelColorId(imageData, pixelOffset) {\n return ((imageData.data[pixelOffset] << 24)\n | (imageData.data[pixelOffset + 1] << 16)\n | (imageData.data[pixelOffset + 2] << 8)\n | (imageData.data[pixelOffset + 3])) >>> 0;\n }\n initColorIndexArray(imgWidth, imgHeight) {\n const imageColorIndex = [];\n for (let h = 0; h < imgHeight + 2; h++) {\n imageColorIndex[h] = new Array(imgWidth + 2).fill(-1);\n }\n return imageColorIndex;\n }\n buildColorCounts(imageData, imageColorIndex, numberOfColors) {\n const colorCounts = this.initColorCounts(numberOfColors);\n for (let h = 0; h < imageData.height; h++) {\n for (let w = 0; w < imageData.width; w++) {\n const closestColorIx = imageColorIndex[h + 1][w + 1];\n const colorCounter = colorCounts[closestColorIx];\n const pixelOffset = (h * imageData.width + w) * 4;\n colorCounter.r += imageData.data[pixelOffset];\n colorCounter.g += imageData.data[pixelOffset + 1];\n colorCounter.b += imageData.data[pixelOffset + 2];\n colorCounter.a += imageData.data[pixelOffset + 3];\n colorCounter.n++;\n }\n }\n return colorCounts;\n }\n initColorCounts(numberOfColors) {\n const colorCounts = [];\n for (let i = 0; i < numberOfColors; i++) {\n colorCounts[i] = { r: 0, g: 0, b: 0, a: 0, n: 0 };\n }\n return colorCounts;\n }\n /**\n * find closest color from palette by measuring (rectilinear) color distance between this pixel and all palette colors\n * @param palette\n * @param color\n * @param pixelOffset\n * @returns\n */\n findClosestPaletteColorIx(imageData, pixelOffset, palette) {\n let closestColorIx = 0;\n let closestDistance = 1024; // 4 * 256 is the maximum RGBA distance\n for (let colorIx = 0; colorIx < palette.length; colorIx++) {\n const color = palette[colorIx];\n const distance = color.calculateDistanceToPixelInArray(imageData.data, pixelOffset);\n if (distance >= closestDistance) {\n continue;\n }\n closestDistance = distance;\n closestColorIx = colorIx;\n }\n return closestColorIx;\n }\n}\n\nvar Trajectory;\n(function (Trajectory) {\n Trajectory[Trajectory[\"RIGHT\"] = 0] = \"RIGHT\";\n Trajectory[Trajectory[\"DOWN_RIGHT\"] = 1] = \"DOWN_RIGHT\";\n Trajectory[Trajectory[\"DOWN\"] = 2] = \"DOWN\";\n Trajectory[Trajectory[\"DOWN_LEFT\"] = 3] = \"DOWN_LEFT\";\n Trajectory[Trajectory[\"LEFT\"] = 4] = \"LEFT\";\n Trajectory[Trajectory[\"UP_LEFT\"] = 5] = \"UP_LEFT\";\n Trajectory[Trajectory[\"UP\"] = 6] = \"UP\";\n Trajectory[Trajectory[\"UP_RIGHT\"] = 7] = \"UP_RIGHT\";\n Trajectory[Trajectory[\"NONE\"] = 0] = \"NONE\";\n})(Trajectory || (Trajectory = {}));\nvar InterpolationMode;\n(function (InterpolationMode) {\n InterpolationMode[\"OFF\"] = \"off\";\n InterpolationMode[\"INTERPOLATE\"] = \"interpolate\";\n})(InterpolationMode || (InterpolationMode = {}));\nclass PointInterpolator {\n interpolate(mode, paths, enhanceRightAngle) {\n const interpolatedPaths = [];\n for (const path of paths) {\n const interpolatedPoints = this.interpolatePointsUsingMode(mode, path.points, enhanceRightAngle);\n const interpolatedPath = {\n points: interpolatedPoints,\n boundingBox: path.boundingBox,\n childHoles: path.childHoles,\n isHole: path.isHole\n };\n interpolatedPaths.push(interpolatedPath);\n }\n return interpolatedPaths;\n }\n interpolatePointsUsingMode(mode, edgePoints, enhanceRightAngle) {\n switch (mode) {\n case InterpolationMode.OFF:\n return edgePoints.map((ep, ix) => this.trajectoryPointFromEdgePoint(edgePoints, ix));\n default:\n case InterpolationMode.INTERPOLATE:\n return this.buildInterpolatedPoints(edgePoints, enhanceRightAngle);\n }\n }\n trajectoryPointFromEdgePoint(points, pointIx) {\n const edgePoint = points[pointIx];\n const nextIx = (pointIx + 1) % points.length;\n const nextPoint = points[nextIx];\n return {\n x: edgePoint.x,\n y: edgePoint.y,\n data: this.geTrajectory(edgePoint.x, edgePoint.y, nextPoint.x, nextPoint.y)\n };\n }\n buildInterpolatedPoints(edgePoints, enhanceRightAngle) {\n const interpolatedPoints = [];\n for (let pointIx = 0; pointIx < edgePoints.length; pointIx++) {\n if (enhanceRightAngle && this.isRightAngle(edgePoints, pointIx)) {\n const cornerPoint = this.buildCornerPoint(edgePoints, pointIx);\n this.updateLastPointTrajectory(interpolatedPoints, edgePoints[pointIx]);\n interpolatedPoints.push(cornerPoint);\n }\n const midPoint = this.interpolateNextTwoPoints(edgePoints, pointIx);\n interpolatedPoints.push(midPoint);\n }\n return interpolatedPoints;\n }\n updateLastPointTrajectory(points, referencePoint) {\n if (points.length === 0) {\n return;\n }\n const lastPointIx = points.length - 1;\n const lastPoint = points[lastPointIx];\n lastPoint.data = this.geTrajectory(lastPoint.x, lastPoint.y, referencePoint.x, referencePoint.y);\n }\n interpolateNextTwoPoints(points, pointIx) {\n const totalPoints = points.length;\n const nextIx1 = (pointIx + 1) % totalPoints;\n const nextIx2 = (pointIx + 2) % totalPoints;\n const currentPoint = points[pointIx];\n const nextPoint = points[nextIx1];\n const nextNextPoint = points[nextIx2];\n const midX = (currentPoint.x + nextPoint.x) / 2;\n const midY = (currentPoint.y + nextPoint.y) / 2;\n const nextMidX = (nextPoint.x + nextNextPoint.x) / 2;\n const nextMidY = (nextPoint.y + nextNextPoint.y) / 2;\n return {\n x: midX,\n y: midY,\n data: this.geTrajectory(midX, midY, nextMidX, nextMidY)\n };\n }\n isRightAngle(points, pointIx) {\n const totalPoints = points.length;\n const currentPoint = points[pointIx];\n const nextIx1 = (pointIx + 1) % totalPoints;\n const nextIx2 = (pointIx + 2) % totalPoints;\n const prevIx1 = (pointIx - 1 + totalPoints) % totalPoints;\n const prevIx2 = (pointIx - 2 + totalPoints) % totalPoints;\n return ((currentPoint.x === points[prevIx2].x) &&\n (currentPoint.x === points[prevIx1].x) &&\n (currentPoint.y === points[nextIx1].y) &&\n (currentPoint.y === points[nextIx2].y)) || ((currentPoint.y === points[prevIx2].y) &&\n (currentPoint.y === points[prevIx1].y) &&\n (currentPoint.x === points[nextIx1].x) &&\n (currentPoint.x === points[nextIx2].x));\n }\n buildCornerPoint(points, pointIx) {\n const nextIx1 = (pointIx + 1) % points.length;\n const currentPoint = points[pointIx];\n const nextPoint = points[nextIx1];\n const midX = (currentPoint.x + nextPoint.x) / 2;\n const midY = (currentPoint.y + nextPoint.y) / 2;\n const trajectory = this.geTrajectory(currentPoint.x, currentPoint.y, midX, midY);\n return {\n x: currentPoint.x,\n y: currentPoint.y,\n data: trajectory,\n };\n }\n geTrajectory(x1, y1, x2, y2) {\n if (x1 < x2) {\n if (y1 < y2) {\n return Trajectory.DOWN_RIGHT;\n }\n if (y1 > y2) {\n return Trajectory.UP_RIGHT;\n }\n return Trajectory.RIGHT;\n }\n else if (x1 > x2) {\n if (y1 < y2) {\n return Trajectory.DOWN_LEFT;\n }\n if (y1 > y2) {\n return Trajectory.UP_LEFT;\n }\n return Trajectory.LEFT;\n }\n else {\n if (y1 < y2) {\n return Trajectory.DOWN;\n }\n if (y1 > y2) {\n return Trajectory.UP;\n }\n }\n return Trajectory.NONE;\n }\n}\n\nvar TrimMode;\n(function (TrimMode) {\n TrimMode[\"OFF\"] = \"off\";\n TrimMode[\"KEEP_RATIO\"] = \"ratio\";\n TrimMode[\"ALL\"] = \"all\";\n})(TrimMode || (TrimMode = {}));\nvar FillStyle;\n(function (FillStyle) {\n FillStyle[\"FILL\"] = \"fill\";\n FillStyle[\"STROKE\"] = \"stroke\";\n FillStyle[\"STROKE_FILL\"] = \"stroke+fill\";\n})(FillStyle || (FillStyle = {}));\nconst SvgDrawerDefaultOptions = {\n strokeWidth: 1,\n lineFilter: false,\n scale: 1,\n decimalPlaces: 1,\n viewBox: true,\n desc: false,\n segmentEndpointRadius: 0,\n curveControlPointRadius: 0,\n fillStyle: FillStyle.STROKE_FILL,\n trim: TrimMode.OFF,\n};\n\nvar CreatePaletteMode;\n(function (CreatePaletteMode) {\n CreatePaletteMode[\"GENERATE\"] = \"generate\";\n CreatePaletteMode[\"SAMPLE\"] = \"sample\";\n CreatePaletteMode[\"SCAN\"] = \"scan\";\n CreatePaletteMode[\"PALETTE\"] = \"palette\";\n})(CreatePaletteMode || (CreatePaletteMode = {}));\nvar LayeringMode;\n(function (LayeringMode) {\n LayeringMode[LayeringMode[\"SEQUENTIAL\"] = 1] = \"SEQUENTIAL\";\n LayeringMode[LayeringMode[\"PARALLEL\"] = 2] = \"PARALLEL\";\n})(LayeringMode || (LayeringMode = {}));\nvar Options;\n(function (Options) {\n /**\n * Create full options object from partial\n */\n function buildFrom(options) {\n return Object.assign({}, defaultOptions, options ?? {});\n }\n Options.buildFrom = buildFrom;\n const defaultOptions = Object.assign(SvgDrawerDefaultOptions, {\n // Tracing\n lineErrorMargin: 1,\n curveErrorMargin: 1,\n minShapeOutline: 8,\n enhanceRightAngles: true,\n // Color quantization\n colorSamplingMode: CreatePaletteMode.SCAN,\n palette: null,\n numberOfColors: 16,\n minColorQuota: 0,\n colorClusteringCycles: 3,\n colorDistanceBuffering: ColorDistanceBuffering.REASONABLE,\n // Layering method\n layeringMode: LayeringMode.PARALLEL,\n interpolation: InterpolationMode.INTERPOLATE,\n // Blur\n blurRadius: 0,\n blurDelta: 20,\n sharpen: false,\n sharpenThreshold: 20,\n });\n const asPresets = (o) => o;\n Options.Presets = asPresets({\n default: defaultOptions,\n posterized1: { colorSamplingMode: CreatePaletteMode.GENERATE, numberOfColors: 2 },\n posterized2: { numberOfColors: 4, blurRadius: 5 },\n curvy: { lineErrorMargin: 0.01, lineFilter: true, enhanceRightAngles: false },\n sharp: { curveErrorMargin: 0.01, lineFilter: false },\n detailed: { minShapeOutline: 0, decimalPlaces: 2, lineErrorMargin: 0.5, curveErrorMargin: 0.5, numberOfColors: 64 },\n smoothed: { blurRadius: 5, blurDelta: 64 },\n grayscale: { colorSamplingMode: CreatePaletteMode.GENERATE, colorClusteringCycles: 1, numberOfColors: 7 },\n fixedpalette: { colorSamplingMode: CreatePaletteMode.GENERATE, colorClusteringCycles: 1, numberOfColors: 27 },\n randomsampling1: { colorSamplingMode: CreatePaletteMode.SAMPLE, numberOfColors: 8 },\n randomsampling2: { colorSamplingMode: CreatePaletteMode.SAMPLE, numberOfColors: 64 },\n artistic1: { colorSamplingMode: CreatePaletteMode.GENERATE, colorClusteringCycles: 1, minShapeOutline: 0, blurRadius: 5, blurDelta: 64, lineErrorMargin: 0.01, lineFilter: true, numberOfColors: 16, strokeWidth: 2 },\n artistic2: { curveErrorMargin: 0.01, colorSamplingMode: CreatePaletteMode.GENERATE, colorClusteringCycles: 1, numberOfColors: 4, strokeWidth: 0 },\n artistic3: { curveErrorMargin: 10, lineErrorMargin: 10, numberOfColors: 8 },\n artistic4: { curveErrorMargin: 10, lineErrorMargin: 10, numberOfColors: 64, blurRadius: 5, blurDelta: 256, strokeWidth: 2 },\n posterized3: {\n lineErrorMargin: 1, curveErrorMargin: 1, minShapeOutline: 20, enhanceRightAngles: true, colorSamplingMode: CreatePaletteMode.GENERATE, numberOfColors: 3,\n minColorQuota: 0, colorClusteringCycles: 3, blurRadius: 3, blurDelta: 20, strokeWidth: 0, lineFilter: false,\n decimalPlaces: 1, palette: [{ r: 0, g: 0, b: 100, a: 255 }, { r: 255, g: 255, b: 255, a: 255 }]\n }\n });\n})(Options || (Options = {}));\n\nconst SPEC_PALETTE = [\n { 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 },\n { 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 },\n { 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 },\n { 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 }\n];\nclass DivRenderer {\n // Helper function: Drawing all edge node layers into a container\n drawLayersToDiv(edgeRasters, scale, parentId) {\n scale = scale || 1;\n var w, h, i, j, k;\n // Preparing container\n var div;\n if (parentId) {\n div = document.getElementById(parentId);\n if (!div) {\n div = document.createElement('div');\n div.id = parentId;\n document.body.appendChild(div);\n }\n }\n else {\n div = document.createElement('div');\n document.body.appendChild(div);\n }\n // Layers loop\n for (k in edgeRasters) {\n if (!edgeRasters.hasOwnProperty(k)) {\n continue;\n }\n // width, height\n w = edgeRasters[k][0].length;\n h = edgeRasters[k].length;\n // Creating new canvas for every layer\n const canvas = document.createElement('canvas');\n canvas.width = w * scale;\n canvas.height = h * scale;\n const context = this.getCanvasContext(canvas);\n // Drawing\n const palette = SPEC_PALETTE;\n for (j = 0; j < h; j++) {\n for (i = 0; i < w; i++) {\n const colorIndex = edgeRasters[k][j][i] % palette.length;\n const color = palette[colorIndex];\n context.fillStyle = this.toRgbaLiteral(color);\n context.fillRect(i * scale, j * scale, scale, scale);\n }\n }\n // Appending canvas to container\n div.appendChild(canvas);\n }\n }\n // Convert color object to rgba string\n toRgbaLiteral(c) {\n return 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + c.a + ')';\n }\n // TODO check duplication\n getCanvasContext(canvas) {\n const context = canvas.getContext('2d');\n if (!context) {\n throw new Error('Could not read canvas');\n }\n return context;\n }\n}\n\nclass EdgeRasterBuilder {\n /**\n *\n * Builds one layer for each color in the given color index.\n *\n * @param colorIndex\n * @returns\n */\n static buildForColors(colorIndex) {\n // Creating layers for each indexed color in arr\n const rows = colorIndex.rows;\n const height = rows.length;\n const width = rows[0].length;\n // Create layers\n const edgeRasters = [];\n for (let colorId = 0; colorId < colorIndex.palette.length; colorId++) {\n edgeRasters[colorId] = [];\n for (let h = 0; h < height; h++) {\n edgeRasters[colorId][h] = new Array(width).fill(0);\n }\n }\n // Looping through all pixels and calculating edge node type\n let n1, n2, n3, n4, n5, n6, n7, n8;\n for (let h = 1; h < height - 1; h++) {\n for (let w = 1; w < width - 1; w++) {\n // This pixel's indexed color\n const colorId = rows[h][w];\n /**\n * n1 n2 n3\n * n4 n5\n * n6 n7 n8\n */\n n1 = rows[h - 1][w - 1] === colorId ? 1 : 0;\n n2 = rows[h - 1][w] === colorId ? 1 : 0;\n n3 = rows[h - 1][w + 1] === colorId ? 1 : 0;\n n4 = rows[h][w - 1] === colorId ? 1 : 0;\n n5 = rows[h][w + 1] === colorId ? 1 : 0;\n n6 = rows[h + 1][w - 1] === colorId ? 1 : 0;\n n7 = rows[h + 1][w] === colorId ? 1 : 0;\n n8 = rows[h + 1][w + 1] === colorId ? 1 : 0;\n // this pixel's type and looking back on previous pixels\n const edgeRaster = edgeRasters[colorId];\n edgeRaster[h + 1][w + 1] = 1 + n5 * 2 + n8 * 4 + n7 * 8;\n if (!n4) {\n edgeRaster[h + 1][w] = 0 + 2 + n7 * 4 + n6 * 8;\n }\n if (!n2) {\n edgeRaster[h][w + 1] = 0 + n3 * 2 + n5 * 4 + 8;\n }\n if (!n1) {\n edgeRaster[h][w] = 0 + n2 * 2 + 4 + n4 * 8;\n }\n }\n }\n return edgeRasters;\n }\n // 2. Layer separation and edge detection\n // Edge node types ( ▓: this layer or 1; ░: not this layer or 0 )\n // 12 ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓\n // 48 ░░ ░░ ░░ ░░ ░▓ ░▓ ░▓ ░▓ ▓░ ▓░ ▓░ ▓░ ▓▓ ▓▓ ▓▓ ▓▓\n // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15\n static buildForColor(colorData, colorId) {\n // Creating layers for each indexed color in arr\n const rows = colorData.rows;\n const height = rows.length;\n const width = rows[0].length;\n // Create layer\n const edgeRaster = [];\n for (let j = 0; j < height; j++) {\n edgeRaster[j] = new Array(width).fill(0);\n }\n // Looping through all pixels and calculating edge node type\n for (let h = 1; h < height; h++) {\n for (let w = 1; w < width; w++) {\n /*\n * current pixel is at 4:\n * ░░ of 1 2\n * ░▓ 8 4\n */\n edgeRaster[h][w] = ((rows[h - 1][w - 1] === colorId ? 1 : 0) +\n (rows[h - 1][w] === colorId ? 2 : 0) +\n (rows[h][w - 1] === colorId ? 8 : 0) +\n (rows[h][w] === colorId ? 4 : 0));\n }\n }\n return edgeRaster;\n }\n}\n\n// Lookup tables for pathscan\n// pathscan_combined_lookup[ arr[py][px] ][ dir ] = [nextarrpypx, nextdir, deltapx, deltapy];\nconst PATH_SCAN_COMBINED_LOOKUP = [\n [[-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1]],\n [[0, 1, 0, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [0, 2, -1, 0]],\n [[-1, -1, -1, -1], [-1, -1, -1, -1], [0, 1, 0, -1], [0, 0, 1, 0]],\n [[0, 0, 1, 0], [-1, -1, -1, -1], [0, 2, -1, 0], [-1, -1, -1, -1]],\n [[-1, -1, -1, -1], [0, 0, 1, 0], [0, 3, 0, 1], [-1, -1, -1, -1]],\n [[13, 3, 0, 1], [13, 2, -1, 0], [7, 1, 0, -1], [7, 0, 1, 0]],\n [[-1, -1, -1, -1], [0, 1, 0, -1], [-1, -1, -1, -1], [0, 3, 0, 1]],\n [[0, 3, 0, 1], [0, 2, -1, 0], [-1, -1, -1, -1], [-1, -1, -1, -1]],\n [[0, 3, 0, 1], [0, 2, -1, 0], [-1, -1, -1, -1], [-1, -1, -1, -1]],\n [[-1, -1, -1, -1], [0, 1, 0, -1], [-1, -1, -1, -1], [0, 3, 0, 1]],\n [[11, 1, 0, -1], [14, 0, 1, 0], [14, 3, 0, 1], [11, 2, -1, 0]],\n [[-1, -1, -1, -1], [0, 0, 1, 0], [0, 3, 0, 1], [-1, -1, -1, -1]],\n [[0, 0, 1, 0], [-1, -1, -1, -1], [0, 2, -1, 0], [-1, -1, -1, -1]],\n [[-1, -1, -1, -1], [-1, -1, -1, -1], [0, 1, 0, -1], [0, 0, 1, 0]],\n [[0, 1, 0, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [0, 2, -1, 0]],\n [[-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1]] // arr[py][px]===15 is invalid\n];\nclass AreaScanner {\n /**\n * 3. Walking through an edge node array, discarding edge node types 0 and 15 and creating paths from the rest.\n * Walk directions: 0 > ; 1 ^ ; 2 < ; 3 v\n * @param edgeRaster\n * @returns\n */\n scan(edgeRaster, pathMinLength) {\n const width = edgeRaster[0].length;\n const height = edgeRaster.length;\n const paths = [];\n let pathIx = 0;\n for (let h = 0; h < height; h++) {\n for (let w = 0; w < width; w++) {\n const edge = edgeRaster[h][w];\n /**\n * 12 ░░ ▓▓\n * 84 ░▓ ▓░\n * 4 11\n */\n if (edge !== 4 && edge !== 11) {\n // Other values are not important\n continue;\n }\n // Init\n let px = w;\n let py = h;\n const pointedArea = {\n points: [],\n boundingBox: [px, py, px, py],\n childHoles: [],\n isHole: (edge === 11)\n };\n paths[pathIx] = pointedArea;\n let areaClosed = false;\n let direction = 1;\n // Path points loop\n while (!areaClosed) {\n const edgeType = edgeRaster[py][px];\n this.addPointToArea(pointedArea, px - 1, py - 1, edgeType);\n // Next: look up the replacement, direction and coordinate changes = clear this cell, turn if required, walk forward\n const lookupRow = PATH_SCAN_COMBINED_LOOKUP[edgeType][direction];\n edgeRaster[py][px] = lookupRow[0];\n direction = lookupRow[1];\n px += lookupRow[2];\n py += lookupRow[3];\n // Close path\n if (px - 1 === pointedArea.points[0].x &&\n py - 1 === pointedArea.points[0].y) {\n areaClosed = true;\n // Discarding paths shorter than pathMinLength\n if (pointedArea.points.length < pathMinLength) {\n paths.pop();\n continue;\n }\n if (pointedArea.isHole) {\n // Finding the parent shape for this hole\n const parentId = this.findParentId(pointedArea, paths, pathIx, width, height);\n paths[parentId].childHoles.push(pathIx);\n }\n pathIx++;\n }\n }\n }\n }\n return paths;\n }\n addPointToArea(area, x, y, edgeType) {\n const point = { x, y, data: edgeType };\n // Bounding box\n if (x < area.boundingBox[0]) {\n area.boundingBox[0] = x;\n }\n if (y < area.boundingBox[1]) {\n area.boundingBox[1] = y;\n }\n if (x > area.boundingBox[2]) {\n area.boundingBox[2] = x;\n }\n if (y > area.boundingBox[3]) {\n area.boundingBox[3] = y;\n }\n return area.points.push(point);\n }\n findParentId(path, paths, maxPath, w, h) {\n let parentId = 0;\n let parentbbox = [-1, -1, w + 1, h + 1];\n for (let parentIx = 0; parentIx < maxPath; parentIx++) {\n const parentPath = paths[parentIx];\n if (!parentPath.isHole\n && this.boundingBoxIncludes(parentPath.boundingBox, path.boundingBox)\n && this.boundingBoxIncludes(parentbbox, parentPath.boundingBox)\n && this.pointInPolygon(path.points[0], parentPath.points)) {\n parentId = parentIx;\n parentbbox = parentPath.boundingBox;\n }\n }\n return parentId;\n }\n boundingBoxIncludes(parentbbox, childbbox) {\n return ((parentbbox[0] < childbbox[0]) &&\n (parentbbox[1] < childbbox[1]) &&\n (parentbbox[2] > childbbox[2]) &&\n (parentbbox[3] > childbbox[3]));\n }\n // Point in polygon test\n pointInPolygon(point, path) {\n let isIn = false;\n for (let i = 0, j = path.length - 1; i < path.length; j = i++) {\n isIn = (((path[i].y > point.y) !== (path[j].y > point.y)) &&\n (point.x < (path[j].x - path[i].x) * (point.y - path[i].y) / (path[j].y - path[i].y) + path[i].x))\n ? !isIn : isIn;\n }\n return isIn;\n }\n}\n\nvar SvgLineAttributes;\n(function (SvgLineAttributes) {\n function toString(la) {\n const str = `${la.type} ${la.x1} ${la.y1} ${la.x2} ${la.y2}`;\n if (la.type === 'L') {\n return str;\n }\n return str + ` ${la.x3} ${la.y3}`;\n }\n SvgLineAttributes.toString = toString;\n})(SvgLineAttributes || (SvgLineAttributes = {}));\nclass PathTracer {\n // 5. tracepath() : recursively trying to fit straight and quadratic spline segments on the 8 direction internode path\n // 5.1. Find sequences of points with only 2 segment types\n // 5.2. Fit a straight line on the sequence\n // 5.3. If the straight line fails (distance error > lineErrorMargin), find the point with the biggest error\n // 5.4. Fit a quadratic spline through errorpoint (project this to get controlpoint), then measure errors on every point in the sequence\n // 5.5. If the spline fails (distance error > curveErrorMargin), find the point with the biggest error, set splitpoint = fitting point\n // 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences\n trace(path, lineErrorMargin, curveErrorMargin) {\n const pathCommands = [];\n const points = [].concat(path.points);\n points.push(points[0]); // we want to end on the point we started on\n for (let sequenceStartIx = 0; sequenceStartIx < points.length - 1;) {\n const nextSequenceStartIx = this.findNextSequenceStartIx(points, sequenceStartIx);\n const commandSequence = this.getPathCommandsOfSequence(points, lineErrorMargin, curveErrorMargin, sequenceStartIx, nextSequenceStartIx);\n pathCommands.push(...commandSequence);\n sequenceStartIx = nextSequenceStartIx;\n }\n return {\n lineAttributes: pathCommands,\n boundingBox: path.boundingBox,\n childHoles: path.childHoles,\n isHole: path.isHole\n };\n }\n /**\n * Find sequence of points with 2 trajectories.\n *\n * @param points\n * @param startIx\n * @returns The index where the next sequence starts\n */\n findNextSequenceStartIx(points, startIx) {\n const startTrajectory = points[startIx].data;\n let nextIx = startIx + 1;\n let nextPoint = points[nextIx];\n let secondTrajectory = null;\n while ((nextPoint.data === startTrajectory ||\n nextPoint.data === secondTrajectory ||\n secondTrajectory === null) && nextIx < points.length - 1) {\n if (nextPoint.data !== startTrajectory &&\n secondTrajectory === null) {\n secondTrajectory = nextPoint.data;\n }\n nextIx++;\n nextPoint = points[nextIx];\n }\n // the very last point is same as start point and part of the last sequence, no matter its type \n return (nextIx === points.length - 2) ? nextIx + 1 : nextIx;\n }\n // 5.2. - 5.6. recursively fitting a straight or quadratic line segment on this sequence of path nodes,\n // called from tracepath()\n getPathCommandsOfSequence(points, lineErrorMargin, curveErrorMargin, sequenceStartIx, sequenceEndIx) {\n if (sequenceEndIx > points.length || sequenceEndIx < 0) {\n return [];\n }\n const isLineResult = this.checkSequenceFitsLine(points, lineErrorMargin, sequenceStartIx, sequenceEndIx);\n if (typeof isLineResult === 'object') {\n return [isLineResult];\n }\n const isCurveResult = this.checkSequenceFitsCurve(points, curveErrorMargin, sequenceStartIx, sequenceEndIx, isLineResult);\n if (typeof isCurveResult === 'object') {\n return [isCurveResult];\n }\n // 5.5. If the spline fails (distance error>curveErrorMargin), find the point with the biggest error\n const splitPoint = isLineResult;\n const seqSplit1 = this.getPathCommandsOfSequence(points, lineErrorMargin, curveErrorMargin, sequenceStartIx, splitPoint);\n const seqSplit2 = this.getPathCommandsOfSequence(points, lineErrorMargin, curveErrorMargin, splitPoint, sequenceEndIx);\n return seqSplit1.concat(seqSplit2);\n }\n checkSequenceFitsLine(points, lineErrorMargin, sequenceStartIx, sequenceEndIx) {\n const sequenceLength = sequenceEndIx - sequenceStartIx;\n const startPoint = points[sequenceStartIx];\n const endPoint = points[sequenceEndIx];\n const gainX = (endPoint.x - startPoint.x) / sequenceLength;\n const gainY = (endPoint.y - startPoint.y) / sequenceLength;\n // 5.2. Fit a straight line on the sequence\n let isStraightLine = true;\n let maxDiffIx = sequenceStartIx;\n let maxDiff = 0;\n for (let pointIx = sequenceStartIx + 1; pointIx < sequenceEndIx; pointIx++) {\n const subsequenceLength = pointIx - sequenceStartIx;\n const expectedX = startPoint.x + subsequenceLength * gainX;\n const expectedY = startPoint.y + subsequenceLength * gainY;\n const point = points[pointIx];\n const diff = (point.x - expectedX) * (point.x - expectedX) + (point.y - expectedY) * (point.y - expectedY);\n if (diff > lineErrorMargin) {\n isStraightLine = false;\n }\n if (diff > maxDiff) {\n maxDiffIx = pointIx;\n maxDiff = diff;\n }\n }\n if (!isStraightLine) {\n return maxDiffIx;\n }\n return {\n type: 'L',\n x1: startPoint.x,\n y1: startPoint.y,\n x2: endPoint.x,\n y2: endPoint.y\n };\n }\n checkSequenceFitsCurve(points, curveErrorMargin, sequenceStartIx, sequenceEndIx, turningPointIx) {\n const sequenceLength = sequenceEndIx - sequenceStartIx;\n const startPoint = points[sequenceStartIx];\n const endPoint = points[sequenceEndIx];\n let isCurve = true;\n let maxDiff = 0;\n let maxDiffIx = sequenceStartIx;\n // 5.4. Fit a quadratic spline through this point, measure errors on every point in the sequence\n // helpers and projecting to get control point\n let subsequenceLength = turningPointIx - sequenceStartIx, t = subsequenceLength / sequenceLength, t1 = (1 - t) * (1 - t), t2 = 2 * (1 - t) * t, t3 = t * t;\n const qControlPointX = (t1 * startPoint.x + t3 * endPoint.x - points[turningPointIx].x) / -t2;\n const qControlPointY = (t1 * startPoint.y + t3 * endPoint.y - points[turningPointIx].y) / -t2;\n // Check every point\n for (let pointIx = sequenceStartIx + 1; pointIx != sequenceEndIx; pointIx = (pointIx + 1) % points.length) {\n subsequenceLength = pointIx - sequenceStartIx;\n t = subsequenceLength / sequenceLength;\n t1 = (1 - t) * (1 - t);\n t2 = 2 * (1 - t) * t;\n t3 = t * t;\n const px = t1 * startPoint.x + t2 * qControlPointX + t3 * endPoint.x;\n const py = t1 * startPoint.y + t2 * qControlPointY + t3 * endPoint.y;\n const point = points[pointIx];\n const diff = (point.x - px) * (point.x - px) + (point.y - py) * (point.y - py);\n if (diff > curveErrorMargin) {\n isCurve = false;\n }\n if (diff > maxDiff) {\n maxDiffIx = pointIx;\n maxDiff = diff;\n }\n }\n if (!isCurve) {\n return maxDiffIx;\n }\n return {\n type: 'Q',\n x1: startPoint.x,\n y1: startPoint.y,\n x2: qControlPointX,\n y2: qControlPointY,\n x3: endPoint.x,\n y3: endPoint.y\n };\n }\n}\n\nvar TraceDataTrimmer;\n(function (TraceDataTrimmer) {\n function trim(traceData, strokeWidth, keepAspectRatio, verbose = false) {\n const offsets = getOffsets(traceData, strokeWidth);\n if (keepAspectRatio) {\n applyAspectRatio(offsets, traceData);\n }\n if (offsets.minX === 0 &&\n offsets.maxX === traceData.width &&\n offsets.minY === 0 &&\n offsets.maxY === traceData.height) {\n return;\n }\n verbose && console.log(`Trimming x[${offsets.minX}|${offsets.maxX}]/${traceData.width}, y[${offsets.minY}|${offsets.maxY}]/${traceData.height}`);\n updateData(traceData, offsets);\n }\n TraceDataTrimmer.trim = trim;\n function getOffsets(traceData, strokeWidth) {\n let minX = traceData.width, minY = traceData.height, maxX = 0, maxY = 0;\n for (let colorId = 0; colorId < traceData.areasByColor.length; colorId++) {\n const color = traceData.colors[colorId];\n if (color.a === 0) {\n continue;\n }\n const colorArea = traceData.areasByColor[colorId];\n for (const area of colorArea) {\n for (const line of area.lineAttributes) {\n const isLineQ = line.type === \"Q\";\n minX = Math.min(minX, line.x1, line.x2, isLineQ ? line.x3 : minX);\n maxX = Math.max(maxX, line.x1, line.x2, isLineQ ? line.x3 : 0);\n minY = Math.min(minY, line.y1, line.y2, isLineQ ? line.y3 : minY);\n maxY = Math.max(maxY, line.y1, line.y2, isLineQ ? line.y3 : 0);\n }\n }\n }\n const strokeBorder = Math.floor(strokeWidth / 2);\n minX -= strokeBorder;\n minY -= strokeBorder;\n maxX += strokeBorder;\n maxY += strokeBorder;\n return { minX, maxX, minY, maxY };\n }\n function applyAspectRatio(offsets, traceData) {\n const trimmedWidth = offsets.maxX - offsets.minX;\n const trimmedHeight = offsets.maxY - offsets.minY;\n const oldWidth = traceData.width;\n const oldHeight = traceData.height;\n const expectedTrimmedWidth = Math.ceil(trimmedHeight * oldWidth / oldHeight);\n if (trimmedWidth === expectedTrimmedWidth) {\n return;\n }\n if (expectedTrimmedWidth > trimmedWidth) {\n const diff = (expectedTrimmedWidth - trimmedWidth) / 2;\n offsets.minX -= Math.ceil(diff);\n offsets.maxX += Math.floor(diff);\n return;\n }\n const expectedTrimmedHeight = Math.ceil(trimmedWidth * oldHeight / oldWidth);\n const diff = (expectedTrimmedHeight - trimmedHeight) / 2;\n offsets.minY -= Math.ceil(diff);\n offsets.maxY += Math.floor(diff);\n }\n function updateData(traceData, offsets) {\n const { minX, maxX, minY, maxY } = offsets;\n for (const colorArea of traceData.areasByColor) {\n for (const area of colorArea) {\n for (const lineAttribute of area.lineAttributes) {\n lineAttribute.x1 -= minX;\n lineAttribute.x2 -= minX;\n lineAttribute.y1 -= minY;\n lineAttribute.y2 -= minY;\n if (lineAttribute.type === 'Q') {\n lineAttribute.x3 -= minX;\n lineAttribute.y3 -= minY;\n }\n }\n }\n }\n traceData.height = maxY - minY;\n traceData.width = maxX - minX;\n }\n})(TraceDataTrimmer || (TraceDataTrimmer = {}));\n\nclass SvgDrawer {\n options;\n useStroke;\n useFill;\n constructor(options) {\n this.options = Object.assign({}, SvgDrawerDefaultOptions, options);\n this.useFill = [FillStyle.FILL, FillStyle.STROKE_FILL].includes(this.options.fillStyle);\n this.useStroke = [FillStyle.STROKE, FillStyle.STROKE_FILL].includes(this.options.fillStyle);\n }\n fixValue(val) {\n if (this.options.scale !== 1) {\n val *= this.options.scale;\n }\n if (this.options.decimalPlaces === -1) {\n return val;\n }\n return +val.toFixed(this.options.decimalPlaces);\n }\n draw(traceData) {\n this.init(traceData);\n const tags = [];\n for (let colorId = 0; colorId < traceData.areasByColor.length; colorId++) {\n for (let areaIx = 0; areaIx < traceData.areasByColor[colorId].length; areaIx++) {\n if (traceDa