/** * 自定义动画指令 * v-custom-animation.scroll.once.parent="{GetRoot, activeClass}" * 修饰符 * scroll: 是否监听滚动事件 * once: 是否只执行一次 * parent: 是否监听父元素滚动事件-优先级(GetRoot > parent > document) * 参数 * GetRoot: 获取根元素函数-优先级(GetRoot > parent > document) * activeClass: 激活类名-默认值(active) * * 子元素动画 *
* 添加动画属性 * 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', } 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'// 激活类名 } = value || {} const { scroll = false,// 是否监听滚动事件 once = false,// 是否只执行一次 parent = false,// 是否监听父元素滚动事件 } = modifiers const root = GetRoot ? GetRoot() : parent ? el.parentElement : document; if (el === root) return; add(el, root) els.set(el, { root,// 根元素 scroll, once, activeClass, isActive: false, }) }, beforeUnmount(el, binding) { remove(el) els.delete(el) } }; function add(el, root = document) { requestAnimationFrame(() => handleScroll({ target: root })) 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) }) }, { 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 = Array.from(el.querySelectorAll(typesStr)) if (Object.values(T).some(v => hasAttr(el, v))) children.push(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) => { item.style.transition = 'transform 0.5s ease-out' const tX = getCurrentValue(item, T.translateX_s, T.translateX, p) const tY = getCurrentValue(item, T.translateY_s, T.translateY, p) const sx = getCurrentValue(item, T.scaleX_s, T.scaleX, p, T.scale_s, T.scale, 1) const sy = getCurrentValue(item, T.scaleY_s, T.scaleY, p, T.scale_s, T.scale, 1) const r = getCurrentValue(item, T.rotate_s, T.rotate, p) const rx = getCurrentValue(item, T.rotateX_s, T.rotateX, p) const ry = getCurrentValue(item, T.rotateY_s, T.rotateY, p) const rz = getCurrentValue(item, T.rotateZ_s, T.rotateZ, p) const transform = `translate(${tX}px, ${tY}px) scale(${sx}, ${sy}) rotate(${r}deg) rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg)` item.style.transform = transform if (hasAttr(item, [T.opacity_s, T.opacity])) { item.style.opacity = getCurrentValue(item, T.opacity_s, T.opacity, p, T.opacity_s, T.opacity, 1) } }) }) } function getCurrentValue(el, start, end, progress, bStart, bEnd, defaultValue = 0) { // const startNum = Number(el.getAttribute(start) || el.getAttribute(bStart)) || defaultValue // const endNum = Number(el.getAttribute(end) || el.getAttribute(bEnd)) || defaultValue const startNum = hasAttr(el, start) ? Number(el.getAttribute(start)) : hasAttr(el, bStart) ? Number(el.getAttribute(bStart)) : defaultValue const endNum = hasAttr(el, end) ? Number(el.getAttribute(end)) : hasAttr(el, bEnd) ? Number(el.getAttribute(bEnd)) : defaultValue return startNum + (endNum - startNum) * progress } 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 }