fleeting-garden/src/utils/settings-slider.ts

138 lines
3.7 KiB
TypeScript

import { formatNumber } from './format-number';
export enum ValueScaling {
Linear,
Quadratic,
Logarithmic,
}
export interface SliderConfiguration {
min: number;
max: number;
unit?: string;
step?: number;
onChangeCallback?: (value: number) => unknown;
scaling: ValueScaling;
rounding: (value: number) => number;
}
export class SettingsSlider<T extends Record<string, number>> {
private static readonly DEFAULT_STEP_COUNT = 200;
private readonly slider: HTMLInputElement;
private readonly valueDisplay: HTMLSpanElement;
private readonly sliderWrapper: HTMLDivElement;
private readonly config: SliderConfiguration = {
min: 0,
max: 1,
scaling: ValueScaling.Linear,
rounding: (value) => value,
};
public constructor(
private readonly settings: T,
private readonly settingName: keyof T & string,
config: Partial<SliderConfiguration> = {}
) {
this.slider = SettingsSlider.createSlider();
this.valueDisplay = SettingsSlider.createValueDisplay();
this.sliderWrapper = SettingsSlider.createSliderWrapper(
this.settingName,
this.slider,
this.valueDisplay
);
this.slider.addEventListener('input', this.onChange.bind(this));
this.updateConfig(config);
}
private static createSlider() {
const input = document.createElement('input');
input.type = 'range';
return input;
}
private static createValueDisplay() {
return document.createElement('span');
}
private static createSliderWrapper(
name: string,
slider: HTMLInputElement,
valueDisplay: HTMLSpanElement
) {
const wrapper = document.createElement('div');
const label = document.createElement('label');
label.innerText = SettingsSlider.formatLabel(name);
label.appendChild(slider);
label.appendChild(valueDisplay);
wrapper.appendChild(label);
return wrapper;
}
private static formatLabel(value: string): string {
const formatted = value.replace(/([A-Z])/g, ' $1');
return (
formatted.charAt(0).toLocaleUpperCase() + formatted.slice(1).toLocaleLowerCase()
);
}
private onChange() {
this.settings[this.settingName] = this.config.rounding(
this.inverseScaling(Number(this.slider.value))
) as any;
this.config.onChangeCallback?.(this.settings[this.settingName]);
this.valueDisplay.innerText = formatNumber(
this.settings[this.settingName],
this.config.unit
);
}
public updateConfig(config: Partial<SliderConfiguration>) {
Object.assign(this.config, config);
if (this.config.step === undefined) {
this.config.step =
this.scaling(this.config.max - this.scaling(this.config.min)) /
SettingsSlider.DEFAULT_STEP_COUNT;
}
this.slider.value = this.scaling(this.settings[this.settingName]).toString();
this.slider.min = this.scaling(this.config.min).toString();
this.slider.max = this.scaling(this.config.max).toString();
this.slider.step = this.config.step.toString();
this.onChange();
}
public get element(): HTMLElement {
return this.sliderWrapper;
}
private get scaling(): (value: number) => number {
switch (this.config.scaling) {
case ValueScaling.Linear:
return (value) => value;
case ValueScaling.Quadratic:
return (value) => Math.sqrt(value);
case ValueScaling.Logarithmic:
return (value) => Math.log10(value);
}
}
private get inverseScaling(): (value: number) => number {
switch (this.config.scaling) {
case ValueScaling.Linear:
return (value) => value;
case ValueScaling.Quadratic:
return (value) => Math.pow(value, 2);
case ValueScaling.Logarithmic:
return (value) => Math.pow(10, value);
}
}
}