/** * 自定义动画指令 * v-custom-animation.scroll.once.parent="{GetRoot, activeClass}" * 修饰符 * scroll: 是否监听滚动事件 * once: 是否只执行一次 * parent: 是否监听父元素滚动事件-优先级(GetRoot > parent > document) * 参数 * GetRoot: 获取根元素函数-优先级(GetRoot > parent > document) * activeClass: 激活类名-默认值(active) * duration: 动画时间-默认值(0.5s) * delay: 延迟时间-默认值(0s) * easing: 缓动函数-默认值(ease-out) * transformDuration: 变换时间-默认值 duration * transformDelay: 变换延迟时间-默认值 delay * transformEasing: 变换缓动函数-默认值 easing * opacityDuration: 透明度时间-默认值 duration * opacityDelay: 透明度延迟时间-默认值 delay * opacityEasing: 透明度缓动函数-默认值 easing * * 子元素动画 *
* 添加动画属性 * translate-x-s: 水平方向移动开始位置 * translate-x: 水平方向移动结束位置 * ......(属性支持查看 T 对象) * */ const roots = new Map() const els = new Map() const T = { translateX_s: 'translate-x-s', translateX: 'translate-x', translateY_s: 'translate-y-s', translateY: 'translate-y', scale_s: 'scale-s', scale: 'scale', scaleX_s: 'scale-x-s', scaleX: 'scale-x', scaleY_s: 'scale-y-s', scaleY: 'scale-y', rotate_s: 'rotate-s', rotate: 'rotate', rotateX_s: 'rotate-x-s', rotateX: 'rotate-x', rotateY_s: 'rotate-y-s', rotateY: 'rotate-y', rotateZ_s: 'rotate-z-s', rotateZ: 'rotate-z', opacity_s: 'opacity-s', opacity: 'opacity', // 动画属性配置 duration: 'duration', delay: 'delay', easing: 'easing', transformDuration: 'transform-duration', transformDelay: 'transform-delay', transformEasing: 'transform-easing', opacityDuration: 'opacity-duration', opacityDelay: 'opacity-delay', opacityEasing: 'opacity-easing', } const types = Object.values(T) const typesStr = types.map(v => `[${v}]`).join(',') const resize = new ResizeObserver((e) => { e.forEach(({ target }) => { requestAnimationFrame(() => handleScroll({ target })) }) }) export default { name: 'custom-animation', mounted(el, binding) { const { value, modifiers } = binding const { GetRoot,// 获取根元素函数 activeClass = 'active',// 激活类名 duration = '0.5s',// 动画时间 delay = '0s',// 延迟时间 easing = 'ease-out',// 缓动函数 } = value || {} const transition = { duration, delay, easing, transformDuration: value?.transformDuration || value?.[T.transformDuration], transformDelay: value?.transformDelay || value?.[T.transformDelay], transformEasing: value?.transformEasing || value?.[T.transformEasing], opacityDuration: value?.opacityDuration || value?.[T.opacityDuration], opacityDelay: value?.opacityDelay || value?.[T.opacityDelay], opacityEasing: value?.opacityEasing || value?.[T.opacityEasing], } const { scroll = false,// 是否监听滚动事件 once = false,// 是否只执行一次 parent = false,// 是否监听父元素滚动事件 } = modifiers const root = GetRoot ? GetRoot() : parent ? el.parentElement : document; if (el === root) return; const config = { root,// 根元素 scroll, once, activeClass, isActive: false, transition, } els.set(el, config) add(el, root, config) }, beforeUnmount(el, binding) { remove(el) els.delete(el) } }; function add(el, root = document, config) { if (config.scroll) { requestAnimationFrame(() => handleScroll({ target: root })) } else { getChildren(el).forEach((child) => setDocumentStyles(el, child, 0)) } resize.observe(el) if (roots.has(root)) { let obj = roots.get(root) obj.els.push(el) obj.observer.observe(el) return } resize.observe(isDocumentRoot(root)) root.addEventListener('scroll', handleScroll) const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { const { target } = entry const obj = els.get(target) if (!obj) return if (obj.once && obj.isActive) return;// 只执行一次,且已可见,不执行 obj.isActive = entry.isIntersecting; target.classList.toggle(obj.activeClass, obj.isActive) getChildren(target).forEach((el) => { setDocumentStyles(target, el, obj.isActive ? 1 : 0) }) }) }, { root }) observer.observe(el) roots.set(root, { els: [el], observer, resize }) } function remove(el, root = document) { if (!roots.has(root)) return const obj = roots.get(root) if (obj.els.includes(el)) { obj.observer.unobserve(el) obj.resize.unobserve(el) obj.els.splice(obj.els.indexOf(el), 1) } if (obj.els.length === 0) { obj.observer.disconnect() resize.unobserve(isDocumentRoot(root)) root.removeEventListener('scroll', handleScroll) roots.delete(root) } } var timer = null async function handleScroll({ target: root }) { clearTimeout(timer) timer = await new Promise(resolve => setTimeout(resolve, 10)) const obj = roots.get(root) if (!obj) return obj.els.forEach((el) => { const item = els.get(el) if (!item) return if (!item.scroll) return const children = getChildren(el) if (children.length === 0) return const rootEl = isDocumentRoot(root) const offsetHeight = root === document ? window.innerHeight : rootEl.offsetHeight const offsetTop = rootEl.offsetTop const scrollTop = rootEl.scrollTop const elTop_bottom = offsetHeight - (el.offsetTop - offsetTop - rootEl.scrollTop) const maxHeight = offsetHeight + el.offsetHeight const p = Math.min(1, Math.max(0, elTop_bottom / maxHeight)) children.forEach((item) => { setDocumentStyles(el, item, p) }) }) } function getChildren(el, oneself = true) { const children = Array.from(el.querySelectorAll(typesStr)) if (oneself && Object.values(T).some(v => hasAttr(el, v))) children.push(el) return children } function setDocumentStyles(parent, el, p = 0) { const item = els.get(parent) if (!item) return const t = getAttrs(el, item.transition) const tDuration = t.duration || t.transformDuration const tDelay = t.delay || t.transformDelay const tEasing = t.easing || t.transformEasing const oDuration = t.duration || t.opacityDuration const oDelay = t.delay || t.opacityDelay const oEasing = t.easing || t.opacityEasing const transitionArr = [ `transform ${tDuration} ${tDelay} ${tEasing}`, `opacity ${oDuration} ${oDelay} ${oEasing}`, ] el.style.transition = transitionArr.join(', ') const { num: tX, unit: tXUnit } = getCurrentValue(el, T.translateX_s, T.translateX, p) const { num: tY, unit: tYUnit } = getCurrentValue(el, T.translateY_s, T.translateY, p) const { num: sx } = getCurrentValue(el, T.scaleX_s, T.scaleX, p, T.scale_s, T.scale, 1) const { num: sy } = getCurrentValue(el, T.scaleY_s, T.scaleY, p, T.scale_s, T.scale, 1) const { num: r } = getCurrentValue(el, T.rotate_s, T.rotate, p) const { num: rx } = getCurrentValue(el, T.rotateX_s, T.rotateX, p) const { num: ry } = getCurrentValue(el, T.rotateY_s, T.rotateY, p) const { num: rz } = getCurrentValue(el, T.rotateZ_s, T.rotateZ, p) const transform = `translate(${tX}${tXUnit || 'px'}, ${tY}${tYUnit || 'px'}) scale(${sx}, ${sy}) rotate(${r}deg) rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg)` el.style.transform = transform if (hasAttr(el, [T.opacity_s, T.opacity])) { el.style.opacity = getCurrentValue(el, T.opacity_s, T.opacity, p, T.opacity_s, T.opacity, 1).num } } function getAttrs(el, attrs = {}) { const arrs = Object.keys(attrs) const obj = {} arrs.forEach((item) => { obj[item] = el.getAttribute(T[item]) || attrs[item] }) return obj } function getCurrentValue(el, start, end, progress, bStart, bEnd, defaultValue = 0) { const startStr = hasAttr(el, start) ? el.getAttribute(start) : hasAttr(el, bStart) ? el.getAttribute(bStart) : String(defaultValue) const endStr = hasAttr(el, end) ? el.getAttribute(end) : hasAttr(el, bEnd) ? el.getAttribute(bEnd) : String(defaultValue) const startNum = parseInt(startStr) const endNum = parseInt(endStr) const starUnit = startStr.match(/(px|deg|%|rem|em|vh|vw|pt|pc|mm|cm|in)$/i)?.[1] || '' const endUnit = endStr.match(/(px|deg|%|rem|em|vh|vw|pt|pc|mm|cm|in)$/i)?.[1] || '' return { num: startNum + (endNum - startNum) * progress, unit: starUnit || endUnit } } function hasAttr(el, attr) { if (Array.isArray(attr)) { return attr.some(v => el.hasAttribute(v)) } else { return el.hasAttribute(attr) } } // 检查document是否为根元素 function isDocumentRoot(root) { return root === document ? document.documentElement : root }