From 9235843f2554ff49962f8dabe5bec4dd17610c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E9=B9=8F?= <2916022834@qq.com> Date: Fri, 15 May 2026 10:50:25 +0800 Subject: [PATCH] 11 --- src/directives/custom-animation.js | 168 +++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/directives/custom-animation.js diff --git a/src/directives/custom-animation.js b/src/directives/custom-animation.js new file mode 100644 index 0000000..e5ddfa3 --- /dev/null +++ b/src/directives/custom-animation.js @@ -0,0 +1,168 @@ +/** + * 自定义动画指令 + * 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(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(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 = el.querySelectorAll(typesStr) + if (children.length === 0) return + const elTop_bottom = root.offsetHeight - (el.offsetTop - root.offsetTop - root.scrollTop) + const maxHeight = root.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) + } +} \ No newline at end of file