/** * The rules of a Knob (#378): where on its turn a value sits, and what a drag or a wheel makes of * it. A turn is a position from 0 to 1; `taper` bends it, so a range whose useful part is small – * an attack of a few milliseconds out of a second – gets most of the turn. */ import { clamp, snapToStep } from './math'; export interface KnobRange { min: number; max: number; /** The grid a value snaps to. */ step: number; /** 1 turns evenly; above it the low end gets more of the turn. */ taper?: number; } /** Pixels of a vertical drag that take the knob from one end to the other. */ export const KNOB_TRAVEL = 200; /** How much of the turn Shift leaves to a drag. */ export const KNOB_FINE = 0.1; /** How far one notch of the wheel turns the knob. */ export const KNOB_NOTCH = 0.02; /** The turn as an angle: from down-left round over the top to down-right, in radians. */ export const KNOB_START = Math.PI * 0.75; export const KNOB_SWEEP = Math.PI * 1.5; export function knobPosition(value: number, range: KnobRange): number { const share = clamp((value - range.min) / (range.max - range.min), 0, 1); return share ** (1 / (range.taper ?? 1)); } export function knobValue(position: number, range: KnobRange): number { const share = clamp(position, 0, 1) ** (range.taper ?? 1); return clamp(snapToStep(range.min + share * (range.max - range.min), range.step), range.min, range.max); } /** * The value a vertical drag reaches from `start`: up turns it up. A drag too short to reach the * next step still moves it by one, so a fine drag over a coarse range is never stuck. */ export function draggedValue(start: number, dy: number, fine: boolean, range: KnobRange): number { const travel = (-dy / KNOB_TRAVEL) * (fine ? KNOB_FINE : 1); const value = knobValue(knobPosition(start, range) + travel, range); if (value !== start || travel === 0) return value; return clamp(snapToStep(start + Math.sign(travel) * range.step, range.step), range.min, range.max); } /** The value one notch of the wheel reaches: up turns it up, by at least one step. */ export function wheeledValue(value: number, deltaY: number, range: KnobRange): number { return draggedValue(value, Math.sign(deltaY) * KNOB_NOTCH * KNOB_TRAVEL, false, range); } /** Where the filled arc starts: at 0 when the range has it inside, else at the low end. */ export function knobOrigin(range: KnobRange): number { return range.min < 0 && range.max > 0 ? knobPosition(0, range) : 0; }