Better code quality
This commit is contained in:
118
src/components/Knob.tsx
Normal file
118
src/components/Knob.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { useRef, useState, useEffect } from 'react'
|
||||
|
||||
interface KnobProps {
|
||||
label: string
|
||||
value: number
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
unit?: string
|
||||
onChange: (value: number) => void
|
||||
formatValue?: (id: string, value: number) => string
|
||||
valueId?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
export function Knob({
|
||||
label,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
unit,
|
||||
onChange,
|
||||
formatValue,
|
||||
valueId,
|
||||
size = 48
|
||||
}: KnobProps) {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const startYRef = useRef<number>(0)
|
||||
const startValueRef = useRef<number>(0)
|
||||
|
||||
const displayValue = formatValue && valueId ? formatValue(valueId, value) : `${value}${unit || ''}`
|
||||
|
||||
const normalizedValue = (value - min) / (max - min)
|
||||
const angle = -225 + normalizedValue * 270
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
setIsDragging(true)
|
||||
startYRef.current = e.clientY
|
||||
startValueRef.current = value
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging) return
|
||||
|
||||
const deltaY = startYRef.current - e.clientY
|
||||
const range = max - min
|
||||
const sensitivity = range / 200
|
||||
const newValue = Math.max(min, Math.min(max, startValueRef.current + deltaY * sensitivity))
|
||||
|
||||
const steppedValue = Math.round(newValue / step) * step
|
||||
onChange(steppedValue)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}
|
||||
}, [isDragging])
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-center">
|
||||
<div
|
||||
className="relative cursor-ns-resize select-none"
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
>
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={(size - 4) / 2}
|
||||
fill="none"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={(size - 8) / 2}
|
||||
fill="black"
|
||||
/>
|
||||
|
||||
<line
|
||||
x1={size / 2 + Math.cos((angle * Math.PI) / 180) * ((size - 16) / 2)}
|
||||
y1={size / 2 + Math.sin((angle * Math.PI) / 180) * ((size - 16) / 2)}
|
||||
x2={size / 2 + Math.cos((angle * Math.PI) / 180) * ((size - 4) / 2)}
|
||||
y2={size / 2 + Math.sin((angle * Math.PI) / 180) * ((size - 4) / 2)}
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="square"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<span className="font-mono text-[9px] tracking-[0.15em] text-white">
|
||||
{isDragging ? displayValue : label.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user