|
"source": "import { DEFAULT_PRECISION } from \"./_constants\"\nimport { convertPathToNode } from \"./_utils\"\nimport Sample from \"./Sample\"\nimport Segment from \"./Segment\"\n\n// The main function responsible for getting data\n// This will take a path, number of samples, number of samples, and a precision value\n// It will return an array of Segments, which in turn contains an array of Samples\n// This can later be used to generate a stroked path, converted to outlines for a filled path, or flattened for plotting SVG circles\nexport const getData = ({\n path,\n segments,\n samples,\n precision = DEFAULT_PRECISION,\n}) => {\n // Convert the given path to a DOM node if it isn't already one\n path = convertPathToNode(path)\n\n // We decrement the number of samples per segment because when we group them later we will add on the first sample of the following segment\n if (samples > 1) samples--\n\n // Get total length of path, total number of samples we will be generating, and two blank arrays to hold samples and segments\n const pathLength = path.getTotalLength(),\n totalSamples = segments * samples,\n allSamples = [],\n allSegments = []\n\n // For the number of total samples, get the x, y, and progress values for each sample along the path\n for (let sample = 0; sample <= totalSamples; sample++) {\n const progress = sample / totalSamples\n\n let { x, y } = path.getPointAtLength(progress * pathLength)\n\n // If the user asks to round our x and y values, do so\n if (precision) {\n x = +x.toFixed(precision)\n y = +y.toFixed(precision)\n }\n\n // Create a new Sample and push it onto the allSamples array\n allSamples.push(new Sample({ x, y, progress }))\n }\n\n // Out of all the samples gathered previously, sort them into groups of segments\n // Each group includes the samples of the current segment, with the last sample being first sample from the next segment\n for (let segment = 0; segment < segments; segment++) {\n const currentStart = segment * samples,\n nextStart = currentStart + samples,\n segmentSamples = []\n\n // Push all current samples onto segmentSamples\n for (let samInSeg = 0; samInSeg < samples; samInSeg++) {\n segmentSamples.push(allSamples[currentStart + samInSeg])\n }\n\n // Push the first sample from the next segment onto segmentSamples\n segmentSamples.push(allSamples[nextStart])\n\n // Create a new Segment with the samples from segmentSamples\n allSegments.push(new Segment({ samples: segmentSamples }))\n }\n\n // Return our group of segments\n return allSegments\n}\n\n// The function responsible for converting strokable data (from getData()) into fillable data\n// This allows any SVG path to be filled instead of just stroked, allowing for the user to fill and stroke paths simultaneously\n// We start by outlining the stroked data given a specified width and the we average together the edges where adjacent segments touch\nexport const strokeToFill = (data, width, precision, pathClosed) => {\n const outlinedStrokes = outlineStrokes(data, width, precision),\n averagedSegmentJoins = averageSegmentJoins(\n outlinedStrokes,\n precision,\n pathClosed\n )\n\n return averagedSegmentJoins\n}\n\n// An internal function for outlining stroked data\nconst outlineStrokes = (data, width, precision) => {\n // We need to get the points perpendicular to a startPoint, given an angle, radius, and precision\n const getPerpSamples = (angle, radius, precision, startPoint) => {\n const p0 = new Sample({\n ...startPoint,\n x: Math.sin(angle) * radius + startPoint.x,\n y: -Math.cos(angle) * radius + startPoint.y,\n }),\n p1 = new Sample({\n ...startPoint,\n x: -Math.sin(angle) * radius + startPoint.x,\n y: Math.cos(angle) * radius + startPoint.y,\n })\n\n // If the user asks to round our x and y values, do so\n if (precision) {\n p0.x = +p0.x.toFixed(precision)\n p0.y = +p0.y.toFixed(precision)\n p1.x = +p1.x.toFixed(precision)\n p1.y = +p1.y.toFixed(precision)\n }\n\n return [p0, p1]\n }\n\n // We need to set the radius (half of the width) and have a holding array for outlined Segments\n const radius = width / 2,\n outlinedData = []\n\n for (let i = 0; i < data.length; i++) {\n const samples = data[i].samples,\n segmentSamples = []\n\n // For each sample point and the following sample point (if there is one) compute the angle\n // Also compute the sample's various perpendicular points (with a distance of radius away from the sample point)\n for (let j = 0; j < samples.length; j++) {\n // If we're at the end of the segment and there are no further points, get outta here!\n if (samples[j + 1] === undefined) break\n\n const p0 = samples[j], // First point\n p1 = samples[j + 1], // Second point\n angle = Math.atan2(p1.y - p0.y, p1.x - p0.x), // Perpendicular angle to p0 and p1\n p0Perps = getPerpSamples(angle, radius, precision, p0), // Get perpedicular points with a distance of radius away from p0\n p1Perps = getPerpSamples(angle, radius, precision, p1) // Get perpedicular points with a distance of radius away from p1\n\n // We only need the p0 perpendenciular points for the first sample\n // The p0 for j > 0 will always be the same as p1 anyhow, so let's not add redundant points\n if (j === 0) {\n segmentSamples.push(...p0Perps)\n }\n\n // Always push the second sample point's perpendicular points\n segmentSamples.push(...p1Perps)\n }\n\n // segmentSamples is out of order...\n // Given a segmentSamples length of 8, the points need to be rearranged from: 0, 2, 4, 6, 7, 5, 3, 1\n outlinedData.push(\n new Segment({\n samples: [\n ...segmentSamples.filter((s, i) => i % 2 === 0),\n ...segmentSamples.filter((s, i) => i % 2 === 1).reverse(),\n ],\n })\n )\n }\n\n return outlinedData\n}\n\n// An internal function taking outlinedData (from outlineStrokes()) and averaging adjacent edges\n// If we didn't do this, our data would be fillable, but it would look stroked\n// This function fixes where segments overlap and underlap each other\nconst averageSegmentJoins = (outlinedData, precision, pathClosed) => {\n // Find the average x and y between two points (p0 and p1)\n const avg = (p0, p1) => ({\n x: (p0.x + p1.x) / 2,\n y: (p0.y + p1.y) / 2,\n })\n\n // Recombine the new x and y positions with all the other keys in the object\n const combine = (segment, pos, avg) => ({\n ...segment[pos],\n x: avg.x,\n y: avg.y,\n })\n\n const init_outlinedData = JSON.parse(JSON.stringify(outlinedData)) //clone initial outlinedData Object\n\n for (let i = 0; i < outlinedData.length; i++) {\n // If path is closed: the current segment's samples;\n // If path is open: the current segments' samples, as long as it's not the last segment; Otherwise, the current segments' sample of the initial outlinedData object\n const currentSamples = pathClosed\n ? outlinedData[i].samples\n : outlinedData[i + 1]\n ? outlinedData[i].samples\n : init_outlinedData[i].samples,\n // If path is closed: the next segment's samples, otherwise, the first segment's samples\n // If path is open: the next segment's samples, otherwise, the first segment's samples of the initial outlinedData object\n nextSamples = pathClosed\n ? outlinedData[i + 1]\n ? outlinedData[i + 1].samples\n : outlinedData[0].samples\n : outlinedData[i + 1]\n ? outlinedData[i + 1].samples\n : init_outlinedData[0].samples,\n currentMiddle = currentSamples.length / 2, // The \"middle\" sample in the current segment's samples\n nextEnd = nextSamples.length - 1 // The last sample in the next segment's samples\n\n // Average two sets of outlined samples to create p0Average and p1Average\n const p0Average = avg(currentSamples[currentMiddle - 1], nextSamples[0]),\n p1Average = avg(currentSamples[currentMiddle], nextSamples[nextEnd])\n\n // If the user asks to round our x and y values, do so\n if (precision) {\n p0Average.x = +p0Average.x.toFixed(precision)\n p0Average.y = +p0Average.y.toFixed(precision)\n p1Average.x = +p1Average.x.toFixed(precision)\n p1Average.y = +p1Average.y.toFixed(precision)\n }\n\n // Replace the previous values with new Samples\n currentSamples[currentMiddle - 1] = new Sample({\n ...combine(currentSamples, currentMiddle - 1, p0Average),\n })\n currentSamples[currentMiddle] = new Sample({\n ...combine(currentSamples, currentMiddle, p1Average),\n })\n nextSamples[0] = new Sample({\n ...combine(nextSamples, 0, p0Average),\n })\n nextSamples[nextEnd] = new Sample({\n ...combine(nextSamples, nextEnd, p1Average),\n })\n }\n\n return outlinedData\n}\n",
|