Files
Code-Create/src/directives/custom-animation.js
2026-05-28 09:46:56 +08:00

259 lines
8.7 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
* duration: 动画时间-默认值0.5s
* delay: 延迟时间-默认值0s
* easing: 缓动函数-默认值ease-out
* transformDuration: 变换时间-默认值 duration
* transformDelay: 变换延迟时间-默认值 delay
* transformEasing: 变换缓动函数-默认值 easing
* opacityDuration: 透明度时间-默认值 duration
* opacityDelay: 透明度延迟时间-默认值 delay
* opacityEasing: 透明度缓动函数-默认值 easing
*
* 子元素动画
* <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',
// 动画属性配置
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,
}
console.log(config)
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 - (getDocumentTop(el) - 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 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
}
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
}