use binary search for perf, refactor, improve readability
This commit is contained in:
@@ -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, TimelineYearMonth } from '$lib/utils/timeline-util';
|
import { findMonthAtScrollPosition, type ScrubberListener, type 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,11 +51,7 @@
|
|||||||
empty,
|
empty,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Constants for timeline calculations
|
|
||||||
const VIEWPORT_MULTIPLIER = 2; // Used to determine if timeline is "small"
|
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
|
|
||||||
|
|
||||||
|
|
||||||
let isInLeadOutSection = $state(false);
|
let isInLeadOutSection = $state(false);
|
||||||
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
|
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
|
||||||
@@ -91,11 +87,6 @@
|
|||||||
return timelineManager.timelineHeight < timelineManager.viewportHeight * VIEWPORT_MULTIPLIER;
|
return timelineManager.timelineHeight < timelineManager.viewportHeight * VIEWPORT_MULTIPLIER;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isNearMonthBoundary = (progress: number) => {
|
|
||||||
return progress > NEAR_END_THRESHOLD;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const resetScrubberMonth = () => {
|
const resetScrubberMonth = () => {
|
||||||
viewportTopMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
viewportTopMonthScrollPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
@@ -116,76 +107,24 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMonthScroll = () => {
|
const handleMonthScroll = () => {
|
||||||
const scrollTop = timelineManager.visibleWindow.top;
|
const scrollPosition = timelineManager.visibleWindow.top;
|
||||||
const months = timelineManager.months;
|
const months = timelineManager.months;
|
||||||
const maxScrollPercent = timelineManager.getMaxScrollPercent();
|
const maxScrollPercent = timelineManager.getMaxScrollPercent();
|
||||||
|
|
||||||
// Early exit if no months
|
|
||||||
if (months.length === 0) {
|
|
||||||
isInLeadOutSection = true;
|
|
||||||
timelineScrollPercent = 1;
|
|
||||||
resetScrubberMonth();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we're before the first month (in lead-in)
|
|
||||||
const firstMonthTop = months[0].top * maxScrollPercent;
|
|
||||||
if (scrollTop < firstMonthTop - SUBPIXEL_TOLERANCE) {
|
|
||||||
isInLeadOutSection = true;
|
|
||||||
timelineScrollPercent = 1;
|
|
||||||
resetScrubberMonth();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we're after the last month (in lead-out)
|
|
||||||
const lastMonth = months[months.length - 1];
|
|
||||||
const lastMonthBottom = (lastMonth.top + lastMonth.height) * maxScrollPercent;
|
|
||||||
if (scrollTop >= lastMonthBottom - SUBPIXEL_TOLERANCE) {
|
|
||||||
isInLeadOutSection = true;
|
|
||||||
timelineScrollPercent = 1;
|
|
||||||
resetScrubberMonth();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary search to find the month containing the viewport top
|
|
||||||
let left = 0;
|
|
||||||
let right = months.length - 1;
|
|
||||||
|
|
||||||
while (left <= right) {
|
|
||||||
const mid = Math.floor((left + right) / 2);
|
|
||||||
const month = months[mid];
|
|
||||||
const monthTop = month.top * maxScrollPercent;
|
|
||||||
const monthBottom = monthTop + month.height * maxScrollPercent;
|
|
||||||
|
|
||||||
if (scrollTop >= monthTop - SUBPIXEL_TOLERANCE && scrollTop < monthBottom - SUBPIXEL_TOLERANCE) {
|
|
||||||
// Found the month containing the viewport top
|
|
||||||
viewportTopMonth = month.yearMonth;
|
|
||||||
const distanceIntoMonth = scrollTop - monthTop;
|
|
||||||
viewportTopMonthScrollPercent = Math.max(0, distanceIntoMonth / (month.height * maxScrollPercent));
|
|
||||||
|
|
||||||
// Handle month boundary edge case
|
|
||||||
if (isNearMonthBoundary(viewportTopMonthScrollPercent) && mid < months.length - 1) {
|
|
||||||
viewportTopMonth = months[mid + 1].yearMonth;
|
|
||||||
viewportTopMonthScrollPercent = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
isInLeadOutSection = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollTop < monthTop) {
|
|
||||||
right = mid - 1;
|
|
||||||
} else {
|
|
||||||
left = mid + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shouldn't reach here, but if we do, we're in lead-out
|
|
||||||
isInLeadOutSection = true;
|
|
||||||
timelineScrollPercent = 1;
|
|
||||||
resetScrubberMonth();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Find the month at the current scroll position
|
||||||
|
const searchResult = findMonthAtScrollPosition(months, scrollPosition, maxScrollPercent);
|
||||||
|
|
||||||
|
if (searchResult) {
|
||||||
|
viewportTopMonth = searchResult.month;
|
||||||
|
viewportTopMonthScrollPercent = searchResult.monthScrollPercent;
|
||||||
|
isInLeadOutSection = false;
|
||||||
|
} else {
|
||||||
|
// We're in lead-out section
|
||||||
|
isInLeadOutSection = true;
|
||||||
|
timelineScrollPercent = 1;
|
||||||
|
resetScrubberMonth();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOverallPercentScroll = (percent: number, scrollTo?: (offset: number) => void) => {
|
const handleOverallPercentScroll = (percent: number, scrollTo?: (offset: number) => void) => {
|
||||||
const maxScroll = timelineManager.getMaxScroll();
|
const maxScroll = timelineManager.getMaxScroll();
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<span class="w-full truncate first-letter:capitalize" title={dayGroup.groupTitle}>
|
<span class="w-full truncate first-letter:capitalize" title={dayGroup.groupTitleFull}>
|
||||||
{dayGroup.groupTitle}
|
{dayGroup.groupTitle}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -243,3 +243,79 @@ export function setDifference<T>(setA: Set<T>, setB: Set<T>): SvelteSet<T> {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MonthGroupForSearch {
|
||||||
|
yearMonth: TimelineYearMonth;
|
||||||
|
top: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BinarySearchResult {
|
||||||
|
month: TimelineYearMonth;
|
||||||
|
monthScrollPercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findMonthAtScrollPosition(
|
||||||
|
months: MonthGroupForSearch[],
|
||||||
|
scrollPosition: number,
|
||||||
|
maxScrollPercent: number,
|
||||||
|
): BinarySearchResult | null {
|
||||||
|
const SUBPIXEL_TOLERANCE = -1; // Tolerance for scroll position checks
|
||||||
|
const NEAR_END_THRESHOLD = 0.9999; // Threshold for detecting near-end of month
|
||||||
|
|
||||||
|
if (months.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're before the first month
|
||||||
|
const firstMonthTop = months[0].top * maxScrollPercent;
|
||||||
|
if (scrollPosition < firstMonthTop - SUBPIXEL_TOLERANCE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're after the last month
|
||||||
|
const lastMonth = months[months.length - 1];
|
||||||
|
const lastMonthBottom = (lastMonth.top + lastMonth.height) * maxScrollPercent;
|
||||||
|
if (scrollPosition >= lastMonthBottom - SUBPIXEL_TOLERANCE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary search to find the month containing the scroll position
|
||||||
|
let left = 0;
|
||||||
|
let right = months.length - 1;
|
||||||
|
|
||||||
|
while (left <= right) {
|
||||||
|
const mid = Math.floor((left + right) / 2);
|
||||||
|
const month = months[mid];
|
||||||
|
const monthTop = month.top * maxScrollPercent;
|
||||||
|
const monthBottom = monthTop + month.height * maxScrollPercent;
|
||||||
|
|
||||||
|
if (scrollPosition >= monthTop - SUBPIXEL_TOLERANCE && scrollPosition < monthBottom - SUBPIXEL_TOLERANCE) {
|
||||||
|
// Found the month containing the scroll position
|
||||||
|
const distanceIntoMonth = scrollPosition - monthTop;
|
||||||
|
let monthScrollPercent = Math.max(0, distanceIntoMonth / (month.height * maxScrollPercent));
|
||||||
|
|
||||||
|
// Handle month boundary edge case
|
||||||
|
if (monthScrollPercent > NEAR_END_THRESHOLD && mid < months.length - 1) {
|
||||||
|
return {
|
||||||
|
month: months[mid + 1].yearMonth,
|
||||||
|
monthScrollPercent: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
month: month.yearMonth,
|
||||||
|
monthScrollPercent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollPosition < monthTop) {
|
||||||
|
right = mid - 1;
|
||||||
|
} else {
|
||||||
|
left = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shouldn't reach here, but return null if we do
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user