Skip to content

Commit

Permalink
color type param (#479)
Browse files Browse the repository at this point in the history
* updated types and updateSketchParams to handle vectors

* addNode as shared func

* imports fix

* vector value passed to sketch

* organise

* ParamVector3 component

* tweak

* loosen lint rules

* comments

* rgb param type

* type shuffling, better checking

* types cleanup

* project json update

* color picker with story

* better handling of body click

* floating color picker position

* ParamColor implemented

* update content security policy to allow for data: URLs

* picker styling

* behaviour fix for color picker

* example project update

* color picker hue slider fix
  • Loading branch information
funwithtriangles authored Jan 3, 2025
1 parent 2df3b1e commit b0ff160
Show file tree
Hide file tree
Showing 16 changed files with 713 additions and 276 deletions.
4 changes: 3 additions & 1 deletion packages/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@floating-ui/react-dom": "^2.1.2",
"@hedron/engine": "^1.0.0-alpha.1",
"@redux-devtools/extension": "^3.3.0",
"@tomjs/electron-devtools-installer": "^2.3.2",
"@uiw/react-color": "^2.3.4",
"chokidar": "^4.0.0",
"date-fns": "^4.1.0",
"electron-updater": "^6.1.7",
Expand Down Expand Up @@ -66,6 +68,6 @@
"electron-vite": "^2.3.0",
"storybook": "^8.2.9",
"vite": "^5.3.1",
"vite-tsconfig-paths": "^5.0.1"
"vite-tsconfig-paths": "^5.1.4"
}
}
31 changes: 31 additions & 0 deletions packages/desktop/src/renderer/components/ParamColor/ParamColor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NodeParamRGB } from '@hedron/engine'
import { useInterval } from 'usehooks-ts'
import { useRef } from 'react'
import { ColorPicker, ColorPickerHandle } from '@components/core/ColorPicker/ColorPicker'
import { useOnNodeVec3ValueChange } from '@components/hooks/useOnNodeVec3ValueChange'
import { engineStore, useEngineStore } from '@renderer/engine'

interface ParamRGBProps {
id: string
}

export const ParamColor = ({ id }: ParamRGBProps) => {
const ref = useRef<ColorPickerHandle>(null)
const node = useEngineStore((state) => state.nodes[id] as NodeParamRGB)
const { childNodeIds } = node

useInterval(() => {
const state = engineStore.getState()
const nodeValues = node.childNodeIds.map((id) => state.nodeValues[id])

ref.current?.updateColor(nodeValues as [number, number, number])
}, 100)

const onVec3ValueChange = useOnNodeVec3ValueChange(
childNodeIds[0],
childNodeIds[1],
childNodeIds[2],
)

return <ColorPicker ref={ref} onValueChange={onVec3ValueChange} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ParamNumber } from '@components/ParamNumber/ParamNumber'
import { ParamBoolean } from '@components/ParamBoolean/ParamBoolean'
import { ParamEnum } from '@components/ParamEnum/ParamEnum'
import { ParamVector3 } from '@components/ParamVector3/ParamVector3'
import { ParamColor } from '@components/ParamColor/ParamColor'
import {
NodeControl,
NodeControlInner,
Expand All @@ -28,6 +29,8 @@ const getInputElement = (valueType: NodeTypes, id: string) => {
return <ParamEnum id={id} />
case NodeTypes.Vector3:
return <ParamVector3 id={id} />
case NodeTypes.RGB:
return <ParamColor id={id} />
default:
return <i>Unsupported type {valueType}</i>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.container {
position: relative;
width: 100%;
height: 100%;
}

.colorBox {
width: 100%;
height: 100%;
cursor: pointer;
border-radius: 0.5rem;
border: 1px solid var(--lineColor1);
}

.pickerContainer {
z-index: 10;
position: relative;
}

.picker {
border-radius: 8px;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.3);
}

.picker::before {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
border: 2px solid var(--lineColor1);
border-radius: 8px;
pointer-events: none;
z-index: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import {
Colorful,
ColorResult,
hexToHsva,
HsvaColor,
hsvaToHex,
rgbaToHex,
rgbaToHsva,
} from '@uiw/react-color'
import { useFloating, shift, offset } from '@floating-ui/react-dom'
import css from './ColorPicker.module.css'

type RGBColor = [number, number, number]

export type ColorPickerHandle = {
updateColor: (value: RGBColor) => void
}

interface ColorPickerProps {
onValueChange: (value: RGBColor) => void
}

const defaultColor: HsvaColor = hexToHsva('#FFFFFF')

export const ColorPicker = forwardRef<ColorPickerHandle, ColorPickerProps>(function ColorPicker(
{ onValueChange },
ref,
) {
const colorBoxRef = useRef<HTMLDivElement>(null)
const [color, setColor] = useState<HsvaColor>(defaultColor)
const colorRef = useRef<HsvaColor>(defaultColor)
const [isOpen, setIsOpen] = useState(false)
const { refs, floatingStyles } = useFloating({
middleware: [shift({ padding: 10 }), offset({ mainAxis: 10 })],
})

const onBoxClick = useCallback(() => {
setColor(colorRef.current)
setIsOpen((isOpen) => !isOpen)
}, [])

const onPickerClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
}, [])

const onChange = useCallback(
({ rgb: { r, g, b }, hsva }: ColorResult) => {
setColor(hsva)
onValueChange([r / 255, g / 255, b / 255])
},
[onValueChange],
)

useEffect(() => {
const handleClick = (e: MouseEvent) => {
// do not close the picker if the color box is clicked
if (colorBoxRef.current?.contains(e.target as Node)) return
setIsOpen(false)
}
document.body.addEventListener('click', handleClick)
return () => {
document.body.removeEventListener('click', handleClick)
}
}, [])

// Avoiding using state to keep external frequent updates performant
const updateColor = useCallback(([r, g, b]: RGBColor) => {
const rgba = { r: r * 255, g: g * 255, b: b * 255, a: 1 }
const hsva = rgbaToHsva(rgba)
colorBoxRef.current?.style.setProperty('background-color', rgbaToHex(rgba))
colorRef.current = hsva
}, [])

useImperativeHandle(ref, () => ({ updateColor }), [updateColor])

const backgroundColor = useMemo(() => hsvaToHex(color), [color])

return (
<div className={css.container} ref={refs.setReference}>
<div
className={css.colorBox}
style={{ backgroundColor }}
onClick={onBoxClick}
ref={colorBoxRef}
/>
{isOpen && (
<div
className={css.pickerContainer}
ref={refs.setFloating}
style={{ ...floatingStyles }}
onClick={onPickerClick}
>
<Colorful className={css.picker} color={color} onChange={onChange} disableAlpha />
</div>
)}
</div>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useCallback } from 'react'
import { useOnNodeValueChange } from './useOnNodeValueChange'

export const useOnNodeVec3ValueChange = (id1: string, id2: string, id3: string) => {
const onValueChange1 = useOnNodeValueChange(id1)
const onValueChange2 = useOnNodeValueChange(id2)
const onValueChange3 = useOnNodeValueChange(id3)

const onVec3ValueChange = useCallback(
(value: [number, number, number]) => {
onValueChange1(value[0])
onValueChange2(value[1])
onValueChange3(value[2])
},
[onValueChange1, onValueChange2, onValueChange3],
)

return onVec3ValueChange
}
5 changes: 4 additions & 1 deletion packages/desktop/src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline'" />
<meta
http-equiv="Content-Security-Policy"
content="default-src * 'unsafe-inline'; img-src 'self' data:;"
/>
</head>

<body>
Expand Down
69 changes: 54 additions & 15 deletions packages/desktop/src/stories/NodeControl.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ControlGrid } from '@components/core/ControlGrid/ControlGrid'

import { FloatSlider, FloatSliderHandle } from '@components/core/FloatSlider/FloatSlider'
import { BooleanToggle, BooleanToggleHandle } from '@components/core/BooleanToggle/BooleanToggle'
import { ColorPickerHandle, ColorPicker } from '@components/core/ColorPicker/ColorPicker'
import { Panel, PanelBody, PanelHeader } from '@components/core/Panel/Panel'

const meta = {
Expand Down Expand Up @@ -65,29 +66,67 @@ export const Boolean = ({ title = 'Boolean Thing', isActive, onClick }: BasicPro
)
}

export const Color = ({ title = 'Color Picker', isActive, onClick }: BasicProps) => {
const ref = useRef<ColorPickerHandle>(null)

useInterval(() => {
ref.current!.updateColor([Math.random(), Math.random(), Math.random()])
}, 3000)

return (
<NodeControl isActive={isActive} onClick={onClick}>
<NodeControlMain>
<NodeControlTitle>{title}</NodeControlTitle>
<NodeControlInner>
<ColorPicker ref={ref} onValueChange={fn()} />
</NodeControlInner>
</NodeControlMain>
</NodeControl>
)
}

const params = [
'Velocity X',
'Velocity Y',
'Rotation Speed X',
'Rotation Speed Y',
'some long name x',
'some other thing y',
'short',
'another long param',
['Fun Param Name', 'number'],
['Another Param', 'boolean'],
['Color Picker', 'color'],
['Color Picker', 'color'],
['Slider', 'number'],
['Toggle', 'boolean'],
['Color Picker', 'color'],
['Slider', 'number'],
['Slider', 'number'],
['Toggle', 'boolean'],
['Color Picker', 'color'],
]

export const WithControlGrid = () => {
const [activeId, setActiveId] = useState(0)

return (
<ControlGrid>
{params.map((item, i) =>
i % 2 == 0 ? (
<Number key={i} title={item} isActive={activeId === i} onClick={() => setActiveId(i)} />
) : (
<Boolean key={i} title={item} isActive={activeId === i} onClick={() => setActiveId(i)} />
),
)}
{params.map(([title, type], i) => (
<>
{type === 'number' && (
<Number
key={i}
title={title}
isActive={activeId === i}
onClick={() => setActiveId(i)}
/>
)}
{type === 'boolean' && (
<Boolean
key={i}
title={title}
isActive={activeId === i}
onClick={() => setActiveId(i)}
/>
)}
{type === 'color' && (
<Color key={i} title={title} isActive={activeId === i} onClick={() => setActiveId(i)} />
)}
</>
))}
</ControlGrid>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { addNode } from '@store/shared/addNode'
import { NodeTypes, SetterCreator } from '@store/types'
import { hasChildNodes, SetterCreator } from '@store/types'
import { createUniqueId } from '@utils/createUniqueId'

export const createUpdateSketchParams: SetterCreator<'updateSketchParams'> =
Expand Down Expand Up @@ -41,7 +41,7 @@ export const createUpdateSketchParams: SetterCreator<'updateSketchParams'> =
delete state.nodeValues[oldParamId]

// Remove vector child nodes if they exist
if (oldNode.valueType === NodeTypes.Vector3) {
if (hasChildNodes(oldNode)) {
oldNode.childNodeIds.forEach((childNodeId) => {
delete state.nodes[childNodeId]
delete state.nodeValues[childNodeId]
Expand Down
11 changes: 6 additions & 5 deletions packages/engine/src/store/selectors/getSketchParamValues.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EngineState } from '@store/types'
import { EngineState, hasChildNodes } from '@store/types'

// Get the values of the parameters of a sketch, dealing with child nodes
export const getSketchParamValues = (state: EngineState, sketchId: string) => {
Expand All @@ -8,13 +8,14 @@ export const getSketchParamValues = (state: EngineState, sketchId: string) => {
const sketch = sketches[sketchId]

sketch.paramIds.forEach((id) => {
const { key, valueType } = nodes[id]
const node = nodes[id]
const { key } = node

let value

if (valueType === 'vector3') {
// Return an array of values for vector3
const childNodeIds = nodes[id].childNodeIds
if (hasChildNodes(node)) {
// Return an array of values for nodes with child nodes
const childNodeIds = node.childNodeIds
value = childNodeIds.map((childNodeId) => nodeValues[childNodeId])
} else {
value = nodeValues[id]
Expand Down
Loading

0 comments on commit b0ff160

Please sign in to comment.