一张绘有所有帧的雪碧图,图与图之间间距相同,每张图的宽高相同。
如:
(本文使用的是纵向雪碧图,可自行更改参数实验效果)
原理:将动画的所有帧导出为一张雪碧图,使用js控制background-position,每一帧移动一张图的距离。
坑点:1. css可以绘制序列帧动画:使用step函数,实现简单,性能好。
如:
.imgContainer{
background-position: 0 0;
transition: all 1s step(24);
}
.imgContainer:hover{
background-position: 0 720px;
}
但是使用css实现的帧动画,很难做到中途中断动画并反向播放。播放完毕后反向播放实现简单,但是中断当前动画反向播放较为困难。例如中途移出鼠标后,动画会从当前帧反向播放直到初始状态,但是这个过程中timing-function还是step(24),而中断后反向播放的总帧数并没有24帧,导致动画错乱。而使用js控制timing-function倒不如直接将动画播放全权控制。
2. requestAnimationFrame与react的state更新机制冲突,时间足够短时react会自动批处理state更新,导致requestAnimationFrame中的state更新不能及时反馈到页面上, 动画丢帧严重,而使用Ref保存相关state并不会触发页面更新。
可以考虑降级使用setTimeout处理。但是遵循了react哲学的同时性能肯定不如requestAnimationFrame了。
所以我选择直接使用ref更新dom。
实现: React:hooks封装:
import { MutableRefObject, useState, useEffect, useRef } from "react";
/**
* @param ref MutableRefObj 目标元素
* @param frameImageNumber number 总帧数(序列图总数)
* @param direction "vertical"|"horizontal" 绘制帧方向 默认"vertical"
* @param frameNumber number 1s内帧数 默认60帧
* @returns setDispatch<"in"|"out"> 设置鼠标移入还是移出
*/
interface UseFrameAnimationProps {
ref: MutableRefObject;
frameImageNumber: number;
direction?: "vertical" | "horizontal"
frameNumber?: number;
}
const useFrameAnimation = (props: UseFrameAnimationProps) => {
const { ref, frameImageNumber, direction = "vertical", frameNumber = 60 } = props;
// 一帧跨越的高度 数值
const [frameHeight, setFrameHeight] = useState(0);
// 一帧跨越的高度 单位
const [frameHeightUnit, setFrameHeightUnit] = useState("px");
useEffect(() => {
const heightString = (ref?.current && window.getComputedStyle(ref.current).height) || "0";
const height = parseInt(heightString);
setFrameHeight(height)
setFrameHeightUnit(heightString.replace(height.toString(), ""));
}, []);
const [type, setType] = useState<"in" | "out" | undefined>();
// 间隔多少秒后绘制一帧
const [frameTime, setFrameTime] = useState(1000 / (frameNumber-1));
useEffect(() => { setFrameTime(1000 / (frameNumber-1)) }, [frameNumber])
// 处理到第几帧画面
const frameImage = useRef(0);
// 上次绘制帧的时间
const enterTiming = useRef(0);
// requestAnimationFrame flag
const requestFlag = useRef(null);
const animationFunc = (type = "in") => {
// 获取当前时间 与 enterTiming对比 超过frameTime则绘制下一帧
const nowTime = Date.now();
if (nowTime - enterTiming.current >= frameTime) {
enterTiming.current = nowTime;
// 通过离散地移动backgroundPosition跳到下一帧
if (direction === "horizontal") {
ref.current && (ref.current.style.backgroundPosition = `-${frameImage.current * frameHeight}${frameHeightUnit} 0`);
} else {
ref.current && (ref.current.style.backgroundPosition = `0 -${frameImage.current * frameHeight}${frameHeightUnit}`);
}
// 判断是否中断绘制,或已绘制到最后一张
if (type === "in") {
frameImage.current += 1;
if (frameImage.current >= frameImageNumber) {
return;
}
} else {
if (frameImage.current <= 0) {
return;
}
frameImage.current -= 1;
}
}
// 继续绘制
requestFlag.current = requestAnimationFrame(() => animationFunc(type));
}
useEffect(() => () => {
requestFlag.current && cancelAnimationFrame(requestFlag.current)
}, [])
// type变更则开始绘制
useEffect(() => {
if (!type) {
return;
}
// 终止上次绘制
requestFlag.current && cancelAnimationFrame(requestFlag.current);
frameImage.current === frameImageNumber && (frameImage.current -= 1)
enterTiming.current = Date.now();
requestFlag.current = requestAnimationFrame(() => animationFunc(type))
}, [type])
return setType;
}
export default useFrameAnimation;
使用:
#animation-container{
background-image: url("step-image.png");
background-repeat: no-repeat;
background-size: 100% auto;
}
const Index = () => {
const animationEl = useRef(null);
const setAnimationType = useFrameAnimation({
ref: animationEl,
frameImageNumber: 24
});
return (
{
backgroundImage: `url(${icon})`,
}}
/>
)
}
原生:
Document
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)