refactor - improve timeline readability, general refactoring, renaming variables, functions, props, etc

This commit is contained in:
midzelis
2025-08-25 00:16:07 +00:00
parent 9e43b0625a
commit 1a754b868c
3 changed files with 107 additions and 67 deletions
@@ -292,7 +292,7 @@
</div> </div>
{/if} {/if}
{/each} {/each}
<!-- spacer for leadout --> <!-- spacer for lead-out -->
<div <div
class="h-[60px]" class="h-[60px]"
style:position="absolute" style:position="absolute"
@@ -5,7 +5,7 @@
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import type { ScrubberListener } from '$lib/utils/timeline-util'; import type { ScrubberListener, TimelineYearMonth } from '$lib/utils/timeline-util';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import Scrubber from './scrubber.svelte'; import Scrubber from './scrubber.svelte';
@@ -51,16 +51,34 @@
empty, empty,
}: Props = $props(); }: Props = $props();
let leadOut = $state(false); // Constants for timeline calculations
let scrubberMonthPercent = $state(0); const VIEWPORT_MULTIPLIER = 2; // Used to determine if timeline is "small"
let scrubberMonth: { year: number; month: number } | undefined = $state(undefined); const SUBPIXEL_TOLERANCE = -1; // Tolerance for scroll position checks
let scrubOverallPercent: number = $state(0); const NEAR_END_THRESHOLD = 0.9999; // Threshold for detecting near-end of month
// Type for month section data
interface MonthSection {
height: number;
monthGroup?: { year: number; month: number };
type: 'lead-in' | 'lead-out' | 'month';
}
// Constants for month loop bounds
const MONTH_LOOP_START = -1; // Represents lead-in section
const getMonthLoopEnd = (monthsLength: number) => monthsLength + 1; // +1 for lead-out
let isInLeadOutSection = $state(false);
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
// Note: There may be multiple months visible within the viewport at any given time.
let viewportTopMonthScrollPercent = $state(0);
let viewportTopMonth: TimelineYearMonth | undefined = $state(undefined);
let timelineScrollPercent: number = $state(0);
let scrubberWidth: number = $state(0); let scrubberWidth: number = $state(0);
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker // note: don't throttle, debounce, or otherwise make this function async - it causes flicker
// this function updates the scrubber position based on the current scroll position in the timeline // this function updates the scrubber position based on the current scroll position in the timeline
const handleTimelineScroll = () => { const handleTimelineScroll = () => {
leadOut = false; isInLeadOutSection = false;
// Handle small timeline edge case: scroll limited due to size of content // Handle small timeline edge case: scroll limited due to size of content
if (isSmallTimeline()) { if (isSmallTimeline()) {
@@ -80,111 +98,131 @@
}; };
const isSmallTimeline = () => { const isSmallTimeline = () => {
return timelineManager.timelineHeight < timelineManager.viewportHeight * 2; return timelineManager.timelineHeight < timelineManager.viewportHeight * VIEWPORT_MULTIPLIER;
};
const isNearMonthBoundary = (progress: number) => {
return progress > NEAR_END_THRESHOLD;
};
const canAdvanceToNextMonth = (currentIndex: number, monthsLength: number) => {
return currentIndex + 1 < monthsLength - 1;
}; };
const resetScrubberMonth = () => { const resetScrubberMonth = () => {
scrubberMonth = undefined; viewportTopMonth = undefined;
scrubberMonthPercent = 0; viewportTopMonthScrollPercent = 0;
};
const calculateTimelineScrollPercent = () => {
const maxScroll = timelineManager.getMaxScroll();
timelineScrollPercent = Math.min(1, timelineManager.visibleWindow.top / maxScroll);
resetScrubberMonth();
}; };
const handleSmallTimelineScroll = () => { const handleSmallTimelineScroll = () => {
const maxScroll = timelineManager.getMaxScroll(); calculateTimelineScrollPercent();
scrubOverallPercent = Math.min(1, timelineManager.visibleWindow.top / maxScroll);
resetScrubberMonth();
}; };
const handleLeadInScroll = () => { const handleLeadInScroll = () => {
const maxScroll = timelineManager.getMaxScroll(); calculateTimelineScrollPercent();
scrubOverallPercent = Math.min(1, timelineManager.visibleWindow.top / maxScroll);
resetScrubberMonth();
}; };
const handleMonthScroll = () => { const handleMonthScroll = () => {
const monthsLength = timelineManager.months.length; const monthsLength = timelineManager.months.length;
const maxScrollPercent = timelineManager.getMaxScrollPercent(); const maxScrollPercent = timelineManager.getMaxScrollPercent();
let top = timelineManager.visibleWindow.top; let remainingScrollDistance = timelineManager.visibleWindow.top;
let found = false; // Tracks if we found the month intersecting the viewport top
let foundIntersectingMonth = false;
for (let i = -1; i < monthsLength + 1; i++) { for (let i = MONTH_LOOP_START; i < getMonthLoopEnd(monthsLength); i++) {
const monthData = getMonthData(i); const monthSection = getMonthSection(i);
const next = top - monthData.height * maxScrollPercent; const nextRemainingDistance = remainingScrollDistance - monthSection.height * maxScrollPercent;
// Check if we're in this month (with subpixel tolerance) // Check if we're in this month (with subpixel tolerance)
if (next < -1 && monthData.monthGroup) { if (nextRemainingDistance < SUBPIXEL_TOLERANCE && monthSection.monthGroup) {
scrubberMonth = monthData.monthGroup; viewportTopMonth = monthSection.monthGroup;
// Calculate month percentage // Calculate how far we've scrolled into this month as a percentage
scrubberMonthPercent = Math.max(0, top / (monthData.height * maxScrollPercent)); viewportTopMonthScrollPercent = Math.max(0, remainingScrollDistance / (monthSection.height * maxScrollPercent));
// Handle rounding errors (and/or subpixel tolerance) - // Handle rounding errors (and/or subpixel tolerance) -
// advance to next month if almost at end // advance to next month if almost at end
if (scrubberMonthPercent > 0.9999 && i + 1 < monthsLength - 1) { if (isNearMonthBoundary(viewportTopMonthScrollPercent) && canAdvanceToNextMonth(i, monthsLength)) {
scrubberMonth = timelineManager.months[i + 1].yearMonth; viewportTopMonth = timelineManager.months[i + 1].yearMonth;
scrubberMonthPercent = 0; viewportTopMonthScrollPercent = 0;
} }
found = true; foundIntersectingMonth = true;
break; break;
} }
top = next; remainingScrollDistance = nextRemainingDistance;
} }
if (!found) { if (!foundIntersectingMonth) {
leadOut = true; isInLeadOutSection = true;
scrubOverallPercent = 1; timelineScrollPercent = 1;
resetScrubberMonth(); resetScrubberMonth();
} }
}; };
const getMonthData = (index: number) => { const getMonthSection = (index: number): MonthSection => {
const monthsLength = timelineManager.months.length; const monthsLength = timelineManager.months.length;
if (index === -1) { if (index === MONTH_LOOP_START) {
// lead-in
return { return {
type: 'lead-in',
height: timelineManager.topSectionHeight, height: timelineManager.topSectionHeight,
monthGroup: undefined, monthGroup: undefined,
}; };
} }
if (index === monthsLength) { if (index === monthsLength) {
// lead-out
return { return {
type: 'lead-out',
height: timelineManager.bottomSectionHeight, height: timelineManager.bottomSectionHeight,
monthGroup: undefined, monthGroup: undefined,
}; };
} }
// normal month
return { return {
type: 'month',
height: timelineManager.months[index].height, height: timelineManager.months[index].height,
monthGroup: timelineManager.months[index].yearMonth, monthGroup: timelineManager.months[index].yearMonth,
}; };
}; };
const handleOverallPercentScroll = (percent: number, scrollTo?: (offset: number) => void) => {
const maxScroll = timelineManager.getMaxScroll();
const offset = maxScroll * percent;
scrollTo?.(offset);
};
const findMonthGroup = (target: TimelineYearMonth) => {
return timelineManager.months.find(
({ yearMonth }) => yearMonth.year === target.year && yearMonth.month === target.month,
);
};
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker // note: don't throttle, debounce, or otherwise make this function async - it causes flicker
// this function scrolls the timeline to the specified month group and offset, based on scrubber interaction // this function scrolls the timeline to the specified month group and offset, based on scrubber interaction
const onScrub: ScrubberListener = (scrubberData) => { const onScrub: ScrubberListener = (scrubberData) => {
const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent, scrollToFunction } = scrubberData; const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent, scrollToFunction } = scrubberData;
if (!scrubberMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) { // Handle edge case or no month selected
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead if (!scrubberMonth || isSmallTimeline()) {
const maxScroll = timelineManager.getMaxScroll(); handleOverallPercentScroll(overallScrollPercent, scrollToFunction);
const offset = maxScroll * overallScrollPercent;
scrollToFunction?.(offset);
return; return;
} }
const monthGroup = timelineManager.months.find(
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month, // Find and scroll to the selected month
); const monthGroup = findMonthGroup(scrubberMonth);
if (!monthGroup) { if (monthGroup) {
return; scrollToPositionWithinMonth(monthGroup, scrubberMonthScrollPercent, scrollToFunction);
} }
scrollToMonthGroupAndOffset(monthGroup, scrubberMonthScrollPercent, scrollToFunction);
}; };
const scrollToMonthGroupAndOffset = ( const scrollToPositionWithinMonth = (
monthGroup: MonthGroup, monthGroup: MonthGroup,
monthGroupScrollPercent: number, monthGroupScrollPercent: number,
handleScrollTop?: (top: number) => void, handleScrollTop?: (top: number) => void,
@@ -218,18 +256,18 @@
{empty} {empty}
{handleTimelineScroll} {handleTimelineScroll}
> >
{#snippet header(scrollTo)} {#snippet header(scrollToFunction)}
{#if timelineManager.months.length > 0} {#if timelineManager.months.length > 0}
<Scrubber <Scrubber
{timelineManager} {timelineManager}
height={timelineManager.viewportHeight} height={timelineManager.viewportHeight}
timelineTopOffset={timelineManager.topSectionHeight} timelineTopOffset={timelineManager.topSectionHeight}
timelineBottomOffset={timelineManager.bottomSectionHeight} timelineBottomOffset={timelineManager.bottomSectionHeight}
{leadOut} {isInLeadOutSection}
{scrubOverallPercent} {timelineScrollPercent}
{scrubberMonthPercent} {viewportTopMonthScrollPercent}
{scrubberMonth} {viewportTopMonth}
onScrub={(args) => onScrub({ ...args, scrollToFunction: scrollTo })} onScrub={(scrubberData) => onScrub({ ...scrubberData, scrollToFunction })}
bind:scrubberWidth bind:scrubberWidth
/> />
{/if} {/if}
@@ -4,7 +4,7 @@
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types'; import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { getTabbable } from '$lib/utils/focus-util'; import { getTabbable } from '$lib/utils/focus-util';
import { type ScrubberListener } from '$lib/utils/timeline-util'; import { type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
import { mdiPlay } from '@mdi/js'; import { mdiPlay } from '@mdi/js';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@@ -15,10 +15,10 @@
timelineBottomOffset?: number; timelineBottomOffset?: number;
height?: number; height?: number;
timelineManager: TimelineManager; timelineManager: TimelineManager;
scrubOverallPercent?: number; timelineScrollPercent?: number;
scrubberMonthPercent?: number; viewportTopMonthScrollPercent?: number;
scrubberMonth?: { year: number; month: number }; viewportTopMonth?: TimelineYearMonth;
leadOut?: boolean; isInLeadOutSection?: boolean;
scrubberWidth?: number; scrubberWidth?: number;
onScrub?: ScrubberListener; onScrub?: ScrubberListener;
onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void; onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
@@ -31,10 +31,10 @@
timelineBottomOffset = 0, timelineBottomOffset = 0,
height = 0, height = 0,
timelineManager, timelineManager,
scrubOverallPercent = 0, timelineScrollPercent = 0,
scrubberMonthPercent = 0, viewportTopMonthScrollPercent = 0,
scrubberMonth = undefined, viewportTopMonth = undefined,
leadOut = false, isInLeadOutSection = false,
onScrub = undefined, onScrub = undefined,
onScrubKeyDown = undefined, onScrubKeyDown = undefined,
startScrub = undefined, startScrub = undefined,
@@ -100,7 +100,7 @@
offset += scrubberMonthPercent * relativeBottomOffset; offset += scrubberMonthPercent * relativeBottomOffset;
} }
return offset; return offset;
} else if (leadOut) { } else if (isInLeadOutSection) {
let offset = relativeTopOffset; let offset = relativeTopOffset;
for (const segment of segments) { for (const segment of segments) {
offset += segment.height; offset += segment.height;
@@ -111,7 +111,9 @@
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM)); return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
} }
}; };
let scrollY = $derived(toScrollFromMonthGroupPercentage(scrubberMonth, scrubberMonthPercent, scrubOverallPercent)); let scrollY = $derived(
toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
);
let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset); let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight)); let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight)); let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));