Files
gacha-event-tracker/src/client/state/zoom.ts
T
Lucas WintherandClaude Opus 5 5761e89163 feat: open the timeline two steps further in
Thirteen px/day opened the board on about a quarter of calendar, which
is more time than these schedules are written in: a patch is six weeks,
so most bars were short enough that their length stopped reading as a
duration and their title no longer fit inside the bar it named. Thirty-
two opens on roughly one patch cycle on a laptop.

Only new readers move. `prefs` is written on every load, so anyone who
has opened the app has a timelineDayWidth of their own and keeps it. A
phone now shows about twelve days rather than thirty, which is the cost;
the longer view is one press of − away and that answer is remembered.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-18 22:23:58 +02:00

72 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* How far the timeline is zoomed in, expressed as the width of one day.
*
* A patch cycle is six weeks and a login campaign can run for months, so no
* single scale answers both "what am I in the middle of this week?" and "how do
* the next three months line up?". The reader picks.
*
* Pure, and its own module rather than a constant inside `Timeline`, because
* `prefs` stores the chosen value and the two must agree on what is valid.
*/
/** The ladder, in px per day. Roughly a third wider at each step. */
export const DAY_WIDTHS = [6, 9, 13, 20, 32, 48] as const;
/**
* The scale the board opens at for a reader who has never touched the control.
*
* Two steps up from where it used to sit. Thirteen px/day opened on about a
* quarter of calendar, and a quarter is more time than these schedules are
* written in: a patch is six weeks, so most bars were short enough that the
* length stopped reading as a duration and the title stopped fitting inside the
* bar it belonged to. Thirty-two opens on roughly a patch cycle on a laptop —
* bars long enough to compare by eye, and wide enough to carry their own name.
* The longer view is one press of away, and that answer is remembered.
*
* This moves new readers only: `prefs` is written on every load, so anyone who
* has opened the app has a `timelineDayWidth` of their own and keeps it.
*/
export const DEFAULT_DAY_WIDTH = 32;
/**
* The nearest valid scale to a stored number.
*
* `prefs` is a file on someone's device that an export/import round trip can
* carry between versions, so the ladder is allowed to change and a value off
* it must not render a board one pixel wide. Anything unusable falls back to
* the default rather than to the nearest edge — a corrupt value is not a
* preference.
*/
export function snapDayWidth(px: number): number {
if (!Number.isFinite(px) || px <= 0) return DEFAULT_DAY_WIDTH;
// `<=` over an ascending ladder means a value sitting exactly between two
// steps takes the wider one. Ties go to the more legible board.
return DAY_WIDTHS.reduce((best, step) =>
Math.abs(step - px) <= Math.abs(best - px) ? step : best,
);
}
/** One step in or out, stopping at the ends of the ladder. */
export function stepDayWidth(px: number, by: 1 | -1): number {
const at = DAY_WIDTHS.indexOf(snapDayWidth(px) as (typeof DAY_WIDTHS)[number]);
return DAY_WIDTHS[Math.min(Math.max(at + by, 0), DAY_WIDTHS.length - 1)] ?? DEFAULT_DAY_WIDTH;
}
/** Whether there is anywhere further to go in that direction. */
export function canStep(px: number, by: 1 | -1): boolean {
return stepDayWidth(px, by) !== snapDayWidth(px);
}
/**
* How many weeks apart the dated ticks on the axis are.
*
* Every Monday is right at the default scale and unreadable at the widest zoom
* out, where a week is 42px and the labels would sit on top of each other. The
* gridlines stay weekly either way — they are hairlines and they carry the
* rhythm; it is only the dates that have to thin out.
*/
export function weekLabelStep(dayWidth: number): number {
const MIN_LABEL_GAP = 64;
return Math.max(1, Math.ceil(MIN_LABEL_GAP / (7 * dayWidth)));
}