Defer parallax snap until pointer is idle
Reset the enter-ease timer on each move so transition:none cannot cut off a retargeted interpolation mid-flight. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -30,14 +30,41 @@ const BACKDROP_EASE = `transform ${BACKDROP_EASE_MS}ms ease-out`;
|
|||||||
// Set on the tracking surface while the pointer is over it.
|
// Set on the tracking surface while the pointer is over it.
|
||||||
const PARALLAX_ACTIVE = "data-backdrop-parallax";
|
const PARALLAX_ACTIVE = "data-backdrop-parallax";
|
||||||
|
|
||||||
// Kept for BACKDROP_EASE_MS after enter so follow-up pointermoves do not flip
|
// Kept until pointer movement has been idle for BACKDROP_EASE_MS after enter
|
||||||
// transition to none mid-ease (that was the edge-enter jump).
|
// (and after each move during that window) so transition is not flipped to
|
||||||
|
// none while a retargeted ease is still interpolating.
|
||||||
const PARALLAX_EASING = "data-backdrop-parallax-easing";
|
const PARALLAX_EASING = "data-backdrop-parallax-easing";
|
||||||
|
|
||||||
const BACKDROP_VARIANT = "data-backdrop-variant";
|
const BACKDROP_VARIANT = "data-backdrop-variant";
|
||||||
|
|
||||||
const enterEaseTimeouts = new WeakMap<HTMLElement, number>();
|
const enterEaseTimeouts = new WeakMap<HTMLElement, number>();
|
||||||
|
|
||||||
|
function clearEaseEnd(surface: HTMLElement) {
|
||||||
|
const previous = enterEaseTimeouts.get(surface);
|
||||||
|
if (previous != null) {
|
||||||
|
window.clearTimeout(previous);
|
||||||
|
enterEaseTimeouts.delete(surface);
|
||||||
|
}
|
||||||
|
surface.removeAttribute(PARALLAX_EASING);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (Re)start the idle timer that ends eased tracking. Call on enter and on every
|
||||||
|
// move while still easing so a late retarget can finish before transition:none.
|
||||||
|
function scheduleEaseEnd(surface: HTMLElement) {
|
||||||
|
surface.setAttribute(PARALLAX_EASING, "");
|
||||||
|
const previous = enterEaseTimeouts.get(surface);
|
||||||
|
if (previous != null) {
|
||||||
|
window.clearTimeout(previous);
|
||||||
|
}
|
||||||
|
enterEaseTimeouts.set(
|
||||||
|
surface,
|
||||||
|
window.setTimeout(() => {
|
||||||
|
surface.removeAttribute(PARALLAX_EASING);
|
||||||
|
enterEaseTimeouts.delete(surface);
|
||||||
|
}, BACKDROP_EASE_MS),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Per-surface Figma specs. Both use a width-based square centered on the frame
|
// Per-surface Figma specs. Both use a width-based square centered on the frame
|
||||||
// (top/left 50% + translate -50%). Logo Tile overflows (~inset -42px on a
|
// (top/left 50% + translate -50%). Logo Tile overflows (~inset -42px on a
|
||||||
// ~157px media → scale 1.53); Subprocessor Card fills the header width at 1×.
|
// ~157px media → scale 1.53); Subprocessor Card fills the header width at 1×.
|
||||||
@@ -144,27 +171,18 @@ export function onBackdropPointerMove(event: PointerEvent<HTMLElement>) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const entering = !surface.hasAttribute(PARALLAX_ACTIVE);
|
const entering = !surface.hasAttribute(PARALLAX_ACTIVE);
|
||||||
|
const easing = entering || surface.hasAttribute(PARALLAX_EASING);
|
||||||
surface.setAttribute(PARALLAX_ACTIVE, "");
|
surface.setAttribute(PARALLAX_ACTIVE, "");
|
||||||
|
|
||||||
if (entering) {
|
if (easing) {
|
||||||
surface.setAttribute(PARALLAX_EASING, "");
|
|
||||||
const previous = enterEaseTimeouts.get(surface);
|
|
||||||
if (previous != null) {
|
|
||||||
window.clearTimeout(previous);
|
|
||||||
}
|
|
||||||
enterEaseTimeouts.set(
|
|
||||||
surface,
|
|
||||||
window.setTimeout(() => {
|
|
||||||
surface.removeAttribute(PARALLAX_EASING);
|
|
||||||
enterEaseTimeouts.delete(surface);
|
|
||||||
}, BACKDROP_EASE_MS),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Apply transition before changing transform so the browser interpolates
|
// Apply transition before changing transform so the browser interpolates
|
||||||
// from the frozen pose (same-frame transition+transform often snaps).
|
// from the frozen pose (same-frame transition+transform often snaps).
|
||||||
image.style.transition = BACKDROP_EASE;
|
image.style.transition = BACKDROP_EASE;
|
||||||
void image.offsetWidth;
|
if (entering) {
|
||||||
} else if (!surface.hasAttribute(PARALLAX_EASING)) {
|
void image.offsetWidth;
|
||||||
|
}
|
||||||
|
scheduleEaseEnd(surface);
|
||||||
|
} else {
|
||||||
image.style.transition = "none";
|
image.style.transition = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,11 +193,6 @@ export function onBackdropPointerMove(event: PointerEvent<HTMLElement>) {
|
|||||||
// ease from that frozen pose instead of jumping.
|
// ease from that frozen pose instead of jumping.
|
||||||
export function onBackdropPointerLeave(event: PointerEvent<HTMLElement>) {
|
export function onBackdropPointerLeave(event: PointerEvent<HTMLElement>) {
|
||||||
const surface = event.currentTarget;
|
const surface = event.currentTarget;
|
||||||
const previous = enterEaseTimeouts.get(surface);
|
clearEaseEnd(surface);
|
||||||
if (previous != null) {
|
|
||||||
window.clearTimeout(previous);
|
|
||||||
enterEaseTimeouts.delete(surface);
|
|
||||||
}
|
|
||||||
surface.removeAttribute(PARALLAX_ACTIVE);
|
surface.removeAttribute(PARALLAX_ACTIVE);
|
||||||
surface.removeAttribute(PARALLAX_EASING);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user