2026-05-15 10:50:25 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 自定义动画指令
|
|
|
|
|
|
* v-custom-animation.scroll.once.parent="{GetRoot, activeClass}"
|
|
|
|
|
|
* 修饰符
|
|
|
|
|
|
* scroll: 是否监听滚动事件
|
|
|
|
|
|
* once: 是否只执行一次
|
|
|
|
|
|
* parent: 是否监听父元素滚动事件-优先级(GetRoot > parent > document)
|
|
|
|
|
|
* 参数
|
|
|
|
|
|
* GetRoot: 获取根元素函数-优先级(GetRoot > parent > document)
|
|
|
|
|
|
* activeClass: 激活类名-默认值(active)
|
2026-05-19 15:55:49 +08:00
|
|
|
|
* duration: 动画时间-默认值(0.5s)
|
|
|
|
|
|
* delay: 延迟时间-默认值(0s)
|
|
|
|
|
|
* easing: 缓动函数-默认值(ease-out)
|
|
|
|
|
|
* transformDuration: 变换时间-默认值 duration
|
|
|
|
|
|
* transformDelay: 变换延迟时间-默认值 delay
|
|
|
|
|
|
* transformEasing: 变换缓动函数-默认值 easing
|
|
|
|
|
|
* opacityDuration: 透明度时间-默认值 duration
|
|
|
|
|
|
* opacityDelay: 透明度延迟时间-默认值 delay
|
|
|
|
|
|
* opacityEasing: 透明度缓动函数-默认值 easing
|
2026-05-15 10:50:25 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 子元素动画
|
|
|
|
|
|
* <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',
|
2026-05-19 15:55:49 +08:00
|
|
|
|
// 动画属性配置
|
|
|
|
|
|
duration: 'duration',
|
|
|
|
|
|
delay: 'delay',
|
|
|
|
|
|
easing: 'easing',
|
|
|
|
|
|
transformDuration: 'transform-duration',
|
|
|
|
|
|
transformDelay: 'transform-delay',
|
|
|
|
|
|
transformEasing: 'transform-easing',
|
|
|
|
|
|
opacityDuration: 'opacity-duration',
|
|
|
|
|
|
opacityDelay: 'opacity-delay',
|
|
|
|
|
|
opacityEasing: 'opacity-easing',
|
2026-05-15 10:50:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
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,// 获取根元素函数
|
2026-05-19 15:55:49 +08:00
|
|
|
|
activeClass = 'active',// 激活类名
|
|
|
|
|
|
duration = '0.5s',// 动画时间
|
|
|
|
|
|
delay = '0s',// 延迟时间
|
|
|
|
|
|
easing = 'ease-out',// 缓动函数
|
2026-05-15 10:50:25 +08:00
|
|
|
|
} = value || {}
|
2026-05-19 15:55:49 +08:00
|
|
|
|
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],
|
|
|
|
|
|
}
|
2026-05-15 10:50:25 +08:00
|
|
|
|
const {
|
|
|
|
|
|
scroll = false,// 是否监听滚动事件
|
|
|
|
|
|
once = false,// 是否只执行一次
|
|
|
|
|
|
parent = false,// 是否监听父元素滚动事件
|
|
|
|
|
|
} = modifiers
|
|
|
|
|
|
const root = GetRoot ? GetRoot() : parent ? el.parentElement : document;
|
|
|
|
|
|
if (el === root) return;
|
2026-05-19 15:55:49 +08:00
|
|
|
|
const config = {
|
2026-05-15 10:50:25 +08:00
|
|
|
|
root,// 根元素
|
|
|
|
|
|
scroll,
|
|
|
|
|
|
once,
|
|
|
|
|
|
activeClass,
|
|
|
|
|
|
isActive: false,
|
2026-05-19 15:55:49 +08:00
|
|
|
|
transition,
|
|
|
|
|
|
}
|
2026-05-27 14:58:04 +08:00
|
|
|
|
console.log(config)
|
2026-05-19 15:55:49 +08:00
|
|
|
|
els.set(el, config)
|
|
|
|
|
|
add(el, root, config)
|
2026-05-15 10:50:25 +08:00
|
|
|
|
},
|
|
|
|
|
|
beforeUnmount(el, binding) {
|
|
|
|
|
|
remove(el)
|
|
|
|
|
|
els.delete(el)
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-05-19 15:55:49 +08:00
|
|
|
|
function add(el, root = document, config) {
|
|
|
|
|
|
if (config.scroll) {
|
|
|
|
|
|
requestAnimationFrame(() => handleScroll({ target: root }))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
getChildren(el).forEach((child) => setDocumentStyles(el, child, 0))
|
|
|
|
|
|
}
|
2026-05-15 10:50:25 +08:00
|
|
|
|
resize.observe(el)
|
|
|
|
|
|
if (roots.has(root)) {
|
|
|
|
|
|
let obj = roots.get(root)
|
|
|
|
|
|
obj.els.push(el)
|
|
|
|
|
|
obj.observer.observe(el)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-15 11:09:36 +08:00
|
|
|
|
resize.observe(isDocumentRoot(root))
|
2026-05-15 10:50:25 +08:00
|
|
|
|
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)
|
2026-05-19 15:55:49 +08:00
|
|
|
|
getChildren(target).forEach((el) => {
|
|
|
|
|
|
setDocumentStyles(target, el, obj.isActive ? 1 : 0)
|
|
|
|
|
|
})
|
2026-05-15 10:50:25 +08:00
|
|
|
|
})
|
|
|
|
|
|
}, { 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()
|
2026-05-15 11:09:36 +08:00
|
|
|
|
resize.unobserve(isDocumentRoot(root))
|
2026-05-15 10:50:25 +08:00
|
|
|
|
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
|
2026-05-19 15:55:49 +08:00
|
|
|
|
const children = getChildren(el)
|
2026-05-15 10:50:25 +08:00
|
|
|
|
if (children.length === 0) return
|
2026-05-15 11:09:36 +08:00
|
|
|
|
const rootEl = isDocumentRoot(root)
|
2026-05-15 12:16:15 +08:00
|
|
|
|
const offsetHeight = root === document ? window.innerHeight : rootEl.offsetHeight
|
|
|
|
|
|
const offsetTop = rootEl.offsetTop
|
|
|
|
|
|
const scrollTop = rootEl.scrollTop
|
2026-05-27 14:58:04 +08:00
|
|
|
|
const elTop_bottom = offsetHeight - (getDocumentTop(el) - offsetTop - rootEl.scrollTop)
|
2026-05-15 12:16:15 +08:00
|
|
|
|
const maxHeight = offsetHeight + el.offsetHeight
|
|
|
|
|
|
const p = Math.min(1, Math.max(0, elTop_bottom / maxHeight))
|
2026-05-15 10:50:25 +08:00
|
|
|
|
children.forEach((item) => {
|
2026-05-19 15:55:49 +08:00
|
|
|
|
setDocumentStyles(el, item, p)
|
2026-05-15 10:50:25 +08:00
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-05-27 14:58:04 +08:00
|
|
|
|
function getDocumentTop(el, root = document, offset = 0) {
|
|
|
|
|
|
const parent = el.parentElement
|
|
|
|
|
|
offset = offset + el.offsetTop
|
|
|
|
|
|
if (parent && parent !== document.documentElement) return getDocumentTop(parent, root, offset)
|
|
|
|
|
|
return offset
|
|
|
|
|
|
}
|
2026-05-19 15:55:49 +08:00
|
|
|
|
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(', ')
|
2026-05-20 15:54:42 +08:00
|
|
|
|
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)`
|
2026-05-19 15:55:49 +08:00
|
|
|
|
el.style.transform = transform
|
|
|
|
|
|
if (hasAttr(el, [T.opacity_s, T.opacity])) {
|
2026-05-20 15:54:42 +08:00
|
|
|
|
el.style.opacity = getCurrentValue(el, T.opacity_s, T.opacity, p, T.opacity_s, T.opacity, 1).num
|
2026-05-19 15:55:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
function getAttrs(el, attrs = {}) {
|
|
|
|
|
|
const arrs = Object.keys(attrs)
|
|
|
|
|
|
const obj = {}
|
|
|
|
|
|
arrs.forEach((item) => {
|
|
|
|
|
|
obj[item] = el.getAttribute(T[item]) || attrs[item]
|
|
|
|
|
|
})
|
|
|
|
|
|
return obj
|
|
|
|
|
|
}
|
2026-05-15 10:50:25 +08:00
|
|
|
|
function getCurrentValue(el, start, end, progress, bStart, bEnd, defaultValue = 0) {
|
2026-05-20 15:54:42 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-05-15 10:50:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
function hasAttr(el, attr) {
|
|
|
|
|
|
if (Array.isArray(attr)) {
|
|
|
|
|
|
return attr.some(v => el.hasAttribute(v))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return el.hasAttribute(attr)
|
|
|
|
|
|
}
|
2026-05-15 11:09:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 检查document是否为根元素
|
|
|
|
|
|
function isDocumentRoot(root) {
|
|
|
|
|
|
return root === document ? document.documentElement : root
|
|
|
|
|
|
}
|