Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
/dist
/storybook-static
.DS_Store
yarn-error.log
yarn-error.log
.vscode
94 changes: 0 additions & 94 deletions src/GradientPath.js

This file was deleted.

151 changes: 151 additions & 0 deletions src/GradientPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { getData, strokeToFill } from './_data';
import { svgElem, styleAttrs, segmentToD, convertPathToNode } from './_utils';
import { DEFAULT_PRECISION } from './_constants';
import Segment from './Segment';

export interface RenderOptions {
type: string;
stroke?: string;
strokeWidth?: number;
fill: Record<string, any>[];
width: number;
}

export default class GradientPath {
public path: Record<string, any>;
public segments: number;
public samples: number;
public precision?: number;

public pathClosed: boolean;
public renders: any[];
public group: SVGElement;
public svg: SVGElement;

public data: Segment[];

constructor({
path,
segments,
samples,
precision = DEFAULT_PRECISION
}: {
path: Record<string, any>;
segments: number;
samples: number;
precision?: number;
}) {
// If the path being passed isn't a DOM node already, make it one
this.path = convertPathToNode(path);

// Check if nodeName is path and that the path is closed, otherwise it's closed by default
this.pathClosed =
this.path.nodeName == 'path'
? this.path.getAttribute('d').match(/z/gi)
: true;

// Get the data
this.data = getData({ path, segments, samples, precision });

this.segments = segments;
this.samples = samples;
this.precision = precision;

// Store the render cycles that the user creates
this.renders = [];

// Append a group to the SVG to capture everything we render and ensure our paths and circles are properly encapsulated
this.svg = path.closest('svg');

this.group = svgElem('g', {
class: 'gradient-path'
});

// 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(this.path);
}

update(options: RenderOptions, path: Record<string, any> = this.path) {
// If the path being passed isn't a DOM node already, make it one
this.path = convertPathToNode(path);

// Check if nodeName is path and that the path is closed, otherwise it's closed by default
this.pathClosed =
this.path.nodeName == 'path'
? this.path.getAttribute('d').match(/z/gi)
: true;

// Get the data
this.data = getData({
path,
segments: this.segments,
samples: this.samples,
precision: this.precision!
});

const { children } = this.renderSegments(options);

const paths = children.map(child => child.getAttribute('d')) as string[];
const els = Array.from(
this.group.querySelectorAll('.path-segment')
) as SVGPathElement[];

els.forEach((el, index) => el.setAttribute('d', paths[index]));
}

renderSegments({ stroke, strokeWidth, fill, width }: RenderOptions) {
const renderCycle: Record<string, any> = {};

renderCycle.data =
width && fill
? strokeToFill(this.data, width, this.precision!, this.pathClosed)
: this.data;

const children = renderCycle.data.map((seg: Segment) => {
const { samples, progress } = seg;

return svgElem('path', {
class: 'path-segment',
d: segmentToD(samples),
...styleAttrs(
(fill as unknown) as string,
stroke!,
strokeWidth!,
progress!
)
});
}) as SVGPathElement[];

return { renderCycle, children };
}

render({ type = 'path', stroke, strokeWidth, fill, width }: RenderOptions) {
// Store information from this render cycle
const { children, renderCycle } = this.renderSegments({
type,
stroke,
strokeWidth,
fill,
width
});

// Create a group for each element
const elemGroup = svgElem('g', { class: `element-${type}` });
this.group.appendChild(elemGroup);

renderCycle.group = elemGroup;

children.forEach(element => {
elemGroup.appendChild(element);
});

// Save the information in the current renderCycle and pop it onto the renders array
this.renders.push(renderCycle);

// Return this for method chaining
return this;
}
}
8 changes: 0 additions & 8 deletions src/Sample.js

This file was deleted.

23 changes: 23 additions & 0 deletions src/Sample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export default class Sample {
public x: number;
public y: number;
public progress?: number;
public segment?: number;

constructor({
x,
y,
progress,
segment
}: {
x: number;
y: number;
progress?: number;
segment?: number;
}) {
this.x = x;
this.y = y;
this.progress = progress;
this.segment = segment;
}
}
6 changes: 5 additions & 1 deletion src/Segment.js → src/Segment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import Sample from './Sample';
import { getMiddleSample } from './_utils';

export default class Segment {
constructor({ samples }) {
public samples: Sample[];
public progress?: number;

constructor({ samples }: { samples: Sample[] }) {
this.samples = samples;
this.progress = getMiddleSample(samples).progress;
}
Expand Down
File renamed without changes.
37 changes: 30 additions & 7 deletions src/_data.js → src/_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const getData = ({
segments,
samples,
precision = DEFAULT_PRECISION
}: {
path: Record<string, any>;
segments: number;
samples: number;
precision: number;
}) => {
// Convert the given path to a DOM node if it isn't already one
path = convertPathToNode(path);
Expand Down Expand Up @@ -67,7 +72,12 @@ export const getData = ({
// 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) => {
export const strokeToFill = (
data: Segment[],
width: number,
precision: number,
pathClosed: boolean
) => {
const outlinedStrokes = outlineStrokes(data, width, precision),
averagedSegmentJoins = averageSegmentJoins(
outlinedStrokes,
Expand All @@ -79,9 +89,14 @@ export const strokeToFill = (data, width, precision, pathClosed) => {
};

// An internal function for outlining stroked data
const outlineStrokes = (data, width, precision) => {
const outlineStrokes = (data: Segment[], width: number, precision: number) => {
// We need to get the points perpendicular to a startPoint, given an angle, radius, and precision
const getPerpSamples = (angle, radius, precision, startPoint) => {
const getPerpSamples = (
angle: number,
radius: number,
precision: number,
startPoint: { x: number; y: number }
) => {
const p0 = new Sample({
...startPoint,
x: Math.sin(angle) * radius + startPoint.x,
Expand Down Expand Up @@ -124,7 +139,7 @@ const outlineStrokes = (data, width, precision) => {
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
// We only need the p0 perpendecular 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) {
segmentSamples.push(...p0Perps);
Expand Down Expand Up @@ -152,15 +167,23 @@ const outlineStrokes = (data, width, precision) => {
// 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) => {
const averageSegmentJoins = (
outlinedData: Segment[],
precision: number,
pathClosed: boolean
) => {
// Find the average x and y between two points (p0 and p1)
const avg = (p0, p1) => ({
const avg = (p0: { x: number; y: number }, p1: { x: number; y: number }) => ({
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) => ({
const combine = (
segment: any[],
pos: number,
avg: { x: number; y: number }
) => ({
...segment[pos],
x: avg.x,
y: avg.y
Expand Down
Loading