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

View File

@@ -292,7 +292,7 @@
</div>
{/if}
{/each}
<!-- spacer for leadout -->
<!-- spacer for lead-out -->
<div
class="h-[60px]"
style:position="absolute"

View File

@@ -5,7 +5,7 @@
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
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 Scrubber from './scrubber.svelte';
@@ -51,16 +51,34 @@
empty,
}: Props = $props();
let leadOut = $state(false);
let scrubberMonthPercent = $state(0);
let scrubberMonth: { year: number; month: number } | undefined = $state(undefined);
let scrubOverallPercent: number = $state(0);
// Constants for timeline calculations
const VIEWPORT_MULTIPLIER = 2; // Used to determine if timeline is "small"
const SUBPIXEL_TOLERANCE = -1; // Tolerance for scroll position checks
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);
// 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
const handleTimelineScroll = () => {
leadOut = false;
isInLeadOutSection = false;
// Handle small timeline edge case: scroll limited due to size of content
if (isSmallTimeline()) {
@@ -80,111 +98,131 @@
};
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 = () => {
scrubberMonth = undefined;
scrubberMonthPercent = 0;
viewportTopMonth = undefined;
viewportTopMonthScrollPercent = 0;
};
const calculateTimelineScrollPercent = () => {
const maxScroll = timelineManager.getMaxScroll();
timelineScrollPercent = Math.min(1, timelineManager.visibleWindow.top / maxScroll);
resetScrubberMonth();
};
const handleSmallTimelineScroll = () => {
const maxScroll = timelineManager.getMaxScroll();
scrubOverallPercent = Math.min(1, timelineManager.visibleWindow.top / maxScroll);
resetScrubberMonth();
calculateTimelineScrollPercent();
};
const handleLeadInScroll = () => {
const maxScroll = timelineManager.getMaxScroll();
scrubOverallPercent = Math.min(1, timelineManager.visibleWindow.top / maxScroll);
resetScrubberMonth();
calculateTimelineScrollPercent();
};
const handleMonthScroll = () => {
const monthsLength = timelineManager.months.length;
const maxScrollPercent = timelineManager.getMaxScrollPercent();
let top = timelineManager.visibleWindow.top;
let found = false;
let remainingScrollDistance = timelineManager.visibleWindow.top;
// Tracks if we found the month intersecting the viewport top
let foundIntersectingMonth = false;
for (let i = -1; i < monthsLength + 1; i++) {
const monthData = getMonthData(i);
const next = top - monthData.height * maxScrollPercent;
for (let i = MONTH_LOOP_START; i < getMonthLoopEnd(monthsLength); i++) {
const monthSection = getMonthSection(i);
const nextRemainingDistance = remainingScrollDistance - monthSection.height * maxScrollPercent;
// Check if we're in this month (with subpixel tolerance)
if (next < -1 && monthData.monthGroup) {
scrubberMonth = monthData.monthGroup;
if (nextRemainingDistance < SUBPIXEL_TOLERANCE && monthSection.monthGroup) {
viewportTopMonth = monthSection.monthGroup;
// Calculate month percentage
scrubberMonthPercent = Math.max(0, top / (monthData.height * maxScrollPercent));
// Calculate how far we've scrolled into this month as a percentage
viewportTopMonthScrollPercent = Math.max(0, remainingScrollDistance / (monthSection.height * maxScrollPercent));
// Handle rounding errors (and/or subpixel tolerance) -
// advance to next month if almost at end
if (scrubberMonthPercent > 0.9999 && i + 1 < monthsLength - 1) {
scrubberMonth = timelineManager.months[i + 1].yearMonth;
scrubberMonthPercent = 0;
if (isNearMonthBoundary(viewportTopMonthScrollPercent) && canAdvanceToNextMonth(i, monthsLength)) {
viewportTopMonth = timelineManager.months[i + 1].yearMonth;
viewportTopMonthScrollPercent = 0;
}
found = true;
foundIntersectingMonth = true;
break;
}
top = next;
remainingScrollDistance = nextRemainingDistance;
}
if (!found) {
leadOut = true;
scrubOverallPercent = 1;
if (!foundIntersectingMonth) {
isInLeadOutSection = true;
timelineScrollPercent = 1;
resetScrubberMonth();
}
};
const getMonthData = (index: number) => {
const getMonthSection = (index: number): MonthSection => {
const monthsLength = timelineManager.months.length;
if (index === -1) {
// lead-in
if (index === MONTH_LOOP_START) {
return {
type: 'lead-in',
height: timelineManager.topSectionHeight,
monthGroup: undefined,
};
}
if (index === monthsLength) {
// lead-out
return {
type: 'lead-out',
height: timelineManager.bottomSectionHeight,
monthGroup: undefined,
};
}
// normal month
return {
type: 'month',
height: timelineManager.months[index].height,
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
// this function scrolls the timeline to the specified month group and offset, based on scrubber interaction
const onScrub: ScrubberListener = (scrubberData) => {
const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent, scrollToFunction } = scrubberData;
if (!scrubberMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
const maxScroll = timelineManager.getMaxScroll();
const offset = maxScroll * overallScrollPercent;
scrollToFunction?.(offset);
// Handle edge case or no month selected
if (!scrubberMonth || isSmallTimeline()) {
handleOverallPercentScroll(overallScrollPercent, scrollToFunction);
return;
}
const monthGroup = timelineManager.months.find(
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
);
if (!monthGroup) {
return;
// Find and scroll to the selected month
const monthGroup = findMonthGroup(scrubberMonth);
if (monthGroup) {
scrollToPositionWithinMonth(monthGroup, scrubberMonthScrollPercent, scrollToFunction);
}
scrollToMonthGroupAndOffset(monthGroup, scrubberMonthScrollPercent, scrollToFunction);
};
const scrollToMonthGroupAndOffset = (
const scrollToPositionWithinMonth = (
monthGroup: MonthGroup,
monthGroupScrollPercent: number,
handleScrollTop?: (top: number) => void,
@@ -218,18 +256,18 @@
{empty}
{handleTimelineScroll}
>
{#snippet header(scrollTo)}
{#snippet header(scrollToFunction)}
{#if timelineManager.months.length > 0}
<Scrubber
{timelineManager}
height={timelineManager.viewportHeight}
timelineTopOffset={timelineManager.topSectionHeight}
timelineBottomOffset={timelineManager.bottomSectionHeight}
{leadOut}
{scrubOverallPercent}
{scrubberMonthPercent}
{scrubberMonth}
onScrub={(args) => onScrub({ ...args, scrollToFunction: scrollTo })}
{isInLeadOutSection}
{timelineScrollPercent}
{viewportTopMonthScrollPercent}
{viewportTopMonth}
onScrub={(scrubberData) => onScrub({ ...scrubberData, scrollToFunction })}
bind:scrubberWidth
/>
{/if}

View File

@@ -4,7 +4,7 @@
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
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 { clamp } from 'lodash-es';
import { onMount } from 'svelte';
@@ -15,10 +15,10 @@
timelineBottomOffset?: number;
height?: number;
timelineManager: TimelineManager;
scrubOverallPercent?: number;
scrubberMonthPercent?: number;
scrubberMonth?: { year: number; month: number };
leadOut?: boolean;
timelineScrollPercent?: number;
viewportTopMonthScrollPercent?: number;
viewportTopMonth?: TimelineYearMonth;
isInLeadOutSection?: boolean;
scrubberWidth?: number;
onScrub?: ScrubberListener;
onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
@@ -31,10 +31,10 @@
timelineBottomOffset = 0,
height = 0,
timelineManager,
scrubOverallPercent = 0,
scrubberMonthPercent = 0,
scrubberMonth = undefined,
leadOut = false,
timelineScrollPercent = 0,
viewportTopMonthScrollPercent = 0,
viewportTopMonth = undefined,
isInLeadOutSection = false,
onScrub = undefined,
onScrubKeyDown = undefined,
startScrub = undefined,
@@ -100,7 +100,7 @@
offset += scrubberMonthPercent * relativeBottomOffset;
}
return offset;
} else if (leadOut) {
} else if (isInLeadOutSection) {
let offset = relativeTopOffset;
for (const segment of segments) {
offset += segment.height;
@@ -111,7 +111,9 @@
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 relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));