@tremolo-ui/functions 0.5.0 → 0.6.0

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/src/piano.ts CHANGED
@@ -84,33 +84,14 @@ export function blackKeyWidth(layout: PianoLayout): number {
84
84
  )
85
85
  }
86
86
 
87
- /** Width of the whole keyboard in pixels. */
88
- export function pianoWidth(layout: PianoLayout): number {
89
- const whiteKeys = getNoteRangeArray(layout.noteRange).filter(isWhiteKey)
90
- return (
91
- (layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)) *
92
- whiteKeys.length
93
- )
94
- }
95
-
96
- /**
97
- * Offset of the left edge of a key from the left edge of the keyboard, in
98
- * pixels.
99
- *
100
- * Notes outside `noteRange` are placed too, so the value is negative below
101
- * `noteRange.first`.
102
- */
103
- export function notePosition(note: number, layout: PianoLayout): number {
87
+ function rawNotePosition(note: number, layout: PianoLayout): number {
104
88
  const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)
105
89
  const target = noteKey(note)
106
90
  const first = noteKey(layout.noteRange.first)
107
91
 
108
92
  const octave = Math.floor((note - layout.noteRange.first) / 12)
109
- // A note whose pitch class comes before the first one belongs to the octave
110
- // above the one the division above gives.
111
93
  const octaveOffset =
112
94
  noteKeys.indexOf(first) > noteKeys.indexOf(target) ? 1 : 0
113
-
114
95
  const whiteKeysIn =
115
96
  whiteKeysBefore[target] -
116
97
  whiteKeysBefore[first] +
@@ -121,6 +102,39 @@ export function notePosition(note: number, layout: PianoLayout): number {
121
102
  : whiteKeysIn * slot
122
103
  }
123
104
 
105
+ function pianoBounds(layout: PianoLayout) {
106
+ const notes = getNoteRangeArray(layout.noteRange)
107
+ if (notes.length === 0) return { left: 0, right: 0 }
108
+
109
+ let left = Infinity
110
+ let right = -Infinity
111
+ const whiteWidth = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)
112
+ for (const note of notes) {
113
+ const noteLeft = rawNotePosition(note, layout)
114
+ const width = isBlackKey(note) ? blackKeyWidth(layout) : whiteWidth
115
+ left = Math.min(left, noteLeft)
116
+ right = Math.max(right, noteLeft + width)
117
+ }
118
+ return { left, right }
119
+ }
120
+
121
+ /** Width of the whole keyboard in pixels. */
122
+ export function pianoWidth(layout: PianoLayout): number {
123
+ const { left, right } = pianoBounds(layout)
124
+ return right - left
125
+ }
126
+
127
+ /**
128
+ * Offset of the left edge of a key from the left edge of the keyboard, in
129
+ * pixels.
130
+ *
131
+ * Notes outside `noteRange` are placed too, so the value is negative below
132
+ * `noteRange.first`.
133
+ */
134
+ export function notePosition(note: number, layout: PianoLayout): number {
135
+ return rawNotePosition(note, layout) - pianoBounds(layout).left
136
+ }
137
+
124
138
  /**
125
139
  * The note drawn at a point, or null where there is none.
126
140
  *
@@ -138,7 +152,7 @@ export function noteAt(
138
152
  height: number,
139
153
  layout: PianoLayout,
140
154
  ): number | null {
141
- if (y < 0 || y >= height) return null
155
+ if (x < 0 || x >= pianoWidth(layout) || y < 0 || y >= height) return null
142
156
 
143
157
  const notes = getNoteRangeArray(layout.noteRange)
144
158
  const blackHeight =
package/src/scales.ts CHANGED
@@ -1,6 +1,10 @@
1
- import { clamp, normalizeValue, rawValue, stepValue } from './math'
2
-
3
- import type { InputEventOption } from './types'
1
+ import { clamp, normalizeValue, rawValue, stepValue, toPrecision } from './math'
2
+ import {
3
+ type InputEventOption,
4
+ type ModifierState,
5
+ type ModifierValue,
6
+ selectInputEvent,
7
+ } from './modifiers'
4
8
 
5
9
  /**
6
10
  * How a value is distributed across the travel of a control.
@@ -28,6 +32,12 @@ function assertRange(min: number, max: number) {
28
32
  if (min >= max) throw new RangeError('requirements: min < max')
29
33
  }
30
34
 
35
+ function assertPositiveFinite(value: number, name: string) {
36
+ if (!Number.isFinite(value) || value <= 0) {
37
+ throw new RangeError(`${name}: requirements: finite and greater than 0`)
38
+ }
39
+ }
40
+
31
41
  /**
32
42
  * Equal travel gives an equal change in value.
33
43
  *
@@ -56,6 +66,7 @@ export const linearScale: Scale = {
56
66
  * @param skew the JUCE skew factor
57
67
  */
58
68
  export function skewScale(skew: number): Scale {
69
+ assertPositiveFinite(skew, 'skewScale')
59
70
  return {
60
71
  // The two expressions JUCE uses, kept verbatim so the numbers agree with
61
72
  // a NormalisableRange: pow() one way, exp(log()) the other.
@@ -81,8 +92,9 @@ export function skewWithCenterValue(
81
92
  min: number,
82
93
  max: number,
83
94
  ) {
84
- if (!(min <= centerValue && centerValue <= max))
85
- throw new RangeError('requirements: min <= centerValue <= max')
95
+ assertRange(min, max)
96
+ if (!(min < centerValue && centerValue < max))
97
+ throw new RangeError('requirements: min < centerValue < max')
86
98
  return Math.log(0.5) / Math.log((centerValue - min) / (max - min))
87
99
  }
88
100
 
@@ -103,15 +115,20 @@ export const exponentialScale: Scale = {
103
115
  // The value is clamped before the logarithm, not after: outside the range
104
116
  // the ratio can be negative, and log() would give NaN rather than a
105
117
  // position to clamp.
118
+ const start = Math.log(Math.abs(min))
119
+ const end = Math.log(Math.abs(max))
106
120
  return clamp(
107
- Math.log(clamp(value, min, max) / min) / Math.log(max / min),
121
+ (Math.log(Math.abs(clamp(value, min, max))) - start) / (end - start),
108
122
  0,
109
123
  1,
110
124
  )
111
125
  },
112
126
  denormalize: (position, min, max) => {
113
127
  assertExponentialRange(min, max)
114
- return min * Math.pow(max / min, clamp(position, 0, 1))
128
+ const start = Math.log(Math.abs(min))
129
+ const end = Math.log(Math.abs(max))
130
+ const magnitude = Math.exp(start + (end - start) * clamp(position, 0, 1))
131
+ return Math.sign(min) * magnitude
115
132
  },
116
133
  }
117
134
 
@@ -146,31 +163,40 @@ function assertExponentialRange(min: number, max: number) {
146
163
  * @param curve how hard the curve bends, and in which direction
147
164
  */
148
165
  export function curveScale(curve: number): Scale {
166
+ // Beyond this the flatter half of the curve no longer has enough distinct
167
+ // double values for normalize and denormalize to remain inverses.
168
+ if (!Number.isFinite(curve) || Math.abs(curve) > 32) {
169
+ throw new RangeError(
170
+ 'curveScale: requirements: finite curve from -32 to 32',
171
+ )
172
+ }
149
173
  // The two coefficients blow up as the curve flattens: `a` divides by
150
174
  // 1 - e^curve, which goes to 0.
151
175
  if (Math.abs(curve) < 0.001) return linearScale
152
176
 
153
- const grow = Math.exp(curve)
154
-
155
- // value(position) = b - a * e^(curve * position), fixed so that
156
- // value(0) = min and value(1) = max.
157
- const coefficients = (min: number, max: number) => {
158
- const a = (max - min) / (1 - grow)
159
- return { a, b: min + a }
160
- }
161
-
162
177
  return {
163
178
  normalize: (value, min, max) => {
164
179
  assertRange(min, max)
165
- const { a, b } = coefficients(min, max)
166
- // Clamped before the logarithm: far outside the range `(b - value) / a`
167
- // turns negative and log() would give NaN.
168
- return clamp(Math.log((b - clamp(value, min, max)) / a) / curve, 0, 1)
180
+ const proportion = clamp((value - min) / (max - min), 0, 1)
181
+ if (proportion === 0 || proportion === 1) return proportion
182
+ if (curve > 0) {
183
+ return (
184
+ 1 + Math.log(proportion + (1 - proportion) * Math.exp(-curve)) / curve
185
+ )
186
+ }
187
+ return Math.log1p(proportion * Math.expm1(curve)) / curve
169
188
  },
170
189
  denormalize: (position, min, max) => {
171
190
  assertRange(min, max)
172
- const { a, b } = coefficients(min, max)
173
- return b - a * Math.pow(grow, clamp(position, 0, 1))
191
+ const p = clamp(position, 0, 1)
192
+ if (p === 0) return min
193
+ if (p === 1) return max
194
+ const proportion =
195
+ curve > 0
196
+ ? (Math.exp(curve * (p - 1)) * (1 - Math.exp(-curve * p))) /
197
+ (1 - Math.exp(-curve))
198
+ : Math.expm1(curve * p) / Math.expm1(curve)
199
+ return min + (max - min) * proportion
174
200
  },
175
201
  }
176
202
  }
@@ -188,6 +214,7 @@ export function curveScale(curve: number): Scale {
188
214
  * @param skew the JUCE skew factor
189
215
  */
190
216
  export function symmetricSkewScale(skew: number): Scale {
217
+ assertPositiveFinite(skew, 'symmetricSkewScale')
191
218
  return {
192
219
  normalize: (value, min, max) => {
193
220
  assertRange(min, max)
@@ -265,20 +292,47 @@ export interface ValueRange {
265
292
  * @param direction which way, and how many times, to apply the option. The
266
293
  * size of one step is `option[1]`, so this is normally `1` or `-1`.
267
294
  *
295
+ * @param modifiers the event, for `options` that name a modifier key. See
296
+ * {@link selectInputEvent}.
297
+ *
268
298
  * @example
269
299
  * // ArrowDown on a slider whose keyboard option is ['raw', 1]
270
300
  * applyDelta(value, -1, keyboard, { min, max, step, scale })
301
+ *
302
+ * @example
303
+ * // Shift+ArrowDown, where `keyboard` is { default: …, shift: ['raw', 0.1] }
304
+ * applyDelta(value, -1, keyboard, range, event)
271
305
  */
272
306
  export function applyDelta(
273
307
  value: number,
274
308
  direction: number,
275
- [mode, amount]: InputEventOption,
309
+ options: ModifierValue<InputEventOption>,
276
310
  { min, max, step, scale = linearScale }: ValueRange,
311
+ modifiers?: ModifierState,
277
312
  ): number {
313
+ assertRange(min, max)
314
+ if (step !== undefined) assertPositiveFinite(step, 'applyDelta step')
315
+
316
+ const {
317
+ option: [mode, amount],
318
+ modifier,
319
+ } = selectInputEvent(options, modifiers)
320
+
278
321
  const x = direction * amount
279
322
  const next =
280
- mode == 'normalized'
323
+ mode === 'normalized'
281
324
  ? scale.denormalize(scale.normalize(value, min, max) + x, min, max)
282
325
  : value + x
283
- return clamp(step ? stepValue(next, step) : next, min, max)
326
+
327
+ // Naming a modifier is a deliberate request to move off the grid, so `step`
328
+ // does not apply to it. Without this a finer amount would round straight
329
+ // back to where it started: `stepValue(3 + 0.1, 1)` is 3.
330
+ const quantum = modifier === null ? step : undefined
331
+ const stepped = quantum !== undefined ? stepValue(next, quantum) : next
332
+
333
+ // Rounded before the clamp, so that `min` and `max` still have the last
334
+ // word and the value can land on them exactly. Without this the artefact
335
+ // accumulates: with no `step` to round it back, twelve presses of a 0.1
336
+ // modifier amount reach 5.699999999999998 rather than 5.7.
337
+ return clamp(toPrecision(stepped), min, max)
284
338
  }
package/src/unit.ts CHANGED
@@ -1,73 +1,204 @@
1
+ import { toPrecision } from './math'
2
+
1
3
  /**
2
- * Units a value can be displayed in, as `[symbol, scale]` pairs ordered from
3
- * the smallest scale up. A value is shown in the largest unit that does not
4
- * exceed it.
4
+ * The SI prefixes {@link unitFormat} chooses between.
5
5
  *
6
- * @example
7
- * [['Hz', 1], ['kHz', 1000]]
8
- * [['ms', 1], ['s', 1000]]
6
+ * Deliberately narrower than the full SI set: yocto through yotta are of no
7
+ * use to an audio control, and every extra prefix is one more symbol `parse`
8
+ * has to tell apart from a unit.
9
9
  */
10
- export type Units = [string, number][]
10
+ export type SIPrefix = 'p' | 'n' | 'µ' | 'm' | '' | 'k' | 'M' | 'G'
11
+
12
+ /** Ordered small to large. The empty symbol is the base unit. */
13
+ const PREFIXES: readonly [SIPrefix, number][] = [
14
+ ['p', 1e-12],
15
+ ['n', 1e-9],
16
+ ['µ', 1e-6],
17
+ ['m', 1e-3],
18
+ ['', 1],
19
+ ['k', 1e3],
20
+ ['M', 1e6],
21
+ ['G', 1e9],
22
+ ]
23
+
24
+ const PREFIX_SCALE = new Map<string, number>(PREFIXES)
11
25
 
12
26
  /**
13
- * Pick the unit a value is displayed in: the largest one whose scale does not
14
- * exceed the magnitude of the value.
27
+ * Micro is written three ways. `µ` (U+00B5 MICRO SIGN) is what `format`
28
+ * writes and what d3-format uses, `μ` (U+03BC GREEK SMALL LETTER MU) looks
29
+ * identical and is what a Greek keyboard produces, and `u` is what everyone
30
+ * actually types. All three read back the same.
15
31
  */
16
- export function selectUnit(units: Units, value: number): [string, number] {
17
- let i = 0
18
- for (; i < units.length; i++) {
19
- if (Math.abs(units[i][1]) > Math.abs(value)) break
20
- }
21
- return units[Math.max(0, i - 1)]
32
+ const MICRO_ALIASES: Record<string, SIPrefix> = { μ: 'µ', u: 'µ' }
33
+
34
+ export interface UnitFormatOptions {
35
+ /**
36
+ * The prefix the stored value is already in.
37
+ *
38
+ * A control that keeps milliseconds in `value` is `{ base: 'm' }` with a
39
+ * unit of `'s'`: 1500 then displays as `1.5s`, and `parse` gives 1500 back.
40
+ *
41
+ * @default ''
42
+ */
43
+ base?: SIPrefix
44
+ /**
45
+ * Whether to scale the number and pick a prefix at all.
46
+ *
47
+ * Turn it off for anything that is not an SI quantity. dB, %, cents and
48
+ * semitones do not take prefixes, and `-6dB` read as "-6 deci-B" is wrong
49
+ * rather than merely unusual.
50
+ *
51
+ * @default true
52
+ */
53
+ prefixes?: boolean
54
+ /**
55
+ * Digits after the decimal point. The number is left as-is when omitted.
56
+ */
57
+ digits?: number
58
+ /**
59
+ * Text placed between the number and the unit.
60
+ * @default ''
61
+ */
62
+ separator?: string
63
+ }
64
+
65
+ /** The `format` / `parse` pair a `NumberInput` takes. */
66
+ export interface UnitFormatter {
67
+ format: (value: number) => string
68
+ parse: (text: string) => number
22
69
  }
23
70
 
24
71
  /**
25
- * Render a value as text, in the unit that suits its magnitude.
26
- *
27
- * @param units a single symbol appended as-is, or a list to choose from.
28
- * @param digit digits after the decimal point. Left as-is when omitted.
72
+ * Divide by a prefix scale without showing the result of doing so in binary.
29
73
  *
30
- * @example
31
- * formatValue(1234, [['Hz', 1], ['kHz', 1000]], 2) // '1.23kHz'
32
- * formatValue(1.5, 'Hz') // '1.5Hz'
74
+ * `0.0005 / 1e-6` is 500.00000000000006, and with no `digits` to round it that
75
+ * lands in the input as written.
33
76
  */
34
- export function formatValue(
35
- value: number,
36
- units?: string | Units,
37
- digit?: number,
38
- ): string {
39
- const fixed = (v: number) =>
40
- digit != undefined ? v.toFixed(digit) : String(v)
41
- if (!units || typeof units == 'string') {
42
- return fixed(value) + (units ?? '')
43
- }
44
- const [unit, scale] = selectUnit(units, value)
45
- return fixed(value / scale) + unit
77
+ function scaleBy(value: number, scale: number): number {
78
+ return toPrecision(value / scale)
46
79
  }
47
80
 
81
+ /** A number, then whatever followed it. */
82
+ const NUMBER_THEN_REST =
83
+ /^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)\s*(.*)$/
84
+
48
85
  /**
49
- * Read a value back out of text, undoing the scaling of {@link formatValue}.
86
+ * Build the `format` and `parse` of a unit, as one pair.
87
+ *
88
+ * They are returned together because they have to agree: a `format` that
89
+ * writes `1.23kHz` is only useful next to a `parse` that reads it back as
90
+ * 1230. Spread the result into the input.
50
91
  *
51
- * With a list of units the text has to be a number followed by an optional
52
- * unit and nothing else, since the unit decides the scale; anything else reads
53
- * as 0. With a single unit, or none, the first number found anywhere in the
54
- * text is taken, so a half-typed entry still yields something.
92
+ * @example
93
+ * unitFormat('Hz') // 1234 -> '1.23kHz'
94
+ * unitFormat('s', { base: 'm' }) // value in ms. 1500 -> '1.5s'
95
+ * unitFormat('s', { base: 'm', digits: 2 }) // 1500 -> '1.50s'
96
+ * unitFormat('dB', { prefixes: false, digits: 1 }) // -6.25 -> '-6.3dB'
55
97
  *
56
98
  * @example
57
- * parseValue('1.23kHz', [['Hz', 1], ['kHz', 1000]]) // 1230
58
- * parseValue('4abc') // 4
99
+ * <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} onChange={setV}>
59
100
  */
60
- export function parseValue(text: string, units?: string | Units): number {
61
- const str = text.trim()
101
+ export function unitFormat(
102
+ unit: string,
103
+ options: UnitFormatOptions = {},
104
+ ): UnitFormatter {
105
+ const { base = '', prefixes = true, digits, separator = '' } = options
106
+ if (unit === '' && base !== '') {
107
+ throw new RangeError('unitFormat: base requires a non-empty unit')
108
+ }
109
+ const baseScale = PREFIX_SCALE.get(base) ?? 1
110
+
111
+ /**
112
+ * `toFixed` renders anything that rounds to zero from below as `-0`, which
113
+ * is never what a control should show.
114
+ */
115
+ const fixed = (value: number) => {
116
+ const text = digits !== undefined ? value.toFixed(digits) : String(value)
117
+ return Number(text) === 0 ? text.replace('-', '') : text
118
+ }
62
119
 
63
- if (!units || typeof units == 'string') {
64
- const m = str.match(/-?\d+(\.\d+)?/)
65
- const v = Number(m?.[0] ?? '0')
66
- return isNaN(v) ? 0 : v
120
+ if (!prefixes) {
121
+ // The stored value goes out untouched, so the symbol has to name the unit
122
+ // it is already in.
123
+ const symbol = base + unit
124
+ return {
125
+ format: (value) =>
126
+ Number.isFinite(value)
127
+ ? fixed(value) + separator + symbol
128
+ : String(value),
129
+ // Nothing after the number can change the scale, so it is all ignored:
130
+ // the number in front is the value, half-typed or not.
131
+ parse: (text) => {
132
+ const match = text.trim().match(NUMBER_THEN_REST)
133
+ if (!match) return NaN
134
+ const value = Number(match[1])
135
+ return Number.isFinite(value) ? value : NaN
136
+ },
137
+ }
67
138
  }
68
139
 
69
- const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/)
70
- if (!m) return 0
71
- const found = units.find(([unit]) => unit == m[3])
72
- return (Number(m[1]) || 0) * (found ? found[1] : 1)
140
+ return {
141
+ format: (value) => {
142
+ if (!Number.isFinite(value)) return String(value)
143
+ const si = value * baseScale
144
+ // Zero has no magnitude to read, so it stays in the base unit.
145
+ let index = PREFIXES.findIndex(([, scale]) => scale === 1)
146
+ if (si !== 0) {
147
+ // The largest prefix that leaves at least one digit before the point.
148
+ // Below the smallest prefix the number just gets small: `p` is the
149
+ // floor, as `G` is the ceiling.
150
+ const magnitude = Math.abs(si)
151
+ index = 0
152
+ for (let i = PREFIXES.length - 1; i >= 0; i--) {
153
+ if (magnitude >= PREFIXES[i][1]) {
154
+ index = i
155
+ break
156
+ }
157
+ }
158
+ }
159
+ let text = fixed(scaleBy(si, PREFIXES[index][1]))
160
+ // Rounding can carry the number up out of its own prefix — 999.99Hz at
161
+ // one digit is 1000.0Hz, which should read 1.0kHz. One step is always
162
+ // enough, since the carry is at most a factor of ten.
163
+ if (Math.abs(Number(text)) >= 1000 && index < PREFIXES.length - 1) {
164
+ index += 1
165
+ text = fixed(scaleBy(si, PREFIXES[index][1]))
166
+ }
167
+ return text + separator + PREFIXES[index][0] + unit
168
+ },
169
+
170
+ parse: (text) => {
171
+ const match = text.trim().match(NUMBER_THEN_REST)
172
+ if (!match) return NaN
173
+ const number = Number(match[1])
174
+ if (!Number.isFinite(number)) return NaN
175
+
176
+ let suffix = match[2].trim()
177
+ const separatorText = separator.trim()
178
+ if (separatorText !== '' && suffix.startsWith(separatorText)) {
179
+ suffix = suffix.slice(separatorText.length).trim()
180
+ }
181
+ // A bare number is in the unit the value is stored in, which is what
182
+ // the input shows once the format is stripped.
183
+ if (suffix === '') return number
184
+
185
+ // The unit symbol is matched first, so a unit that is itself a prefix
186
+ // letter wins over the prefix reading: `5m` for a unit of `m` is five
187
+ // metres, not five milli-.
188
+ let prefix: string | null = null
189
+ if (unit !== '' && suffix.endsWith(unit)) {
190
+ prefix = suffix.slice(0, suffix.length - unit.length)
191
+ } else if (suffix.length <= 1) {
192
+ prefix = suffix
193
+ }
194
+ if (prefix === null) return number
195
+
196
+ const normalized = MICRO_ALIASES[prefix] ?? prefix
197
+ const scale = PREFIX_SCALE.get(normalized)
198
+ // Unrecognised text after the number is ignored rather than rejected,
199
+ // so that a half-typed entry still yields the number in front of it.
200
+ if (scale === undefined) return number
201
+ return (number * scale) / baseScale
202
+ },
203
+ }
73
204
  }
package/src/util.ts CHANGED
@@ -1,42 +1,7 @@
1
- type Operator = '+' | '-' | '*' | '/'
2
-
3
- export function styleHelper(value: string | number): string
4
- export function styleHelper(
5
- value: string | number,
6
- op: Operator,
7
- influencer?: number,
8
- ): string
9
- export function styleHelper(
10
- value: string | number,
11
- op?: Operator,
12
- influencer?: number,
13
- ) {
14
- if (op && influencer) {
15
- if (typeof value == 'number') {
16
- if (op == '+') return `${value + influencer}px`
17
- if (op == '-') return `${value - influencer}px`
18
- if (op == '*') return `${value * influencer}px`
19
- if (op == '/') return `${value / influencer}px`
20
- } else {
21
- return `calc(${value}px ${op} ${influencer})`
22
- }
23
- } else {
24
- if (typeof value == 'number') {
25
- return `${value}px`
26
- } else {
27
- return value
28
- }
29
- }
30
- }
31
-
32
- export function isEmpty(obj: object) {
33
- return Object.keys(obj).length == 0
34
- }
35
-
36
1
  export function mod(n: number, m: number) {
37
2
  return ((n % m) + m) % m
38
3
  }
39
4
 
40
5
  export function xor(a = false, b = false) {
41
- return (a || b) && a != b
6
+ return (a || b) && a !== b
42
7
  }
package/src/types.ts DELETED
@@ -1,4 +0,0 @@
1
- /**
2
- * Options for setting the amount of keyboard and mouse wheel changes.
3
- */
4
- export type InputEventOption = ['normalized' | 'raw', number]