feat: feature/rainbow-component Added Gradient Path locally and started work on rainbow effect wrapper component
This commit is contained in:
parent
49e6152c28
commit
302302fa8f
@ -1,6 +1,7 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { Box, Center } from "@chakra-ui/react"
|
import { Box, Center } from "@chakra-ui/react"
|
||||||
import { GradientPath } from "gradient-path"
|
|
||||||
|
import { GradientPath } from "~/gp/GradientPath"
|
||||||
|
|
||||||
export const Rainbow = (props: any): JSX.Element => {
|
export const Rainbow = (props: any): JSX.Element => {
|
||||||
const rainbow = ["#f34a4a", "#ffbc48", "#58ca70", "#47b5e6", "#a555e8"]
|
const rainbow = ["#f34a4a", "#ffbc48", "#58ca70", "#47b5e6", "#a555e8"]
|
||||||
@ -34,13 +35,49 @@ export const Rainbow = (props: any): JSX.Element => {
|
|||||||
|
|
||||||
const svgRef = React.useRef(null)
|
const svgRef = React.useRef(null)
|
||||||
|
|
||||||
|
const colourKeyframes = React.useMemo(() => {
|
||||||
|
const ret = {}
|
||||||
|
|
||||||
|
repeatedColours.map((colour, i) => {
|
||||||
|
const keyframe = `${(i / (repeatedColours.length - 1)) * 100}%`
|
||||||
|
ret[keyframe] = {
|
||||||
|
fill: colour,
|
||||||
|
stroke: colour,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}, [repeatedColours])
|
||||||
|
|
||||||
|
const colourClasses = React.useMemo(() => {
|
||||||
|
const ret = {}
|
||||||
|
|
||||||
|
const steps = 500
|
||||||
|
|
||||||
|
repeatedColours.forEach((color, i) => {
|
||||||
|
for (let j = 0; j < steps; j++) {
|
||||||
|
const nth = `:nth-child(${repeatedColours?.length * j}n+${i + j})`
|
||||||
|
ret[".path-segment" + nth] = {
|
||||||
|
// animation: `rainbow-psych 2s linear infinite`,
|
||||||
|
// "animation-delay": `-${(i + j) * 0.05}s`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}, [repeatedColours])
|
||||||
|
|
||||||
const svg = React.useMemo(() => {
|
const svg = React.useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
"svg g:nth-child(2)": {
|
rainbowPsych: {
|
||||||
display: "none",
|
rainbowPsych: "rainbowPsych",
|
||||||
},
|
},
|
||||||
|
"@keyframes rainbow-psych": {
|
||||||
|
...colourKeyframes,
|
||||||
|
},
|
||||||
|
...colourClasses,
|
||||||
}}
|
}}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
@ -51,9 +88,17 @@ export const Rainbow = (props: any): JSX.Element => {
|
|||||||
viewBox={viewBox}
|
viewBox={viewBox}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
preserveAspectRatio="none"
|
// preserveAspectRatio="none"
|
||||||
>
|
>
|
||||||
<rect x={0} y={0} width={100} height={100} rx={10} ry={10}></rect>
|
<rect
|
||||||
|
// stroke="url(#linear-gradient)"
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
rx={10}
|
||||||
|
ry={10}
|
||||||
|
></rect>
|
||||||
{/* <path
|
{/* <path
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="blue"
|
stroke="blue"
|
||||||
@ -64,7 +109,16 @@ export const Rainbow = (props: any): JSX.Element => {
|
|||||||
</svg>
|
</svg>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}, [pathString, strokeWidth, extraStroke, viewBox])
|
}, [
|
||||||
|
pathString,
|
||||||
|
strokeWidth,
|
||||||
|
extraStroke,
|
||||||
|
viewBox,
|
||||||
|
repeatedColours,
|
||||||
|
colors,
|
||||||
|
colourKeyframes,
|
||||||
|
colourClasses,
|
||||||
|
])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const path = svgRef.current.querySelector("rect")
|
const path = svgRef.current.querySelector("rect")
|
||||||
@ -72,9 +126,9 @@ export const Rainbow = (props: any): JSX.Element => {
|
|||||||
if (path) {
|
if (path) {
|
||||||
const gp = new GradientPath({
|
const gp = new GradientPath({
|
||||||
path,
|
path,
|
||||||
segments: props?.segments || 2000,
|
segments: props?.segments || 250,
|
||||||
samples: props?.samples || 10,
|
samples: props?.samples || 5,
|
||||||
precision: props?.precision || 10,
|
precision: props?.precision || 5,
|
||||||
})
|
})
|
||||||
|
|
||||||
const colors = repeatedColours?.map((color, idx) => {
|
const colors = repeatedColours?.map((color, idx) => {
|
||||||
@ -87,46 +141,18 @@ export const Rainbow = (props: any): JSX.Element => {
|
|||||||
gp.render({
|
gp.render({
|
||||||
type: "path",
|
type: "path",
|
||||||
width: 10,
|
width: 10,
|
||||||
fill: colors,
|
fill: ["orange", "blue", "orange"],
|
||||||
strokeWidth: 1,
|
// fill: colors,
|
||||||
stroke: colors,
|
strokeWidth: 0.5,
|
||||||
|
stroke: ["orange", "blue", "orange"],
|
||||||
|
// stroke: colors,
|
||||||
})
|
})
|
||||||
|
|
||||||
let prevColours = colors
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
// pop first colour and append new first colour to end
|
|
||||||
const prevColoursClone = prevColours.map((colour) => {
|
|
||||||
return {
|
|
||||||
...colour,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const newColoursStart = prevColoursClone.slice(1)
|
|
||||||
const newColours = [...newColoursStart, { ...newColoursStart[0] }]
|
|
||||||
|
|
||||||
const adjustedPosition = newColours.map((colour, idx) => {
|
|
||||||
return {
|
|
||||||
...colour,
|
|
||||||
pos: idx / (newColours.length - 1),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
prevColours = adjustedPosition
|
|
||||||
|
|
||||||
gp.render({
|
|
||||||
type: "path",
|
|
||||||
width: 10,
|
|
||||||
fill: adjustedPosition,
|
|
||||||
strokeWidth: 1,
|
|
||||||
stroke: adjustedPosition,
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval)
|
// clearInterval(interval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [props, repeatedColours])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Center
|
<Center
|
||||||
|
94
remix/app/gp/GradientPath.js
Normal file
94
remix/app/gp/GradientPath.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { DEFAULT_PRECISION } from "./_constants"
|
||||||
|
import { getData, strokeToFill } from "./_data"
|
||||||
|
import { convertPathToNode, segmentToD, styleAttrs, svgElem } from "./_utils"
|
||||||
|
|
||||||
|
export const GradientPath = class {
|
||||||
|
constructor({ path, segments, samples, precision = DEFAULT_PRECISION }) {
|
||||||
|
// If the path being passed isn't a DOM node already, make it one
|
||||||
|
this.path = convertPathToNode(path)
|
||||||
|
|
||||||
|
this.segments = segments
|
||||||
|
this.samples = samples
|
||||||
|
this.precision = precision
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the data
|
||||||
|
this.data = getData({ path, segments, samples, 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(this.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
render({ type, stroke, strokeWidth, fill, width }) {
|
||||||
|
// Store information from this render cycle
|
||||||
|
const renderCycle = {}
|
||||||
|
|
||||||
|
// Create a group for each element
|
||||||
|
const elemGroup = svgElem("g", { class: `element-${type}` })
|
||||||
|
|
||||||
|
this.group.appendChild(elemGroup)
|
||||||
|
renderCycle.group = elemGroup
|
||||||
|
|
||||||
|
if (type === "path") {
|
||||||
|
// If we specify a width and fill, then we need to outline the path and then average the join points of the segments
|
||||||
|
// If we do not specify a width and fill, then we will be stroking and can leave the data "as is"
|
||||||
|
renderCycle.data =
|
||||||
|
width && fill
|
||||||
|
? strokeToFill(this.data, width, this.precision, this.pathClosed)
|
||||||
|
: this.data
|
||||||
|
|
||||||
|
for (let j = 0; j < renderCycle.data.length; j++) {
|
||||||
|
const { samples, progress } = renderCycle.data[j]
|
||||||
|
|
||||||
|
// Create a path for each segment and append it to its elemGroup
|
||||||
|
elemGroup.appendChild(
|
||||||
|
svgElem("path", {
|
||||||
|
class: "path-segment",
|
||||||
|
d: segmentToD(samples),
|
||||||
|
...styleAttrs(fill, stroke, strokeWidth, progress),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (type === "circle") {
|
||||||
|
renderCycle.data = this.data.flatMap(({ samples }) => samples)
|
||||||
|
|
||||||
|
for (let j = 0; j < renderCycle.data.length; j++) {
|
||||||
|
const { x, y, progress } = renderCycle.data[j]
|
||||||
|
|
||||||
|
// Create a circle for each sample and append it to its elemGroup
|
||||||
|
elemGroup.appendChild(
|
||||||
|
svgElem("circle", {
|
||||||
|
class: "circle-sample",
|
||||||
|
cx: x,
|
||||||
|
cy: y,
|
||||||
|
r: width / 2,
|
||||||
|
...styleAttrs(fill, stroke, strokeWidth, progress),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
remix/app/gp/Sample.js
Normal file
8
remix/app/gp/Sample.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default class Sample {
|
||||||
|
constructor({ x, y, progress, segment }) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.progress = progress;
|
||||||
|
this.segment = segment;
|
||||||
|
}
|
||||||
|
}
|
8
remix/app/gp/Segment.js
Normal file
8
remix/app/gp/Segment.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { getMiddleSample } from './_utils';
|
||||||
|
|
||||||
|
export default class Segment {
|
||||||
|
constructor({ samples }) {
|
||||||
|
this.samples = samples;
|
||||||
|
this.progress = getMiddleSample(samples).progress;
|
||||||
|
}
|
||||||
|
}
|
1
remix/app/gp/_constants.js
Normal file
1
remix/app/gp/_constants.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const DEFAULT_PRECISION = 2;
|
219
remix/app/gp/_data.js
Normal file
219
remix/app/gp/_data.js
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import Sample from './Sample';
|
||||||
|
import Segment from './Segment';
|
||||||
|
import { convertPathToNode } from './_utils';
|
||||||
|
import { DEFAULT_PRECISION } from './_constants';
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
precision = DEFAULT_PRECISION
|
||||||
|
}) => {
|
||||||
|
// Convert the given path to a DOM node if it isn't already one
|
||||||
|
path = convertPathToNode(path);
|
||||||
|
|
||||||
|
// 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 (samples > 1) samples--;
|
||||||
|
|
||||||
|
// 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 = [],
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new Sample and push it onto the allSamples array
|
||||||
|
allSamples.push(new Sample({ x, y, progress }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
segmentSamples = [];
|
||||||
|
|
||||||
|
// Push all current samples onto segmentSamples
|
||||||
|
for (let samInSeg = 0; samInSeg < samples; samInSeg++) {
|
||||||
|
segmentSamples.push(allSamples[currentStart + samInSeg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the first sample from the next segment onto segmentSamples
|
||||||
|
segmentSamples.push(allSamples[nextStart]);
|
||||||
|
|
||||||
|
// Create a new Segment with the samples from segmentSamples
|
||||||
|
allSegments.push(new Segment({ samples: segmentSamples }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return our group of segments
|
||||||
|
return allSegments;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
|
||||||
|
return averagedSegmentJoins;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
y: -Math.cos(angle) * radius + startPoint.y
|
||||||
|
}),
|
||||||
|
p1 = new Sample({
|
||||||
|
...startPoint,
|
||||||
|
x: -Math.sin(angle) * radius + startPoint.x,
|
||||||
|
y: Math.cos(angle) * radius + startPoint.y
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the user asks to round our x and y values, do so
|
||||||
|
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];
|
||||||
|
};
|
||||||
|
|
||||||
|
// We need to set the radius (half of the width) and have a holding array for outlined Segments
|
||||||
|
const radius = width / 2,
|
||||||
|
outlinedData = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const samples = data[i].samples,
|
||||||
|
segmentSamples = [];
|
||||||
|
|
||||||
|
// 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!
|
||||||
|
if (samples[j + 1] === undefined) break;
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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({
|
||||||
|
samples: [
|
||||||
|
...segmentSamples.filter((s, i) => i % 2 === 0),
|
||||||
|
...segmentSamples.filter((s, i) => i % 2 === 1).reverse()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outlinedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
const init_outlinedData = JSON.parse(JSON.stringify(outlinedData)); //clone initial outlinedData Object
|
||||||
|
|
||||||
|
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
|
||||||
|
nextEnd = nextSamples.length - 1; // The last sample in the next segment's samples
|
||||||
|
|
||||||
|
// Average two sets of outlined samples to create p0Average and p1Average
|
||||||
|
const p0Average = avg(currentSamples[currentMiddle - 1], nextSamples[0]),
|
||||||
|
p1Average = avg(currentSamples[currentMiddle], nextSamples[nextEnd]);
|
||||||
|
|
||||||
|
// If the user asks to round our x and y values, do so
|
||||||
|
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 with new Samples
|
||||||
|
currentSamples[currentMiddle - 1] = new Sample({
|
||||||
|
...combine(currentSamples, currentMiddle - 1, p0Average)
|
||||||
|
});
|
||||||
|
currentSamples[currentMiddle] = new Sample({
|
||||||
|
...combine(currentSamples, currentMiddle, p1Average)
|
||||||
|
});
|
||||||
|
nextSamples[0] = new Sample({
|
||||||
|
...combine(nextSamples, 0, p0Average)
|
||||||
|
});
|
||||||
|
nextSamples[nextEnd] = new Sample({
|
||||||
|
...combine(nextSamples, nextEnd, p1Average)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return outlinedData;
|
||||||
|
};
|
71
remix/app/gp/_utils.js
Normal file
71
remix/app/gp/_utils.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import tinygradient from 'tinygradient';
|
||||||
|
|
||||||
|
// An internal function to help with easily creating SVG elements with an object of attributes
|
||||||
|
export const svgElem = (type, attrs) => {
|
||||||
|
const elem = document.createElementNS('http://www.w3.org/2000/svg', type),
|
||||||
|
attributes = Object.keys(attrs);
|
||||||
|
|
||||||
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
|
const attr = attributes[i];
|
||||||
|
|
||||||
|
elem.setAttribute(attr, attrs[attr]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elem;
|
||||||
|
};
|
||||||
|
|
||||||
|
// An internal function to help with the repetition of adding fill, stroke, and stroke-width attributes
|
||||||
|
export const styleAttrs = (fill, stroke, strokeWidth, progress) => {
|
||||||
|
const determineColor = (type, progress) =>
|
||||||
|
typeof type === 'string' ? type : tinygradient(type).rgbAt(progress);
|
||||||
|
|
||||||
|
const attrs = {};
|
||||||
|
|
||||||
|
if (stroke) {
|
||||||
|
attrs['stroke'] = determineColor(stroke, progress);
|
||||||
|
attrs['stroke-width'] = strokeWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fill) {
|
||||||
|
attrs['fill'] = determineColor(fill, progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// An internal function to convert any array of samples into a "d" attribute to be passed to an SVG path
|
||||||
|
export const segmentToD = samples => {
|
||||||
|
let d = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < samples.length; i++) {
|
||||||
|
const { x, y } = samples[i],
|
||||||
|
prevSample = i === 0 ? null : samples[i - 1];
|
||||||
|
|
||||||
|
if (i === 0 && i !== samples.length - 1) {
|
||||||
|
d += `M${x},${y}`;
|
||||||
|
} else if (x !== prevSample.x && y !== prevSample.y) {
|
||||||
|
d += `L${x},${y}`;
|
||||||
|
} else if (x !== prevSample.x) {
|
||||||
|
d += `H${x}`;
|
||||||
|
} else if (y !== prevSample.y) {
|
||||||
|
d += `V${y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === samples.length - 1) {
|
||||||
|
d += 'Z';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
|
||||||
|
// An internal function for getting the colors of a segment, we need to get middle most sample (sorted by progress along the path)
|
||||||
|
export const getMiddleSample = samples => {
|
||||||
|
const sortedSamples = [...samples].sort((a, b) => a.progress - b.progress);
|
||||||
|
|
||||||
|
return sortedSamples[(sortedSamples.length / 2) | 0];
|
||||||
|
};
|
||||||
|
|
||||||
|
// An internal function for converting any D3 selection or DOM-like element into a DOM node
|
||||||
|
export const convertPathToNode = path =>
|
||||||
|
path instanceof Element || path instanceof HTMLDocument ? path : path.node();
|
2
remix/app/gp/index.js
Normal file
2
remix/app/gp/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as GradientPath } from './GradientPath';
|
||||||
|
export { getData, strokeToFill } from './_data';
|
@ -20,6 +20,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-click-away-listener": "^2.2.3",
|
"react-click-away-listener": "^2.2.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"tinygradient": "^1.1.5",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -41,6 +41,9 @@ dependencies:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
|
tinygradient:
|
||||||
|
specifier: ^1.1.5
|
||||||
|
version: 1.1.5
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.0.0
|
version: 9.0.0
|
||||||
|
Loading…
Reference in New Issue
Block a user