zds-pickers 4.0.5 → 4.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +19 -0
- package/.github/workflows/codeql-analysis.yml +67 -0
- package/.nvmrc +1 -0
- package/.prettierignore +0 -0
- package/.prettierrc +13 -0
- package/.storybook/main.ts +42 -0
- package/.storybook/manager.ts +12 -0
- package/.storybook/preview.tsx +26 -0
- package/.vscode/extensions.json +32 -0
- package/.vscode/javascript.code-snippets +229 -0
- package/.vscode/settings.json +358 -0
- package/biome.jsonc +158 -0
- package/dist/index.es.js +3 -1
- package/dist/types/index.d.ts +266 -2
- package/dist/types/pickers/CCPicker.d.ts +2 -1
- package/dist/types/pickers/ChannelMappingPicker.d.ts +2 -1
- package/dist/types/pickers/ChannelPicker.d.ts +5 -3
- package/dist/types/pickers/LatchPicker.d.ts +5 -3
- package/dist/types/pickers/MappingPicker.d.ts +2 -1
- package/dist/types/pickers/NotePicker.d.ts +2 -1
- package/dist/types/pickers/PolarityPicker.d.ts +6 -4
- package/dist/types/pickers/ResponseCurve.d.ts +14 -5
- package/dist/types/pickers/ResponseCurvePicker.d.ts +6 -4
- package/dist/types/pickers/StatusPicker.d.ts +2 -1
- package/dist/types/pickers/ValuePicker.d.ts +2 -1
- package/lib/@types/nebiru-react-rotary-knob.d.ts +20 -0
- package/lib/@types/react-svgmt.d.ts +74 -0
- package/lib/@types/zds-mappings.d.ts +55 -0
- package/lib/@types/zds-react-piano.d.ts +17 -0
- package/lib/hooks/useComponentSize.ts +52 -0
- package/lib/hooks/useStateWithDynamicDefault.ts +13 -0
- package/lib/main.ts +34 -0
- package/lib/midi/ccValues.ts +139 -0
- package/lib/midi/export.ts +54 -0
- package/lib/other/DefaultTooltip.ts +14 -0
- package/lib/other/OctavePlayer.tsx +141 -0
- package/lib/other/SVGText.tsx +221 -0
- package/lib/other/SoundFontProvider.tsx +130 -0
- package/lib/pickers/CCPicker.tsx +28 -0
- package/lib/pickers/ChannelMappingPicker.tsx +135 -0
- package/lib/pickers/ChannelPicker.tsx +76 -0
- package/lib/pickers/Knob.tsx +93 -0
- package/lib/pickers/KnobPicker.tsx +93 -0
- package/lib/pickers/LatchPicker.tsx +33 -0
- package/lib/pickers/MappingPicker.tsx +68 -0
- package/lib/pickers/NotePicker.tsx +95 -0
- package/lib/pickers/PianoPicker.tsx +77 -0
- package/lib/pickers/PolarityPicker.tsx +39 -0
- package/lib/pickers/ResponseCurve.tsx +304 -0
- package/lib/pickers/ResponseCurvePicker.tsx +74 -0
- package/lib/pickers/Select.tsx +248 -0
- package/lib/pickers/StatusPicker.tsx +28 -0
- package/lib/pickers/ValuePicker.tsx +67 -0
- package/lib/pickers/knobSkin10.tsx +94 -0
- package/lib/rotaryKnob/InternalInput.tsx +43 -0
- package/lib/rotaryKnob/Types.ts +6 -0
- package/lib/rotaryKnob/helpers/DrawCircle.tsx +41 -0
- package/lib/rotaryKnob/helpers/DrawLine.tsx +32 -0
- package/lib/rotaryKnob/helpers/HelpersOverlay.tsx +43 -0
- package/lib/rotaryKnob/helpers/KnobVisualHelpers.tsx +138 -0
- package/lib/rotaryKnob/index.tsx +417 -0
- package/lib/rotaryKnob/knobdefaultskin.ts +56 -0
- package/lib/rotaryKnob/utils.ts +112 -0
- package/lib/temp/acoustic_grand_piano-mp3.js +94 -0
- package/lib/utils.ts +41 -0
- package/package.json +2 -6
- package/stories/assets/sampleStyles.css +349 -0
- package/stories/assets/utils.js +7 -0
- package/stories/picker.stories.tsx +313 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +34 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +45 -0
- package/dist/index.d.ts +0 -244
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { Point } from '../Types'
|
|
3
|
+
import utils from '../utils'
|
|
4
|
+
import DrawCircle from './DrawCircle'
|
|
5
|
+
import DrawLine from './DrawLine'
|
|
6
|
+
import HelpersOverlay from './HelpersOverlay'
|
|
7
|
+
|
|
8
|
+
/** Precision visual helpers */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Precision mode visual helpers
|
|
12
|
+
*/
|
|
13
|
+
type KnobVisualHelpersProps = {
|
|
14
|
+
svgRef?: SVGSVGElement
|
|
15
|
+
radius: number
|
|
16
|
+
minimumDragDistance: number
|
|
17
|
+
valueAngle: number
|
|
18
|
+
mousePos: Point
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type KnobVisualHelpersState = {
|
|
22
|
+
centerX: number
|
|
23
|
+
centerY: number
|
|
24
|
+
valueMarkerX: number
|
|
25
|
+
valueMarkerY: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class KnobVisualHelpers extends React.Component<
|
|
29
|
+
KnobVisualHelpersProps,
|
|
30
|
+
KnobVisualHelpersState
|
|
31
|
+
> {
|
|
32
|
+
state = {
|
|
33
|
+
centerX: 0,
|
|
34
|
+
centerY: 0,
|
|
35
|
+
valueMarkerX: 0,
|
|
36
|
+
valueMarkerY: 0,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static getDerivedStateFromProps(
|
|
40
|
+
props: KnobVisualHelpersProps,
|
|
41
|
+
state: KnobVisualHelpersState,
|
|
42
|
+
) {
|
|
43
|
+
//Calculate position
|
|
44
|
+
if (!props.svgRef) return state
|
|
45
|
+
|
|
46
|
+
const vbox = props.svgRef.getBoundingClientRect()
|
|
47
|
+
const halfWidth = vbox.width / 2
|
|
48
|
+
|
|
49
|
+
//Calculate current angle segment end point
|
|
50
|
+
//Note: Not sure why we need to subtract 90 here
|
|
51
|
+
const valueMarkerX =
|
|
52
|
+
props.radius * Math.cos(utils.toRadians(props.valueAngle - 90))
|
|
53
|
+
const valueMarkerY =
|
|
54
|
+
props.radius * Math.sin(utils.toRadians(props.valueAngle - 90))
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...state,
|
|
58
|
+
centerX: vbox.left + halfWidth,
|
|
59
|
+
centerY: vbox.top + halfWidth,
|
|
60
|
+
valueMarkerX,
|
|
61
|
+
valueMarkerY,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render() {
|
|
66
|
+
const markCircleColor =
|
|
67
|
+
this.props.minimumDragDistance <= this.props.radius ? 'green' : 'grey'
|
|
68
|
+
const fillColor =
|
|
69
|
+
this.props.minimumDragDistance <= this.props.radius
|
|
70
|
+
? '#88E22D'
|
|
71
|
+
: '#D8D8D8'
|
|
72
|
+
const minDistanceCueVisible =
|
|
73
|
+
this.props.minimumDragDistance >= this.props.radius
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<HelpersOverlay>
|
|
77
|
+
<svg
|
|
78
|
+
style={{ position: 'absolute', top: '0', left: '0' }}
|
|
79
|
+
width="100%"
|
|
80
|
+
height="100%">
|
|
81
|
+
<defs>
|
|
82
|
+
<marker
|
|
83
|
+
id="Triangle"
|
|
84
|
+
viewBox="0 0 10 10"
|
|
85
|
+
refX="1"
|
|
86
|
+
refY="5"
|
|
87
|
+
markerWidth="6"
|
|
88
|
+
markerHeight="6"
|
|
89
|
+
orient="auto">
|
|
90
|
+
<path d="M 0 0 L 10 5 L 0 10 z" />
|
|
91
|
+
</marker>
|
|
92
|
+
</defs>
|
|
93
|
+
|
|
94
|
+
{/* Current drag distance */}
|
|
95
|
+
<DrawCircle
|
|
96
|
+
borderColor={markCircleColor}
|
|
97
|
+
fillColor={fillColor}
|
|
98
|
+
fillOpacity={0.02}
|
|
99
|
+
r={this.props.radius}
|
|
100
|
+
cx={this.state.centerX}
|
|
101
|
+
cy={this.state.centerY}
|
|
102
|
+
/>
|
|
103
|
+
{/* Minimum drag distance marker */}
|
|
104
|
+
{minDistanceCueVisible && (
|
|
105
|
+
<DrawCircle
|
|
106
|
+
borderColor={markCircleColor}
|
|
107
|
+
fillColor={fillColor}
|
|
108
|
+
fillOpacity={0}
|
|
109
|
+
r={this.props.minimumDragDistance}
|
|
110
|
+
cx={this.state.centerX}
|
|
111
|
+
cy={this.state.centerY}
|
|
112
|
+
//strokeDasharray="5, 5"
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{/* Line to mouse position */}
|
|
117
|
+
{/* <DrawLine
|
|
118
|
+
p1={{ x: this.state.centerX, y: this.state.centerY }}
|
|
119
|
+
p2={{ x: this.props.mousePos.x, y: this.props.mousePos.y }}
|
|
120
|
+
opacity={0.4}
|
|
121
|
+
strokeDasharray="5, 5"
|
|
122
|
+
/> */}
|
|
123
|
+
{/* Line to current value */}
|
|
124
|
+
<DrawLine
|
|
125
|
+
p1={{ x: this.state.centerX, y: this.state.centerY }}
|
|
126
|
+
p2={{
|
|
127
|
+
x: this.state.valueMarkerX + this.state.centerX,
|
|
128
|
+
y: this.state.valueMarkerY + this.state.centerY,
|
|
129
|
+
}}
|
|
130
|
+
markerEnd="url(#Triangle)"
|
|
131
|
+
/>
|
|
132
|
+
</svg>
|
|
133
|
+
</HelpersOverlay>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { KnobVisualHelpers }
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show the rotation circle and marker
|
|
3
|
+
* dispatches drag events
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type D3DragEvent, drag } from 'd3-drag'
|
|
7
|
+
import { type ScaleLinear, scaleLinear } from 'd3-scale'
|
|
8
|
+
import { select } from 'd3-selection'
|
|
9
|
+
import type { BaseType, Selection } from 'd3-selection'
|
|
10
|
+
import { useEffect, useRef, useState } from 'react'
|
|
11
|
+
import { SvgLoader, SvgProxy } from 'react-svgmt'
|
|
12
|
+
import InternalInput from './InternalInput'
|
|
13
|
+
import type { Point } from './Types'
|
|
14
|
+
import { KnobVisualHelpers } from './helpers/KnobVisualHelpers'
|
|
15
|
+
import defaultSkin from './knobdefaultskin'
|
|
16
|
+
import utils from './utils'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* type definition for the skin system attribute modification element
|
|
20
|
+
*/
|
|
21
|
+
type AttributeSetValue = {
|
|
22
|
+
name: string
|
|
23
|
+
value: (props: unknown, value: unknown) => string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Type definition for the skin element manipulation block
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
interface UpdateElement {
|
|
31
|
+
element: string
|
|
32
|
+
content: (_props: React.CSSProperties, value: number) => string
|
|
33
|
+
attrs: AttributeSetValue[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Skin {
|
|
37
|
+
svg: string
|
|
38
|
+
knobX: number
|
|
39
|
+
knobY: number
|
|
40
|
+
updateAttributes: UpdateElement[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type KnobProps = Omit<React.ComponentProps<'div'>, 'onChange'> & {
|
|
44
|
+
clampMax?: number
|
|
45
|
+
clampMin?: number
|
|
46
|
+
defaultValue?: number
|
|
47
|
+
disabled?: boolean
|
|
48
|
+
format?: (val: number) => string
|
|
49
|
+
max?: number
|
|
50
|
+
min?: number
|
|
51
|
+
onChange?: (val: number) => void
|
|
52
|
+
onEnd?: () => void
|
|
53
|
+
onStart?: () => void
|
|
54
|
+
preciseMode?: boolean
|
|
55
|
+
rotateDegrees?: number
|
|
56
|
+
skin?: Skin
|
|
57
|
+
step?: number
|
|
58
|
+
style: React.CSSProperties
|
|
59
|
+
unlockDistance?: number
|
|
60
|
+
value?: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type KnobState = {
|
|
64
|
+
svgRef?: SVGSVGElement
|
|
65
|
+
dragging: boolean
|
|
66
|
+
dragDistance: number
|
|
67
|
+
mousePos: Point
|
|
68
|
+
valueAngle: number
|
|
69
|
+
uncontrolledValue?: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// function printDebugValues(obj) {
|
|
73
|
+
// console.log('------------')
|
|
74
|
+
// Object.keys(obj).forEach(key => {
|
|
75
|
+
// const value =
|
|
76
|
+
// typeof obj[key] === 'object' ? JSON.stringify(obj[key]) : obj[key]
|
|
77
|
+
// console.log(`${key}: ${value}`)
|
|
78
|
+
// })
|
|
79
|
+
// console.log('------------')
|
|
80
|
+
// }
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generic knob component
|
|
84
|
+
*/
|
|
85
|
+
const RotaryKnob = (props: KnobProps) => {
|
|
86
|
+
const {
|
|
87
|
+
clampMax = 360,
|
|
88
|
+
clampMin = 0,
|
|
89
|
+
defaultValue = 0,
|
|
90
|
+
disabled = false,
|
|
91
|
+
format = (val: number) => val.toFixed(0),
|
|
92
|
+
max = 100,
|
|
93
|
+
min = 0,
|
|
94
|
+
onChange = () => {},
|
|
95
|
+
onEnd = () => {},
|
|
96
|
+
onStart = () => {},
|
|
97
|
+
preciseMode = true,
|
|
98
|
+
rotateDegrees = 0,
|
|
99
|
+
skin = defaultSkin as Skin,
|
|
100
|
+
step = 1,
|
|
101
|
+
style,
|
|
102
|
+
unlockDistance = 100,
|
|
103
|
+
value,
|
|
104
|
+
...rest
|
|
105
|
+
} = props
|
|
106
|
+
|
|
107
|
+
const [isControlled, setIsControlled] = useState(value !== undefined)
|
|
108
|
+
|
|
109
|
+
const container = useRef<HTMLDivElement>(null)
|
|
110
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
111
|
+
|
|
112
|
+
let scale: ScaleLinear<number, number>
|
|
113
|
+
let scaleProps: {
|
|
114
|
+
min: number
|
|
115
|
+
max: number
|
|
116
|
+
clampMin: number
|
|
117
|
+
clampMax: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const [state, setState] = useState<KnobState>({
|
|
121
|
+
svgRef: undefined,
|
|
122
|
+
dragging: false,
|
|
123
|
+
dragDistance: 0,
|
|
124
|
+
mousePos: { x: 0, y: 0 },
|
|
125
|
+
valueAngle: 0,
|
|
126
|
+
uncontrolledValue: 0,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// returns angles between 0 and 360 for values lower 0 or greater 360
|
|
130
|
+
const normalizeAngle = (angle: number): number => {
|
|
131
|
+
if (angle < 0) {
|
|
132
|
+
//E.g -1 turns 359
|
|
133
|
+
return angle + 360
|
|
134
|
+
}
|
|
135
|
+
if (angle > 360) {
|
|
136
|
+
return angle - 360
|
|
137
|
+
}
|
|
138
|
+
return angle
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// converts a value to an angle for the knob respecting the additional rotateDegrees prop
|
|
142
|
+
const convertValueToAngle = (value: number): number => {
|
|
143
|
+
const angle = (getScale()(value) + rotateDegrees) % 360
|
|
144
|
+
return normalizeAngle(angle)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// converts an angle to a value for the knob respecting the additional rotateDegrees prop
|
|
148
|
+
const convertAngleToValue = (angle: number): number => {
|
|
149
|
+
const angle2 = angle - rotateDegrees
|
|
150
|
+
return getScale().invert(normalizeAngle(angle2))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const getScale = () => {
|
|
154
|
+
// Memoize scale so it's not recalculated every time
|
|
155
|
+
if (
|
|
156
|
+
!scaleProps ||
|
|
157
|
+
scaleProps.min !== min ||
|
|
158
|
+
scaleProps.max !== max ||
|
|
159
|
+
scaleProps.clampMin !== clampMin ||
|
|
160
|
+
scaleProps.clampMax !== clampMax
|
|
161
|
+
) {
|
|
162
|
+
if (rotateDegrees < 270 && rotateDegrees > 90) {
|
|
163
|
+
scale = scaleLinear().domain([min, max]).range([clampMin, clampMax])
|
|
164
|
+
} else {
|
|
165
|
+
scale = scaleLinear().domain([max, min]).range([clampMax, clampMin])
|
|
166
|
+
}
|
|
167
|
+
scale.clamp(true)
|
|
168
|
+
scaleProps = {
|
|
169
|
+
min: min,
|
|
170
|
+
max: max,
|
|
171
|
+
clampMin: clampMin,
|
|
172
|
+
clampMax: clampMax,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return scale
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
setIsControlled(value !== undefined)
|
|
180
|
+
|
|
181
|
+
setState(prevState => ({
|
|
182
|
+
...prevState,
|
|
183
|
+
uncontrolledValue: value ?? defaultValue ?? 0,
|
|
184
|
+
}))
|
|
185
|
+
|
|
186
|
+
if (container.current) {
|
|
187
|
+
// Remove existing drag behavior
|
|
188
|
+
select(container.current).on('.drag', null)
|
|
189
|
+
// Only setup new drag if not disabled
|
|
190
|
+
if (!disabled) {
|
|
191
|
+
setupDragging(select(container.current))
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, [value, defaultValue, disabled])
|
|
195
|
+
|
|
196
|
+
const saveRef = (elem: SVGElement) => {
|
|
197
|
+
if (!state.svgRef) {
|
|
198
|
+
setState({ ...state, svgRef: elem as SVGSVGElement })
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const onAngleChanged = (angle: number) => {
|
|
203
|
+
//Calculate domain value
|
|
204
|
+
const domainValue = convertAngleToValue(angle)
|
|
205
|
+
if (!isControlled) {
|
|
206
|
+
/**
|
|
207
|
+
* If the mode is 'uncontrolled' we need to update our local state
|
|
208
|
+
*/
|
|
209
|
+
setState(st => {
|
|
210
|
+
return { ...st, uncontrolledValue: domainValue }
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
if (!disabled) onChange(domainValue)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Returns the current value (depending on controlled/uncontrolled mode)
|
|
218
|
+
*/
|
|
219
|
+
const getValue = (): number =>
|
|
220
|
+
isControlled ? (value ?? 0) : (state.uncontrolledValue ?? 0)
|
|
221
|
+
|
|
222
|
+
const setupDragging = (
|
|
223
|
+
elem: Selection<HTMLDivElement, unknown, BaseType, unknown>,
|
|
224
|
+
) => {
|
|
225
|
+
if (!elem?.node()) return
|
|
226
|
+
const node = elem.node()
|
|
227
|
+
if (!node) return
|
|
228
|
+
|
|
229
|
+
let vbox = node.getBoundingClientRect()
|
|
230
|
+
const cx = vbox.width / 2
|
|
231
|
+
const cy = vbox.height / 2
|
|
232
|
+
|
|
233
|
+
// Shared state via closure
|
|
234
|
+
let startPos: Point
|
|
235
|
+
let startAngle: number
|
|
236
|
+
let initialAngle: number
|
|
237
|
+
let monitoring = false
|
|
238
|
+
|
|
239
|
+
function started(event: D3DragEvent<HTMLDivElement, unknown, unknown>) {
|
|
240
|
+
if (!event?.sourceEvent) return
|
|
241
|
+
if (!node) return
|
|
242
|
+
|
|
243
|
+
const currentValue = getValue()
|
|
244
|
+
|
|
245
|
+
vbox = node.getBoundingClientRect()
|
|
246
|
+
elem.classed('dragging', true)
|
|
247
|
+
onStart()
|
|
248
|
+
|
|
249
|
+
startPos = { x: event.x - cx, y: event.y - cy }
|
|
250
|
+
startAngle = utils.getAngleForPoint(startPos.x, startPos.y)
|
|
251
|
+
initialAngle = convertValueToAngle(currentValue)
|
|
252
|
+
monitoring = false
|
|
253
|
+
|
|
254
|
+
// ...existing setState code...
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function dragged(event: D3DragEvent<HTMLDivElement, unknown, unknown>) {
|
|
258
|
+
const currentPos = { x: event.x - cx, y: event.y - cy }
|
|
259
|
+
const distanceFromCenter = Math.sqrt(
|
|
260
|
+
currentPos.x ** 2 + currentPos.y ** 2,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if (preciseMode) {
|
|
264
|
+
if (!monitoring && distanceFromCenter >= unlockDistance) {
|
|
265
|
+
startPos = currentPos
|
|
266
|
+
startAngle = utils.getAngleForPoint(currentPos.x, currentPos.y)
|
|
267
|
+
monitoring = true
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
monitoring = true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const currentAngle = utils.getAngleForPoint(currentPos.x, currentPos.y)
|
|
274
|
+
const deltaAngle = currentAngle - startAngle
|
|
275
|
+
let finalAngle = initialAngle + deltaAngle
|
|
276
|
+
finalAngle = normalizeAngle(finalAngle)
|
|
277
|
+
|
|
278
|
+
if (monitoring) {
|
|
279
|
+
onAngleChanged(finalAngle)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
setState(prevState => ({
|
|
283
|
+
...prevState,
|
|
284
|
+
dragDistance: distanceFromCenter,
|
|
285
|
+
mousePos: {
|
|
286
|
+
x: event.sourceEvent.clientX,
|
|
287
|
+
y: event.sourceEvent.clientY,
|
|
288
|
+
},
|
|
289
|
+
valueAngle: monitoring ? finalAngle : initialAngle,
|
|
290
|
+
}))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function ended() {
|
|
294
|
+
elem.classed('dragging', false)
|
|
295
|
+
setState(prevState => ({ ...prevState, dragging: false }))
|
|
296
|
+
onEnd()
|
|
297
|
+
inputRef.current?.focus()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const dragInstance = drag<HTMLDivElement, unknown>()
|
|
301
|
+
.container(() => node)
|
|
302
|
+
.on('start', started)
|
|
303
|
+
.on('drag', dragged)
|
|
304
|
+
.on('end', ended)
|
|
305
|
+
|
|
306
|
+
elem.call(dragInstance)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const onFormControlChange = (newVal: number) => {
|
|
310
|
+
if (!isControlled) {
|
|
311
|
+
/**
|
|
312
|
+
* If the mode is 'uncontrolled' we need to update our local state
|
|
313
|
+
*/
|
|
314
|
+
setState(prevState => ({ ...prevState, uncontrolledValue: newVal }))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!disabled) onChange(newVal)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const currentValue: number = getValue()
|
|
321
|
+
const angle = convertValueToAngle(currentValue)
|
|
322
|
+
|
|
323
|
+
const styles: {
|
|
324
|
+
container: React.CSSProperties
|
|
325
|
+
input: React.CSSProperties
|
|
326
|
+
} = {
|
|
327
|
+
container: Object.assign(
|
|
328
|
+
{},
|
|
329
|
+
{
|
|
330
|
+
width: '50px',
|
|
331
|
+
height: '50px',
|
|
332
|
+
overflow: 'hidden',
|
|
333
|
+
position: 'relative',
|
|
334
|
+
userSelect: 'none',
|
|
335
|
+
cursor: 'crosshair',
|
|
336
|
+
},
|
|
337
|
+
style,
|
|
338
|
+
),
|
|
339
|
+
input: {
|
|
340
|
+
width: '50%',
|
|
341
|
+
position: 'absolute',
|
|
342
|
+
top: '0',
|
|
343
|
+
left: '-100%',
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
//Get custom updates defined by the skin
|
|
348
|
+
//Transform the updateAttributes array into a SvgProxy array
|
|
349
|
+
const updateAttrs = skin.updateAttributes
|
|
350
|
+
const skinElemUpdates = updateAttrs
|
|
351
|
+
? updateAttrs.map((elemUpdate, ix: number) => {
|
|
352
|
+
let elemContent: string | null = null
|
|
353
|
+
if (elemUpdate.content) {
|
|
354
|
+
elemContent = elemUpdate.content(props, currentValue)
|
|
355
|
+
}
|
|
356
|
+
const attributes: Record<string, string> = {}
|
|
357
|
+
//Call value function
|
|
358
|
+
//TODO: support string values
|
|
359
|
+
for (const attr of elemUpdate.attrs || []) {
|
|
360
|
+
attributes[attr.name] = attr.value(props, currentValue)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<SvgProxy
|
|
365
|
+
key={ix}
|
|
366
|
+
selector={elemUpdate.element}
|
|
367
|
+
{...attributes}>
|
|
368
|
+
{elemContent}
|
|
369
|
+
</SvgProxy>
|
|
370
|
+
)
|
|
371
|
+
})
|
|
372
|
+
: null
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<>
|
|
376
|
+
<div
|
|
377
|
+
ref={container}
|
|
378
|
+
style={styles.container}
|
|
379
|
+
{...rest}>
|
|
380
|
+
<InternalInput
|
|
381
|
+
disabled={disabled}
|
|
382
|
+
inputRef={inputRef}
|
|
383
|
+
style={styles.input}
|
|
384
|
+
value={currentValue}
|
|
385
|
+
min={min}
|
|
386
|
+
max={max}
|
|
387
|
+
step={step}
|
|
388
|
+
onChange={onFormControlChange}
|
|
389
|
+
/>
|
|
390
|
+
|
|
391
|
+
<SvgLoader
|
|
392
|
+
height="100%"
|
|
393
|
+
svgXML={skin.svg}
|
|
394
|
+
width="100%"
|
|
395
|
+
onSVGReady={saveRef}>
|
|
396
|
+
<SvgProxy
|
|
397
|
+
selector="#knob"
|
|
398
|
+
transform={`$ORIGINAL rotate(${angle}, ${skin.knobX}, ${skin.knobY})`}
|
|
399
|
+
/>
|
|
400
|
+
{skinElemUpdates}
|
|
401
|
+
</SvgLoader>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
{state.dragging && preciseMode && (
|
|
405
|
+
<KnobVisualHelpers
|
|
406
|
+
svgRef={state.svgRef}
|
|
407
|
+
radius={state.dragDistance}
|
|
408
|
+
mousePos={state.mousePos}
|
|
409
|
+
minimumDragDistance={unlockDistance}
|
|
410
|
+
valueAngle={state.valueAngle}
|
|
411
|
+
/>
|
|
412
|
+
)}
|
|
413
|
+
</>
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export default RotaryKnob
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
knobX: 71,
|
|
3
|
+
knobY: 71,
|
|
4
|
+
svg: `
|
|
5
|
+
|
|
6
|
+
<svg width="204px" height="204px" viewBox="0 0 204 204" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
7
|
+
<!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
|
|
8
|
+
<desc>Created with Sketch.</desc>
|
|
9
|
+
<defs>
|
|
10
|
+
<linearGradient x1="50%" y1="50%" x2="50%" y2="100%" id="linearGradient-1">
|
|
11
|
+
<stop stop-color="#444040" stop-opacity="0.51098279" offset="0%"></stop>
|
|
12
|
+
<stop stop-color="#131111" stop-opacity="0.893200861" offset="100%"></stop>
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<circle id="path-2" cx="98.0400009" cy="98.0400009" r="98.0400009"></circle>
|
|
15
|
+
<filter x="-3.3%" y="-3.3%" width="106.6%" height="106.6%" filterUnits="objectBoundingBox" id="filter-3">
|
|
16
|
+
<feMorphology radius="0.5" operator="dilate" in="SourceAlpha" result="shadowSpreadOuter1"></feMorphology>
|
|
17
|
+
<feOffset dx="0" dy="0" in="shadowSpreadOuter1" result="shadowOffsetOuter1"></feOffset>
|
|
18
|
+
<feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
|
19
|
+
<feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"></feComposite>
|
|
20
|
+
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
|
21
|
+
</filter>
|
|
22
|
+
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-4">
|
|
23
|
+
<stop stop-color="#FFFFFF" stop-opacity="0.5" offset="0%"></stop>
|
|
24
|
+
<stop stop-color="#000000" stop-opacity="0.5" offset="100%"></stop>
|
|
25
|
+
</linearGradient>
|
|
26
|
+
<circle id="path-5" cx="98" cy="98" r="86"></circle>
|
|
27
|
+
<filter x="-4.1%" y="-3.5%" width="108.1%" height="108.1%" filterUnits="objectBoundingBox" id="filter-6">
|
|
28
|
+
<feMorphology radius="0.5" operator="dilate" in="SourceAlpha" result="shadowSpreadOuter1"></feMorphology>
|
|
29
|
+
<feOffset dx="0" dy="1" in="shadowSpreadOuter1" result="shadowOffsetOuter1"></feOffset>
|
|
30
|
+
<feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
|
31
|
+
<feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"></feComposite>
|
|
32
|
+
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
|
33
|
+
</filter>
|
|
34
|
+
</defs>
|
|
35
|
+
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
36
|
+
<g id="s13" transform="translate(4.000000, 4.000000)">
|
|
37
|
+
<g id="container">
|
|
38
|
+
<g id="Oval-2">
|
|
39
|
+
<use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-2"></use>
|
|
40
|
+
<use stroke="#979797" stroke-width="1" fill="url(#linearGradient-1)" fill-rule="evenodd" xlink:href="#path-2"></use>
|
|
41
|
+
</g>
|
|
42
|
+
<g id="Oval-2">
|
|
43
|
+
<use fill="black" fill-opacity="1" filter="url(#filter-6)" xlink:href="#path-5"></use>
|
|
44
|
+
<use fill="" fill-rule="evenodd" xlink:href="#path-5"></use>
|
|
45
|
+
<use stroke="#4A4A4A" stroke-width="1" fill="url(#linearGradient-4)" fill-rule="evenodd" xlink:href="#path-5"></use>
|
|
46
|
+
</g>
|
|
47
|
+
<g id="knob" transform="translate(27.431373, 27.431373)">
|
|
48
|
+
<circle id="Oval-5" fill="#322E2E" cx="71" cy="71" r="71"></circle>
|
|
49
|
+
<path d="M70.0686275,4.17063061 L86.5686275,65.2689929 C81.3642501,66.811221 75.8642501,67.5735686 70.0686275,67.5560356 C64.2730048,67.5385027 58.7730048,66.7761551 53.5686275,65.2689929 L70.0686275,4.17063061 Z" id="Rectangle-Copy" fill="#E6D7D7" transform="translate(70.068627, 35.863481) scale(1, -1) translate(-70.068627, -35.863481) "></path>
|
|
50
|
+
</g>
|
|
51
|
+
</g>
|
|
52
|
+
</g>
|
|
53
|
+
</g>
|
|
54
|
+
</svg>
|
|
55
|
+
`,
|
|
56
|
+
}
|