Skip to content

Commit

Permalink
adding UI controls for the plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
stone-skipper committed Feb 2, 2025
1 parent f1c281a commit 3724bc0
Show file tree
Hide file tree
Showing 8 changed files with 462 additions and 26 deletions.
3 changes: 3 additions & 0 deletions packages/shadergradient-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"@chialab/esbuild-plugin-commonjs": "^0.18.0",
"@react-spring/three": "^9.7.3",
"@react-three/fiber": "^8.17.10",
"@uiw/color-convert": "^1.1.1",
"@uiw/react-color-shade-slider": "^1.1.1",
"@uiw/react-color-wheel": "^1.1.1",
"@types/socket.io": "^3.0.2",
"camera-controls": "2.9.0",
"concurrently": "^9.0.0",
Expand Down
193 changes: 193 additions & 0 deletions packages/shadergradient-v2/src/ShaderGradientUI/ColorInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import * as React from 'react'
import { hexToHsva, hsvaToHex } from '@uiw/color-convert'
import ShadeSlider from '@uiw/react-color-shade-slider'
import Wheel from '@uiw/react-color-wheel'
import { useOnClickOutside } from '@/utils/hooks/useOnClickOutside'
import './slider.css'

type ColorInputPropsT = {
defaultValue: number
setValue: any
} & React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>

export function ColorInput({
defaultValue,
setValue,
}: ColorInputPropsT): JSX.Element {
const [sharedValue, setSharedValue] = React.useState<any>(defaultValue)
const [isClicked, setIsClicked] = React.useState<boolean>(false)
const colorPickerRef = React.useRef<HTMLDivElement>(null)
const triggerRef = React.useRef<HTMLDivElement>(null)

// React.useEffect(() => {
// setSharedValue(defaultValue) // init once with the passed value (from search params)
// }, [])

// React.useEffect(() => {
// setValue(sharedValue)
// }, [sharedValue])

React.useEffect(() => {
setSharedValue(defaultValue) // init once with the passed value (from search params)
}, [])

React.useEffect(() => {
setValue(sharedValue)
}, [sharedValue])

React.useEffect(() => {
setSharedValue(defaultValue)
}, [defaultValue])

React.useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (!entry.isIntersecting && isClicked) {
setIsClicked(false)
}
},
{ threshold: 0.5 } // Trigger when any part of the element is not visible
)

if (triggerRef.current) {
observer.observe(triggerRef.current)
}

return () => {
if (triggerRef.current) {
observer.unobserve(triggerRef.current)
}
}
}, [isClicked])

const updateColorWheelPosition = React.useCallback(() => {
if (isClicked && colorPickerRef.current && triggerRef.current) {
const triggerRect = triggerRef.current.getBoundingClientRect()
const colorWheelRect = colorPickerRef.current.getBoundingClientRect()

// Center horizontally relative to trigger
const left =
triggerRect.left + triggerRect.width / 2 - colorWheelRect.width / 2
// Position above trigger with 20px gap
const top = triggerRect.top - colorWheelRect.height - 5

colorPickerRef.current.style.left = `${left}px`
colorPickerRef.current.style.top = `${top}px`
}
}, [isClicked])

useOnClickOutside(colorPickerRef, () => setIsClicked(false))

React.useEffect(() => {
updateColorWheelPosition()

// Add scroll event listener to update position
const handleScroll = () => {
updateColorWheelPosition()
}

if (isClicked) {
window.addEventListener('scroll', handleScroll, true) // true for capture phase
}

return () => {
window.removeEventListener('scroll', handleScroll, true)
}
}, [isClicked, updateColorWheelPosition])

return (
<div className='flex items-center w-full h-full flex-row gap-2'>
<div className='flex items-center gap-2 w-full relative h-full'>
<div
ref={triggerRef}
className='w-full h-[26px] rounded-md cursor-pointer'
style={{
background: sharedValue,
border:
sharedValue === '#ffffff'
? '1px solid #F2F2F2'
: '0px solid transparent',
}}
onClick={() => {
setIsClicked(!isClicked)
}}
></div>

{/* color control */}
<div
ref={colorPickerRef}
id='colorwheel'
style={{
width: 'fit-content',
height: 'fit-content',
position: 'fixed',
zIndex: 100,
display: isClicked === true ? 'block' : 'none',
}}
>
<div
style={{
display: 'flex',
width: 'fit-content',
height: 'fit-content',
background: 'white',
padding: 16,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: 16,
borderRadius: 5,
filter: 'drop-shadow(0px 0px 10px rgba(0,0,0,0.10))',
}}
>
<Wheel
color={sharedValue}
onChange={(color) => {
setSharedValue(color.hex)
}}
width={200}
height={200}
></Wheel>
<ShadeSlider
width={200}
radius={4}
style={{ display: 'flex', alignItems: 'center' }}
hsva={hexToHsva(sharedValue)}
onChange={(color) => {
setSharedValue(
hsvaToHex({
h: hexToHsva(sharedValue).h,
// @ts-ignore
s: color.s,
v: color.v,
a: 1,
})
)
}}
/>
<div
style={{
width: 16,
height: 16,
background: 'white',
position: 'absolute',
borderRadius: 3,
bottom: -5,
transform: 'rotate(45deg)',
}}
></div>
</div>
</div>
</div>
<input
type='text'
value={sharedValue}
onChange={(e) => setSharedValue(e.target.value)}
className='w-[84px] h-[26px] outline-none text-center bg-[#F2F2F2] rounded-md flex items-center justify-center'
/>
</div>
)
}
119 changes: 119 additions & 0 deletions packages/shadergradient-v2/src/ShaderGradientUI/RangeSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import ReactSlider from 'react-slider'
import { useState, useEffect } from 'react'
import './slider.css'

type RangeSliderPropsT = {
title: string
defaultValue: [number, number]
value: [number, number]
setValue: (value: [number, number]) => void
step: number
min: number
max: number
}

export function RangeSlider({
title,
defaultValue,
setValue,
step,
min,
max,
}: RangeSliderPropsT): JSX.Element {
const [rangeValue, setRangeValue] = useState<[number, number]>(defaultValue)
const [isMouseOver, setIsMouseOver] = useState(false)

useEffect(() => {
setRangeValue(defaultValue)
}, [defaultValue])

useEffect(() => {
setValue(rangeValue)
}, [rangeValue])

return (
<div
className='flex items-center w-full h-[26px] flex-row gap-2'
style={{ fontFamily: 'Inter Medium' }}
>
<div className='w-[100px] flex-shrink-0 flex items-center'>
<p className='font-medium whitespace-nowrap'>{title}</p>
</div>
<div
className='flex items-center w-full h-fit flex-row gap-2'
onMouseOver={() => setIsMouseOver(true)}
onMouseLeave={() => setIsMouseOver(false)}
>
<input
type='number'
value={rangeValue[0]}
onChange={(e) => {
setRangeValue([Number(e.target.value), rangeValue[1]])
}}
min={0}
className={
'font-medium w-[42px] h-[26px] outline-none text-center bg-[#F2F2F2] rounded-md flex items-center justify-center [&::-webkit-inner-spin-button]:appearance-none ' +
(isMouseOver === true ? 'text-[#ff340a]' : 'text-[#000000]')
}
step={step}
/>
<ReactSlider
value={rangeValue}
step={step}
min={min}
max={max}
onChange={(values) => {
setRangeValue(values as [number, number])
}}
className={
'w-full rounded-md bg-[#F2F2F2] cursor-ew-resize overflow-hidden transition-height duration-300 ' +
(isMouseOver === true ? 'h-[26px]' : 'h-[5px]')
}
trackClassName={
'h-full duration-300 ' +
(isMouseOver === true ? 'bg-[#ff340a]' : 'bg-[#ABABAB]')
}
renderTrack={(props, state) => (
<div
{...props}
className={
'h-full flex relative ' +
(isMouseOver === true ? 'bg-[#ff340a]' : 'bg-[#ABABAB]')
}
style={{
...props.style,
opacity: state.index === 1 ? 1 : 0,
}}
/>
)}
renderThumb={(props, state) => (
<div
{...props}
className='w-[8px] h-full justify-center items-center flex'
>
<div
className={
'absolute w-[2px] bg-[#ffffff] rounded-full pointer-events-none duration-200 h-[30%] ' +
(isMouseOver === true ? 'opacity-100' : 'opacity-0')
}
/>
</div>
)}
/>
<input
type='number'
value={rangeValue[1]}
onChange={(e) => {
setRangeValue([rangeValue[0], Number(e.target.value)])
}}
className={
'font-medium w-[42px] h-[26px] outline-none text-center bg-[#F2F2F2] rounded-md flex items-center justify-center [&::-webkit-inner-spin-button]:appearance-none ' +
(isMouseOver === true ? 'text-[#ff340a]' : 'text-[#000000]')
}
step={step}
max={max}
/>
</div>
</div>
)
}
Loading

0 comments on commit 3724bc0

Please sign in to comment.