Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New plugin controls #126

Merged
merged 2 commits into from
Feb 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading