diff --git a/src/features/editor/Timbre.js b/src/features/editor/Timbre.js index a20a2d9..dc03a45 100644 --- a/src/features/editor/Timbre.js +++ b/src/features/editor/Timbre.js @@ -13,7 +13,7 @@ import { selectById } from './timbreSlice.js'; export default function Timbre(props) { const ids = useSelector((state) => selectById(state, props.id)); return ( -
+
- - + */}
); } diff --git a/src/features/editor/card.js b/src/features/editor/card.js index 393f4ab..73e6c76 100644 --- a/src/features/editor/card.js +++ b/src/features/editor/card.js @@ -5,7 +5,7 @@ export default function Card(props = { className: 'col-span-2' }) { props.className + ' flex flex-col justify-start border border-gray-200 rounded-md shadow-md bg-white' }> -
+

{props.header}

diff --git a/src/features/editor/index.js b/src/features/editor/index.js index af45d38..759c92b 100644 --- a/src/features/editor/index.js +++ b/src/features/editor/index.js @@ -7,7 +7,7 @@ import { useSelector } from 'react-redux'; export default function Editor() { const activeTab = useSelector((state) => state.parameters.activeTab); return ( -
+
{activeTab === 'Timbre1' ? : null} {activeTab === 'Timbre2' ? : null} {activeTab === 'Vocoder' ? : null} diff --git a/src/features/editor/parameters/Filter.js b/src/features/editor/parameters/Filter.js index c31439d..ccac83e 100644 --- a/src/features/editor/parameters/Filter.js +++ b/src/features/editor/parameters/Filter.js @@ -1,4 +1,6 @@ import React from 'react'; +import Card from '../card'; +import { Select, Knob } from '../../utils/components'; import { useSelector, useDispatch } from 'react-redux'; import { selectById, parameterUpdated } from './parameterSlice.js'; import { Slider, SelectList } from '../../helpers/Helpers'; @@ -8,83 +10,102 @@ export default function Filter(props) { const dispatch = useDispatch(); return ( -
- {`${props.name}`} - Filter Type - - dispatch( - parameterUpdated({ - id: props.id, - changes: { filterType: value }, - }) - ) - } - /> - Cutoff - - dispatch( - parameterUpdated({ - id: props.id, - changes: { cutoff: value }, - }) - ) - } - /> - Resonance - - dispatch( - parameterUpdated({ - id: props.id, - changes: { resonance: value }, - }) - ) - } - /> - EG Intensity - - dispatch( - parameterUpdated({ - id: props.id, - changes: { egIntensity: value }, - }) - ) - } - /> - Keyboard Tracking - - dispatch( - parameterUpdated({ - id: props.id, - changes: { keyboardTrack: value }, - }) - ) - } - /> -
+ +
+

Filter Type

+ - dispatch( - parameterUpdated({ - id: props.id, - changes: { voiceAssign: value }, - }) - ) - } - /> -

Trigger Mode

- + dispatch( + parameterUpdated({ + id: props.id, + changes: { voiceAssign: value }, + }) + ) + } + /> +
+
+

Transpose

+ + dispatch( + parameterUpdated({ + id: props.id, + changes: { transpose: value }, + }) + ) + } + /> +
+
+

Tune

+ + dispatch( + parameterUpdated({ + id: props.id, + changes: { tune: value }, + }) + ) + } + /> +
+
+

+ Unison Detune +

+ + dispatch( + parameterUpdated({ + id: props.id, + changes: { unisonDetune: value }, + }) + ) + } + /> +
-
-
-

Unison Detune

- - dispatch( - parameterUpdated({ - id: props.id, - changes: { unisonDetune: value }, - }) - ) - } - /> -

Portamento

- - dispatch( - parameterUpdated({ - id: props.id, - changes: { portamento: value }, - }) - ) - } - /> -

Bend Range

- - dispatch( - parameterUpdated({ - id: props.id, - changes: { bendRange: value }, - }) - ) - } - /> -

Vibrato Intensity

- - dispatch( - parameterUpdated({ - id: props.id, - changes: { vibratoIntensity: value }, - }) - ) - } - /> +
+
+
+

+ Trigger Mode +

+ @@ -27,10 +27,10 @@ export default function VCO(props) { } />
-
+

{`${props.dwgsOrModTypeName}`}

{ onChange={(e) => props.onChange(e.target.value)} value={props.value}> {props.list.map((element) => ( - + ))} ); }; -export { Select }; +const RAD = Math.PI / 180; +const START_ANGLE = Math.PI * 1.5 - (RAD * 45) / 2; +const END_ANGLE = Math.PI * -0.5 + (RAD * 45) / 2; + +function getPointPair(r, angle, distance) { + return [r + distance * Math.cos(angle), r - distance * Math.sin(angle)]; +} + +function DialMask({ id, size, startAngle, endAngle }) { + const r = size / 2; + + const intermediatePoints = [0, 0.25, 0.5, 0.75, 1] + .map((fraction) => + getPointPair( + r, + startAngle - (startAngle - endAngle) * fraction, + r * 4 + ).join(',') + ) + .join(' '); + + return ( + + + + ); +} + +function getKnobValue(dragStart, currentY, bipolar) { + const { y, value } = dragStart; + const deltaY = y - currentY; + const slope = 0.01; + let newValue = value + deltaY * slope; + if (bipolar) { + const zeroCrossingResistance = 0.3; + if (value >= 0 && newValue < 0) { + newValue += Math.min(zeroCrossingResistance, -newValue); + } else if (value < 0 && newValue > 0) { + newValue -= Math.min(zeroCrossingResistance, newValue); + } + } + // Clamp + return Math.max(bipolar ? -1 : 0, Math.min(1, newValue)); +} + +let knob_id = 0; + +function Knob({ + maxValue = 127, + dual = false, + size = 42, + value = 0, + fillWidth = 4, + onChange, +}) { + value = value / maxValue; + const uid = useMemo(() => knob_id++, []); + const radius = size / 2; + const dial_zero = dual ? Math.PI / 2 : START_ANGLE; + const dial_range = dual + ? (START_ANGLE - END_ANGLE) / 2 + : START_ANGLE - END_ANGLE; + const valueAngle = dual + ? Math.PI / 2 - value * dial_range + : START_ANGLE - value * dial_range; + + const [lineX1, lineY1] = getPointPair( + size / 2, + valueAngle, + radius - fillWidth + 2 + ); + + const [dragStart, setDragStart] = useState(null); + const isDragging = Boolean(dragStart); + + const onMouseMove = useCallback( + (e) => { + if (!dragStart) return; + + const newValue = getKnobValue(dragStart, e.clientY, dual); + onChange(Math.round(newValue * maxValue)); + }, + [dragStart, onChange, dual, maxValue] + ); + const onMouseDown = useCallback( + (e) => { + if (e.nativeEvent.button !== 0) return; + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + setDragStart({ value, y: e.nativeEvent.clientY }); + window.addEventListener('mousemove', onMouseMove); + }, + [setDragStart, onMouseMove, value] + ); + const onMouseUp = useCallback(() => { + setDragStart(null); + }, [setDragStart]); + + useEffect(() => { + if (!isDragging) { + window.removeEventListener('mousemove', onMouseMove); + } + window.addEventListener('mousemove', onMouseMove); + return () => window.removeEventListener('mousemove', onMouseMove); + }, [isDragging, onMouseMove]); + + useEffect(() => { + window.addEventListener('mouseup', onMouseUp); + return () => { + window.removeEventListener('mouseup', onMouseUp); + }; + }, [onMouseUp]); + + useEffect(() => { + if (!onMouseMove) return; + return () => window.removeEventListener('mousemove', onMouseMove); + }, [onMouseMove]); + + return ( +
+ + + + + + {/* Track outer */} + + {/* Value fill */} + + + + {/* Knob */} + + {/* Highlight */} + + + { + + } + + +
+ ); +} +export { Select, Knob }; diff --git a/src/index.css b/src/index.css index 24f69b6..820ef8e 100644 --- a/src/index.css +++ b/src/index.css @@ -18,6 +18,12 @@ code { @tailwind components; @tailwind utilities; +@layer base { + h3 { + @apply pointer-events-none select-none; + } +} + @layer components { .btn { @apply mx-4 my-1 bg-gray-100 rounded-md w-32 h-10 border-transparent hover:bg-gray-200; @@ -26,6 +32,6 @@ code { @apply opacity-60 hover:bg-gray-100 cursor-default; } .select { - @apply block rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50; + @apply select-none block rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50; } }