Zum Inhalt

src/ui/sliderInput.ts

// Pure math helpers for ParamSlider input handling. Extracted so the
// modifier-key behaviour (Shift = fine, Alt = coarse) can be tested
// without a DOM. The component glues these to pointer / wheel / key
// events.

export interface Range {
  readonly min: number;
  readonly max: number;
  readonly step?: number;
}

export type DragMode = 'absolute' | 'fine' | 'coarse';

/** Shift = fine (10× slower), Alt = coarse (4× faster). Plain = absolute. */
export function dragModeFrom(e: { shiftKey: boolean; altKey: boolean }): DragMode {
  if (e.shiftKey) return 'fine';
  if (e.altKey) return 'coarse';
  return 'absolute';
}

/** Snap to step if present and >0. Otherwise pass through. */
export function snapToStep(v: number, step: number | undefined): number {
  if (step && step > 0) return Math.round(v / step) * step;
  return v;
}

export function clampToRange(v: number, range: Range): number {
  return Math.max(range.min, Math.min(range.max, v));
}

/** Maps a pointer x-coordinate inside the track to the absolute value. */
export function valueFromAbsolute(
  clientX: number,
  rectLeft: number,
  rectWidth: number,
  range: Range,
): number {
  if (rectWidth <= 0) return range.min;
  const ratio = Math.max(0, Math.min(1, (clientX - rectLeft) / rectWidth));
  const raw = range.min + ratio * (range.max - range.min);
  return clampToRange(snapToStep(raw, range.step), range);
}

/**
 * Incremental drag mode: the value moves relative to where the pointer
 * was when the drag started. Shift = ×0.1, Alt = ×4. The "base rate" is
 * the same as absolute mode (track width covers the full range), so a
 * full drag across the track equals a full sweep in default mode, 0.1
 * sweeps in fine mode, and 4 sweeps in coarse mode.
 */
export function valueFromIncremental(args: {
  clientX: number;
  startX: number;
  startValue: number;
  rectWidth: number;
  mode: 'fine' | 'coarse';
  range: Range;
}): number {
  if (args.rectWidth <= 0) return clampToRange(args.startValue, args.range);
  const baseRate = (args.range.max - args.range.min) / args.rectWidth;
  const mult = args.mode === 'fine' ? 0.1 : 4;
  const delta = (args.clientX - args.startX) * baseRate * mult;
  return clampToRange(snapToStep(args.startValue + delta, args.range.step), args.range);
}

/** Wheel: 1 step at default, 0.1× with Shift (fine), 10× with Alt (coarse). */
export function valueFromWheel(
  current: number,
  deltaY: number,
  modifier: { shiftKey: boolean; altKey: boolean },
  range: Range,
): number {
  const step = range.step ?? (range.max - range.min) / 100;
  const mult = modifier.shiftKey ? 0.1 : modifier.altKey ? 10 : 1;
  // Wheel up (negative deltaY) = increase value, the way browser sliders behave.
  const delta = -Math.sign(deltaY) * step * mult;
  return clampToRange(snapToStep(current + delta, range.step), range);
}

/**
 * Keyboard arrows / Home / End. Returns the new value or `null` if the
 * key isn't a slider control. Caller is responsible for preventDefault.
 */
export function valueFromKey(
  current: number,
  key: string,
  modifier: { shiftKey: boolean; altKey: boolean },
  range: Range,
): number | null {
  if (key === 'Home') return clampToRange(range.min, range);
  if (key === 'End') return clampToRange(range.max, range);
  const step = range.step ?? (range.max - range.min) / 100;
  const mult = modifier.shiftKey ? 0.1 : modifier.altKey ? 10 : 1;
  let dir = 0;
  if (key === 'ArrowLeft' || key === 'ArrowDown') dir = -1;
  else if (key === 'ArrowRight' || key === 'ArrowUp') dir = 1;
  else return null;
  return clampToRange(snapToStep(current + dir * step * mult, range.step), range);
}