|
| 1 | +const SLIDER_LIMITS = Object.freeze({ |
| 2 | + attackMs: Object.freeze({ min: 0, max: 250, step: 5, defaultValue: 5 }), |
| 3 | + durationMs: Object.freeze({ min: 60, max: 2000, step: 5, defaultValue: 180 }), |
| 4 | + frequencyHz: Object.freeze({ min: 80, max: 1800, step: 1, defaultValue: 880 }), |
| 5 | + noiseAmount: Object.freeze({ min: 0, max: 1, step: 0.01, defaultValue: 0.65 }), |
| 6 | + noiseDecayMs: Object.freeze({ min: 20, max: 600, step: 5, defaultValue: 95 }), |
| 7 | + noiseFilterHz: Object.freeze({ min: 400, max: 9000, step: 50, defaultValue: 5200 }), |
| 8 | + pitchSweepCents: Object.freeze({ min: -1200, max: 1200, step: 5, defaultValue: 700 }), |
| 9 | + releaseMs: Object.freeze({ min: 20, max: 700, step: 5, defaultValue: 90 }), |
| 10 | + volume: Object.freeze({ min: 0, max: 1, step: 0.01, defaultValue: 0.42 }) |
| 11 | +}); |
| 12 | + |
| 13 | +const SLIDER_INPUTS = Object.freeze([ |
| 14 | + Object.freeze({ soundKey: "attackMs", inputProperty: "attackInput" }), |
| 15 | + Object.freeze({ soundKey: "durationMs", inputProperty: "durationInput" }), |
| 16 | + Object.freeze({ soundKey: "frequencyHz", inputProperty: "frequencyInput" }), |
| 17 | + Object.freeze({ soundKey: "noiseAmount", inputProperty: "noiseAmountInput" }), |
| 18 | + Object.freeze({ soundKey: "noiseDecayMs", inputProperty: "noiseDecayInput" }), |
| 19 | + Object.freeze({ soundKey: "noiseFilterHz", inputProperty: "noiseFilterInput" }), |
| 20 | + Object.freeze({ soundKey: "pitchSweepCents", inputProperty: "pitchSweepInput" }), |
| 21 | + Object.freeze({ soundKey: "releaseMs", inputProperty: "releaseInput" }), |
| 22 | + Object.freeze({ soundKey: "volume", inputProperty: "volumeInput" }) |
| 23 | +]); |
| 24 | + |
1 | 25 | const DEFAULT_SOUND = Object.freeze({ |
2 | | - attackMs: 5, |
3 | | - durationMs: 180, |
4 | | - frequencyHz: 880, |
| 26 | + attackMs: SLIDER_LIMITS.attackMs.defaultValue, |
| 27 | + durationMs: SLIDER_LIMITS.durationMs.defaultValue, |
| 28 | + frequencyHz: SLIDER_LIMITS.frequencyHz.defaultValue, |
5 | 29 | name: "Coin", |
6 | 30 | noise: false, |
7 | | - noiseAmount: 0.65, |
8 | | - noiseDecayMs: 95, |
9 | | - noiseFilterHz: 5200, |
10 | | - pitchSweepCents: 700, |
11 | | - releaseMs: 90, |
12 | | - volume: 0.42, |
| 31 | + noiseAmount: SLIDER_LIMITS.noiseAmount.defaultValue, |
| 32 | + noiseDecayMs: SLIDER_LIMITS.noiseDecayMs.defaultValue, |
| 33 | + noiseFilterHz: SLIDER_LIMITS.noiseFilterHz.defaultValue, |
| 34 | + pitchSweepCents: SLIDER_LIMITS.pitchSweepCents.defaultValue, |
| 35 | + releaseMs: SLIDER_LIMITS.releaseMs.defaultValue, |
| 36 | + volume: SLIDER_LIMITS.volume.defaultValue, |
13 | 37 | waveform: "square" |
14 | 38 | }); |
15 | 39 |
|
@@ -138,10 +162,9 @@ function toNumber(input) { |
138 | 162 | return Number.parseFloat(input.value); |
139 | 163 | } |
140 | 164 |
|
141 | | -function readRange(input) { |
| 165 | +function readRange(input, limits) { |
142 | 166 | const value = toNumber(input); |
143 | | - const min = Number.parseFloat(input.min); |
144 | | - const max = Number.parseFloat(input.max); |
| 167 | + const { min, max } = limits; |
145 | 168 | if (!Number.isFinite(value) || value < min || value > max) { |
146 | 169 | return { ok: false, message: `${input.id} must be between ${min} and ${max}.` }; |
147 | 170 | } |
@@ -204,6 +227,7 @@ export class SfxControlPanel { |
204 | 227 | } |
205 | 228 |
|
206 | 229 | mount({ onAdd, onChange, onDelete }) { |
| 230 | + this.applySliderLimits(); |
207 | 231 | this.loadSound(DEFAULT_SOUND); |
208 | 232 | this.addButton.addEventListener("click", onAdd); |
209 | 233 | this.deleteButton.addEventListener("click", onDelete); |
@@ -236,6 +260,31 @@ export class SfxControlPanel { |
236 | 260 | onChange(); |
237 | 261 | }); |
238 | 262 | }); |
| 263 | + this.sliderInputs().forEach((input) => { |
| 264 | + input.addEventListener("pointerdown", () => this.focusSlider(input)); |
| 265 | + input.addEventListener("input", () => this.focusSlider(input)); |
| 266 | + input.addEventListener("change", () => this.focusSlider(input)); |
| 267 | + }); |
| 268 | + } |
| 269 | + |
| 270 | + sliderInputs() { |
| 271 | + return SLIDER_INPUTS.map((item) => this[item.inputProperty]); |
| 272 | + } |
| 273 | + |
| 274 | + applySliderLimits() { |
| 275 | + SLIDER_INPUTS.forEach((item) => { |
| 276 | + const input = this[item.inputProperty]; |
| 277 | + const limits = SLIDER_LIMITS[item.soundKey]; |
| 278 | + input.min = String(limits.min); |
| 279 | + input.max = String(limits.max); |
| 280 | + input.step = String(limits.step); |
| 281 | + }); |
| 282 | + } |
| 283 | + |
| 284 | + focusSlider(input) { |
| 285 | + if (typeof input.focus === "function") { |
| 286 | + input.focus({ preventScroll: true }); |
| 287 | + } |
239 | 288 | } |
240 | 289 |
|
241 | 290 | loadSound(sound) { |
@@ -297,15 +346,15 @@ export class SfxControlPanel { |
297 | 346 | if (!ALLOWED_WAVEFORMS.has(this.waveformSelect.value)) { |
298 | 347 | return { valid: false, message: `Unsupported waveform: ${this.waveformSelect.value}.` }; |
299 | 348 | } |
300 | | - const frequency = readRange(this.frequencyInput); |
301 | | - const duration = readRange(this.durationInput); |
302 | | - const attack = readRange(this.attackInput); |
303 | | - const release = readRange(this.releaseInput); |
304 | | - const volume = readRange(this.volumeInput); |
305 | | - const pitchSweep = readRange(this.pitchSweepInput); |
306 | | - const noiseAmount = readRange(this.noiseAmountInput); |
307 | | - const noiseDecay = readRange(this.noiseDecayInput); |
308 | | - const noiseFilter = readRange(this.noiseFilterInput); |
| 349 | + const frequency = readRange(this.frequencyInput, SLIDER_LIMITS.frequencyHz); |
| 350 | + const duration = readRange(this.durationInput, SLIDER_LIMITS.durationMs); |
| 351 | + const attack = readRange(this.attackInput, SLIDER_LIMITS.attackMs); |
| 352 | + const release = readRange(this.releaseInput, SLIDER_LIMITS.releaseMs); |
| 353 | + const volume = readRange(this.volumeInput, SLIDER_LIMITS.volume); |
| 354 | + const pitchSweep = readRange(this.pitchSweepInput, SLIDER_LIMITS.pitchSweepCents); |
| 355 | + const noiseAmount = readRange(this.noiseAmountInput, SLIDER_LIMITS.noiseAmount); |
| 356 | + const noiseDecay = readRange(this.noiseDecayInput, SLIDER_LIMITS.noiseDecayMs); |
| 357 | + const noiseFilter = readRange(this.noiseFilterInput, SLIDER_LIMITS.noiseFilterHz); |
309 | 358 | const failed = [frequency, duration, attack, release, volume, pitchSweep, noiseAmount, noiseDecay, noiseFilter].find((result) => !result.ok); |
310 | 359 | if (failed) { |
311 | 360 | return { valid: false, message: failed.message }; |
|
0 commit comments