Files
Code-Create/src/directives/custom-animation.js
李志鹏 d237dab098 aaa
2026-05-15 17:31:43 +08:00

178 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 自定义动画指令
* v-custom-animation.scroll.once.parent="{GetRoot, activeClass}"
* 修饰符
* scroll: 是否监听滚动事件
* once: 是否只执行一次
* parent: 是否监听父元素滚动事件-优先级GetRoot > parent > document
* 参数
* GetRoot: 获取根元素函数-优先级GetRoot > parent > document
* activeClass: 激活类名-默认值active
*
* 子元素动画
* <div translate-x-s="-100" translate-x="100"></div>
* 添加动画属性
* 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
}