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.
Files changed (74) hide show
  1. package/.editorconfig +19 -0
  2. package/.github/workflows/codeql-analysis.yml +67 -0
  3. package/.nvmrc +1 -0
  4. package/.prettierignore +0 -0
  5. package/.prettierrc +13 -0
  6. package/.storybook/main.ts +42 -0
  7. package/.storybook/manager.ts +12 -0
  8. package/.storybook/preview.tsx +26 -0
  9. package/.vscode/extensions.json +32 -0
  10. package/.vscode/javascript.code-snippets +229 -0
  11. package/.vscode/settings.json +358 -0
  12. package/biome.jsonc +158 -0
  13. package/dist/index.es.js +3 -1
  14. package/dist/types/index.d.ts +266 -2
  15. package/dist/types/pickers/CCPicker.d.ts +2 -1
  16. package/dist/types/pickers/ChannelMappingPicker.d.ts +2 -1
  17. package/dist/types/pickers/ChannelPicker.d.ts +5 -3
  18. package/dist/types/pickers/LatchPicker.d.ts +5 -3
  19. package/dist/types/pickers/MappingPicker.d.ts +2 -1
  20. package/dist/types/pickers/NotePicker.d.ts +2 -1
  21. package/dist/types/pickers/PolarityPicker.d.ts +6 -4
  22. package/dist/types/pickers/ResponseCurve.d.ts +14 -5
  23. package/dist/types/pickers/ResponseCurvePicker.d.ts +6 -4
  24. package/dist/types/pickers/StatusPicker.d.ts +2 -1
  25. package/dist/types/pickers/ValuePicker.d.ts +2 -1
  26. package/lib/@types/nebiru-react-rotary-knob.d.ts +20 -0
  27. package/lib/@types/react-svgmt.d.ts +74 -0
  28. package/lib/@types/zds-mappings.d.ts +55 -0
  29. package/lib/@types/zds-react-piano.d.ts +17 -0
  30. package/lib/hooks/useComponentSize.ts +52 -0
  31. package/lib/hooks/useStateWithDynamicDefault.ts +13 -0
  32. package/lib/main.ts +34 -0
  33. package/lib/midi/ccValues.ts +139 -0
  34. package/lib/midi/export.ts +54 -0
  35. package/lib/other/DefaultTooltip.ts +14 -0
  36. package/lib/other/OctavePlayer.tsx +141 -0
  37. package/lib/other/SVGText.tsx +221 -0
  38. package/lib/other/SoundFontProvider.tsx +130 -0
  39. package/lib/pickers/CCPicker.tsx +28 -0
  40. package/lib/pickers/ChannelMappingPicker.tsx +135 -0
  41. package/lib/pickers/ChannelPicker.tsx +76 -0
  42. package/lib/pickers/Knob.tsx +93 -0
  43. package/lib/pickers/KnobPicker.tsx +93 -0
  44. package/lib/pickers/LatchPicker.tsx +33 -0
  45. package/lib/pickers/MappingPicker.tsx +68 -0
  46. package/lib/pickers/NotePicker.tsx +95 -0
  47. package/lib/pickers/PianoPicker.tsx +77 -0
  48. package/lib/pickers/PolarityPicker.tsx +39 -0
  49. package/lib/pickers/ResponseCurve.tsx +304 -0
  50. package/lib/pickers/ResponseCurvePicker.tsx +74 -0
  51. package/lib/pickers/Select.tsx +248 -0
  52. package/lib/pickers/StatusPicker.tsx +28 -0
  53. package/lib/pickers/ValuePicker.tsx +67 -0
  54. package/lib/pickers/knobSkin10.tsx +94 -0
  55. package/lib/rotaryKnob/InternalInput.tsx +43 -0
  56. package/lib/rotaryKnob/Types.ts +6 -0
  57. package/lib/rotaryKnob/helpers/DrawCircle.tsx +41 -0
  58. package/lib/rotaryKnob/helpers/DrawLine.tsx +32 -0
  59. package/lib/rotaryKnob/helpers/HelpersOverlay.tsx +43 -0
  60. package/lib/rotaryKnob/helpers/KnobVisualHelpers.tsx +138 -0
  61. package/lib/rotaryKnob/index.tsx +417 -0
  62. package/lib/rotaryKnob/knobdefaultskin.ts +56 -0
  63. package/lib/rotaryKnob/utils.ts +112 -0
  64. package/lib/temp/acoustic_grand_piano-mp3.js +94 -0
  65. package/lib/utils.ts +41 -0
  66. package/package.json +2 -6
  67. package/stories/assets/sampleStyles.css +349 -0
  68. package/stories/assets/utils.js +7 -0
  69. package/stories/picker.stories.tsx +313 -0
  70. package/tsconfig.app.json +26 -0
  71. package/tsconfig.json +34 -0
  72. package/tsconfig.node.json +24 -0
  73. package/vite.config.ts +45 -0
  74. 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
+ }