Minor improvements

This commit is contained in:
Andras Schmelczer 2026-05-26 20:59:01 +01:00
commit 4e24df1511
2 changed files with 175 additions and 100 deletions

View file

@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest';
import {
ERASER_SIZE_MAX,
ERASER_SIZE_MIN,
getEffectiveEraserSize,
getEraserSizeFromSliderRatio,
getEraserSizeMaxForCssSize,
getEraserSliderRatioFromSize,
} from './eraser-size-control';
} from '../config/eraser-size';
describe('eraser size slider mapping', () => {
it('maps slider position quadratically to eraser size', () => {
@ -23,4 +25,15 @@ describe('eraser size slider mapping', () => {
expect(getEraserSliderRatioFromSize(quarterRangeSize)).toBe(0.5);
expect(getEraserSliderRatioFromSize(ERASER_SIZE_MAX)).toBe(1);
});
it('uses a responsive max size on small canvases', () => {
const mobileMax = getEraserSizeMaxForCssSize({ height: 640, width: 390 });
expect(mobileMax).toBeLessThan(ERASER_SIZE_MAX);
expect(getEraserSizeFromSliderRatio(1, mobileMax)).toBe(mobileMax);
expect(getEraserSliderRatioFromSize(ERASER_SIZE_MAX, mobileMax)).toBe(1);
expect(getEffectiveEraserSize(ERASER_SIZE_MAX, { height: 640, width: 390 })).toBe(
mobileMax
);
});
});

View file

@ -1,40 +1,25 @@
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';
export const ERASER_SIZE_MIN = 24;
export const ERASER_SIZE_MAX = 480;
const ERASER_CONTROL_SCALE_MIN = 0.74;
const ERASER_CONTROL_SCALE_MAX = 1.34;
const clampEraserSize = (value: number): number => {
const safeValue = Number.isFinite(value) ? value : DEFAULT_ERASER_SIZE;
return Math.min(ERASER_SIZE_MAX, Math.max(ERASER_SIZE_MIN, Math.round(safeValue)));
};
const ERASER_SLIDER_MIN = 0;
const ERASER_SLIDER_MAX = 1;
const ERASER_SLIDER_STEP = 0.001;
const clampSliderRatio = (value: number): number => {
const safeValue = Number.isFinite(value) ? value : ERASER_SLIDER_MIN;
return Math.min(ERASER_SLIDER_MAX, Math.max(ERASER_SLIDER_MIN, safeValue));
};
const getEraserSizeRatio = (size: number): number => {
return (clampEraserSize(size) - ERASER_SIZE_MIN) / (ERASER_SIZE_MAX - ERASER_SIZE_MIN);
};
export const getEraserSizeFromSliderRatio = (sliderRatio: number): number => {
return clampEraserSize(
ERASER_SIZE_MIN +
(ERASER_SIZE_MAX - ERASER_SIZE_MIN) * clampSliderRatio(sliderRatio) ** 2
);
};
export const getEraserSliderRatioFromSize = (size: number): number =>
Math.sqrt(getEraserSizeRatio(size));
const clampStoredEraserSize = (value: number): number =>
clampEraserSize(value, ERASER_SIZE_MAX, DEFAULT_ERASER_SIZE);
interface EraserSizeControlOptions {
getGame: () => GameLoop | null;
@ -48,6 +33,7 @@ export class EraserSizeControl {
HTMLLabelElement
);
private readonly slider = queryRequiredElement('.eraser-size-slider', HTMLInputElement);
private readonly canvas = queryRequiredElement('canvas', HTMLCanvasElement);
private isActive = false;
public constructor(private readonly options: EraserSizeControlOptions) {
@ -55,7 +41,10 @@ export class EraserSizeControl {
this.control.addEventListener('click', this.activate);
this.slider.addEventListener('focus', this.activate);
this.slider.addEventListener('input', () => {
settings.eraserSize = getEraserSizeFromSliderRatio(Number(this.slider.value));
settings.eraserSize = getEraserSizeFromSliderRatio(
Number(this.slider.value),
this.getResponsiveMaxSize()
);
this.activate();
this.render();
this.options.onChange();
@ -63,19 +52,21 @@ export class EraserSizeControl {
}
public render(): void {
const size = clampEraserSize(settings.eraserSize);
if (settings.eraserSize !== size) {
settings.eraserSize = size;
const maxSize = this.getResponsiveMaxSize();
const storedSize = clampStoredEraserSize(settings.eraserSize);
if (settings.eraserSize !== storedSize) {
settings.eraserSize = storedSize;
}
const sliderRatio = getEraserSliderRatioFromSize(size);
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);
const sizeRatio = getEraserSizeRatio(size, maxSize);
const scale =
ERASER_CONTROL_SCALE_MIN +
(ERASER_CONTROL_SCALE_MAX - ERASER_CONTROL_SCALE_MIN) * sizeRatio;
@ -95,6 +86,10 @@ export class EraserSizeControl {
this.options.onActivate();
};
private getResponsiveMaxSize(): number {
return getEraserSizeMaxForCssSize(getElementCssPixelSize(this.canvas));
}
private syncActiveState(): void {
this.control.classList.toggle('active', this.isActive);
this.slider.setAttribute(