pixuireactcomponents 1.5.59 → 1.5.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,28 +1,58 @@
|
|
|
1
1
|
import { h } from 'preact';
|
|
2
2
|
/**
|
|
3
|
+
* 帧动画组件
|
|
3
4
|
*
|
|
4
5
|
* @param rootId 根节点id
|
|
5
6
|
* @param rootClassName 根节点className
|
|
6
7
|
* @param totalTime 动画总时长
|
|
7
8
|
* @param width 组件宽度
|
|
8
9
|
* @param height 组件高度
|
|
9
|
-
* @param srcArr 图片url数组
|
|
10
|
+
* @param srcArr 图片 url 数组
|
|
10
11
|
* @param onShow 每一帧显示时的回调
|
|
11
12
|
* @param onFinish 动画结束时的回调
|
|
12
|
-
* @param preloadCount 预加载节点的数量
|
|
13
|
+
* @param preloadCount 预加载节点的数量 (最少 3)
|
|
13
14
|
* @param loop 是否循环播放
|
|
14
15
|
* @param showLastFrame 播放结束后是否显示最后一帧
|
|
16
|
+
* @param cRef 外部组件引用(暴露 seek(frameIndex:number) 方法)
|
|
15
17
|
*/
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
export interface FrameAnimationHandle {
|
|
19
|
+
/** 跳转到指定帧(从 0 开始计数) */
|
|
20
|
+
seek: (frameIndex: number) => void;
|
|
21
|
+
/** 开始播放(从当前已显示帧继续) */
|
|
22
|
+
play: () => void;
|
|
23
|
+
/** 暂停播放(不清理预加载) */
|
|
24
|
+
pause: () => void;
|
|
25
|
+
}
|
|
26
|
+
export interface FrameAnimationProps {
|
|
27
|
+
rootId?: string;
|
|
28
|
+
rootClassName?: string;
|
|
29
|
+
/** 动画总时长(毫秒) */
|
|
19
30
|
totalTime: number;
|
|
31
|
+
/** 组件宽度,如 "500px" */
|
|
20
32
|
width: string;
|
|
33
|
+
/** 组件高度,如 "300px" */
|
|
21
34
|
height: string;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
/** 每帧图片 URL 数组(与 spriteClassArr 二选一) */
|
|
36
|
+
srcArr?: string[];
|
|
37
|
+
/** 每帧对应的样式类名数组(className),在样式中指定 background-image 等 */
|
|
38
|
+
spriteClassArr?: string[];
|
|
39
|
+
/** 每一帧显示的回调 */
|
|
40
|
+
onShow?: (nowframe: number) => void;
|
|
41
|
+
/** 动画结束回调 */
|
|
42
|
+
onFinish?: () => void;
|
|
43
|
+
/** 预加载节点数量,最少 3 */
|
|
44
|
+
preloadCount?: number;
|
|
45
|
+
/** 是否循环播放 */
|
|
46
|
+
loop?: boolean;
|
|
47
|
+
/** 播放结束后是否显示最后一帧 */
|
|
48
|
+
showLastFrame?: boolean;
|
|
49
|
+
/** 播放范围起始帧 (包含) */
|
|
50
|
+
rangeStart?: number;
|
|
51
|
+
/** 播放范围结束帧 (包含) */
|
|
52
|
+
rangeEnd?: number;
|
|
53
|
+
/** 外部组件引用,用于调用 seek 方法 */
|
|
54
|
+
cRef?: any;
|
|
55
|
+
/** 是否在初始化后自动播放(默认 true) */
|
|
56
|
+
autoplay?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare const FrameAnimation: (props: FrameAnimationProps) => h.JSX.Element;
|
|
@@ -1,96 +1,255 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'preact/hooks';
|
|
2
2
|
import { createRef, h } from 'preact';
|
|
3
|
-
// let requestAnimationFrame = (f) => {
|
|
4
|
-
// return setTimeout(f, 300);
|
|
5
|
-
// };
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
8
|
-
* @param rootId 根节点id
|
|
9
|
-
* @param rootClassName 根节点className
|
|
10
|
-
* @param totalTime 动画总时长
|
|
11
|
-
* @param width 组件宽度
|
|
12
|
-
* @param height 组件高度
|
|
13
|
-
* @param srcArr 图片url数组
|
|
14
|
-
* @param onShow 每一帧显示时的回调
|
|
15
|
-
* @param onFinish 动画结束时的回调
|
|
16
|
-
* @param preloadCount 预加载节点的数量
|
|
17
|
-
* @param loop 是否循环播放
|
|
18
|
-
* @param showLastFrame 播放结束后是否显示最后一帧
|
|
19
|
-
*/
|
|
20
3
|
export var FrameAnimation = function (props) {
|
|
21
|
-
|
|
4
|
+
// ------------------------------ 参数与前置处理 ------------------------------
|
|
22
5
|
props.preloadCount = Math.max(props.preloadCount || 3, 3);
|
|
23
|
-
var totalTime = props.totalTime, width = props.width, height = props.height,
|
|
6
|
+
var totalTime = props.totalTime, width = props.width, height = props.height, _a = props.srcArr, srcArr = _a === void 0 ? [] : _a, spriteClassArr = props.spriteClassArr, onShow = props.onShow, preloadCount = props.preloadCount, _b = props.loop, loop = _b === void 0 ? false : _b, onFinish = props.onFinish, _c = props.showLastFrame, showLastFrame = _c === void 0 ? true : _c, rootId = props.rootId, rootClassName = props.rootClassName, rangeStart = props.rangeStart, rangeEnd = props.rangeEnd, cRef = props.cRef, _d = props.autoplay, autoplay = _d === void 0 ? true : _d;
|
|
7
|
+
// 模式判定:sprite 模式或图片模式
|
|
8
|
+
var useSprite = Array.isArray(spriteClassArr) && spriteClassArr.length > 0;
|
|
9
|
+
var frameCount = useSprite ? spriteClassArr.length : srcArr.length;
|
|
10
|
+
// 每帧耗时(保护除零)
|
|
11
|
+
var frameTime = frameCount > 0 ? totalTime / frameCount : 0;
|
|
12
|
+
// ------------------------------ 播放范围处理 ------------------------------
|
|
13
|
+
var startIndex = Math.max(0, Math.min(rangeStart != null ? rangeStart : 0, frameCount - 1));
|
|
14
|
+
var endIndex = Math.max(startIndex, Math.min(rangeEnd != null ? rangeEnd : frameCount - 1, frameCount - 1));
|
|
15
|
+
var rangeLen = endIndex - startIndex + 1;
|
|
16
|
+
// ------------------------------ DOM Ref 初始化 ------------------------------
|
|
24
17
|
var cRefArr = useRef(Array.from({ length: preloadCount }, function () { return createRef(); })).current;
|
|
25
|
-
var imgEleArr = useRef(Array.from({ length: preloadCount }, function (
|
|
26
|
-
|
|
27
|
-
})).current;
|
|
18
|
+
var imgEleArr = useRef(Array.from({ length: preloadCount }, function (_, i) { return (h("img", { ref: cRefArr[i], src: srcArr[i], style: { position: 'absolute', visibility: 'hidden', width: width, height: height } })); })).current;
|
|
19
|
+
// sprite 模式所需的 div 预加载节点
|
|
20
|
+
var dRefArr = useRef(Array.from({ length: preloadCount }, function () { return createRef(); })).current;
|
|
21
|
+
var spriteEleArr = useRef(Array.from({ length: preloadCount }, function (_, i) { return (h("div", { ref: dRefArr[i], style: { position: 'absolute', visibility: 'hidden', width: width, height: height } })); })).current;
|
|
22
|
+
// ------------------------------ 状态引用 ------------------------------
|
|
23
|
+
var startTimeRef = useRef(0); // 当前动画起始时间
|
|
24
|
+
var showedFrameRef = useRef(0); // 已经展示的帧序号
|
|
25
|
+
var isPlayingRef = useRef(autoplay !== false); // 播放状态(仅初始化受 autoplay 影响)
|
|
26
|
+
var rootRef = useRef(null); // 根容器,用于判断是否真的卸载
|
|
27
|
+
// ------------------------------ seek 功能实现 ------------------------------
|
|
28
|
+
var seek = function (frameIndex) {
|
|
29
|
+
console.log('frame seek !!', frameIndex);
|
|
30
|
+
if (frameCount <= 0)
|
|
31
|
+
return;
|
|
32
|
+
if (frameIndex < 0 || frameIndex >= frameCount)
|
|
33
|
+
return;
|
|
34
|
+
// 1. 直接更新可见帧及预加载
|
|
35
|
+
var bufferLen = useSprite ? dRefArr.length : cRefArr.length;
|
|
36
|
+
var nowArrIndex = bufferLen > 0 ? frameIndex % bufferLen : 0;
|
|
37
|
+
if (useSprite) {
|
|
38
|
+
dRefArr.forEach(function (ref, i) {
|
|
39
|
+
var divNode = ref.current;
|
|
40
|
+
if (!divNode)
|
|
41
|
+
return;
|
|
42
|
+
if (i === nowArrIndex) {
|
|
43
|
+
divNode.style.visibility = 'visible';
|
|
44
|
+
divNode.className = spriteClassArr[frameIndex];
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
divNode.style.visibility = 'hidden';
|
|
48
|
+
var offset = (i + dRefArr.length - nowArrIndex) % dRefArr.length;
|
|
49
|
+
var offsetFrame = startIndex + ((frameIndex - startIndex + offset) % rangeLen);
|
|
50
|
+
divNode.className = spriteClassArr[offsetFrame];
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
cRefArr.forEach(function (ref, i) {
|
|
56
|
+
var imgNode = ref.current;
|
|
57
|
+
if (!imgNode)
|
|
58
|
+
return;
|
|
59
|
+
if (i === nowArrIndex) {
|
|
60
|
+
imgNode.style.visibility = 'visible';
|
|
61
|
+
imgNode.src = srcArr[frameIndex];
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
imgNode.style.visibility = 'hidden';
|
|
65
|
+
var offset = (i + cRefArr.length - nowArrIndex) % cRefArr.length;
|
|
66
|
+
var offsetFrame = startIndex + ((frameIndex - startIndex + offset) % rangeLen);
|
|
67
|
+
imgNode.src = srcArr[offsetFrame];
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// 2. 回调 & 状态同步
|
|
72
|
+
showedFrameRef.current = frameIndex;
|
|
73
|
+
onShow && onShow(frameIndex);
|
|
74
|
+
// 3. 调整起始时间,让主循环从下一帧继续
|
|
75
|
+
startTimeRef.current = Date.now() - frameIndex * frameTime;
|
|
76
|
+
if (isPlayingRef.current) {
|
|
77
|
+
nextFrame(startTimeRef.current, frameIndex);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
// 开始/暂停控制
|
|
81
|
+
var play = function () {
|
|
82
|
+
if (isPlayingRef.current)
|
|
83
|
+
return;
|
|
84
|
+
isPlayingRef.current = true;
|
|
85
|
+
// 从当前帧继续播放
|
|
86
|
+
startTimeRef.current = Date.now() - showedFrameRef.current * frameTime;
|
|
87
|
+
nextFrame(startTimeRef.current, showedFrameRef.current);
|
|
88
|
+
};
|
|
89
|
+
var pause = function () {
|
|
90
|
+
// 不清理任何节点或预加载,仅停止后续调度
|
|
91
|
+
isPlayingRef.current = false;
|
|
92
|
+
};
|
|
93
|
+
// 对外暴露方法
|
|
94
|
+
if (cRef) {
|
|
95
|
+
cRef.current = {
|
|
96
|
+
seek: seek,
|
|
97
|
+
play: play,
|
|
98
|
+
pause: pause,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// ------------------------------ 主循环初始化 ------------------------------
|
|
28
102
|
useEffect(function () {
|
|
29
|
-
|
|
30
|
-
var
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
103
|
+
// 范围或模式或数据变化时,保证重新初始化并对齐到合法帧
|
|
104
|
+
var target = Math.max(startIndex, Math.min(showedFrameRef.current, endIndex));
|
|
105
|
+
seek(target);
|
|
106
|
+
// 卸载时无需特殊处理:nextFrame 在组件卸载时会检测到 ref 已为 null 并停止调度
|
|
107
|
+
}, [startIndex, endIndex, spriteClassArr, srcArr, useSprite]);
|
|
108
|
+
// 尺寸变化时,同步更新已挂载节点尺寸
|
|
109
|
+
useEffect(function () {
|
|
110
|
+
cRefArr.forEach(function (ref) {
|
|
111
|
+
if (ref.current) {
|
|
112
|
+
ref.current.style.width = width;
|
|
113
|
+
ref.current.style.height = height;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
dRefArr.forEach(function (ref) {
|
|
117
|
+
if (ref.current) {
|
|
118
|
+
ref.current.style.width = width;
|
|
119
|
+
ref.current.style.height = height;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}, [width, height]);
|
|
123
|
+
// 时长或帧数变化时,若在播放中,重设起始时间以尽量平滑过渡
|
|
124
|
+
useEffect(function () {
|
|
125
|
+
if (!isPlayingRef.current)
|
|
126
|
+
return;
|
|
127
|
+
var v = frameCount > 0 ? totalTime / frameCount : 0;
|
|
128
|
+
if (v > 0) {
|
|
129
|
+
startTimeRef.current = Date.now() - showedFrameRef.current * v;
|
|
130
|
+
}
|
|
131
|
+
}, [totalTime, frameCount]);
|
|
132
|
+
// 循环开关变化时,重启主循环以应用最新逻辑
|
|
133
|
+
useEffect(function () {
|
|
134
|
+
if (!isPlayingRef.current)
|
|
135
|
+
return;
|
|
136
|
+
var v = frameCount > 0 ? totalTime / frameCount : 0;
|
|
137
|
+
startTimeRef.current = v > 0 ? Date.now() - showedFrameRef.current * v : Date.now();
|
|
138
|
+
nextFrame(startTimeRef.current, showedFrameRef.current);
|
|
139
|
+
}, [loop]);
|
|
140
|
+
// ------------------------------ 帧动画主循环 ------------------------------
|
|
141
|
+
var nextFrame = function (startTime, showedFrame) {
|
|
142
|
+
// 如果这不是最新的一轮动画,直接忽略
|
|
143
|
+
if (startTime !== startTimeRef.current)
|
|
144
|
+
return;
|
|
145
|
+
// 暂停状态下不再推进帧,也不调度下一轮
|
|
146
|
+
if (!isPlayingRef.current)
|
|
147
|
+
return;
|
|
148
|
+
var v = frameCount > 0 ? totalTime / frameCount : 0;
|
|
149
|
+
if (v <= 0)
|
|
47
150
|
return;
|
|
151
|
+
var nowTime = Date.now();
|
|
152
|
+
var rawFrame = Math.floor((nowTime - startTime) / v); // 从动画开始以来的原始帧序号
|
|
153
|
+
// 计算在播放区间内的实际帧序号
|
|
154
|
+
var nowFrame = loop ? startIndex + ((((rawFrame - startIndex) % rangeLen) + rangeLen) % rangeLen) : rawFrame;
|
|
155
|
+
var bufferLen = useSprite ? dRefArr.length : cRefArr.length;
|
|
156
|
+
var nowArrIndex = bufferLen > 0 ? nowFrame % bufferLen : 0; // 使用第几个预加载元素
|
|
157
|
+
var showedArrIndex = bufferLen > 0 ? showedFrame % bufferLen : 0;
|
|
158
|
+
// 组件已卸载
|
|
159
|
+
if (useSprite) {
|
|
160
|
+
if (!dRefArr[0].current) {
|
|
161
|
+
// 若根容器仍在,说明正处于模式切换的挂载窗口期,等待下一帧重试
|
|
162
|
+
if (rootRef.current) {
|
|
163
|
+
requestAnimationFrame(function () {
|
|
164
|
+
if (isPlayingRef.current)
|
|
165
|
+
nextFrame(startTime, showedFrame);
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
onFinish && onFinish();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
if (!cRefArr[0].current) {
|
|
175
|
+
if (rootRef.current) {
|
|
176
|
+
requestAnimationFrame(function () {
|
|
177
|
+
if (isPlayingRef.current)
|
|
178
|
+
nextFrame(startTime, showedFrame);
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
onFinish && onFinish();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
48
185
|
}
|
|
49
|
-
|
|
186
|
+
// 动画结束(非循环情况下)
|
|
187
|
+
if (!loop && nowFrame > endIndex) {
|
|
50
188
|
onFinish && onFinish();
|
|
51
|
-
if (
|
|
52
|
-
|
|
189
|
+
if (useSprite) {
|
|
190
|
+
dRefArr.forEach(function (ref) {
|
|
191
|
+
if (ref.current)
|
|
192
|
+
ref.current.style.visibility = 'hidden';
|
|
193
|
+
});
|
|
194
|
+
if (showLastFrame && dRefArr[0].current) {
|
|
195
|
+
dRefArr[0].current.className = spriteClassArr[endIndex];
|
|
196
|
+
dRefArr[0].current.style.visibility = 'visible';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
cRefArr.forEach(function (ref) {
|
|
53
201
|
ref.current.style.visibility = 'hidden';
|
|
54
202
|
});
|
|
55
203
|
if (showLastFrame) {
|
|
56
|
-
|
|
57
|
-
cRefArr[0].current.src = srcArr[srcArr.length - 1];
|
|
204
|
+
cRefArr[0].current.src = srcArr[endIndex];
|
|
58
205
|
cRefArr[0].current.style.visibility = 'visible';
|
|
59
206
|
}
|
|
60
|
-
return;
|
|
61
207
|
}
|
|
62
|
-
//重置img标签的 src 和 visibility
|
|
63
|
-
cRefArr.forEach(function (ref, i) {
|
|
64
|
-
ref.current.style.visibility = 'hidden';
|
|
65
|
-
ref.current.src = srcArr[i];
|
|
66
|
-
});
|
|
67
|
-
startTime = Date.now();
|
|
68
|
-
cRefArr[0].current.style.visibility = 'visible';
|
|
69
|
-
requestAnimationFrame(function () {
|
|
70
|
-
nextFrame(startTime, v, 0);
|
|
71
|
-
});
|
|
72
208
|
return;
|
|
73
209
|
}
|
|
210
|
+
// 帧更新
|
|
74
211
|
if (nowFrame !== showedFrame) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
212
|
+
if (useSprite) {
|
|
213
|
+
dRefArr[nowArrIndex].current.style.visibility = 'visible';
|
|
214
|
+
dRefArr[showedArrIndex].current.style.visibility = 'hidden';
|
|
215
|
+
// 可能出现跳帧,为其余 div 做预加载(通过 className 触发 background 图片加载)
|
|
216
|
+
dRefArr.forEach(function (ref, i) {
|
|
217
|
+
if (i !== nowArrIndex) {
|
|
218
|
+
var offset = (i + dRefArr.length - nowArrIndex) % dRefArr.length;
|
|
219
|
+
var offsetFrame = startIndex + ((nowFrame - startIndex + offset) % rangeLen);
|
|
220
|
+
if (ref.current.className !== spriteClassArr[offsetFrame]) {
|
|
221
|
+
ref.current.className = spriteClassArr[offsetFrame];
|
|
222
|
+
}
|
|
85
223
|
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
cRefArr[nowArrIndex].current.style.visibility = 'visible';
|
|
228
|
+
cRefArr[showedArrIndex].current.style.visibility = 'hidden';
|
|
229
|
+
// 可能出现跳帧,为其余 img 做预加载
|
|
230
|
+
cRefArr.forEach(function (ref, i) {
|
|
231
|
+
if (i !== nowArrIndex) {
|
|
232
|
+
var offset = (i + cRefArr.length - nowArrIndex) % cRefArr.length;
|
|
233
|
+
var offsetFrame = startIndex + ((nowFrame - startIndex + offset) % rangeLen);
|
|
234
|
+
if (ref.current.src !== srcArr[offsetFrame]) {
|
|
235
|
+
ref.current.src = srcArr[offsetFrame];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
88
240
|
showedFrame = nowFrame;
|
|
241
|
+
showedFrameRef.current = nowFrame;
|
|
89
242
|
onShow && onShow(nowFrame);
|
|
243
|
+
var curnode = useSprite ? dRefArr[nowArrIndex].current : cRefArr[nowArrIndex].current;
|
|
244
|
+
console.log('curnode', curnode === null || curnode === void 0 ? void 0 : curnode.getBoundingClientRect());
|
|
245
|
+
}
|
|
246
|
+
// 下一帧(仅在播放状态下调度)
|
|
247
|
+
if (isPlayingRef.current) {
|
|
248
|
+
requestAnimationFrame(function () {
|
|
249
|
+
nextFrame(startTime, showedFrame);
|
|
250
|
+
});
|
|
90
251
|
}
|
|
91
|
-
requestAnimationFrame(function () {
|
|
92
|
-
nextFrame(startTime, v, showedFrame);
|
|
93
|
-
});
|
|
94
252
|
};
|
|
95
|
-
|
|
253
|
+
// ------------------------------ 组件渲染 ------------------------------
|
|
254
|
+
return (h("div", { ref: rootRef, style: { width: width, height: height }, id: rootId, className: rootClassName }, useSprite ? spriteEleArr : imgEleArr));
|
|
96
255
|
};
|