Skip to content

Commit e707154

Browse files
committed
Merge remote-tracking branch 'origin/new-plugin-controls'
2 parents a0b9318 + 3724bc0 commit e707154

File tree

9 files changed

+13020
-10041
lines changed

9 files changed

+13020
-10041
lines changed

packages/shadergradient-v2/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
"@chialab/esbuild-plugin-commonjs": "^0.18.0",
3636
"@react-spring/three": "^9.7.3",
3737
"@react-three/fiber": "^8.17.10",
38+
"@uiw/color-convert": "^1.1.1",
39+
"@uiw/react-color-shade-slider": "^1.1.1",
40+
"@uiw/react-color-wheel": "^1.1.1",
3841
"@types/socket.io": "^3.0.2",
3942
"camera-controls": "2.9.0",
4043
"concurrently": "^9.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import * as React from 'react'
2+
import { hexToHsva, hsvaToHex } from '@uiw/color-convert'
3+
import ShadeSlider from '@uiw/react-color-shade-slider'
4+
import Wheel from '@uiw/react-color-wheel'
5+
import { useOnClickOutside } from '@/utils/hooks/useOnClickOutside'
6+
import './slider.css'
7+
8+
type ColorInputPropsT = {
9+
defaultValue: number
10+
setValue: any
11+
} & React.DetailedHTMLProps<
12+
React.InputHTMLAttributes<HTMLInputElement>,
13+
HTMLInputElement
14+
>
15+
16+
export function ColorInput({
17+
defaultValue,
18+
setValue,
19+
}: ColorInputPropsT): JSX.Element {
20+
const [sharedValue, setSharedValue] = React.useState<any>(defaultValue)
21+
const [isClicked, setIsClicked] = React.useState<boolean>(false)
22+
const colorPickerRef = React.useRef<HTMLDivElement>(null)
23+
const triggerRef = React.useRef<HTMLDivElement>(null)
24+
25+
// React.useEffect(() => {
26+
// setSharedValue(defaultValue) // init once with the passed value (from search params)
27+
// }, [])
28+
29+
// React.useEffect(() => {
30+
// setValue(sharedValue)
31+
// }, [sharedValue])
32+
33+
React.useEffect(() => {
34+
setSharedValue(defaultValue) // init once with the passed value (from search params)
35+
}, [])
36+
37+
React.useEffect(() => {
38+
setValue(sharedValue)
39+
}, [sharedValue])
40+
41+
React.useEffect(() => {
42+
setSharedValue(defaultValue)
43+
}, [defaultValue])
44+
45+
React.useEffect(() => {
46+
const observer = new IntersectionObserver(
47+
([entry]) => {
48+
if (!entry.isIntersecting && isClicked) {
49+
setIsClicked(false)
50+
}
51+
},
52+
{ threshold: 0.5 } // Trigger when any part of the element is not visible
53+
)
54+
55+
if (triggerRef.current) {
56+
observer.observe(triggerRef.current)
57+
}
58+
59+
return () => {
60+
if (triggerRef.current) {
61+
observer.unobserve(triggerRef.current)
62+
}
63+
}
64+
}, [isClicked])
65+
66+
const updateColorWheelPosition = React.useCallback(() => {
67+
if (isClicked && colorPickerRef.current && triggerRef.current) {
68+
const triggerRect = triggerRef.current.getBoundingClientRect()
69+
const colorWheelRect = colorPickerRef.current.getBoundingClientRect()
70+
71+
// Center horizontally relative to trigger
72+
const left =
73+
triggerRect.left + triggerRect.width / 2 - colorWheelRect.width / 2
74+
// Position above trigger with 20px gap
75+
const top = triggerRect.top - colorWheelRect.height - 5
76+
77+
colorPickerRef.current.style.left = `${left}px`
78+
colorPickerRef.current.style.top = `${top}px`
79+
}
80+
}, [isClicked])
81+
82+
useOnClickOutside(colorPickerRef, () => setIsClicked(false))
83+
84+
React.useEffect(() => {
85+
updateColorWheelPosition()
86+
87+
// Add scroll event listener to update position
88+
const handleScroll = () => {
89+
updateColorWheelPosition()
90+
}
91+
92+
if (isClicked) {
93+
window.addEventListener('scroll', handleScroll, true) // true for capture phase
94+
}
95+
96+
return () => {
97+
window.removeEventListener('scroll', handleScroll, true)
98+
}
99+
}, [isClicked, updateColorWheelPosition])
100+
101+
return (
102+
<div className='flex items-center w-full h-full flex-row gap-2'>
103+
<div className='flex items-center gap-2 w-full relative h-full'>
104+
<div
105+
ref={triggerRef}
106+
className='w-full h-[26px] rounded-md cursor-pointer'
107+
style={{
108+
background: sharedValue,
109+
border:
110+
sharedValue === '#ffffff'
111+
? '1px solid #F2F2F2'
112+
: '0px solid transparent',
113+
}}
114+
onClick={() => {
115+
setIsClicked(!isClicked)
116+
}}
117+
></div>
118+
119+
{/* color control */}
120+
<div
121+
ref={colorPickerRef}
122+
id='colorwheel'
123+
style={{
124+
width: 'fit-content',
125+
height: 'fit-content',
126+
position: 'fixed',
127+
zIndex: 100,
128+
display: isClicked === true ? 'block' : 'none',
129+
}}
130+
>
131+
<div
132+
style={{
133+
display: 'flex',
134+
width: 'fit-content',
135+
height: 'fit-content',
136+
background: 'white',
137+
padding: 16,
138+
flexDirection: 'column',
139+
justifyContent: 'center',
140+
alignItems: 'center',
141+
gap: 16,
142+
borderRadius: 5,
143+
filter: 'drop-shadow(0px 0px 10px rgba(0,0,0,0.10))',
144+
}}
145+
>
146+
<Wheel
147+
color={sharedValue}
148+
onChange={(color) => {
149+
setSharedValue(color.hex)
150+
}}
151+
width={200}
152+
height={200}
153+
></Wheel>
154+
<ShadeSlider
155+
width={200}
156+
radius={4}
157+
style={{ display: 'flex', alignItems: 'center' }}
158+
hsva={hexToHsva(sharedValue)}
159+
onChange={(color) => {
160+
setSharedValue(
161+
hsvaToHex({
162+
h: hexToHsva(sharedValue).h,
163+
// @ts-ignore
164+
s: color.s,
165+
v: color.v,
166+
a: 1,
167+
})
168+
)
169+
}}
170+
/>
171+
<div
172+
style={{
173+
width: 16,
174+
height: 16,
175+
background: 'white',
176+
position: 'absolute',
177+
borderRadius: 3,
178+
bottom: -5,
179+
transform: 'rotate(45deg)',
180+
}}
181+
></div>
182+
</div>
183+
</div>
184+
</div>
185+
<input
186+
type='text'
187+
value={sharedValue}
188+
onChange={(e) => setSharedValue(e.target.value)}
189+
className='w-[84px] h-[26px] outline-none text-center bg-[#F2F2F2] rounded-md flex items-center justify-center'
190+
/>
191+
</div>
192+
)
193+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import ReactSlider from 'react-slider'
2+
import { useState, useEffect } from 'react'
3+
import './slider.css'
4+
5+
type RangeSliderPropsT = {
6+
title: string
7+
defaultValue: [number, number]
8+
value: [number, number]
9+
setValue: (value: [number, number]) => void
10+
step: number
11+
min: number
12+
max: number
13+
}
14+
15+
export function RangeSlider({
16+
title,
17+
defaultValue,
18+
setValue,
19+
step,
20+
min,
21+
max,
22+
}: RangeSliderPropsT): JSX.Element {
23+
const [rangeValue, setRangeValue] = useState<[number, number]>(defaultValue)
24+
const [isMouseOver, setIsMouseOver] = useState(false)
25+
26+
useEffect(() => {
27+
setRangeValue(defaultValue)
28+
}, [defaultValue])
29+
30+
useEffect(() => {
31+
setValue(rangeValue)
32+
}, [rangeValue])
33+
34+
return (
35+
<div
36+
className='flex items-center w-full h-[26px] flex-row gap-2'
37+
style={{ fontFamily: 'Inter Medium' }}
38+
>
39+
<div className='w-[100px] flex-shrink-0 flex items-center'>
40+
<p className='font-medium whitespace-nowrap'>{title}</p>
41+
</div>
42+
<div
43+
className='flex items-center w-full h-fit flex-row gap-2'
44+
onMouseOver={() => setIsMouseOver(true)}
45+
onMouseLeave={() => setIsMouseOver(false)}
46+
>
47+
<input
48+
type='number'
49+
value={rangeValue[0]}
50+
onChange={(e) => {
51+
setRangeValue([Number(e.target.value), rangeValue[1]])
52+
}}
53+
min={0}
54+
className={
55+
'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 ' +
56+
(isMouseOver === true ? 'text-[#ff340a]' : 'text-[#000000]')
57+
}
58+
step={step}
59+
/>
60+
<ReactSlider
61+
value={rangeValue}
62+
step={step}
63+
min={min}
64+
max={max}
65+
onChange={(values) => {
66+
setRangeValue(values as [number, number])
67+
}}
68+
className={
69+
'w-full rounded-md bg-[#F2F2F2] cursor-ew-resize overflow-hidden transition-height duration-300 ' +
70+
(isMouseOver === true ? 'h-[26px]' : 'h-[5px]')
71+
}
72+
trackClassName={
73+
'h-full duration-300 ' +
74+
(isMouseOver === true ? 'bg-[#ff340a]' : 'bg-[#ABABAB]')
75+
}
76+
renderTrack={(props, state) => (
77+
<div
78+
{...props}
79+
className={
80+
'h-full flex relative ' +
81+
(isMouseOver === true ? 'bg-[#ff340a]' : 'bg-[#ABABAB]')
82+
}
83+
style={{
84+
...props.style,
85+
opacity: state.index === 1 ? 1 : 0,
86+
}}
87+
/>
88+
)}
89+
renderThumb={(props, state) => (
90+
<div
91+
{...props}
92+
className='w-[8px] h-full justify-center items-center flex'
93+
>
94+
<div
95+
className={
96+
'absolute w-[2px] bg-[#ffffff] rounded-full pointer-events-none duration-200 h-[30%] ' +
97+
(isMouseOver === true ? 'opacity-100' : 'opacity-0')
98+
}
99+
/>
100+
</div>
101+
)}
102+
/>
103+
<input
104+
type='number'
105+
value={rangeValue[1]}
106+
onChange={(e) => {
107+
setRangeValue([rangeValue[0], Number(e.target.value)])
108+
}}
109+
className={
110+
'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 ' +
111+
(isMouseOver === true ? 'text-[#ff340a]' : 'text-[#000000]')
112+
}
113+
step={step}
114+
max={max}
115+
/>
116+
</div>
117+
</div>
118+
)
119+
}

0 commit comments

Comments
 (0)