Skip to content

Commit aef4d2f

Browse files
committed
#FEAT: 增加加载动画的退场动画、修复进度条停留在100%一小会问题
1 parent 752c736 commit aef4d2f

6 files changed

Lines changed: 178 additions & 20 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
- 自定义加载动画:主加载与背景层加载均可自定义绘制
1818
- 并发控制与重试:`maxConcurrentLoads`
1919

20-
文档:https://byte-n.github.io/scroll-frame-canvas/
20+
文档 & Demohttps://byte-n.github.io/scroll-frame-canvas/
2121

2222
---
2323

demo/index.html

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,106 @@ <h3 style="color: #2a2a2a; margin-bottom: 20px; font-size: 28px; text-align: cen
165165
player.init().then(() => player.play());</code></pre>
166166
</div>
167167

168+
<!-- ScrollFrameCanvas 初始化参数说明 -->
169+
<div style="background: #f8f9fa; padding: 40px; border-radius: 12px; margin-bottom: 40px;">
170+
<h3 style="color: #2a2a2a; margin-bottom: 30px; font-size: 28px; text-align: center;">
171+
📋 ScrollFrameCanvas 初始化参数说明
172+
</h3>
173+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 30px;">
174+
<div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
175+
<h4 style="color: #007acc; margin-bottom: 15px; font-size: 18px; font-weight: 600;">基础配置</h4>
176+
<div style="font-size: 14px; line-height: 1.6; color: #333;">
177+
<div style="margin-bottom: 8px;"><strong>canvasEle:</strong> HTMLCanvasElement - Canvas 元素(必填)</div>
178+
<div style="margin-bottom: 8px;"><strong>scrollbarEle:</strong> HTMLElement | Window | Document - 滚动容器(必填)</div>
179+
<div style="margin-bottom: 8px;"><strong>scrollAxis:</strong> 'x' | 'y' - 滚动轴方向(必填)</div>
180+
<div style="margin-bottom: 8px;"><strong>frameMapper:</strong> Function - 滚动像素到帧索引的映射函数(必填)</div>
181+
</div>
182+
</div>
183+
<div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
184+
<h4 style="color: #007acc; margin-bottom: 15px; font-size: 18px; font-weight: 600;">数据源配置</h4>
185+
<div style="font-size: 14px; line-height: 1.6; color: #333;">
186+
<div style="margin-bottom: 8px;"><strong>images:</strong> Array&lt;HTMLImageElement | ImageBitmap | string&gt; - 图片数组</div>
187+
<div style="margin-bottom: 8px;"><strong>total:</strong> number - 总帧数</div>
188+
<div style="margin-bottom: 8px;"><strong>imageFactory:</strong> Function - 异步图片工厂函数</div>
189+
<div style="margin-bottom: 8px;"><strong>maxConcurrentLoads:</strong> number - 最大并发加载数(默认: 6)</div>
190+
</div>
191+
</div>
192+
<div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
193+
<h4 style="color: #007acc; margin-bottom: 15px; font-size: 18px; font-weight: 600;">渲染配置</h4>
194+
<div style="font-size: 14px; line-height: 1.6; color: #333;">
195+
<div style="margin-bottom: 8px;"><strong>scaleMode:</strong> 'contain' | 'cover' | 'fill' - 缩放模式(默认: 'contain')</div>
196+
<div style="margin-bottom: 8px;"><strong>resetScrollOnInit:</strong> boolean - 初始化时重置滚动位置(默认: true)</div>
197+
<div style="margin-bottom: 8px;"><strong>exitAnimationFrameRate:</strong> number - 退出动画帧率(默认: 75)</div>
198+
</div>
199+
</div>
200+
<div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
201+
<h4 style="color: #007acc; margin-bottom: 15px; font-size: 18px; font-weight: 600;">性能优化</h4>
202+
<div style="font-size: 14px; line-height: 1.6; color: #333;">
203+
<div style="margin-bottom: 8px;"><strong>large.enabled:</strong> boolean - 启用分层加载(默认: true)</div>
204+
<div style="margin-bottom: 8px;"><strong>large.factor:</strong> number - 分层因子,必须是2的幂次方(默认: 4)</div>
205+
<div style="margin-bottom: 8px;"><strong>large.showBackgroundProgress:</strong> boolean - 显示后台加载进度(默认: true)</div>
206+
</div>
207+
</div>
208+
<div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
209+
<h4 style="color: #007acc; margin-bottom: 15px; font-size: 18px; font-weight: 600;">事件回调</h4>
210+
<div style="font-size: 14px; line-height: 1.6; color: #333;">
211+
<div style="margin-bottom: 8px;"><strong>onFrame:</strong> (frame: number, offset: number) => void - 帧更新回调</div>
212+
<div style="margin-bottom: 8px;"><strong>onComplete:</strong> () => void - 动画完成回调</div>
213+
<div style="margin-bottom: 8px;"><strong>onInitComplete:</strong> () => void - 初始化完成回调</div>
214+
<div style="margin-bottom: 8px;"><strong>onError:</strong> (error: Error) => void - 错误回调</div>
215+
</div>
216+
</div>
217+
<div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
218+
<h4 style="color: #007acc; margin-bottom: 15px; font-size: 18px; font-weight: 600;">自定义渲染</h4>
219+
<div style="font-size: 14px; line-height: 1.6; color: #333;">
220+
<div style="margin-bottom: 8px;"><strong>customMainLoading:</strong> Function - 自定义主加载动画</div>
221+
<div style="margin-bottom: 8px;"><strong>customBackgroundLoading:</strong> Function - 自定义后台加载动画</div>
222+
</div>
223+
</div>
224+
</div>
225+
</div>
226+
227+
<!-- ScrollFrameCanvas 事件、实例函数说明 -->
228+
<div style="background: #f8f9fa; padding: 40px; border-radius: 12px; margin-bottom: 40px;">
229+
<h3 style="color: #2a2a2a; margin-bottom: 30px; font-size: 28px; text-align: center;">
230+
🔧 ScrollFrameCanvas 事件、实例函数说明
231+
</h3>
232+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 30px;">
233+
<div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
234+
<h4 style="color: #28a745; margin-bottom: 15px; font-size: 18px; font-weight: 600;">生命周期方法</h4>
235+
<div style="font-size: 14px; line-height: 1.6; color: #333;">
236+
<div style="margin-bottom: 8px;"><strong>init():</strong> Promise&lt;this&gt; - 初始化并预加载所有帧</div>
237+
<div style="margin-bottom: 8px;"><strong>destroy():</strong> void - 销毁实例并清理资源</div>
238+
</div>
239+
</div>
240+
<div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
241+
<h4 style="color: #28a745; margin-bottom: 15px; font-size: 18px; font-weight: 600;">播放控制</h4>
242+
<div style="font-size: 14px; line-height: 1.6; color: #333;">
243+
<div style="margin-bottom: 8px;"><strong>play():</strong> void - 开始播放动画</div>
244+
<div style="margin-bottom: 8px;"><strong>pause():</strong> void - 暂停播放</div>
245+
<div style="margin-bottom: 8px;"><strong>stop():</strong> void - 停止并重置到第0帧</div>
246+
</div>
247+
</div>
248+
<div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
249+
<h4 style="color: #28a745; margin-bottom: 15px; font-size: 18px; font-weight: 600;">帧控制</h4>
250+
<div style="font-size: 14px; line-height: 1.6; color: #333;">
251+
<div style="margin-bottom: 8px;"><strong>setFrame(frameIndex: number):</strong> void - 设置当前帧</div>
252+
<div style="margin-bottom: 8px;"><strong>getCurrentFrame():</strong> number - 获取当前帧索引</div>
253+
<div style="margin-bottom: 8px;"><strong>getTotalFrames():</strong> number - 获取总帧数</div>
254+
</div>
255+
</div>
256+
<div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
257+
<h4 style="color: #28a745; margin-bottom: 15px; font-size: 18px; font-weight: 600;">事件系统</h4>
258+
<div style="font-size: 14px; line-height: 1.6; color: #333;">
259+
<div style="margin-bottom: 8px;"><strong>on('frame', callback):</strong> 监听帧更新事件</div>
260+
<div style="margin-bottom: 8px;"><strong>on('complete', callback):</strong> 监听动画完成事件</div>
261+
<div style="margin-bottom: 8px;"><strong>on('initComplete', callback):</strong> 监听初始化完成事件</div>
262+
<div style="margin-bottom: 8px;"><strong>on('error', callback):</strong> 监听错误事件</div>
263+
</div>
264+
</div>
265+
</div>
266+
</div>
267+
168268
<div style="text-align: center;">
169269
<h3 style="color: #2a2a2a; margin-bottom: 20px; font-size: 24px;">
170270
适用场景

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@byte.n/scroll-frame-canvas",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"description": "基于滚动条驱动的 Canvas 帧序列渲染器,支持渐进式大序列加载、OffscreenCanvas 预渲染与事件回调。",
55
"type": "module",
66
"main": "dist/scroll-frame-canvas.umd.js",

src/ScrollFrameCanvas.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EventEmitter } from './events';
2-
import { clamp, deepMerge, noop, runTasks, selectFit } from './utils';
2+
import { clamp, deepMerge, noop, runTasks, selectFit, drawExitAnimation } from './utils';
33

44
export interface ScrollFrameCanvasOptions {
55
canvasEle: HTMLCanvasElement;
@@ -39,6 +39,7 @@ export interface ScrollFrameCanvasOptions {
3939
frameMapper: (scrollPixels: number, totalScrollPixels: number, totalFrames: number) => number;
4040
resetScrollOnInit?: boolean;
4141
maxConcurrentLoads?: number;
42+
exitAnimationFrameRate?: number;
4243
}
4344

4445
/**
@@ -90,6 +91,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
9091
scaleMode: 'contain',
9192
resetScrollOnInit: true,
9293
maxConcurrentLoads: 6,
94+
exitAnimationFrameRate: 75,
9395
onFrame: noop,
9496
onComplete: noop,
9597
onError: noop,
@@ -205,6 +207,9 @@ export default class ScrollFrameCanvas extends EventEmitter {
205207
}),
206208
this.options.maxConcurrentLoads!,
207209
);
210+
211+
await this._stopLoadingAnimation();
212+
208213
this.totalProgress = this.total;
209214
Promise.resolve()
210215
.then(async () => {
@@ -242,9 +247,9 @@ export default class ScrollFrameCanvas extends EventEmitter {
242247
);
243248
// 移除 null 元素并更新 total
244249
this._cleanupNullFrames();
250+
await this._stopLoadingAnimation();
245251
}
246252

247-
248253
// 恢复滚动
249254
this._enableScroll();
250255

@@ -254,9 +259,8 @@ export default class ScrollFrameCanvas extends EventEmitter {
254259
} catch (error) {
255260
// 出错时也要停止动画并恢复滚动
256261
this._enableScroll();
262+
await this._stopLoadingAnimation();
257263
throw error;
258-
} finally {
259-
this._stopLoadingAnimation();
260264
}
261265
}
262266

@@ -364,7 +368,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
364368
this._preRenderedFrames[index] = null as any; // 标记为 null
365369
if (!isBack) {
366370
this.totalProgress--;
367-
this._updateLoadingProgress();
371+
this._updateLoadingProgress(0);
368372
}
369373
return;
370374
}
@@ -579,7 +583,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
579583
return clamp(Math.round(frameIndex), 0, total - 1);
580584
}
581585

582-
private _renderIfNeeded (force: boolean = false, usePrevRealFrame = false) {
586+
private _renderIfNeeded (force: boolean = false, usePrevRealFrame = false, event = true) {
583587
if (this._destroyed) return;
584588
const frame = this._currentFrame;
585589
if (!force) {
@@ -617,9 +621,11 @@ export default class ScrollFrameCanvas extends EventEmitter {
617621
this._lastDrawnFrame = frame;
618622
this._lastRealDrawFrame = realFrame;
619623

620-
const { scrollPixels, totalScrollPixels } = this._getScrollPixels();
621-
const offset = totalScrollPixels > 0 ? scrollPixels / totalScrollPixels : 0;
622-
this.emit('frame', frame, offset);
624+
if (event) {
625+
const { scrollPixels, totalScrollPixels } = this._getScrollPixels();
626+
const offset = totalScrollPixels > 0 ? scrollPixels / totalScrollPixels : 0;
627+
this.emit('frame', frame, offset);
628+
}
623629

624630
if (frame === this.getTotalFrames() - 1) {
625631
this.emit('complete');
@@ -649,19 +655,27 @@ export default class ScrollFrameCanvas extends EventEmitter {
649655
this._loadingCompleted = false;
650656
}
651657

652-
private _stopLoadingAnimation (): void {
658+
private _stopLoadingAnimation (): Promise<void> {
659+
this._drawLoadingAnimation();
660+
653661
this._loadingCompleted = true;
654662
cancelAnimationFrame(this._loadingAnimationId);
655663
this._loadingAnimationId = 0;
656-
if (this._destroyed) return;
657-
// 清空画布
658-
if (this.ctx) {
659-
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
660-
}
664+
if (this._destroyed) return Promise.resolve();
665+
666+
this._currentFrame = 0;
667+
return drawExitAnimation({
668+
ctx: this.ctx, canvas: this.canvas,
669+
totalFrame: this.options.exitAnimationFrameRate!,
670+
exitDirection: 'top',
671+
drawBackground: () => {
672+
this._renderIfNeeded(true, false, false)
673+
}
674+
})
661675
}
662676

663-
private _updateLoadingProgress (): void {
664-
this._loadingProgressCount++;
677+
private _updateLoadingProgress (incr = 1): void {
678+
this._loadingProgressCount += incr;
665679
this._loadingProgress = Math.min(1, Math.max(0, this._loadingProgressCount / this.totalProgress));
666680

667681
const animate = () => {

src/demo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import ScrollFrameCanvas from './ScrollFrameCanvas';
22

33
// 全局变量
44
let scrollFrameCanvas: ScrollFrameCanvas | null = null;
5-
let currentFrameRatio = 0;
5+
let currentFrameRatio = 3;
66
let scrollHint: HTMLElement | null = null;
77
let hintShown = false;
88

src/utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,47 @@ export function deepMerge<T> (obj: T, obj2: T): T {
149149

150150
return result;
151151
}
152+
153+
export function easeOutQuad (t: number): number {
154+
const tt = clamp(t, 0, 1);
155+
return 1 - (1 - tt) * (1 - tt);
156+
}
157+
158+
export function drawExitAnimation (config: {
159+
ctx: CanvasRenderingContext2D,
160+
canvas: HTMLCanvasElement,
161+
drawBackground: () => void,
162+
totalFrame: number;
163+
exitDirection: 'top' | 'bottom'
164+
}) {
165+
const { ctx, canvas, drawBackground, totalFrame, exitDirection } = config;
166+
const ofc = new OffscreenCanvas(canvas.width, canvas.height);
167+
const opcCtx = ofc.getContext('2d');
168+
opcCtx.drawImage(canvas, 0, 0);
169+
170+
return new Promise<void>((resolve) => {
171+
172+
let frame = 0;
173+
const drawExit = () => {
174+
if (frame === totalFrame) {
175+
ctx.globalAlpha = 1;
176+
return resolve();
177+
}
178+
ctx.clearRect(0, 0, canvas.width, canvas.height);
179+
180+
ctx.globalAlpha = 1;
181+
drawBackground();
182+
183+
const t = frame / totalFrame;
184+
const eased = easeOutQuad(t);
185+
186+
ctx.globalAlpha = 1 - eased;
187+
ctx.drawImage(ofc, 0, exitDirection === 'top' ? -canvas.height * eased : canvas.height * eased);
188+
189+
frame++;
190+
requestAnimationFrame(drawExit);
191+
};
192+
193+
requestAnimationFrame(drawExit);
194+
});
195+
}

0 commit comments

Comments
 (0)