calendar: enforce two-phase zoom-in morph before view switch
This commit is contained in:
@@ -2281,6 +2281,7 @@ type CalendarZoomGhost = {
|
||||
};
|
||||
|
||||
const calendarContentWrapRef = ref<HTMLElement | null>(null);
|
||||
const calendarZoomOverlayRef = ref<HTMLElement | null>(null);
|
||||
const calendarHoveredMonthIndex = ref<number | null>(null);
|
||||
const calendarHoveredWeekStartKey = ref("");
|
||||
const calendarHoveredDayKey = ref("");
|
||||
@@ -2400,13 +2401,29 @@ function getElementRectInCalendar(element: HTMLElement | null): CalendarRect | n
|
||||
const wrapRect = calendarContentWrapRef.value?.getBoundingClientRect();
|
||||
if (!wrapRect) return null;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const left = Math.max(0, rect.left - wrapRect.left);
|
||||
const top = Math.max(0, rect.top - wrapRect.top);
|
||||
const width = Math.max(24, Math.min(rect.width, wrapRect.width - left));
|
||||
const height = Math.max(24, Math.min(rect.height, wrapRect.height - top));
|
||||
const left = Math.max(0, Math.min(rect.left - wrapRect.left, wrapRect.width));
|
||||
const top = Math.max(0, Math.min(rect.top - wrapRect.top, wrapRect.height));
|
||||
const right = Math.max(0, Math.min(rect.right - wrapRect.left, wrapRect.width));
|
||||
const bottom = Math.max(0, Math.min(rect.bottom - wrapRect.top, wrapRect.height));
|
||||
const visibleWidth = right - left;
|
||||
const visibleHeight = bottom - top;
|
||||
if (visibleWidth < 2 || visibleHeight < 2) return null;
|
||||
const width = Math.min(Math.max(24, visibleWidth), wrapRect.width - left);
|
||||
const height = Math.min(Math.max(24, visibleHeight), wrapRect.height - top);
|
||||
return { left, top, width, height };
|
||||
}
|
||||
|
||||
function fallbackZoomOriginRect(viewportRect: CalendarRect): CalendarRect {
|
||||
const width = Math.max(96, Math.round(viewportRect.width * 0.28));
|
||||
const height = Math.max(64, Math.round(viewportRect.height * 0.24));
|
||||
return {
|
||||
left: Math.max(0, Math.round((viewportRect.width - width) / 2)),
|
||||
top: Math.max(0, Math.round((viewportRect.height - height) / 2)),
|
||||
width: Math.min(width, viewportRect.width),
|
||||
height: Math.min(height, viewportRect.height),
|
||||
};
|
||||
}
|
||||
|
||||
function weekRowStartForDate(key: string) {
|
||||
const date = new Date(`${key}T00:00:00`);
|
||||
date.setDate(date.getDate() - date.getDay());
|
||||
@@ -2490,30 +2507,61 @@ function primeCalendarRect(rect: CalendarRect) {
|
||||
}
|
||||
|
||||
function morphCalendarRect(toRect: CalendarRect) {
|
||||
requestAnimationFrame(() => {
|
||||
calendarZoomOverlay.value = {
|
||||
active: true,
|
||||
left: toRect.left,
|
||||
top: toRect.top,
|
||||
width: toRect.width,
|
||||
height: toRect.height,
|
||||
};
|
||||
calendarZoomOverlay.value = {
|
||||
active: true,
|
||||
left: toRect.left,
|
||||
top: toRect.top,
|
||||
width: toRect.width,
|
||||
height: toRect.height,
|
||||
};
|
||||
}
|
||||
|
||||
function nextAnimationFrame() {
|
||||
return new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
function waitCalendarZoom() {
|
||||
async function flushCalendarZoomStartFrame() {
|
||||
await nextTick();
|
||||
await nextAnimationFrame();
|
||||
calendarZoomOverlayRef.value?.getBoundingClientRect();
|
||||
await nextAnimationFrame();
|
||||
}
|
||||
|
||||
function waitCalendarZoomTransition() {
|
||||
const overlay = calendarZoomOverlayRef.value;
|
||||
if (!overlay) {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => resolve(), CALENDAR_ZOOM_DURATION_MS);
|
||||
});
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => resolve(), CALENDAR_ZOOM_DURATION_MS);
|
||||
let settled = false;
|
||||
const finish = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
overlay.removeEventListener("transitionend", onTransitionEnd);
|
||||
clearTimeout(fallbackTimer);
|
||||
resolve();
|
||||
};
|
||||
const onTransitionEnd = (event: TransitionEvent) => {
|
||||
if (event.target !== overlay) return;
|
||||
if (!["left", "top", "width", "height"].includes(event.propertyName)) return;
|
||||
finish();
|
||||
};
|
||||
const fallbackTimer = setTimeout(() => finish(), CALENDAR_ZOOM_DURATION_MS + 140);
|
||||
overlay.addEventListener("transitionend", onTransitionEnd);
|
||||
});
|
||||
}
|
||||
|
||||
async function animateCalendarZoomIn(sourceElement: HTMLElement | null, ghost: CalendarZoomGhost, apply: () => void) {
|
||||
const fromRect = getElementRectInCalendar(sourceElement);
|
||||
const viewportRect = getCalendarViewportRect();
|
||||
if (!fromRect || !viewportRect) {
|
||||
if (!viewportRect) {
|
||||
apply();
|
||||
return;
|
||||
}
|
||||
const fromRect = getElementRectInCalendar(sourceElement) ?? fallbackZoomOriginRect(viewportRect);
|
||||
|
||||
clearCalendarZoomPrime();
|
||||
calendarZoomBusy.value = true;
|
||||
@@ -2521,9 +2569,9 @@ async function animateCalendarZoomIn(sourceElement: HTMLElement | null, ghost: C
|
||||
primeCalendarRect(fromRect);
|
||||
calendarZoomGhost.value = ghost;
|
||||
calendarSceneMasked.value = true;
|
||||
await nextTick();
|
||||
await flushCalendarZoomStartFrame();
|
||||
morphCalendarRect(viewportRect);
|
||||
await waitCalendarZoom();
|
||||
await waitCalendarZoomTransition();
|
||||
apply();
|
||||
await nextTick();
|
||||
} finally {
|
||||
@@ -2546,7 +2594,7 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT
|
||||
primeCalendarRect(viewportRect);
|
||||
calendarZoomGhost.value = zoomGhostForCurrentView();
|
||||
calendarSceneMasked.value = true;
|
||||
await nextTick();
|
||||
await flushCalendarZoomStartFrame();
|
||||
apply();
|
||||
await nextTick();
|
||||
const targetRect = getElementRectInCalendar(resolveTarget());
|
||||
@@ -2558,7 +2606,7 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT
|
||||
return;
|
||||
}
|
||||
morphCalendarRect(targetRect);
|
||||
await waitCalendarZoom();
|
||||
await waitCalendarZoomTransition();
|
||||
} finally {
|
||||
clearCalendarZoomOverlay();
|
||||
calendarSceneMasked.value = false;
|
||||
@@ -4964,6 +5012,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
</div>
|
||||
<div
|
||||
v-if="calendarZoomOverlay.active"
|
||||
ref="calendarZoomOverlayRef"
|
||||
class="calendar-zoom-overlay"
|
||||
:style="calendarZoomOverlayStyle"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user