Merge pull request #1 from lopugit/feature/rainbow-component

Feature/rainbow component
This commit is contained in:
Nikolaj 2023-07-05 20:06:25 +10:00 committed by GitHub
commit 4f82ab875d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1385 additions and 620 deletions

View File

@ -17,7 +17,14 @@ module.exports = {
version: "detect",
},
},
plugins: ["@typescript-eslint", "react", "prettier", "unused-imports", "simple-import-sort", "chakra-ui"],
plugins: [
"@typescript-eslint",
"react",
// "unused-imports",
"prettier",
"simple-import-sort",
"chakra-ui",
],
extends: [
"@remix-run/eslint-config",
"eslint:recommended",
@ -26,7 +33,10 @@ module.exports = {
"plugin:@typescript-eslint/recommended",
],
rules: {
"react/jsx-curly-brace-presence": ["error", { props: "never", children: "never" }],
"react/jsx-curly-brace-presence": [
"error",
{ props: "never", children: "never" },
],
"no-async-promise-executor": "off",
"react/prop-types": "off",
"react/display-name": "off",
@ -35,8 +45,8 @@ module.exports = {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": "error",
// "unused-imports/no-unused-imports": "error",
// "unused-imports/no-unused-vars": "error",
"react/react-in-jsx-scope": "off",
"chakra-ui/props-order": "error",
"chakra-ui/props-shorthand": [

View File

@ -20,7 +20,14 @@ try {
// nothing
}
const forceable = {
const force = {
settings: {
commanderActive: true,
},
version: 22,
}
const newVersionData = {
Content: {
hidden1: "Edit this to your heart's desire.",
"How?": "Just search for Content and edit the value to whatever you want.",
@ -30,26 +37,25 @@ const forceable = {
},
}
const initialThingtime = {
nav: {},
version: 22,
...forceable,
}
const userData = {
const initialValues = {
settings: {
showCommander: true,
commanderActive: true,
clearCommanderOnToggle: true,
clearCommanderContextOnToggle: true,
},
...forceable,
Content: {
hidden1: "Edit this to your heart's desire.",
"How?": "Just search for Content and edit the value to whatever you want.",
"Example:": `Content = New Content!
Content.Nested Content = New Nested Content!
`,
},
}
const initialThingtime = smarts.merge(initialValues, force)
export const ThingtimeProvider = (props: any): JSX.Element => {
const [thingtime, set] = React.useState({
...initialThingtime,
...userData,
})
const [thingtime, set] = React.useState(smarts.merge(initialValues, force))
const thingtimeRef = React.useRef(thingtime)
@ -61,15 +67,15 @@ export const ThingtimeProvider = (props: any): JSX.Element => {
if (thingtimeFromLocalStorage) {
const parsed = JSON.parse(thingtimeFromLocalStorage)
if (parsed) {
const invalidLocalStorage =
!parsed.version || parsed.version < initialThingtime.version
if (!invalidLocalStorage) {
set(parsed)
const localIsValid =
!parsed.version || parsed.version >= force.version
if (localIsValid) {
const newThingtime = smarts.merge(force, parsed)
console.log("nik comm newThingtime", newThingtime)
set(newThingtime)
} else {
const newThingtime = {
...parsed,
...initialThingtime,
}
const withVersionUpdates = smarts.merge(newVersionData, parsed)
const newThingtime = smarts.merge(force, withVersionUpdates)
set(newThingtime)
}
}
@ -144,6 +150,7 @@ export const ThingtimeProvider = (props: any): JSX.Element => {
try {
window.setThingtime = setThingtime
window.thingtime = thingtime
window.tt = thingtime
} catch {
// nothing
}

View File

@ -2,6 +2,7 @@ import React from "react"
import ClickAwayListener from "react-click-away-listener"
import { Center, Flex, Input } from "@chakra-ui/react"
import { Rainbow } from "../Rainbow/Rainbow"
import { Thingtime } from "../Thingtime/Thingtime"
import { useThingtime } from "../Thingtime/useThingtime"
@ -14,7 +15,7 @@ export const Commander = (props) => {
const inputRef = React.useRef()
const [value, setValue] = React.useState("")
const [active, setActive] = React.useState(false)
const [contextPath, setContextPath] = React.useState()
const [showContext, setShowContextState] = React.useState(false)
@ -33,23 +34,25 @@ export const Commander = (props) => {
return ret
}, [contextPath, getThingtime])
const showCommander = React.useMemo(() => {
return thingtime?.settings?.showCommander
}, [thingtime?.settings?.showCommander])
const commanderActive = React.useMemo(() => {
return thingtime?.settings?.commanderActive
}, [thingtime?.settings?.commanderActive])
// showCommander useEffect
console.log("nik commanderActive", commanderActive)
// commanderActive useEffect
React.useEffect(() => {
if (showCommander) {
if (commanderActive) {
inputRef?.current?.focus?.()
} else {
if (thingtimeRef?.current?.settings?.clearCommanderOnToggle) {
setValue("")
}
if (thingtimeRef?.current?.settings?.clearCommanderContextOnToggle) {
setShowContext(false, "showCommander useEffect")
setShowContext(false, "commanderActive useEffect")
}
}
}, [showCommander, thingtimeRef, setShowContext])
}, [commanderActive, thingtimeRef, setShowContext])
const onChange = React.useCallback((e) => {
setValue(e.target.value)
@ -165,7 +168,7 @@ export const Commander = (props) => {
// setShowContext(true, 'Thingtime changes update suggestions')
// }
}
}, [value, thingtime, commandPath, setShowContext])
}, [value, thingtime, commandPath])
const onEnter = React.useCallback(
(props) => {
@ -202,6 +205,7 @@ export const Commander = (props) => {
setShowContext,
escapedCommandValue,
setThingtime,
getThingtime,
commandIsAction,
commandPath,
commandContainsPath,
@ -216,8 +220,8 @@ export const Commander = (props) => {
e.stopPropagation()
onEnter({ e })
// setThingtime(
// 'settings.showCommander',
// !thingtime?.settings?.showCommander
// 'settings.commanderActive',
// !thingtime?.settings?.commanderActive
// )
}
},
@ -225,13 +229,18 @@ export const Commander = (props) => {
)
const openCommander = React.useCallback(() => {
setThingtime("settings.showCommander", true)
console.log("nik commander opening commander")
setThingtime("settings.commanderActive", true)
}, [setThingtime])
const closeCommander = React.useCallback(() => {
if (thingtime?.settings?.showCommander) {
setThingtime("settings.showCommander", false)
if (thingtime?.settings?.commanderActive) {
console.log("nik commander closing commander")
setThingtime("settings.commanderActive", false)
}
document.activeElement.blur()
if (value !== "") {
setValue("")
}
@ -241,15 +250,22 @@ export const Commander = (props) => {
if (showContext !== false) {
setShowContext(false)
}
}, [setThingtime, setShowContext, value, contextPath, showContext])
}, [
setThingtime,
setShowContext,
value,
contextPath,
showContext,
thingtime?.settings?.commanderActive,
])
const toggleCommander = React.useCallback(() => {
if (thingtime?.settings?.showCommander) {
if (thingtime?.settings?.commanderActive) {
closeCommander()
} else {
openCommander()
}
}, [thingtime?.settings?.showCommander, closeCommander, openCommander])
}, [thingtime?.settings?.commanderActive, closeCommander, openCommander])
React.useEffect(() => {
const keyListener = (e: any) => {
@ -295,11 +311,13 @@ export const Commander = (props) => {
return "calc(100vw - 45px)"
}, [])
const rainbowRepeats = 1
return (
<ClickAwayListener onClickAway={closeCommander}>
<Flex
position="absolute"
// display={['flex', showCommander ? 'flex' : 'none']}
// display={['flex', commanderActive ? 'flex' : 'none']}
top={0}
// zIndex={99999}
// position='fixed'
@ -368,38 +386,55 @@ export const Commander = (props) => {
<Thingtime thing={contextValue}></Thingtime>
</Flex>
</Flex>
<Center
position="relative"
overflow="hidden"
width={["100%", "400px"]}
maxWidth={[mobileVW, "100%"]}
height="100%"
padding="1px"
background="grey"
borderRadius="6px"
pointerEvents="all"
outline="none"
<Rainbow
filter="blur(15px)"
opacity={commanderActive ? 0.25 : 0}
repeats={rainbowRepeats}
thickness={8}
overflow="visible"
>
<Input
// display='none'
// opacity={0}
ref={inputRef}
sx={{
"&::placeholder": {
color: "greys.dark",
},
}}
width="100%"
<Center
position="relative"
overflow="hidden"
width={["100%", "400px"]}
maxWidth={[mobileVW, "100%"]}
height="100%"
border="none"
borderRadius="5px"
padding="1px"
borderRadius="6px"
pointerEvents="all"
outline="none"
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={"What's on your mind?"}
value={value}
></Input>
</Center>
>
<Rainbow
opacity={commanderActive ? 0.5 : 0}
position="absolute"
expand
repeats={rainbowRepeats}
opacityTransition="all 3000ms ease"
thickness={10}
></Rainbow>
<Input
// display='none'
// opacity={0}
ref={inputRef}
sx={{
"&::placeholder": {
color: "greys.dark",
},
}}
width="100%"
height="100%"
background="grey"
border="none"
borderRadius="5px"
outline="none"
onChange={onChange}
onFocus={openCommander}
onKeyDown={onKeyDown}
placeholder={"What's on your mind?"}
value={value}
></Input>
</Center>
</Rainbow>
</Flex>
</ClickAwayListener>
)

View File

@ -1,14 +1,239 @@
import React from "react"
import { Box } from "@chakra-ui/react"
import { Box, Center } from "@chakra-ui/react"
export const Rainbow = (props: any): JSX.Element => {
import { GradientPath } from "~/gp/GradientPath"
import { useProps } from "~/hooks/useProps"
import { useTrace } from "~/hooks/useTrace"
import { useUuid } from "~/hooks/useUuid"
export const Rainbow = (allProps: any): JSX.Element => {
const rainbow = ["#f34a4a", "#ffbc48", "#58ca70", "#47b5e6", "#a555e8"]
const props = useProps(allProps)
const uuid = useUuid()
const [hidden, setHidden] = React.useState(true)
const repeats = props?.repeats || 1
const [filter, setFilter] = React.useState(props?.filter)
const opacity = props?.opacity !== undefined ? props?.opacity : 1
const [colors, setColors] = React.useState(props?.colors || rainbow)
const [pathWidth, setPathWidth] = React.useState(props?.thickness || 1)
const [overflow, setOverflow] = React.useState(props?.overflow || "hidden")
const [opacityTransition, setOpacityTransition] = React.useState(
props?.opacityTransition || "all 10000ms ease"
)
const parentRef = React.useRef(null)
const repeatedColours = React.useMemo(() => {
const ret = []
for (let i = 0; i < repeats; i++) {
ret.push(...colors)
}
ret.push(colors[0])
return ret
}, [colors, repeats])
// make SVG that takes makes path in the shape of a box
// which adjusts to the parent containers size
const [state, setState] = React.useState({
width: 100,
height: 100,
})
const [strokeWidth, setStrokeWidth] = React.useState(1)
const [extraStroke, setExtraStroke] = React.useState(0)
const [width, setWidth] = React.useState(props?.width || "100%")
const [height, setHeight] = React.useState(props?.height || "100%")
const pathString = React.useMemo(() => {
const startPoint = 0 + strokeWidth / 2
const endPoint = 100 - strokeWidth / 2
return `M -${4} ${startPoint} H ${endPoint} V ${endPoint} H ${startPoint} V ${startPoint}`
}, [strokeWidth])
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])
React.useEffect(() => {
const updateChildSize = () => {
const { width, height } = parentRef?.current?.getBoundingClientRect()
console.log("nik width height", width, height)
setState({ width, height })
}
updateChildSize()
new ResizeObserver(updateChildSize).observe(parentRef?.current)
}, [])
const rect = React.useMemo(() => {
return (
<rect
// stroke="url(#linear-gradient)"
x={0}
y={0}
width={state?.width || 100}
height={state?.height || 100}
rx={10}
ry={10}
></rect>
)
}, [state?.width, state?.height])
const svg = React.useMemo(() => {
const id = Math.random().toString(36).substring(2, 15)
return (
<Box width="100%" height="100%" id={id}>
<svg
overflow="visible"
viewBox={`0 0 ${state?.width || 100} ${state?.height || 100}`}
width="100%"
height="100%"
preserveAspectRatio="none"
>
{rect}
{/* <path
fill="none"
stroke="blue"
strokeAlignment="inner"
strokeWidth={`${strokeWidth + extraStroke}px`}
d={pathString}
></path> */}
</svg>
</Box>
)
}, [state, rect])
React.useEffect(() => {
if (uuid) {
const svg = svgRef?.current?.querySelector("svg")
// path is rect or insert new rect if empty svg
const rectSource = parentRef?.current?.querySelector(".svg-source rect")
const path =
svg?.querySelector?.("rect") ||
svg?.appendChild?.(rectSource?.cloneNode?.())
if (path) {
console.log("nik re-rendering rainbow")
const gp = new GradientPath({
path,
segments: props?.segments || 1000,
samples: props?.samples || 1,
precision: props?.precision || 5,
})
gp.render({
type: "path",
width: pathWidth || 1,
animation: {
name: `rainbow-${uuid}`,
duration: 5,
},
})
setTimeout(() => {
setHidden(false)
}, 500)
return () => {
// setHidden(true)
gp.remove()
}
}
}
}, [
uuid,
props?.segments,
props?.samples,
props?.precision,
pathWidth,
repeatedColours,
parentRef,
svgRef,
rect,
])
const render = true
useTrace("Rainbow", {
props,
})
return (
<Box width={20} height={20} background="green">
{props?.children}
</Box>
<>
<Center
className="Rainbow_n__n"
ref={parentRef}
sx={{
[`@keyframes rainbow-${uuid}`]: {
...colourKeyframes,
},
}}
position={props?.position || "relative"}
overflow={overflow}
width={props?.expand ? "100%" : ""}
height={props?.expand ? "100%" : ""}
>
<Center
className="main-svg-container"
position="absolute"
top={0}
left={0}
overflow="visible"
width="100%"
height="100%"
opacity={hidden ? "0" : opacity}
transition={opacityTransition}
>
{/* debug svg */}
<Box
className="svg-source"
flexShrink={0}
display="none"
overflow="visible"
width={width}
height={height}
>
{svg}
</Box>
<Box
ref={svgRef}
flexShrink={0}
overflow="visible"
width={width}
height={height}
opacity={hidden ? 0 : !render ? "0" : 1}
filter={filter}
>
{svg}
</Box>
</Center>
{allProps?.children}
</Center>
</>
)
}

View File

@ -0,0 +1,105 @@
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)
}
remove() {
this.group.parentNode.removeChild(this.group)
}
render({
type,
stroke = ["white", "black", "white"],
strokeWidth = 1,
fill = ["white", "black", "white"],
width,
animation = {},
}) {
// 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, animation),
})
)
}
} 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, animation),
})
)
}
}
// 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
View 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
View File

@ -0,0 +1,8 @@
import { getMiddleSample } from './_utils';
export default class Segment {
constructor({ samples }) {
this.samples = samples;
this.progress = getMiddleSample(samples).progress;
}
}

View File

@ -0,0 +1 @@
export const DEFAULT_PRECISION = 2;

219
remix/app/gp/_data.js Normal file
View 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;
};

90
remix/app/gp/_utils.js Normal file
View File

@ -0,0 +1,90 @@
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,
animation
) => {
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)
}
if (animation?.name) {
// TODO: add animation-direction support
const duration = animation.duration || 5
attrs["style"] = `
animation-name: ${animation?.name};
animation-delay: ${progress * duration - duration}s;
animation-duration: ${duration}s;
animation-iteration-count: infinite;
animation-timing-function: linear;`
}
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
View File

@ -0,0 +1,2 @@
export { default as GradientPath } from './GradientPath';
export { getData, strokeToFill } from './_data';

View File

@ -0,0 +1,10 @@
import React from "react"
export const useProps = (allProps) => {
const deps = Object.values(allProps).filter((v) => v !== allProps.children)
return React.useMemo(() => {
const { children, ...other } = allProps
return other
}, deps)
}

View File

@ -0,0 +1,25 @@
import React from "react"
export const useTrace = (name, props) => {
const prev = React.useRef(props)
React.useEffect(() => {
const changedProps = Object.entries(props).reduce(
(changedValues, [key, newValue]) => {
window.trace = window.trace || {}
window.trace[key] = newValue
if (prev.current[key] !== newValue) {
changedValues[key] = {
old: prev.current[key],
new: newValue,
}
}
return changedValues
},
{}
)
if (Object.keys(changedProps)?.length) {
console.table({ TT: { name }, ...changedProps })
}
prev.current = props
})
}

View File

@ -0,0 +1,11 @@
import React from "react"
export const useUuid = () => {
const [uuid, setUuid] = React.useState()
React.useEffect(() => {
setUuid(Math.random().toString(36).substring(7))
}, [])
return uuid
}

View File

@ -1,11 +1,11 @@
import { Box, Flex } from "@chakra-ui/react"
import { ProfileDrawer } from "~/components/Nav/ProfileDrawer"
import { Rainbow } from "~/components/Rainbow/Rainbow"
import { Splash } from "~/components/Splash/Splash"
import { Thingtime } from "~/components/Thingtime/Thingtime"
import { ThingtimeDemo } from "~/components/Thingtime/ThingtimeDemo"
import { useThingtime } from "~/components/Thingtime/useThingtime"
import { GradientPath } from "~/gp/GradientPath"
export default function Index() {
const { thingtime } = useThingtime()
@ -17,10 +17,7 @@ export default function Index() {
flexDirection="column"
maxWidth="100%"
>
<Box paddingTop={200}></Box>
<Rainbow>
<Box width="200px" height="20px" background="grey"></Box>
</Rainbow>
{/* <Box paddingTop={200}></Box> */}
<Splash></Splash>
<Thingtime
marginBottom={200}

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@
"react": "^18.2.0",
"react-click-away-listener": "^2.2.3",
"react-dom": "^18.2.0",
"tinygradient": "^1.1.5",
"uuid": "^9.0.0"
},
"devDependencies": {

View File

@ -41,6 +41,9 @@ dependencies:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
tinygradient:
specifier: ^1.1.5
version: 1.1.5
uuid:
specifier: ^9.0.0
version: 9.0.0