Skip to content

Commit

Permalink
Complete transformation to classes, now for the cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
cereallarceny committed Jul 17, 2019
1 parent 0510330 commit e4cefbc
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 656 deletions.
83 changes: 83 additions & 0 deletions src/GradientPath.js
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;
}
}
7 changes: 7 additions & 0 deletions src/Sample.js
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;
}
}
8 changes: 8 additions & 0 deletions src/Segment.js
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;
}
}
20 changes: 13 additions & 7 deletions src/_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ export const styleAttrs = (fill, stroke, strokeWidth, i) => {
return attrs;
};

export const segmentToD = segment => {
export const segmentToD = samples => {
let d = '';

segment.forEach((sample, i) => {
samples.forEach((sample, i) => {
const { x, y } = sample,
prevSample = i === 0 ? null : segment[i - 1];
prevSample = i === 0 ? null : samples[i - 1];

if (i === 0 && i !== segment.length - 1) {
if (i === 0 && i !== samples.length - 1) {
d += `M${x},${y}`;
} else if (
i === segment.length - 1 &&
x === segment[0].x &&
y === segment[0].y
i === samples.length - 1 &&
x === samples[0].x &&
y === samples[0].y
) {
d += 'Z';
} else if (x !== prevSample.x && y !== prevSample.y) {
Expand All @@ -54,3 +54,9 @@ export const segmentToD = segment => {

return d;
};

export const getMiddleSample = samples => {
const sortedSamples = [...samples].sort((a, b) => a.progress - b.progress);

return sortedSamples[(sortedSamples.length / 2) | 0];
};
171 changes: 171 additions & 0 deletions src/data.js
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();
Loading

0 comments on commit e4cefbc

Please sign in to comment.