Merge pull request #1 from lopugit/feature/rainbow-component
Feature/rainbow component
This commit is contained in:
commit
4f82ab875d
@ -17,7 +17,14 @@ module.exports = {
|
|||||||
version: "detect",
|
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: [
|
extends: [
|
||||||
"@remix-run/eslint-config",
|
"@remix-run/eslint-config",
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
@ -26,7 +33,10 @@ module.exports = {
|
|||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
],
|
],
|
||||||
rules: {
|
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",
|
"no-async-promise-executor": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"react/display-name": "off",
|
"react/display-name": "off",
|
||||||
@ -35,8 +45,8 @@ module.exports = {
|
|||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"unused-imports/no-unused-imports": "error",
|
// "unused-imports/no-unused-imports": "error",
|
||||||
"unused-imports/no-unused-vars": "error",
|
// "unused-imports/no-unused-vars": "error",
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
"chakra-ui/props-order": "error",
|
"chakra-ui/props-order": "error",
|
||||||
"chakra-ui/props-shorthand": [
|
"chakra-ui/props-shorthand": [
|
||||||
|
@ -20,7 +20,14 @@ try {
|
|||||||
// nothing
|
// nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
const forceable = {
|
const force = {
|
||||||
|
settings: {
|
||||||
|
commanderActive: true,
|
||||||
|
},
|
||||||
|
version: 22,
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVersionData = {
|
||||||
Content: {
|
Content: {
|
||||||
hidden1: "Edit this to your heart's desire.",
|
hidden1: "Edit this to your heart's desire.",
|
||||||
"How?": "Just search for Content and edit the value to whatever you want.",
|
"How?": "Just search for Content and edit the value to whatever you want.",
|
||||||
@ -30,26 +37,25 @@ const forceable = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialThingtime = {
|
const initialValues = {
|
||||||
nav: {},
|
|
||||||
version: 22,
|
|
||||||
...forceable,
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = {
|
|
||||||
settings: {
|
settings: {
|
||||||
showCommander: true,
|
commanderActive: true,
|
||||||
clearCommanderOnToggle: true,
|
clearCommanderOnToggle: true,
|
||||||
clearCommanderContextOnToggle: 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 => {
|
export const ThingtimeProvider = (props: any): JSX.Element => {
|
||||||
const [thingtime, set] = React.useState({
|
const [thingtime, set] = React.useState(smarts.merge(initialValues, force))
|
||||||
...initialThingtime,
|
|
||||||
...userData,
|
|
||||||
})
|
|
||||||
|
|
||||||
const thingtimeRef = React.useRef(thingtime)
|
const thingtimeRef = React.useRef(thingtime)
|
||||||
|
|
||||||
@ -61,15 +67,15 @@ export const ThingtimeProvider = (props: any): JSX.Element => {
|
|||||||
if (thingtimeFromLocalStorage) {
|
if (thingtimeFromLocalStorage) {
|
||||||
const parsed = JSON.parse(thingtimeFromLocalStorage)
|
const parsed = JSON.parse(thingtimeFromLocalStorage)
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
const invalidLocalStorage =
|
const localIsValid =
|
||||||
!parsed.version || parsed.version < initialThingtime.version
|
!parsed.version || parsed.version >= force.version
|
||||||
if (!invalidLocalStorage) {
|
if (localIsValid) {
|
||||||
set(parsed)
|
const newThingtime = smarts.merge(force, parsed)
|
||||||
|
console.log("nik comm newThingtime", newThingtime)
|
||||||
|
set(newThingtime)
|
||||||
} else {
|
} else {
|
||||||
const newThingtime = {
|
const withVersionUpdates = smarts.merge(newVersionData, parsed)
|
||||||
...parsed,
|
const newThingtime = smarts.merge(force, withVersionUpdates)
|
||||||
...initialThingtime,
|
|
||||||
}
|
|
||||||
set(newThingtime)
|
set(newThingtime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,6 +150,7 @@ export const ThingtimeProvider = (props: any): JSX.Element => {
|
|||||||
try {
|
try {
|
||||||
window.setThingtime = setThingtime
|
window.setThingtime = setThingtime
|
||||||
window.thingtime = thingtime
|
window.thingtime = thingtime
|
||||||
|
window.tt = thingtime
|
||||||
} catch {
|
} catch {
|
||||||
// nothing
|
// nothing
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import React from "react"
|
|||||||
import ClickAwayListener from "react-click-away-listener"
|
import ClickAwayListener from "react-click-away-listener"
|
||||||
import { Center, Flex, Input } from "@chakra-ui/react"
|
import { Center, Flex, Input } from "@chakra-ui/react"
|
||||||
|
|
||||||
|
import { Rainbow } from "../Rainbow/Rainbow"
|
||||||
import { Thingtime } from "../Thingtime/Thingtime"
|
import { Thingtime } from "../Thingtime/Thingtime"
|
||||||
import { useThingtime } from "../Thingtime/useThingtime"
|
import { useThingtime } from "../Thingtime/useThingtime"
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ export const Commander = (props) => {
|
|||||||
const inputRef = React.useRef()
|
const inputRef = React.useRef()
|
||||||
|
|
||||||
const [value, setValue] = React.useState("")
|
const [value, setValue] = React.useState("")
|
||||||
|
const [active, setActive] = React.useState(false)
|
||||||
const [contextPath, setContextPath] = React.useState()
|
const [contextPath, setContextPath] = React.useState()
|
||||||
|
|
||||||
const [showContext, setShowContextState] = React.useState(false)
|
const [showContext, setShowContextState] = React.useState(false)
|
||||||
@ -33,23 +34,25 @@ export const Commander = (props) => {
|
|||||||
return ret
|
return ret
|
||||||
}, [contextPath, getThingtime])
|
}, [contextPath, getThingtime])
|
||||||
|
|
||||||
const showCommander = React.useMemo(() => {
|
const commanderActive = React.useMemo(() => {
|
||||||
return thingtime?.settings?.showCommander
|
return thingtime?.settings?.commanderActive
|
||||||
}, [thingtime?.settings?.showCommander])
|
}, [thingtime?.settings?.commanderActive])
|
||||||
|
|
||||||
// showCommander useEffect
|
console.log("nik commanderActive", commanderActive)
|
||||||
|
|
||||||
|
// commanderActive useEffect
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (showCommander) {
|
if (commanderActive) {
|
||||||
inputRef?.current?.focus?.()
|
inputRef?.current?.focus?.()
|
||||||
} else {
|
} else {
|
||||||
if (thingtimeRef?.current?.settings?.clearCommanderOnToggle) {
|
if (thingtimeRef?.current?.settings?.clearCommanderOnToggle) {
|
||||||
setValue("")
|
setValue("")
|
||||||
}
|
}
|
||||||
if (thingtimeRef?.current?.settings?.clearCommanderContextOnToggle) {
|
if (thingtimeRef?.current?.settings?.clearCommanderContextOnToggle) {
|
||||||
setShowContext(false, "showCommander useEffect")
|
setShowContext(false, "commanderActive useEffect")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [showCommander, thingtimeRef, setShowContext])
|
}, [commanderActive, thingtimeRef, setShowContext])
|
||||||
|
|
||||||
const onChange = React.useCallback((e) => {
|
const onChange = React.useCallback((e) => {
|
||||||
setValue(e.target.value)
|
setValue(e.target.value)
|
||||||
@ -165,7 +168,7 @@ export const Commander = (props) => {
|
|||||||
// setShowContext(true, 'Thingtime changes update suggestions')
|
// setShowContext(true, 'Thingtime changes update suggestions')
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
}, [value, thingtime, commandPath, setShowContext])
|
}, [value, thingtime, commandPath])
|
||||||
|
|
||||||
const onEnter = React.useCallback(
|
const onEnter = React.useCallback(
|
||||||
(props) => {
|
(props) => {
|
||||||
@ -202,6 +205,7 @@ export const Commander = (props) => {
|
|||||||
setShowContext,
|
setShowContext,
|
||||||
escapedCommandValue,
|
escapedCommandValue,
|
||||||
setThingtime,
|
setThingtime,
|
||||||
|
getThingtime,
|
||||||
commandIsAction,
|
commandIsAction,
|
||||||
commandPath,
|
commandPath,
|
||||||
commandContainsPath,
|
commandContainsPath,
|
||||||
@ -216,8 +220,8 @@ export const Commander = (props) => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onEnter({ e })
|
onEnter({ e })
|
||||||
// setThingtime(
|
// setThingtime(
|
||||||
// 'settings.showCommander',
|
// 'settings.commanderActive',
|
||||||
// !thingtime?.settings?.showCommander
|
// !thingtime?.settings?.commanderActive
|
||||||
// )
|
// )
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -225,13 +229,18 @@ export const Commander = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const openCommander = React.useCallback(() => {
|
const openCommander = React.useCallback(() => {
|
||||||
setThingtime("settings.showCommander", true)
|
console.log("nik commander opening commander")
|
||||||
|
setThingtime("settings.commanderActive", true)
|
||||||
}, [setThingtime])
|
}, [setThingtime])
|
||||||
|
|
||||||
const closeCommander = React.useCallback(() => {
|
const closeCommander = React.useCallback(() => {
|
||||||
if (thingtime?.settings?.showCommander) {
|
if (thingtime?.settings?.commanderActive) {
|
||||||
setThingtime("settings.showCommander", false)
|
console.log("nik commander closing commander")
|
||||||
|
setThingtime("settings.commanderActive", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.activeElement.blur()
|
||||||
|
|
||||||
if (value !== "") {
|
if (value !== "") {
|
||||||
setValue("")
|
setValue("")
|
||||||
}
|
}
|
||||||
@ -241,15 +250,22 @@ export const Commander = (props) => {
|
|||||||
if (showContext !== false) {
|
if (showContext !== false) {
|
||||||
setShowContext(false)
|
setShowContext(false)
|
||||||
}
|
}
|
||||||
}, [setThingtime, setShowContext, value, contextPath, showContext])
|
}, [
|
||||||
|
setThingtime,
|
||||||
|
setShowContext,
|
||||||
|
value,
|
||||||
|
contextPath,
|
||||||
|
showContext,
|
||||||
|
thingtime?.settings?.commanderActive,
|
||||||
|
])
|
||||||
|
|
||||||
const toggleCommander = React.useCallback(() => {
|
const toggleCommander = React.useCallback(() => {
|
||||||
if (thingtime?.settings?.showCommander) {
|
if (thingtime?.settings?.commanderActive) {
|
||||||
closeCommander()
|
closeCommander()
|
||||||
} else {
|
} else {
|
||||||
openCommander()
|
openCommander()
|
||||||
}
|
}
|
||||||
}, [thingtime?.settings?.showCommander, closeCommander, openCommander])
|
}, [thingtime?.settings?.commanderActive, closeCommander, openCommander])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const keyListener = (e: any) => {
|
const keyListener = (e: any) => {
|
||||||
@ -295,11 +311,13 @@ export const Commander = (props) => {
|
|||||||
return "calc(100vw - 45px)"
|
return "calc(100vw - 45px)"
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const rainbowRepeats = 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClickAwayListener onClickAway={closeCommander}>
|
<ClickAwayListener onClickAway={closeCommander}>
|
||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
// display={['flex', showCommander ? 'flex' : 'none']}
|
// display={['flex', commanderActive ? 'flex' : 'none']}
|
||||||
top={0}
|
top={0}
|
||||||
// zIndex={99999}
|
// zIndex={99999}
|
||||||
// position='fixed'
|
// position='fixed'
|
||||||
@ -368,38 +386,55 @@ export const Commander = (props) => {
|
|||||||
<Thingtime thing={contextValue}></Thingtime>
|
<Thingtime thing={contextValue}></Thingtime>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Center
|
<Rainbow
|
||||||
position="relative"
|
filter="blur(15px)"
|
||||||
overflow="hidden"
|
opacity={commanderActive ? 0.25 : 0}
|
||||||
width={["100%", "400px"]}
|
repeats={rainbowRepeats}
|
||||||
maxWidth={[mobileVW, "100%"]}
|
thickness={8}
|
||||||
height="100%"
|
overflow="visible"
|
||||||
padding="1px"
|
|
||||||
background="grey"
|
|
||||||
borderRadius="6px"
|
|
||||||
pointerEvents="all"
|
|
||||||
outline="none"
|
|
||||||
>
|
>
|
||||||
<Input
|
<Center
|
||||||
// display='none'
|
position="relative"
|
||||||
// opacity={0}
|
overflow="hidden"
|
||||||
ref={inputRef}
|
width={["100%", "400px"]}
|
||||||
sx={{
|
maxWidth={[mobileVW, "100%"]}
|
||||||
"&::placeholder": {
|
|
||||||
color: "greys.dark",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
height="100%"
|
||||||
border="none"
|
padding="1px"
|
||||||
borderRadius="5px"
|
borderRadius="6px"
|
||||||
|
pointerEvents="all"
|
||||||
outline="none"
|
outline="none"
|
||||||
onChange={onChange}
|
>
|
||||||
onKeyDown={onKeyDown}
|
<Rainbow
|
||||||
placeholder={"What's on your mind?"}
|
opacity={commanderActive ? 0.5 : 0}
|
||||||
value={value}
|
position="absolute"
|
||||||
></Input>
|
expand
|
||||||
</Center>
|
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>
|
</Flex>
|
||||||
</ClickAwayListener>
|
</ClickAwayListener>
|
||||||
)
|
)
|
||||||
|
@ -1,14 +1,239 @@
|
|||||||
import React from "react"
|
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 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 [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 (
|
return (
|
||||||
<Box width={20} height={20} background="green">
|
<>
|
||||||
{props?.children}
|
<Center
|
||||||
</Box>
|
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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
105
remix/app/gp/GradientPath.js
Normal file
105
remix/app/gp/GradientPath.js
Normal 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
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;
|
||||||
|
};
|
90
remix/app/gp/_utils.js
Normal file
90
remix/app/gp/_utils.js
Normal 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
2
remix/app/gp/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as GradientPath } from './GradientPath';
|
||||||
|
export { getData, strokeToFill } from './_data';
|
10
remix/app/hooks/useProps.tsx
Normal file
10
remix/app/hooks/useProps.tsx
Normal 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)
|
||||||
|
}
|
25
remix/app/hooks/useTrace.tsx
Normal file
25
remix/app/hooks/useTrace.tsx
Normal 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
|
||||||
|
})
|
||||||
|
}
|
11
remix/app/hooks/useUuid.tsx
Normal file
11
remix/app/hooks/useUuid.tsx
Normal 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
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
import { Box, Flex } from "@chakra-ui/react"
|
import { Box, Flex } from "@chakra-ui/react"
|
||||||
|
|
||||||
import { ProfileDrawer } from "~/components/Nav/ProfileDrawer"
|
import { ProfileDrawer } from "~/components/Nav/ProfileDrawer"
|
||||||
import { Rainbow } from "~/components/Rainbow/Rainbow"
|
|
||||||
import { Splash } from "~/components/Splash/Splash"
|
import { Splash } from "~/components/Splash/Splash"
|
||||||
import { Thingtime } from "~/components/Thingtime/Thingtime"
|
import { Thingtime } from "~/components/Thingtime/Thingtime"
|
||||||
import { ThingtimeDemo } from "~/components/Thingtime/ThingtimeDemo"
|
import { ThingtimeDemo } from "~/components/Thingtime/ThingtimeDemo"
|
||||||
import { useThingtime } from "~/components/Thingtime/useThingtime"
|
import { useThingtime } from "~/components/Thingtime/useThingtime"
|
||||||
|
import { GradientPath } from "~/gp/GradientPath"
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { thingtime } = useThingtime()
|
const { thingtime } = useThingtime()
|
||||||
@ -17,10 +17,7 @@ export default function Index() {
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
maxWidth="100%"
|
maxWidth="100%"
|
||||||
>
|
>
|
||||||
<Box paddingTop={200}></Box>
|
{/* <Box paddingTop={200}></Box> */}
|
||||||
<Rainbow>
|
|
||||||
<Box width="200px" height="20px" background="grey"></Box>
|
|
||||||
</Rainbow>
|
|
||||||
<Splash></Splash>
|
<Splash></Splash>
|
||||||
<Thingtime
|
<Thingtime
|
||||||
marginBottom={200}
|
marginBottom={200}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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