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