From d87ed12b603e6a4ffdbe666d984bb464df9b620f Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Fri, 20 Sep 2024 13:22:58 +0800 Subject: feat: add throttle for DOMRect animation --- src/dumbymap.mjs | 31 +++++++++++++++++++++---------- src/utils.mjs | 56 ++++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 9a38e24..6e4c00e 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs @@ -6,10 +6,8 @@ import MarkdownItTocDoneRight from 'markdown-it-toc-done-right' import LeaderLine from 'leader-line' import PlainDraggable from 'plain-draggable' import { renderWith, parseConfigsFromYaml } from 'mapclay' -import { onRemove, animateRectTransition } from './utils' +import { onRemove, animateRectTransition, throttle } from './utils' -// Utils {{{ -// }}} // FUNCTION: Get DocLinks from special anchor element {{{ const docLinkSelector = 'a[href^="#"][title^="=>"]' @@ -229,6 +227,9 @@ export const generateMaps = async (container, callback) => { showcase.classList.add('Showcase') // Focus Map {{{ + const toShowcaseWithThrottle = throttle(animateRectTransition, 300) + const fromShowCaseWithThrottle = throttle(animateRectTransition, 300) + const mapFocusObserver = () => new MutationObserver((mutations) => { const mutation = mutations.at(-1) const target = mutation.target @@ -238,31 +239,41 @@ export const generateMaps = async (container, callback) => { if (shouldBeInShowcase) { if (showcase.contains(target)) return - // Placeholder for map in Showcase, it should has the same rect + // Placeholder for map in Showcase, it should has the same DOMRect const placeholder = target.cloneNode(true) placeholder.classList.remove('map-container') placeholder.setAttribute('data-placeholder', target.id) target.parentElement.replaceChild(placeholder, target) // To fit showcase, remove all inline style - target.style = "" + target.removeAttribute('style') showcase.appendChild(target) // Resume rect from Semantic HTML to Showcase, with animation - animateRectTransition(target, placeholder.getBoundingClientRect(), true) + toShowcaseWithThrottle(target, placeholder.getBoundingClientRect(), { + duration: 300, + resume: true + }) } else if (showcase.contains(target)) { const placeholder = htmlHolder.querySelector(`[data-placeholder="${target.id}"]`) if (!placeholder) throw Error(`Cannot fine placeholder for map "${target.id}"`) + const animation = fromShowCaseWithThrottle(target, placeholder.getBoundingClientRect(), { + duration: 300 + }) const afterAnimation = () => { placeholder.parentElement.replaceChild(target, placeholder) target.style = placeholder.style.cssText placeholder.remove() } - animateRectTransition(target, placeholder.getBoundingClientRect()) - .finished - .then(afterAnimation) - .catch(afterAnimation) + + if (animation) { + animation.finished + .then(afterAnimation) + .catch(afterAnimation) + } else { + afterAnimation() + } } }) // }}} diff --git a/src/utils.mjs b/src/utils.mjs index 4c58dc1..e694d4a 100644 --- a/src/utils.mjs +++ b/src/utils.mjs @@ -1,4 +1,9 @@ -// Disconnect MutationObserver if element is removed +/** + * Do callback when HTMLElement in removed + * + * @param {HTMLElement} element observing + * @param {Function} callback + */ export const onRemove = (element, callback) => { const parent = element.parentNode; if (!parent) throw new Error("The node must already be attached"); @@ -16,10 +21,21 @@ export const onRemove = (element, callback) => { obs.observe(parent, { childList: true, }); } -// Animation for new rectangle -export const animateRectTransition = (child, rect, resume = false) => { +/** + * Animate transition of DOMRect, with Web Animation API + * + * @param {HTMLElement} element Element which animation applies + * @param {DOMRect} rect DOMRect for transition + * @param {Object} options + * @param {Boolean} options.resume If true, transition starts from rect to DOMRect of element + * @param {Number} options.duration Duration of animation in milliseconds + * @returns {Animation} https://developer.mozilla.org/en-US/docs/Web/API/Animation + */ +export const animateRectTransition = (element, rect, options = {}) => { + if (!element.parentElement) throw new Error("The node must already be attached"); + const { width: w1, height: h1, left: x1, top: y1 } = rect - const { width: w2, height: h2, left: x2, top: y2 } = child.getBoundingClientRect() + const { width: w2, height: h2, left: x2, top: y2 } = element.getBoundingClientRect() const rw = w1 / w2 const rh = h1 / h2 @@ -36,13 +52,33 @@ export const animateRectTransition = (child, rect, resume = false) => { { transform: transform1, opacity: 1 }, { transform: transform2, opacity: 0.3 }, ] + if (options.resume === true) keyframes.reverse() - return child.animate( - resume - ? keyframes.reverse() - : keyframes, + return element.animate( + keyframes, { - duration: 300, + duration: options.duration ?? 300, easing: 'ease-in-out', - }); + } + ); +} + + +/** + * Throttle for function call + * + * @param {Function} func + * @param {Number} delay milliseconds + * @returns {Any} return value of function call, or null if throttled + */ +export function throttle(func, delay) { + let timerFlag = null; + + return (...args) => { + if (timerFlag !== null) return null + timerFlag = setTimeout(() => { + timerFlag = null; + }, delay); + return func(...args); + }; } -- cgit v1.2.3-70-g09d2