diff --git a/src/page/collapsible-panel-animator.ts b/src/page/collapsible-panel-animator.ts new file mode 100644 index 0000000..db81376 --- /dev/null +++ b/src/page/collapsible-panel-animator.ts @@ -0,0 +1,44 @@ +export class CollapsiblePanelAnimator { + private isOpen = false; + + public onOpen: () => unknown = () => {}; + public onClose: () => unknown = () => {}; + + public constructor( + infoButton: HTMLButtonElement, + private readonly infoPage: HTMLElement + ) { + infoButton.addEventListener('click', this.toggle.bind(this)); + window.addEventListener('click', (event) => { + if ([infoButton, this.infoPage].includes(event.target as HTMLElement)) { + return; + } + + if (this.infoPage.contains(event.target as Node)) { + return; + } + + this.close(); + }); + } + + public open() { + this.isOpen = true; + this.infoPage.classList.remove('hidden'); + this.onOpen(); + } + + public close() { + this.isOpen = false; + this.infoPage.classList.add('hidden'); + this.onClose(); + } + + public toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } +} diff --git a/src/page/info-page-handler.ts b/src/page/info-page-handler.ts deleted file mode 100644 index 91de6e2..0000000 --- a/src/page/info-page-handler.ts +++ /dev/null @@ -1,10 +0,0 @@ -export class InfoPageHandler { - public constructor( - private readonly infoButton: HTMLButtonElement, - private readonly infoPage: HTMLElement - ) { - infoButton.addEventListener('click', () => { - infoPage.classList.toggle('hidden'); - }); - } -} diff --git a/src/utils/persist.ts b/src/utils/persist.ts index bbab041..77142c5 100644 --- a/src/utils/persist.ts +++ b/src/utils/persist.ts @@ -1,9 +1,8 @@ export const persist = >(wrapee: T): T => { - const initialState = { ...wrapee }; const keys = Object.keys(wrapee); const keysToShortKeys = Object.fromEntries( - keys.map((key, i) => [key, String.fromCharCode(65 + i)]) + keys.map((key, i) => [key, String.fromCharCode(97 + i)]) ); const params = new URLSearchParams(window.location.search); @@ -25,11 +24,7 @@ export const persist = >(wrapee: T): T => { set: (target, key: string, value: number) => { const params = new URLSearchParams(window.location.search); - if (initialState[key] === value) { - params.delete(keysToShortKeys[key]); - } else { - params.set(keysToShortKeys[key], value.toString()); - } + params.set(keysToShortKeys[key], value.toString()); (target as any)[key] = value; diff --git a/src/utils/settings-slider.ts b/src/utils/settings-slider.ts index 1082c53..f72a69b 100644 --- a/src/utils/settings-slider.ts +++ b/src/utils/settings-slider.ts @@ -1,11 +1,19 @@ 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> { @@ -17,6 +25,8 @@ export class SettingsSlider> { private readonly config: SliderConfiguration = { min: 0, max: 1, + scaling: ValueScaling.Linear, + rounding: (value) => value, }; public constructor( @@ -24,7 +34,7 @@ export class SettingsSlider> { private readonly settingName: keyof T & string, config: Partial = {} ) { - this.slider = SettingsSlider.createSlider(this.settings[this.settingName]); + this.slider = SettingsSlider.createSlider(); this.valueDisplay = SettingsSlider.createValueDisplay(); this.sliderWrapper = SettingsSlider.createSliderWrapper( this.settingName, @@ -37,12 +47,9 @@ export class SettingsSlider> { this.updateConfig(config); } - private static createSlider(initialValue: any) { + private static createSlider() { const input = document.createElement('input'); - input.type = 'range'; - input.value = initialValue.toString(); - return input; } @@ -75,7 +82,9 @@ export class SettingsSlider> { } private onChange() { - this.settings[this.settingName] = Number(this.slider.value) as any; + 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], @@ -88,11 +97,14 @@ export class SettingsSlider> { if (this.config.step === undefined) { this.config.step = - (this.config.max - this.config.min) / SettingsSlider.DEFAULT_STEP_COUNT; + this.scaling(this.config.max - this.scaling(this.config.min)) / + SettingsSlider.DEFAULT_STEP_COUNT; } - this.slider.min = this.config.min.toString(); - this.slider.max = this.config.max.toString(); + 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(); @@ -101,4 +113,26 @@ export class SettingsSlider> { 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); + } + } }