-
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Complete transformation to classes, now for the cleanup
- Loading branch information
1 parent
0510330
commit e4cefbc
Showing
8 changed files
with
340 additions
and
656 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { getData, strokeToFill, flattenSegments } from './data'; | ||
import { svgElem, styleAttrs, segmentToD } from './_utils'; | ||
|
||
export default class GradientPath { | ||
constructor(path, numSegments, numSamples, precision = 2) { | ||
// If the path being passed isn't a DOM node already, make it one | ||
path = | ||
path instanceof Element || path instanceof HTMLDocument | ||
? path | ||
: path.node(); | ||
|
||
this.path = path; | ||
this.numSegments = numSegments; | ||
this.numSamples = numSamples; | ||
this.precision = precision; | ||
this.renders = []; | ||
|
||
this.svg = path.closest('svg'); | ||
this.group = svgElem('g', { | ||
class: 'gradient-path' | ||
}); | ||
|
||
this.data = getData(path, numSegments, numSamples, precision); | ||
|
||
// Append the main group to the SVG | ||
this.svg.appendChild(this.group); | ||
|
||
// Remove the main path once we have the data values | ||
this.path.parentNode.removeChild(path); | ||
} | ||
|
||
render({ type, stroke, strokeWidth, fill, width }) { | ||
const { group, precision } = this, | ||
renderCycle = {}; | ||
|
||
// Create a group for each element | ||
const elemGroup = svgElem('g', { class: `element-${type}` }); | ||
group.appendChild(elemGroup); | ||
|
||
renderCycle.group = elemGroup; | ||
|
||
if (type === 'path') { | ||
// If we specify a width, we will be filling, so we need to outline the path and then average the join points of the segments | ||
renderCycle.data = | ||
width && fill ? strokeToFill(this.data, width, precision) : this.data; | ||
|
||
for (let j = 0; j < renderCycle.data.length; j++) { | ||
const segment = renderCycle.data[j]; | ||
|
||
// Create a path for each segment (array of samples) and append it to its elemGroup | ||
elemGroup.appendChild( | ||
svgElem('path', { | ||
class: 'path-segment', | ||
d: segmentToD(segment.samples), | ||
...styleAttrs(fill, stroke, strokeWidth, segment.progress) | ||
}) | ||
); | ||
} | ||
} else if (type === 'circle') { | ||
renderCycle.data = flattenSegments(this.data); | ||
|
||
for (let j = 0; j < renderCycle.data.length; j++) { | ||
const sample = renderCycle.data[j]; | ||
|
||
// Create a circle for each sample (because we called "flattenSegments(data)" on the line before) and append it to its elemGroup | ||
elemGroup.appendChild( | ||
svgElem('circle', { | ||
class: 'circle-sample', | ||
cx: sample.x, | ||
cy: sample.y, | ||
r: width / 2, | ||
...styleAttrs(fill, stroke, strokeWidth, sample.progress) | ||
}) | ||
); | ||
} | ||
} | ||
|
||
// Save the information in the current renderCycle and pop it onto the renders array | ||
this.renders.push(renderCycle); | ||
|
||
return this; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export default class Sample { | ||
constructor(x, y, progress) { | ||
this.x = x; | ||
this.y = y; | ||
this.progress = progress; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { getMiddleSample } from './_utils'; | ||
|
||
export default class Segment { | ||
constructor(samples) { | ||
this.samples = samples; | ||
this.progress = getMiddleSample(samples).progress; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import Sample from './Sample'; | ||
import Segment from './Segment'; | ||
|
||
export const getData = (path, numSegments, numSamples, precision) => { | ||
// 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 | ||
if (numSamples > 1) numSamples--; | ||
|
||
// Get total length of path, total number of samples, and two blank arrays to hold samples and segments | ||
const pathLength = path.getTotalLength(), | ||
totalSamples = numSegments * numSamples, | ||
allSamples = [], | ||
allSegments = []; | ||
|
||
// 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++) { | ||
const progress = sample / totalSamples; | ||
let { x, y } = path.getPointAtLength(progress * pathLength); | ||
|
||
// If the user asks to round our x and y values, do so | ||
if (precision) { | ||
x = +x.toFixed(precision); | ||
y = +y.toFixed(precision); | ||
} | ||
|
||
allSamples.push(new Sample(x, y, progress)); | ||
} | ||
|
||
// Out of all the samples gathered, sort them into groups of length = numSamples | ||
// Each group includes the samples of the current segment, with the last sample being first sample from the next group | ||
// This "nextStart" becomes the "currentStart" every time segment is interated | ||
for (let segment = 0; segment < numSegments; segment++) { | ||
const currentStart = segment * numSamples; | ||
const nextStart = currentStart + numSamples; | ||
const samples = []; | ||
|
||
for (let samInSeg = 0; samInSeg < numSamples; samInSeg++) { | ||
samples.push(allSamples[currentStart + samInSeg]); | ||
} | ||
|
||
samples.push(allSamples[nextStart]); | ||
|
||
allSegments.push(new Segment(samples)); | ||
} | ||
|
||
return allSegments; | ||
}; | ||
|
||
export const strokeToFill = (data, width, precision) => { | ||
const outlinedStrokes = outlineStrokes(data, width, precision); | ||
const averagedSegmentJoins = averageSegmentJoins(outlinedStrokes, precision); | ||
|
||
return averagedSegmentJoins; | ||
}; | ||
|
||
const outlineStrokes = (data, width, precision) => { | ||
// We need to get the points perpendicular to a startPoint, given angle, radius, and precision | ||
const getPerpSamples = (angle, radius, precision, startPoint) => { | ||
const p0 = new Sample( | ||
Math.sin(angle) * radius + startPoint.x, | ||
-Math.cos(angle) * radius + startPoint.y, | ||
startPoint.progress | ||
), | ||
p1 = new Sample( | ||
-Math.sin(angle) * radius + startPoint.x, | ||
Math.cos(angle) * radius + startPoint.y, | ||
startPoint.progress | ||
); | ||
|
||
if (precision) { | ||
p0.x = +p0.x.toFixed(precision); | ||
p0.y = +p0.y.toFixed(precision); | ||
p1.x = +p1.x.toFixed(precision); | ||
p1.y = +p1.y.toFixed(precision); | ||
} | ||
|
||
return [p0, p1]; | ||
}; | ||
|
||
const radius = width / 2, | ||
outlinedData = []; | ||
|
||
for (let i = 0; i < data.length; i++) { | ||
const segment = data[i], | ||
segmentSamples = []; | ||
|
||
// For each sample point and the following sample point (if there is one) compute the angle and then various perpendicular points | ||
for (let j = 0; j < segment.samples.length; j++) { | ||
// If we're at the end of the segment and there are no further points | ||
if (segment.samples[j + 1] === undefined) break; | ||
|
||
const p0 = segment.samples[j], // The current sample point | ||
p1 = segment.samples[j + 1], // The next sample 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 | ||
p1Perps = getPerpSamples(angle, radius, precision, p1); // Get perpedicular points with a distance of radius away from p1 | ||
|
||
// We only need the p0 perpendenciular points for the first sample | ||
if (j === 0) { | ||
segmentSamples.push(...p0Perps); | ||
} | ||
|
||
// Always push the second sample point's perpendicular points | ||
segmentSamples.push(...p1Perps); | ||
} | ||
|
||
// 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([ | ||
...segmentSamples.filter((s, i) => i % 2 === 0), | ||
...segmentSamples.filter((s, i) => i % 2 === 1).reverse() | ||
]) | ||
); | ||
} | ||
|
||
return outlinedData; | ||
}; | ||
|
||
const averageSegmentJoins = (outlinedData, precision) => { | ||
// Find the average x and y between two points (p0 and p1) | ||
const avg = (p0, p1) => ({ | ||
x: (p0.x + p1.x) / 2, | ||
y: (p0.y + p1.y) / 2 | ||
}); | ||
|
||
// 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, | ||
y: avg.y | ||
}); | ||
|
||
for (let i = 0; i < outlinedData.length; i++) { | ||
let curSeg = outlinedData[i], // The current segment | ||
nextSeg = outlinedData[i + 1] ? outlinedData[i + 1] : outlinedData[0], // The next segment, otherwise, the first segment | ||
curMiddle = curSeg.samples.length / 2, // The "middle" item in the current segment | ||
nextEnd = nextSeg.samples.length - 1; // The last item in the next segment | ||
|
||
// Average two sets of outlined points to create p0Average and p1Average | ||
const p0Average = avg(curSeg.samples[curMiddle - 1], nextSeg.samples[0]), | ||
p1Average = avg(curSeg.samples[curMiddle], nextSeg.samples[nextEnd]); | ||
|
||
if (precision) { | ||
p0Average.x = +p0Average.x.toFixed(precision); | ||
p0Average.y = +p0Average.y.toFixed(precision); | ||
p1Average.x = +p1Average.x.toFixed(precision); | ||
p1Average.y = +p1Average.y.toFixed(precision); | ||
} | ||
|
||
// Replace the previous values | ||
curSeg.samples[curMiddle - 1] = combine( | ||
curSeg.samples, | ||
curMiddle - 1, | ||
p0Average | ||
); | ||
curSeg.samples[curMiddle] = combine(curSeg.samples, curMiddle, p1Average); | ||
nextSeg.samples[0] = combine(nextSeg.samples, 0, p0Average); | ||
nextSeg.samples[nextEnd] = combine(nextSeg.samples, nextEnd, p1Average); | ||
} | ||
|
||
return outlinedData; | ||
}; | ||
|
||
export const flattenSegments = data => | ||
data | ||
.map((segment, i) => { | ||
return segment.samples.map(sample => { | ||
return { ...sample, id: i }; | ||
}); | ||
}) | ||
.flat(); |
Oops, something went wrong.