fleeting-garden/src/page/eraser-size-control.ts

100 lines
3.2 KiB
TypeScript

import {
clampEraserSize,
ERASER_SIZE_MAX,
getElementCssPixelSize,
getEraserSizeFromSliderRatio,
getEraserSizeMaxForCssSize,
getEraserSizeRatio,
getEraserSliderRatioFromSize,
} from '../config/eraser-size';
import type GameLoop from '../game-loop/game-loop';
import { DEFAULT_ERASER_SIZE, settings } from '../settings';
import { queryRequiredElement } from '../utils/dom';
const ERASER_CONTROL_SCALE_MIN = 0.74;
const ERASER_CONTROL_SCALE_MAX = 1.34;
const ERASER_SLIDER_MIN = 0;
const ERASER_SLIDER_MAX = 1;
const ERASER_SLIDER_STEP = 0.001;
const clampStoredEraserSize = (value: number): number =>
clampEraserSize(value, ERASER_SIZE_MAX, DEFAULT_ERASER_SIZE);
interface EraserSizeControlOptions {
getGame: () => GameLoop | null;
onActivate: () => void;
onChange: () => void;
}
export class EraserSizeControl {
private readonly control = queryRequiredElement(
'.eraser-size-control',
HTMLLabelElement
);
private readonly slider = queryRequiredElement('.eraser-size-slider', HTMLInputElement);
private readonly canvas = queryRequiredElement('canvas', HTMLCanvasElement);
private isActive = false;
public constructor(private readonly options: EraserSizeControlOptions) {
this.control.addEventListener('pointerdown', this.activate);
this.control.addEventListener('click', this.activate);
this.slider.addEventListener('focus', this.activate);
this.slider.addEventListener('input', () => {
settings.eraserSize = getEraserSizeFromSliderRatio(
Number(this.slider.value),
this.getResponsiveMaxSize()
);
this.activate();
this.render();
this.options.onChange();
});
}
public render(): void {
const maxSize = this.getResponsiveMaxSize();
const storedSize = clampStoredEraserSize(settings.eraserSize);
if (settings.eraserSize !== storedSize) {
settings.eraserSize = storedSize;
}
const size = clampEraserSize(storedSize, maxSize, DEFAULT_ERASER_SIZE);
const sliderRatio = getEraserSliderRatioFromSize(size, maxSize);
this.slider.min = ERASER_SLIDER_MIN.toString();
this.slider.max = ERASER_SLIDER_MAX.toString();
this.slider.step = ERASER_SLIDER_STEP.toString();
this.slider.value = sliderRatio.toString();
this.slider.setAttribute('aria-valuetext', `${size}px`);
const sizeRatio = getEraserSizeRatio(size, maxSize);
const scale =
ERASER_CONTROL_SCALE_MIN +
(ERASER_CONTROL_SCALE_MAX - ERASER_CONTROL_SCALE_MIN) * sizeRatio;
this.control.style.setProperty('--eraser-progress', `${sliderRatio * 100}%`);
this.control.style.setProperty('--eraser-control-scale', scale.toFixed(3));
this.syncActiveState();
this.options.getGame()?.updateEraserPreview();
}
public setActive(isActive: boolean): void {
this.isActive = isActive;
this.syncActiveState();
}
private readonly activate = () => {
this.setActive(true);
this.options.onActivate();
};
private getResponsiveMaxSize(): number {
return getEraserSizeMaxForCssSize(getElementCssPixelSize(this.canvas));
}
private syncActiveState(): void {
this.control.classList.toggle('active', this.isActive);
this.slider.setAttribute(
'aria-label',
this.isActive ? 'Eraser size, active' : 'Eraser size'
);
}
}