@libshub/gif-tools 1.0.7 → 1.0.9
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/README.md +233 -0
- package/dist/components/GifPlayer.d.ts +2 -0
- package/dist/gif-tools.css +1 -1
- package/dist/gif-tools.es.js +308 -207
- package/dist/index.d.ts +2 -2
- package/dist/utils/fetchTiming.d.ts +1 -0
- package/dist/utils/gifResourceManager.d.ts +4 -2
- package/dist/utils/gifWorkerPool.d.ts +11 -0
- package/dist/utils/index.d.ts +2 -3
- package/dist/utils/loadStats.d.ts +0 -3
- package/dist/utils/types.d.ts +10 -7
- package/dist/workers/gifDecode.worker.d.ts +1 -0
- package/package.json +4 -2
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# @libshub/gif-tools
|
|
2
|
+
|
|
3
|
+
基于 Canvas 的 React GIF 播放组件,使用 [gifuct-js](https://github.com/matt-way/gifuct-js) 解码,支持播放控制、循环次数、资源缓存、Worker 解码与加载性能统计。
|
|
4
|
+
|
|
5
|
+
## 演示
|
|
6
|
+
|
|
7
|
+
[](http://www.hanxiaoxin.cn/gif-tools/)
|
|
8
|
+
|
|
9
|
+
在线体验:[http://www.hanxiaoxin.cn/gif-tools/](http://www.hanxiaoxin.cn/gif-tools/)
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @libshub/gif-tools
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
需要 React 17+:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install react react-dom
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 快速开始
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { GifPlayer } from '@libshub/gif-tools'
|
|
27
|
+
import '@libshub/gif-tools/style.css'
|
|
28
|
+
|
|
29
|
+
function App() {
|
|
30
|
+
return (
|
|
31
|
+
<GifPlayer
|
|
32
|
+
src="https://example.com/demo.gif"
|
|
33
|
+
autoPlay
|
|
34
|
+
showControls
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## GifPlayer
|
|
41
|
+
|
|
42
|
+
### Props
|
|
43
|
+
|
|
44
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
45
|
+
|------|------|--------|------|
|
|
46
|
+
| `src` | `string` | — | GIF 地址(必填) |
|
|
47
|
+
| `width` | `number \| string` | — | 画布宽度 |
|
|
48
|
+
| `height` | `number \| string` | — | 画布高度 |
|
|
49
|
+
| `className` | `string` | — | 根节点 class |
|
|
50
|
+
| `style` | `CSSProperties` | — | 根节点样式 |
|
|
51
|
+
| `autoPlay` | `boolean` | `true` | 加载完成后自动播放 |
|
|
52
|
+
| `showControls` | `boolean` | `false` | 显示播放/暂停按钮 |
|
|
53
|
+
| `debug` | `boolean` | `false` | 在画面上叠加加载耗时调试信息 |
|
|
54
|
+
| `useWorker` | `boolean` | `false` | 在 Web Worker 中解码,减轻主线程压力 |
|
|
55
|
+
| `workerConcurrency` | `number` | `min(hardwareConcurrency, 4)` | Worker 池并发数,范围 1–16,仅 `useWorker=true` 时生效 |
|
|
56
|
+
| `loopCount` | `number` | — | 循环次数,不传则按 GIF 自身循环或无限循环 |
|
|
57
|
+
| `onLoaded` | `(stats: GifLoadStats) => void` | — | 加载完成 |
|
|
58
|
+
| `onPlay` | `() => void` | — | 开始播放 |
|
|
59
|
+
| `onPause` | `() => void` | — | 暂停 |
|
|
60
|
+
| `onEnd` | `() => void` | — | 达到循环上限后结束 |
|
|
61
|
+
| `onError` | `(error: Error) => void` | — | 加载或解码失败 |
|
|
62
|
+
|
|
63
|
+
### Ref 方法
|
|
64
|
+
|
|
65
|
+
通过 `ref` 可命令式控制播放器:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { useRef } from 'react'
|
|
69
|
+
import { GifPlayer, type GifPlayerRef } from '@libshub/gif-tools'
|
|
70
|
+
|
|
71
|
+
const ref = useRef<GifPlayerRef>(null)
|
|
72
|
+
|
|
73
|
+
ref.current?.play() // 播放
|
|
74
|
+
ref.current?.pause() // 暂停
|
|
75
|
+
ref.current?.toggle() // 切换播放/暂停
|
|
76
|
+
ref.current?.reset() // 回到第一帧并暂停
|
|
77
|
+
ref.current?.reload() // 重新加载(跳过 pending 复用,强制 fresh)
|
|
78
|
+
ref.current?.isPlaying() // 是否正在播放
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Worker 解码
|
|
82
|
+
|
|
83
|
+
多 GIF 并发加载时,可将 decode 放到 Worker 池,避免阻塞主线程:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<GifPlayer
|
|
87
|
+
src="https://example.com/demo.gif"
|
|
88
|
+
useWorker
|
|
89
|
+
workerConcurrency={8}
|
|
90
|
+
debug
|
|
91
|
+
/>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
也可在应用层全局设置 Worker 池大小:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { setWorkerPoolSize } from '@libshub/gif-tools'
|
|
98
|
+
|
|
99
|
+
setWorkerPoolSize(8) // 1–16
|
|
100
|
+
setWorkerPoolSize() // 恢复默认 min(hardwareConcurrency, 4)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 完整示例
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { useRef } from 'react'
|
|
107
|
+
import { GifPlayer, type GifPlayerRef } from '@libshub/gif-tools'
|
|
108
|
+
import '@libshub/gif-tools/style.css'
|
|
109
|
+
|
|
110
|
+
export default function Demo() {
|
|
111
|
+
const playerRef = useRef<GifPlayerRef>(null)
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
<button onClick={() => playerRef.current?.toggle()}>播放/暂停</button>
|
|
116
|
+
|
|
117
|
+
<GifPlayer
|
|
118
|
+
ref={playerRef}
|
|
119
|
+
src="https://example.com/demo.gif"
|
|
120
|
+
width="100%"
|
|
121
|
+
height="auto"
|
|
122
|
+
className="my-gif"
|
|
123
|
+
style={{ borderRadius: 8 }}
|
|
124
|
+
autoPlay
|
|
125
|
+
showControls
|
|
126
|
+
useWorker
|
|
127
|
+
workerConcurrency={4}
|
|
128
|
+
loopCount={2}
|
|
129
|
+
debug
|
|
130
|
+
onLoaded={(stats) => console.log(`loaded in ${stats.totalMs.toFixed(1)}ms`)}
|
|
131
|
+
onPlay={() => console.log('play')}
|
|
132
|
+
onPause={() => console.log('pause')}
|
|
133
|
+
onEnd={() => console.log('end')}
|
|
134
|
+
onError={(e) => console.error(e)}
|
|
135
|
+
/>
|
|
136
|
+
</>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 资源缓存
|
|
142
|
+
|
|
143
|
+
相同 `src` 的 GIF 在全局共享解码结果(不论是否使用 Worker),多个 `GifPlayer` 实例不会重复 fetch / decode。
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { clearGifResourceCache } from '@libshub/gif-tools'
|
|
147
|
+
|
|
148
|
+
clearGifResourceCache()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 加载统计 `GifLoadStats`
|
|
152
|
+
|
|
153
|
+
`onLoaded` 回调中的 `GifLoadStats` 可区分三种加载模式,并拆分 network / queue / decode 三阶段:
|
|
154
|
+
|
|
155
|
+
| 字段 | 说明 |
|
|
156
|
+
|------|------|
|
|
157
|
+
| `networkTimeMs` | 网络或浏览器缓存读取耗时(Resource Timing) |
|
|
158
|
+
| `queueWaitMs` | Worker 池排队,或 pending 跟随方等待进行中的加载 |
|
|
159
|
+
| `decodeTimeMs` | 实际 decode 耗时 |
|
|
160
|
+
| `totalMs` | 墙钟总耗时(该实例从开始加载到就绪) |
|
|
161
|
+
| `fromCache` | 命中内存缓存 |
|
|
162
|
+
| `fromPending` | 等待同一 `src` 的进行中加载 |
|
|
163
|
+
|
|
164
|
+
| 模式 | 判断 | 典型字段 |
|
|
165
|
+
|------|------|----------|
|
|
166
|
+
| **fresh** | `!fromCache && !fromPending` | `networkTimeMs`、`queueWaitMs`、`decodeTimeMs` |
|
|
167
|
+
| **pending** | `fromPending` | `queueWaitMs`(其余为 0) |
|
|
168
|
+
| **cache** | `fromCache` | 均为 0 |
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import type { GifLoadStats } from '@libshub/gif-tools'
|
|
172
|
+
|
|
173
|
+
onLoaded={(stats: GifLoadStats) => {
|
|
174
|
+
if (stats.fromCache) {
|
|
175
|
+
console.log('cache hit')
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
if (stats.fromPending) {
|
|
179
|
+
console.log(`pending queue ${stats.queueWaitMs.toFixed(1)}ms`)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
console.log(
|
|
183
|
+
`network ${stats.networkTimeMs.toFixed(1)}ms`,
|
|
184
|
+
`queue ${stats.queueWaitMs.toFixed(1)}ms`,
|
|
185
|
+
`decode ${stats.decodeTimeMs.toFixed(1)}ms`,
|
|
186
|
+
)
|
|
187
|
+
console.log(`wall ${stats.totalMs.toFixed(1)}ms`)
|
|
188
|
+
}}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
> 并发场景下,各实例 `totalMs` 为各自墙钟时间;整批加载的总耗时应取最慢实例的 `totalMs`,而非简单累加。
|
|
192
|
+
|
|
193
|
+
## 底层 API
|
|
194
|
+
|
|
195
|
+
不依赖 React 时,可直接在 Canvas 上创建控制器:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { createGifController } from '@libshub/gif-tools'
|
|
199
|
+
|
|
200
|
+
const canvas = document.querySelector('canvas')!
|
|
201
|
+
const { controller, stats } = await createGifController(canvas, src, {
|
|
202
|
+
useWorker: true,
|
|
203
|
+
workerConcurrency: 4,
|
|
204
|
+
loopCount: 3,
|
|
205
|
+
onPlay: () => {},
|
|
206
|
+
onPause: () => {},
|
|
207
|
+
onEnd: () => {},
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
controller.play()
|
|
211
|
+
controller.pause()
|
|
212
|
+
controller.reset()
|
|
213
|
+
controller.isPlaying()
|
|
214
|
+
controller.getCompletedLoops()
|
|
215
|
+
controller.destroy()
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## 导出一览
|
|
219
|
+
|
|
220
|
+
**组件**
|
|
221
|
+
|
|
222
|
+
- `GifPlayer`
|
|
223
|
+
|
|
224
|
+
**类型**
|
|
225
|
+
|
|
226
|
+
- `GifPlayerProps`、`GifPlayerRef`、`GifPlayerComponent`
|
|
227
|
+
- `GifLoadStats`、`GifController`、`CreateGifOptions`
|
|
228
|
+
|
|
229
|
+
**工具函数**
|
|
230
|
+
|
|
231
|
+
- `createGifController` — 在 Canvas 上创建 GIF 控制器
|
|
232
|
+
- `clearGifResourceCache` — 清空 GIF 资源缓存
|
|
233
|
+
- `setWorkerPoolSize` — 设置全局 Worker 池并发数(1–16),不传参则恢复默认 `min(hardwareConcurrency, 4)`
|
package/dist/gif-tools.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
.gif-player{line-height:0;display:inline-block;position:relative;overflow:visible}.gif-player__media{max-width:100%;height:auto;display:block}.gif-player__controls{opacity:0;gap:6px;transition:opacity .2s;display:flex;position:absolute;bottom:8px;right:8px}.gif-player:hover .gif-player__controls,.gif-player--show-controls .gif-player__controls{opacity:1}.gif-player__btn{color:#fff;cursor:pointer;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#0000008c;border:none;border-radius:50%;justify-content:center;align-items:center;width:32px;height:32px;padding:0;display:flex}.gif-player__btn:hover{background:#000000bf}.gif-player__btn svg{fill:currentColor;width:16px;height:16px}.gif-player__debug{z-index:1;pointer-events:none;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);font-variant-numeric:tabular-nums;color:#e8eaed;background:#0c0e12d1;font-family:ui-monospace,Cascadia Code,SF Mono,monospace;position:absolute;top:0;left:0;box-shadow:0 2px 8px #0003}.gif-player__debug--compact{white-space:nowrap;border-radius:0 0 4px;padding:2px 5px;font-size:8px;line-height:1.2}.gif-player__debug--medium{border-radius:0 0 5px;padding:3px 6px;font-size:9px;line-height:1.2}.gif-player__debug--collapsible{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none}.gif-player__debug--collapsible:hover{background:#12161ceb}.gif-player__debug--collapsible:focus-visible{outline-offset:1px;outline:1px solid #7ec8ffbf}.gif-player__debug--full{border-radius:0 0 6px;min-width:
|
|
1
|
+
.gif-player{line-height:0;display:inline-block;position:relative;overflow:visible}.gif-player__media{max-width:100%;height:auto;display:block}.gif-player__controls{opacity:0;gap:6px;transition:opacity .2s;display:flex;position:absolute;bottom:8px;right:8px}.gif-player:hover .gif-player__controls,.gif-player--show-controls .gif-player__controls{opacity:1}.gif-player__btn{color:#fff;cursor:pointer;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#0000008c;border:none;border-radius:50%;justify-content:center;align-items:center;width:32px;height:32px;padding:0;display:flex}.gif-player__btn:hover{background:#000000bf}.gif-player__btn svg{fill:currentColor;width:16px;height:16px}.gif-player__debug{z-index:1;pointer-events:none;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);font-variant-numeric:tabular-nums;color:#e8eaed;background:#0c0e12d1;font-family:ui-monospace,Cascadia Code,SF Mono,monospace;position:absolute;top:0;left:0;box-shadow:0 2px 8px #0003}.gif-player__debug--compact{white-space:nowrap;border-radius:0 0 4px;padding:2px 5px;font-size:8px;line-height:1.2}.gif-player__debug--medium{border-radius:0 0 5px;padding:3px 6px;font-size:9px;line-height:1.2}.gif-player__debug--collapsible{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none}.gif-player__debug--collapsible:hover{background:#12161ceb}.gif-player__debug--collapsible:focus-visible{outline-offset:1px;outline:1px solid #7ec8ffbf}.gif-player__debug--full{border-radius:0 0 6px;min-width:120px;padding:5px 7px;font-size:10px;line-height:1.35}.gif-player__debug--expanded{z-index:2;min-width:108px;max-width:none!important}.gif-player__debug-head{justify-content:space-between;align-items:center;gap:8px;display:flex}.gif-player__debug-badge{letter-spacing:.04em;color:#fff;background:#ffffff24;border-radius:3px;padding:1px 4px;font-size:8px;font-weight:600;display:inline-block}.gif-player__debug--full .gif-player__debug-badge{margin-bottom:4px;padding:1px 5px;font-size:9px}.gif-player__debug[data-mode=fresh] .gif-player__debug-badge{background:#4a90e28c}.gif-player__debug[data-mode=pending] .gif-player__debug-badge{background:#e6a23c8c}.gif-player__debug[data-mode=cache] .gif-player__debug-badge{background:#52c4808c}.gif-player__debug-body{flex-direction:column;gap:2px;display:flex}.gif-player__debug-row,.gif-player__debug-total{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.gif-player__debug-label{color:#e8eaedb8;white-space:nowrap}.gif-player__debug-value{color:#fff;white-space:nowrap}.gif-player__debug-value--total{color:#7ec8ff;font-weight:600}.gif-player__debug[data-mode=cache] .gif-player__debug-value--total{color:#82e0aa}.gif-player__debug-total{border-top:1px solid #ffffff1f;margin-top:4px;padding-top:4px}.gif-player__debug-total .gif-player__debug-label{color:#e8eaede6;font-weight:600}.gif-player__debug-total .gif-player__debug-value{color:#7ec8ff;font-weight:600}.gif-player__debug[data-mode=cache] .gif-player__debug-total .gif-player__debug-value{color:#82e0aa}
|
|
2
2
|
/*$vite$:1*/
|
package/dist/gif-tools.es.js
CHANGED
|
@@ -298,123 +298,234 @@ var c = (e, t) => () => (t || (e((t = { exports: {} }).exports, t), e = null), t
|
|
|
298
298
|
return c(n, e.gct, t);
|
|
299
299
|
});
|
|
300
300
|
};
|
|
301
|
-
})))()
|
|
302
|
-
function
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
301
|
+
})))();
|
|
302
|
+
function h(e, t) {
|
|
303
|
+
if (typeof performance > "u") return t;
|
|
304
|
+
let n = performance.getEntriesByName(e.url), r = n[n.length - 1];
|
|
305
|
+
return r ? r.duration > 0 ? r.duration : r.responseEnd > r.startTime ? r.responseEnd - r.startTime : t : t;
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/workers/gifDecode.worker.ts?worker&inline
|
|
309
|
+
var g = "(function(){var e=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),t=e((e=>{Object.defineProperty(e,\"__esModule\",{value:!0}),e.loop=e.conditional=e.parse=void 0,e.parse=function e(t,n){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{},i=arguments.length>3&&arguments[3]!==void 0?arguments[3]:r;if(Array.isArray(n))n.forEach(function(n){return e(t,n,r,i)});else if(typeof n==`function`)n(t,r,i,e);else{var a=Object.keys(n)[0];Array.isArray(n[a])?(i[a]={},e(t,n[a],r,i[a])):i[a]=n[a](t,r,i,e)}return r},e.conditional=function(e,t){return function(n,r,i,a){t(n,r,i)&&a(n,e,r,i)}},e.loop=function(e,t){return function(n,r,i,a){for(var o=[],s=n.pos;t(n,r,i);){var c={};if(a(n,e,r,c),n.pos===s)break;s=n.pos,o.push(c)}return o}}})),n=e((e=>{Object.defineProperty(e,\"__esModule\",{value:!0}),e.readBits=e.readArray=e.readUnsigned=e.readString=e.peekBytes=e.readBytes=e.peekByte=e.readByte=e.buildStream=void 0,e.buildStream=function(e){return{data:e,pos:0}};var t=function(){return function(e){return e.data[e.pos++]}};e.readByte=t,e.peekByte=function(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:0;return function(t){return t.data[t.pos+e]}};var n=function(e){return function(t){return t.data.subarray(t.pos,t.pos+=e)}};e.readBytes=n,e.peekBytes=function(e){return function(t){return t.data.subarray(t.pos,t.pos+e)}},e.readString=function(e){return function(t){return Array.from(n(e)(t)).map(function(e){return String.fromCharCode(e)}).join(``)}},e.readUnsigned=function(e){return function(t){var r=n(2)(t);return e?(r[1]<<8)+r[0]:(r[0]<<8)+r[1]}},e.readArray=function(e,t){return function(r,i,a){for(var o=typeof t==`function`?t(r,i,a):t,s=n(e),c=Array(o),l=0;l<o;l++)c[l]=s(r);return c}};var r=function(e,t,n){for(var r=0,i=0;i<n;i++)r+=e[t+i]&&2**(n-i-1);return r};e.readBits=function(e){return function(n){for(var i=t()(n),a=Array(8),o=0;o<8;o++)a[7-o]=!!(i&1<<o);return Object.keys(e).reduce(function(t,n){var i=e[n];return i.length?t[n]=r(a,i.index,i.length):t[n]=a[i.index],t},{})}}})),r=e((e=>{Object.defineProperty(e,\"__esModule\",{value:!0}),e.default=void 0;var r=t(),i=n(),a={blocks:function(e){for(var t=0,n=[],r=e.data.length,a=0,o=(0,i.readByte)()(e);o!==t&&o;o=(0,i.readByte)()(e)){if(e.pos+o>=r){var s=r-e.pos;n.push((0,i.readBytes)(s)(e)),a+=s;break}n.push((0,i.readBytes)(o)(e)),a+=o}for(var c=new Uint8Array(a),l=0,u=0;u<n.length;u++)c.set(n[u],l),l+=n[u].length;return c}},o=(0,r.conditional)({gce:[{codes:(0,i.readBytes)(2)},{byteSize:(0,i.readByte)()},{extras:(0,i.readBits)({future:{index:0,length:3},disposal:{index:3,length:3},userInput:{index:6},transparentColorGiven:{index:7}})},{delay:(0,i.readUnsigned)(!0)},{transparentColorIndex:(0,i.readByte)()},{terminator:(0,i.readByte)()}]},function(e){var t=(0,i.peekBytes)(2)(e);return t[0]===33&&t[1]===249}),s=(0,r.conditional)({image:[{code:(0,i.readByte)()},{descriptor:[{left:(0,i.readUnsigned)(!0)},{top:(0,i.readUnsigned)(!0)},{width:(0,i.readUnsigned)(!0)},{height:(0,i.readUnsigned)(!0)},{lct:(0,i.readBits)({exists:{index:0},interlaced:{index:1},sort:{index:2},future:{index:3,length:2},size:{index:5,length:3}})}]},(0,r.conditional)({lct:(0,i.readArray)(3,function(e,t,n){return 2**(n.descriptor.lct.size+1)})},function(e,t,n){return n.descriptor.lct.exists}),{data:[{minCodeSize:(0,i.readByte)()},a]}]},function(e){return(0,i.peekByte)()(e)===44}),c=(0,r.conditional)({text:[{codes:(0,i.readBytes)(2)},{blockSize:(0,i.readByte)()},{preData:function(e,t,n){return(0,i.readBytes)(n.text.blockSize)(e)}},a]},function(e){var t=(0,i.peekBytes)(2)(e);return t[0]===33&&t[1]===1}),l=(0,r.conditional)({application:[{codes:(0,i.readBytes)(2)},{blockSize:(0,i.readByte)()},{id:function(e,t,n){return(0,i.readString)(n.blockSize)(e)}},a]},function(e){var t=(0,i.peekBytes)(2)(e);return t[0]===33&&t[1]===255}),u=(0,r.conditional)({comment:[{codes:(0,i.readBytes)(2)},a]},function(e){var t=(0,i.peekBytes)(2)(e);return t[0]===33&&t[1]===254});e.default=[{header:[{signature:(0,i.readString)(3)},{version:(0,i.readString)(3)}]},{lsd:[{width:(0,i.readUnsigned)(!0)},{height:(0,i.readUnsigned)(!0)},{gct:(0,i.readBits)({exists:{index:0},resolution:{index:1,length:3},sort:{index:4},size:{index:5,length:3}})},{backgroundColorIndex:(0,i.readByte)()},{pixelAspectRatio:(0,i.readByte)()}]},(0,r.conditional)({gct:(0,i.readArray)(3,function(e,t){return 2**(t.lsd.gct.size+1)})},function(e,t){return t.lsd.gct.exists}),{frames:(0,r.loop)([o,l,u,s,c],function(e){var t=(0,i.peekByte)()(e);return t===33||t===44})}]})),i=e((e=>{Object.defineProperty(e,\"__esModule\",{value:!0}),e.deinterlace=void 0,e.deinterlace=function(e,t){for(var n=Array(e.length),r=e.length/t,i=function(r,i){var a=e.slice(i*t,(i+1)*t);n.splice.apply(n,[r*t,t].concat(a))},a=[0,4,2,1],o=[8,8,4,2],s=0,c=0;c<4;c++)for(var l=a[c];l<r;l+=o[c])i(l,s),s++;return n}})),a=e((e=>{Object.defineProperty(e,\"__esModule\",{value:!0}),e.lzw=void 0,e.lzw=function(e,t,n){var r=4096,i=-1,a=n,o,s,c,l,u,d,f,p,m,h,g,_,v,y,b,x,S=Array(n),C=Array(r),w=Array(r),T=Array(r+1);for(_=e,s=1<<_,u=s+1,o=s+2,f=i,l=_+1,c=(1<<l)-1,m=0;m<s;m++)C[m]=0,w[m]=m;var g=p=v=y=x=b=0,p,v,y,x,b;for(h=0;h<a;){if(y===0){if(p<l){g+=t[b]<<p,p+=8,b++;continue}if(m=g&c,g>>=l,p-=l,m>o||m==u)break;if(m==s){l=_+1,c=(1<<l)-1,o=s+2,f=i;continue}if(f==i){T[y++]=w[m],f=m,v=m;continue}for(d=m,m==o&&(T[y++]=v,m=f);m>s;)T[y++]=w[m],m=C[m];v=w[m]&255,T[y++]=v,o<r&&(C[o]=f,w[o]=v,o++,(o&c)===0&&o<r&&(l++,c+=o)),f=d}y--,S[x++]=T[y],h++}for(h=x;h<a;h++)S[h]=0;return S}})),o=e((e=>{Object.defineProperty(e,\"__esModule\",{value:!0}),e.decompressFrames=e.decompressFrame=e.parseGIF=void 0;var o=d(r()),s=t(),c=n(),l=i(),u=a();function d(e){return e&&e.__esModule?e:{default:e}}e.parseGIF=function(e){var t=new Uint8Array(e);return(0,s.parse)((0,c.buildStream)(t),o.default)};var f=function(e){for(var t=e.pixels.length,n=new Uint8ClampedArray(t*4),r=0;r<t;r++){var i=r*4,a=e.pixels[r],o=e.colorTable[a]||[0,0,0];n[i]=o[0],n[i+1]=o[1],n[i+2]=o[2],n[i+3]=a===e.transparentIndex?0:255}return n},p=function(e,t,n){if(!e.image){console.warn(`gif frame does not have associated image.`);return}var r=e.image,i=r.descriptor.width*r.descriptor.height,a=(0,u.lzw)(r.data.minCodeSize,r.data.blocks,i);r.descriptor.lct.interlaced&&(a=(0,l.deinterlace)(a,r.descriptor.width));var o={pixels:a,dims:{top:e.image.descriptor.top,left:e.image.descriptor.left,width:e.image.descriptor.width,height:e.image.descriptor.height}};return r.descriptor.lct&&r.descriptor.lct.exists?o.colorTable=r.lct:o.colorTable=t,e.gce&&(o.delay=(e.gce.delay||10)*10,o.disposalType=e.gce.extras.disposal,e.gce.extras.transparentColorGiven&&(o.transparentIndex=e.gce.transparentColorIndex)),n&&(o.patch=f(o)),o};e.decompressFrame=p,e.decompressFrames=function(e,t){return e.frames.filter(function(e){return e.image}).map(function(n){return p(n,e.gct,t)})}}))();function s(e){let t=new ArrayBuffer(e.patch.byteLength);return new Uint8ClampedArray(t).set(e.patch),{dims:e.dims,delay:e.delay,disposalType:e.disposalType,patchBuffer:t}}self.onmessage=e=>{let{id:t,type:n,buffer:r}=e.data;if(n===`decode`)try{let e=performance.now(),n=(0,o.parseGIF)(r),i=(0,o.decompressFrames)(n,!0),a=performance.now()-e;if(!i.length){let e={id:t,type:`decode`,ok:!1,error:`GIF has no frames`};self.postMessage(e);return}let c=i.map(s),l=c.map(e=>e.patchBuffer),u={id:t,type:`decode`,ok:!0,width:n.lsd.width,height:n.lsd.height,frames:c,decodeTimeMs:a};self.postMessage(u,{transfer:l})}catch(e){let n={id:t,type:`decode`,ok:!1,error:e instanceof Error?e.message:String(e)};self.postMessage(n)}}})();", _ = typeof self < "u" && self.Blob && new Blob(["(self.URL || self.webkitURL).revokeObjectURL(self.location.href);", g], { type: "text/javascript;charset=utf-8" });
|
|
310
|
+
function v(e) {
|
|
311
|
+
let t;
|
|
312
|
+
try {
|
|
313
|
+
if (t = _ && (self.URL || self.webkitURL).createObjectURL(_), !t) throw "";
|
|
314
|
+
let n = new Worker(t, { name: e?.name });
|
|
315
|
+
return n.addEventListener("error", () => {
|
|
316
|
+
(self.URL || self.webkitURL).revokeObjectURL(t);
|
|
317
|
+
}), n;
|
|
318
|
+
} catch {
|
|
319
|
+
return new Worker("data:text/javascript;charset=utf-8," + encodeURIComponent(g), { name: e?.name });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/utils/gifWorkerPool.ts
|
|
324
|
+
var y = typeof navigator < "u" ? Math.min(navigator.hardwareConcurrency || 4, 4) : 4, b = 16;
|
|
325
|
+
function x(e) {
|
|
326
|
+
return e === void 0 ? y : Math.min(Math.max(Math.floor(e), 1), b);
|
|
327
|
+
}
|
|
328
|
+
function S(e) {
|
|
329
|
+
return {
|
|
330
|
+
dims: e.dims,
|
|
331
|
+
delay: e.delay,
|
|
332
|
+
disposalType: e.disposalType,
|
|
333
|
+
patch: new Uint8ClampedArray(e.patchBuffer),
|
|
334
|
+
colorTable: [],
|
|
335
|
+
pixels: [],
|
|
336
|
+
transparentIndex: -1
|
|
309
337
|
};
|
|
310
338
|
}
|
|
311
|
-
|
|
339
|
+
var C = new class {
|
|
340
|
+
workers = [];
|
|
341
|
+
idleWorkers = [];
|
|
342
|
+
pendingJobs = /* @__PURE__ */ new Map();
|
|
343
|
+
workerJobs = /* @__PURE__ */ new Map();
|
|
344
|
+
waitQueue = [];
|
|
345
|
+
jobId = 0;
|
|
346
|
+
initialized = !1;
|
|
347
|
+
targetSize = y;
|
|
348
|
+
spawnWorker() {
|
|
349
|
+
let e = new v();
|
|
350
|
+
return e.onmessage = (t) => {
|
|
351
|
+
this.handleMessage(e, t.data);
|
|
352
|
+
}, e.onerror = (t) => {
|
|
353
|
+
this.handleWorkerError(e, t.message || "Worker error");
|
|
354
|
+
}, this.workers.push(e), this.idleWorkers.push(e), e;
|
|
355
|
+
}
|
|
356
|
+
terminateWorker(e) {
|
|
357
|
+
this.idleWorkers = this.idleWorkers.filter((t) => t !== e), this.workerJobs.delete(e);
|
|
358
|
+
let t = this.workers.indexOf(e);
|
|
359
|
+
t >= 0 && this.workers.splice(t, 1), e.terminate();
|
|
360
|
+
}
|
|
361
|
+
init() {
|
|
362
|
+
this.initialized || (this.initialized = !0, this.growToTarget());
|
|
363
|
+
}
|
|
364
|
+
growToTarget() {
|
|
365
|
+
for (; this.workers.length < this.targetSize;) this.spawnWorker();
|
|
366
|
+
}
|
|
367
|
+
shrinkIdleToTarget() {
|
|
368
|
+
for (; this.workers.length > this.targetSize && this.idleWorkers.length > 0;) this.terminateWorker(this.idleWorkers.pop());
|
|
369
|
+
}
|
|
370
|
+
setSize(e) {
|
|
371
|
+
this.targetSize = x(e), this.init(), this.growToTarget(), this.shrinkIdleToTarget();
|
|
372
|
+
}
|
|
373
|
+
decode(e) {
|
|
374
|
+
return this.init(), new Promise((t, n) => {
|
|
375
|
+
let r = {
|
|
376
|
+
buffer: e,
|
|
377
|
+
enqueuedAt: performance.now(),
|
|
378
|
+
queueWaitMs: 0,
|
|
379
|
+
resolve: t,
|
|
380
|
+
reject: n
|
|
381
|
+
}, i = this.idleWorkers.pop();
|
|
382
|
+
i ? this.runJob(i, r) : this.waitQueue.push(r);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
runJob(e, t) {
|
|
386
|
+
t.queueWaitMs = performance.now() - t.enqueuedAt;
|
|
387
|
+
let n = ++this.jobId;
|
|
388
|
+
this.pendingJobs.set(n, t), this.workerJobs.set(e, n), e.postMessage({
|
|
389
|
+
id: n,
|
|
390
|
+
type: "decode",
|
|
391
|
+
buffer: t.buffer
|
|
392
|
+
}, [t.buffer]);
|
|
393
|
+
}
|
|
394
|
+
handleMessage(e, t) {
|
|
395
|
+
let n = this.pendingJobs.get(t.id);
|
|
396
|
+
if (!n) return;
|
|
397
|
+
this.pendingJobs.delete(t.id), this.workerJobs.delete(e), this.idleWorkers.push(e), this.shrinkIdleToTarget(), t.ok === !1 ? n.reject(Error(t.error)) : n.resolve({
|
|
398
|
+
gif: {
|
|
399
|
+
frames: t.frames.map(S),
|
|
400
|
+
width: t.width,
|
|
401
|
+
height: t.height
|
|
402
|
+
},
|
|
403
|
+
decodeTimeMs: t.decodeTimeMs,
|
|
404
|
+
queueWaitMs: n.queueWaitMs
|
|
405
|
+
});
|
|
406
|
+
let r = this.waitQueue.shift();
|
|
407
|
+
if (r) {
|
|
408
|
+
let e = this.idleWorkers.pop();
|
|
409
|
+
e && this.runJob(e, r);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
handleWorkerError(e, t) {
|
|
413
|
+
let n = this.workerJobs.get(e), r = n === void 0 ? void 0 : this.pendingJobs.get(n);
|
|
414
|
+
for (n !== void 0 && this.pendingJobs.delete(n), this.workerJobs.delete(e), this.terminateWorker(e), r && r.reject(Error(t)), this.growToTarget(); this.waitQueue.length > 0 && this.idleWorkers.length > 0;) {
|
|
415
|
+
let e = this.waitQueue.shift(), t = this.idleWorkers.pop();
|
|
416
|
+
this.runJob(t, e);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}();
|
|
420
|
+
function w(e) {
|
|
421
|
+
C.setSize(e);
|
|
422
|
+
}
|
|
423
|
+
function T(e, t) {
|
|
424
|
+
return t !== void 0 && C.setSize(t), C.decode(e);
|
|
425
|
+
}
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region src/utils/gifResourceManager.ts
|
|
428
|
+
var E = /* @__PURE__ */ new Map(), D = /* @__PURE__ */ new Map(), O = /* @__PURE__ */ new Map();
|
|
429
|
+
function k(e) {
|
|
430
|
+
return e;
|
|
431
|
+
}
|
|
432
|
+
function A() {
|
|
312
433
|
return {
|
|
313
|
-
|
|
434
|
+
networkTimeMs: 0,
|
|
435
|
+
queueWaitMs: 0,
|
|
314
436
|
decodeTimeMs: 0,
|
|
315
|
-
|
|
316
|
-
pendingWaitDecodeMs: 0,
|
|
437
|
+
totalMs: 0,
|
|
317
438
|
fromCache: !0,
|
|
318
439
|
fromPending: !1
|
|
319
440
|
};
|
|
320
441
|
}
|
|
321
|
-
function
|
|
442
|
+
function j(e, t, n, r, i) {
|
|
322
443
|
return {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
fromCache: !1,
|
|
328
|
-
fromPending: !1
|
|
444
|
+
networkTimeMs: e,
|
|
445
|
+
queueWaitMs: t,
|
|
446
|
+
decodeTimeMs: n,
|
|
447
|
+
totalMs: r,
|
|
448
|
+
fromCache: i.fromCache ?? !1,
|
|
449
|
+
fromPending: i.fromPending ?? !1
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async function M(e, t, n) {
|
|
453
|
+
if (t) return T(e, n);
|
|
454
|
+
let r = performance.now(), i = (0, m.parseGIF)(e), a = (0, m.decompressFrames)(i, !0), o = performance.now() - r;
|
|
455
|
+
if (!a.length) throw Error("GIF has no frames");
|
|
456
|
+
return {
|
|
457
|
+
gif: {
|
|
458
|
+
frames: a,
|
|
459
|
+
width: i.lsd.width,
|
|
460
|
+
height: i.lsd.height
|
|
461
|
+
},
|
|
462
|
+
decodeTimeMs: o,
|
|
463
|
+
queueWaitMs: 0
|
|
329
464
|
};
|
|
330
465
|
}
|
|
331
|
-
async function
|
|
466
|
+
async function N(e, t) {
|
|
332
467
|
let n = performance.now(), r = await fetch(e, {
|
|
333
468
|
mode: "cors",
|
|
334
469
|
credentials: "omit"
|
|
335
470
|
});
|
|
336
471
|
if (!r.ok) throw Error(`Failed to load gif: ${r.status}`);
|
|
337
|
-
let i = await r.arrayBuffer(), a = performance.now() - n;
|
|
338
|
-
t && (t.fetchDoneAt = performance.now());
|
|
339
|
-
let o = performance.now(), s = (0, m.parseGIF)(i), c = (0, m.decompressFrames)(s, !0), l = performance.now() - o;
|
|
340
|
-
if (!c.length) throw Error("GIF has no frames");
|
|
472
|
+
let i = await r.arrayBuffer(), a = h(r, performance.now() - n), { gif: o, decodeTimeMs: s, queueWaitMs: c } = await M(i, t?.useWorker, t?.workerConcurrency);
|
|
341
473
|
return {
|
|
342
|
-
gif:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
fetchTimeMs: a,
|
|
348
|
-
decodeTimeMs: l
|
|
474
|
+
gif: o,
|
|
475
|
+
networkTimeMs: a,
|
|
476
|
+
queueWaitMs: c,
|
|
477
|
+
decodeTimeMs: s,
|
|
478
|
+
wallMs: performance.now() - n
|
|
349
479
|
};
|
|
350
480
|
}
|
|
351
|
-
async function
|
|
352
|
-
let n =
|
|
353
|
-
if (
|
|
354
|
-
gif:
|
|
355
|
-
stats:
|
|
481
|
+
async function P(e, t) {
|
|
482
|
+
let n = k(e), r = E.get(n);
|
|
483
|
+
if (r) return {
|
|
484
|
+
gif: r.data,
|
|
485
|
+
stats: A()
|
|
356
486
|
};
|
|
357
|
-
if (!t?.skipPending &&
|
|
358
|
-
let
|
|
359
|
-
return
|
|
487
|
+
if (!t?.skipPending && D.has(n)) {
|
|
488
|
+
let e = D.get(n), t = performance.now(), { gif: r } = await e.promise, i = performance.now() - t, a = E.get(n);
|
|
489
|
+
return a ||= (E.set(n, {
|
|
360
490
|
data: r,
|
|
361
|
-
refCount: 0
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
gif: c.data,
|
|
366
|
-
stats: {
|
|
367
|
-
fetchTimeMs: 0,
|
|
368
|
-
decodeTimeMs: 0,
|
|
369
|
-
pendingWaitFetchMs: o,
|
|
370
|
-
pendingWaitDecodeMs: s,
|
|
371
|
-
fromCache: !1,
|
|
372
|
-
fromPending: !0
|
|
373
|
-
}
|
|
491
|
+
refCount: 0
|
|
492
|
+
}), E.get(n)), {
|
|
493
|
+
gif: a.data,
|
|
494
|
+
stats: j(0, i, 0, i, { fromPending: !0 })
|
|
374
495
|
};
|
|
375
496
|
}
|
|
376
|
-
let
|
|
377
|
-
|
|
378
|
-
let
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
data:
|
|
384
|
-
refCount: 0
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}),
|
|
388
|
-
|
|
389
|
-
fetchTimeMs: n,
|
|
390
|
-
decodeTimeMs: i
|
|
391
|
-
}) : {
|
|
392
|
-
gif: t,
|
|
393
|
-
fetchTimeMs: n,
|
|
394
|
-
decodeTimeMs: i
|
|
395
|
-
}).catch((t) => {
|
|
396
|
-
throw _.get(e) === r && g.delete(e), t;
|
|
397
|
-
}), g.set(e, i);
|
|
398
|
-
let { gif: a, fetchTimeMs: o, decodeTimeMs: s } = await i.promise;
|
|
497
|
+
let i = (O.get(n) ?? 0) + 1;
|
|
498
|
+
O.set(n, i);
|
|
499
|
+
let a = { promise: void 0 };
|
|
500
|
+
a.promise = N(e, {
|
|
501
|
+
useWorker: t?.useWorker,
|
|
502
|
+
workerConcurrency: t?.workerConcurrency
|
|
503
|
+
}).then((e) => O.get(n) === i ? (D.delete(n), E.set(n, {
|
|
504
|
+
data: e.gif,
|
|
505
|
+
refCount: 0
|
|
506
|
+
}), e) : e).catch((e) => {
|
|
507
|
+
throw O.get(n) === i && D.delete(n), e;
|
|
508
|
+
}), D.set(n, a);
|
|
509
|
+
let { gif: o, networkTimeMs: s, queueWaitMs: c, decodeTimeMs: l, wallMs: u } = await a.promise;
|
|
399
510
|
return {
|
|
400
|
-
gif:
|
|
401
|
-
stats:
|
|
511
|
+
gif: o,
|
|
512
|
+
stats: j(s, c, l, u, {})
|
|
402
513
|
};
|
|
403
514
|
}
|
|
404
|
-
function
|
|
405
|
-
let
|
|
406
|
-
|
|
515
|
+
function F(e, t) {
|
|
516
|
+
let n = E.get(k(e));
|
|
517
|
+
n && (n.refCount += 1);
|
|
407
518
|
}
|
|
408
|
-
function
|
|
409
|
-
let
|
|
410
|
-
|
|
519
|
+
function I(e, t) {
|
|
520
|
+
let n = k(e), r = E.get(n);
|
|
521
|
+
r && (--r.refCount, r.refCount <= 0 && E.delete(n));
|
|
411
522
|
}
|
|
412
|
-
function
|
|
413
|
-
|
|
523
|
+
function L() {
|
|
524
|
+
E.clear(), D.clear(), O.clear();
|
|
414
525
|
}
|
|
415
526
|
//#endregion
|
|
416
527
|
//#region src/utils/gifController.ts
|
|
417
|
-
function
|
|
528
|
+
function R(e, t, n, r, i = {}) {
|
|
418
529
|
let a = e.getContext("2d");
|
|
419
530
|
if (!a) throw Error("Canvas 2d context unavailable");
|
|
420
531
|
let o = a;
|
|
@@ -470,82 +581,67 @@ function E(e, t, n, r, i = {}) {
|
|
|
470
581
|
getCompletedLoops: () => d
|
|
471
582
|
};
|
|
472
583
|
}
|
|
473
|
-
async function
|
|
474
|
-
let { skipPending: r,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
584
|
+
async function z(e, t, n = {}) {
|
|
585
|
+
let { skipPending: r, useWorker: i, workerConcurrency: a, onLoaded: o, ...s } = n, { gif: c, stats: l } = await P(t, {
|
|
586
|
+
skipPending: r,
|
|
587
|
+
useWorker: i,
|
|
588
|
+
workerConcurrency: a
|
|
589
|
+
});
|
|
590
|
+
return o?.(l), {
|
|
591
|
+
controller: R(e, c.frames, c.width, c.height, s),
|
|
592
|
+
stats: l
|
|
478
593
|
};
|
|
479
594
|
}
|
|
480
595
|
//#endregion
|
|
481
596
|
//#region src/utils/loadStats.ts
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}],
|
|
505
|
-
totalMs: e.pendingWaitFetchMs + e.pendingWaitDecodeMs
|
|
506
|
-
} : {
|
|
507
|
-
mode: "fresh",
|
|
508
|
-
lines: [{
|
|
509
|
-
label: "fetch",
|
|
510
|
-
valueMs: e.fetchTimeMs
|
|
511
|
-
}, {
|
|
512
|
-
label: "decode",
|
|
513
|
-
valueMs: e.decodeTimeMs
|
|
514
|
-
}],
|
|
515
|
-
totalMs: e.fetchTimeMs + e.decodeTimeMs
|
|
597
|
+
var B = [
|
|
598
|
+
{
|
|
599
|
+
key: "networkTimeMs",
|
|
600
|
+
label: "network"
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
key: "queueWaitMs",
|
|
604
|
+
label: "queue"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
key: "decodeTimeMs",
|
|
608
|
+
label: "decode"
|
|
609
|
+
}
|
|
610
|
+
];
|
|
611
|
+
function V(e) {
|
|
612
|
+
return {
|
|
613
|
+
mode: e.fromCache ? "cache" : e.fromPending ? "pending" : "fresh",
|
|
614
|
+
lines: B.map(({ key: t, label: n }) => ({
|
|
615
|
+
label: n,
|
|
616
|
+
valueMs: e[t]
|
|
617
|
+
})),
|
|
618
|
+
totalMs: e.totalMs
|
|
516
619
|
};
|
|
517
620
|
}
|
|
518
|
-
function
|
|
519
|
-
let t = k(e), n = t.lines.map((e) => `${e.label} ${e.valueMs.toFixed(1)}ms`).join("\n");
|
|
520
|
-
return `${t.mode}\n${n}\ntotal ${t.totalMs.toFixed(1)}ms`;
|
|
521
|
-
}
|
|
522
|
-
function j(e) {
|
|
621
|
+
function H(e) {
|
|
523
622
|
return `${e.toFixed(1)}ms`;
|
|
524
623
|
}
|
|
525
|
-
function
|
|
624
|
+
function U(e, t) {
|
|
526
625
|
let n = Math.min(e, t);
|
|
527
626
|
return n < 130 ? "compact" : n < 220 ? "medium" : "full";
|
|
528
627
|
}
|
|
529
|
-
var
|
|
628
|
+
var W = {
|
|
530
629
|
fresh: "F",
|
|
531
630
|
pending: "P",
|
|
532
631
|
cache: "C"
|
|
533
632
|
};
|
|
534
|
-
function
|
|
535
|
-
let t =
|
|
536
|
-
return t.mode === "cache" ? `${n} · 0` : `${n} · ${
|
|
537
|
-
}
|
|
538
|
-
function F(e, t) {
|
|
539
|
-
return t === "fresh" ? e.label === "fetch" ? "f" : "d" : e.label === "wait fetch" ? "wf" : "wd";
|
|
633
|
+
function G(e) {
|
|
634
|
+
let t = V(e), n = W[t.mode];
|
|
635
|
+
return t.mode === "cache" ? `${n} · 0` : `${n} · ${H(e.totalMs)}`;
|
|
540
636
|
}
|
|
541
637
|
//#endregion
|
|
542
638
|
//#region src/components/GifLoadStatsDebug.tsx
|
|
543
|
-
var
|
|
639
|
+
var K = {
|
|
544
640
|
fresh: "FRESH",
|
|
545
641
|
pending: "PND",
|
|
546
642
|
cache: "CACHE"
|
|
547
643
|
};
|
|
548
|
-
function
|
|
644
|
+
function q(e, t) {
|
|
549
645
|
let [r, i] = a({
|
|
550
646
|
width: 0,
|
|
551
647
|
height: 0
|
|
@@ -564,7 +660,7 @@ function L(e, t) {
|
|
|
564
660
|
return a.observe(n), () => a.disconnect();
|
|
565
661
|
}, [e, t]), r;
|
|
566
662
|
}
|
|
567
|
-
function
|
|
663
|
+
function J({ label: e, valueMs: t }) {
|
|
568
664
|
return /* @__PURE__ */ s("div", {
|
|
569
665
|
className: "gif-player__debug-row",
|
|
570
666
|
children: [/* @__PURE__ */ o("span", {
|
|
@@ -572,11 +668,12 @@ function R({ label: e, valueMs: t }) {
|
|
|
572
668
|
children: e
|
|
573
669
|
}), /* @__PURE__ */ o("span", {
|
|
574
670
|
className: "gif-player__debug-value",
|
|
575
|
-
children:
|
|
671
|
+
children: H(t)
|
|
576
672
|
})]
|
|
577
673
|
});
|
|
578
674
|
}
|
|
579
|
-
function
|
|
675
|
+
function Y({ view: e, width: t, expanded: n, onToggle: r }) {
|
|
676
|
+
let i = e.mode.toUpperCase();
|
|
580
677
|
return /* @__PURE__ */ s("div", {
|
|
581
678
|
className: [
|
|
582
679
|
"gif-player__debug",
|
|
@@ -585,7 +682,7 @@ function z({ view: e, width: t, expanded: n, onToggle: r }) {
|
|
|
585
682
|
r && "gif-player__debug--collapsible"
|
|
586
683
|
].filter(Boolean).join(" "),
|
|
587
684
|
"data-mode": e.mode,
|
|
588
|
-
style: { maxWidth: !n && t > 0 ? Math.min(152, Math.round(t * .52)) : void 0 },
|
|
685
|
+
style: { maxWidth: r && !n && t > 0 ? Math.min(152, Math.round(t * .52)) : void 0 },
|
|
589
686
|
role: r ? "button" : void 0,
|
|
590
687
|
tabIndex: r ? 0 : void 0,
|
|
591
688
|
title: r ? n ? "点击收起" : "点击展开详情" : void 0,
|
|
@@ -596,14 +693,14 @@ function z({ view: e, width: t, expanded: n, onToggle: r }) {
|
|
|
596
693
|
children: [
|
|
597
694
|
/* @__PURE__ */ o("span", {
|
|
598
695
|
className: "gif-player__debug-badge",
|
|
599
|
-
children:
|
|
696
|
+
children: i
|
|
600
697
|
}),
|
|
601
698
|
/* @__PURE__ */ o("div", {
|
|
602
699
|
className: "gif-player__debug-body",
|
|
603
|
-
children: e.lines.map((
|
|
604
|
-
label:
|
|
605
|
-
valueMs:
|
|
606
|
-
},
|
|
700
|
+
children: e.lines.map((e) => /* @__PURE__ */ o(J, {
|
|
701
|
+
label: e.label,
|
|
702
|
+
valueMs: e.valueMs
|
|
703
|
+
}, e.label))
|
|
607
704
|
}),
|
|
608
705
|
/* @__PURE__ */ s("div", {
|
|
609
706
|
className: "gif-player__debug-total",
|
|
@@ -612,14 +709,14 @@ function z({ view: e, width: t, expanded: n, onToggle: r }) {
|
|
|
612
709
|
children: "Σ"
|
|
613
710
|
}), /* @__PURE__ */ o("span", {
|
|
614
711
|
className: "gif-player__debug-value",
|
|
615
|
-
children:
|
|
712
|
+
children: H(e.totalMs)
|
|
616
713
|
})]
|
|
617
714
|
})
|
|
618
715
|
]
|
|
619
716
|
});
|
|
620
717
|
}
|
|
621
|
-
function
|
|
622
|
-
let { width: i, height: c } =
|
|
718
|
+
function X({ stats: e, canvasRef: t, visible: r }) {
|
|
719
|
+
let { width: i, height: c } = q(t, r), l = V(e), u = U(i, c), [d, f] = a(!1), p = u !== "full";
|
|
623
720
|
n(() => {
|
|
624
721
|
f(!1);
|
|
625
722
|
}, [e]), n(() => {
|
|
@@ -628,7 +725,7 @@ function B({ stats: e, canvasRef: t, visible: r }) {
|
|
|
628
725
|
let m = () => {
|
|
629
726
|
p && f((e) => !e);
|
|
630
727
|
};
|
|
631
|
-
return p && d ? /* @__PURE__ */ o(
|
|
728
|
+
return p && d ? /* @__PURE__ */ o(Y, {
|
|
632
729
|
view: l,
|
|
633
730
|
width: i,
|
|
634
731
|
expanded: !0,
|
|
@@ -643,7 +740,7 @@ function B({ stats: e, canvasRef: t, visible: r }) {
|
|
|
643
740
|
onKeyDown: (e) => {
|
|
644
741
|
(e.key === "Enter" || e.key === " ") && (e.preventDefault(), m());
|
|
645
742
|
},
|
|
646
|
-
children:
|
|
743
|
+
children: G(e)
|
|
647
744
|
}) : u === "medium" ? /* @__PURE__ */ o("div", {
|
|
648
745
|
className: "gif-player__debug gif-player__debug--medium gif-player__debug--collapsible",
|
|
649
746
|
"data-mode": l.mode,
|
|
@@ -658,13 +755,13 @@ function B({ stats: e, canvasRef: t, visible: r }) {
|
|
|
658
755
|
className: "gif-player__debug-head",
|
|
659
756
|
children: [/* @__PURE__ */ o("span", {
|
|
660
757
|
className: "gif-player__debug-badge",
|
|
661
|
-
children:
|
|
758
|
+
children: K[l.mode]
|
|
662
759
|
}), /* @__PURE__ */ o("span", {
|
|
663
760
|
className: "gif-player__debug-value gif-player__debug-value--total",
|
|
664
|
-
children:
|
|
761
|
+
children: H(l.totalMs)
|
|
665
762
|
})]
|
|
666
763
|
})
|
|
667
|
-
}) : /* @__PURE__ */ o(
|
|
764
|
+
}) : /* @__PURE__ */ o(Y, {
|
|
668
765
|
view: l,
|
|
669
766
|
width: i,
|
|
670
767
|
expanded: !1
|
|
@@ -672,99 +769,103 @@ function B({ stats: e, canvasRef: t, visible: r }) {
|
|
|
672
769
|
}
|
|
673
770
|
//#endregion
|
|
674
771
|
//#region src/components/GifPlayer.tsx
|
|
675
|
-
var
|
|
676
|
-
let
|
|
677
|
-
E.current =
|
|
678
|
-
let
|
|
679
|
-
T.current?.play(),
|
|
680
|
-
}, []), W = t(() => {
|
|
681
|
-
T.current?.pause(), N(!1);
|
|
772
|
+
var Z = e(({ src: e, autoPlay: c = !0, showControls: l = !1, debug: u = !1, useWorker: d = !1, workerConcurrency: f, loopCount: p, className: m, style: h, width: g, height: _, onPlay: v, onPause: y, onEnd: b, onLoaded: x, onError: S }, C) => {
|
|
773
|
+
let w = i(null), T = i(null), E = i(v), D = i(y), O = i(b), k = i(x), A = i(S), [j, M] = a(c), [N, P] = a(!1), [L, R] = a(null), [B, V] = a(0), H = i(!1), U = i(0);
|
|
774
|
+
E.current = v, D.current = y, O.current = b, k.current = x, A.current = S;
|
|
775
|
+
let W = t(() => {
|
|
776
|
+
T.current?.play(), M(!0);
|
|
682
777
|
}, []), G = t(() => {
|
|
683
|
-
T.current?.
|
|
684
|
-
}, [
|
|
685
|
-
T.current?.
|
|
686
|
-
}, []), q = t(() => {
|
|
687
|
-
|
|
778
|
+
T.current?.pause(), M(!1);
|
|
779
|
+
}, []), K = t(() => {
|
|
780
|
+
T.current?.isPlaying() ? G() : W();
|
|
781
|
+
}, [G, W]), q = t(() => {
|
|
782
|
+
T.current?.reset(), M(!1);
|
|
688
783
|
}, []), J = t(() => {
|
|
784
|
+
H.current = !0, V((e) => e + 1);
|
|
785
|
+
}, []), Y = t(() => {
|
|
689
786
|
if (!e) return () => {};
|
|
690
|
-
let t = ++
|
|
787
|
+
let t = ++U.current, n = !1, r = w.current;
|
|
691
788
|
if (!r) return () => {};
|
|
692
|
-
let i =
|
|
693
|
-
return
|
|
789
|
+
let i = H.current;
|
|
790
|
+
return H.current = !1, P(!1), M(!1), R(null), T.current?.destroy(), T.current = null, z(r, e, {
|
|
694
791
|
skipPending: i,
|
|
695
|
-
|
|
792
|
+
useWorker: d,
|
|
793
|
+
workerConcurrency: f,
|
|
794
|
+
loopCount: p,
|
|
696
795
|
onPlay: () => {
|
|
697
|
-
t ===
|
|
796
|
+
t === U.current && M(!0), E.current?.();
|
|
698
797
|
},
|
|
699
798
|
onPause: () => {
|
|
700
|
-
t ===
|
|
799
|
+
t === U.current && M(!1), D.current?.();
|
|
701
800
|
},
|
|
702
801
|
onEnd: () => {
|
|
703
|
-
t ===
|
|
802
|
+
t === U.current && M(!1), O.current?.();
|
|
704
803
|
}
|
|
705
804
|
}).then(({ controller: r, stats: i }) => {
|
|
706
|
-
if (
|
|
805
|
+
if (k.current?.(i), n || t !== U.current) {
|
|
707
806
|
r.destroy({ clearCanvas: !1 });
|
|
708
807
|
return;
|
|
709
808
|
}
|
|
710
|
-
|
|
809
|
+
F(e, d), T.current = r;
|
|
711
810
|
let a = r.destroy.bind(r);
|
|
712
811
|
r.destroy = (t) => {
|
|
713
|
-
a(t),
|
|
714
|
-
},
|
|
812
|
+
a(t), I(e, d);
|
|
813
|
+
}, P(!0), R(i), c && (r.play(), M(!0));
|
|
715
814
|
}).catch((e) => {
|
|
716
|
-
t ===
|
|
815
|
+
t === U.current && R(null), A.current?.(e instanceof Error ? e : Error(String(e)));
|
|
717
816
|
}), () => {
|
|
718
|
-
n = !0, t ===
|
|
817
|
+
n = !0, t === U.current && (T.current?.destroy(), T.current = null);
|
|
719
818
|
};
|
|
720
819
|
}, [
|
|
721
820
|
e,
|
|
821
|
+
p,
|
|
822
|
+
c,
|
|
722
823
|
d,
|
|
723
|
-
|
|
824
|
+
f
|
|
724
825
|
]);
|
|
725
|
-
return r(
|
|
726
|
-
play:
|
|
727
|
-
pause:
|
|
728
|
-
toggle:
|
|
729
|
-
reset:
|
|
730
|
-
reload:
|
|
826
|
+
return r(C, () => ({
|
|
827
|
+
play: W,
|
|
828
|
+
pause: G,
|
|
829
|
+
toggle: K,
|
|
830
|
+
reset: q,
|
|
831
|
+
reload: J,
|
|
731
832
|
isPlaying: () => T.current?.isPlaying() ?? !1
|
|
732
833
|
}), [
|
|
733
|
-
U,
|
|
734
834
|
W,
|
|
735
835
|
G,
|
|
736
836
|
K,
|
|
737
|
-
q
|
|
738
|
-
|
|
837
|
+
q,
|
|
838
|
+
J
|
|
839
|
+
]), n(() => Y(), [Y, B]), /* @__PURE__ */ s("div", {
|
|
739
840
|
className: [
|
|
740
841
|
"gif-player",
|
|
741
842
|
l && "gif-player--show-controls",
|
|
742
|
-
|
|
843
|
+
m
|
|
743
844
|
].filter(Boolean).join(" "),
|
|
744
|
-
style:
|
|
845
|
+
style: h,
|
|
745
846
|
children: [
|
|
746
847
|
/* @__PURE__ */ o("canvas", {
|
|
747
|
-
ref:
|
|
848
|
+
ref: w,
|
|
748
849
|
className: "gif-player__media",
|
|
749
850
|
role: "img",
|
|
750
851
|
style: {
|
|
751
|
-
...
|
|
752
|
-
...
|
|
753
|
-
...
|
|
852
|
+
...g === void 0 ? {} : { width: g },
|
|
853
|
+
..._ === void 0 ? {} : { height: _ },
|
|
854
|
+
...N ? {} : { visibility: "hidden" }
|
|
754
855
|
}
|
|
755
856
|
}),
|
|
756
|
-
u &&
|
|
757
|
-
stats:
|
|
758
|
-
canvasRef:
|
|
759
|
-
visible:
|
|
857
|
+
u && L !== null && N && /* @__PURE__ */ o(X, {
|
|
858
|
+
stats: L,
|
|
859
|
+
canvasRef: w,
|
|
860
|
+
visible: N
|
|
760
861
|
}),
|
|
761
|
-
l &&
|
|
862
|
+
l && N && /* @__PURE__ */ o("div", {
|
|
762
863
|
className: "gif-player__controls",
|
|
763
864
|
children: /* @__PURE__ */ o("button", {
|
|
764
865
|
type: "button",
|
|
765
866
|
className: "gif-player__btn",
|
|
766
|
-
onClick:
|
|
767
|
-
children:
|
|
867
|
+
onClick: K,
|
|
868
|
+
children: j ? /* @__PURE__ */ s("svg", {
|
|
768
869
|
viewBox: "0 0 24 24",
|
|
769
870
|
"aria-hidden": "true",
|
|
770
871
|
children: [/* @__PURE__ */ o("rect", {
|
|
@@ -790,6 +891,6 @@ var V = e(({ src: e, autoPlay: c = !0, showControls: l = !1, debug: u = !1, loop
|
|
|
790
891
|
]
|
|
791
892
|
});
|
|
792
893
|
});
|
|
793
|
-
|
|
894
|
+
Z.displayName = "GifPlayer";
|
|
794
895
|
//#endregion
|
|
795
|
-
export {
|
|
896
|
+
export { Z as GifPlayer, L as clearGifResourceCache, z as createGifController, w as setWorkerPoolSize };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { GifPlayer } from './components/GifPlayer';
|
|
2
2
|
export type { GifPlayerComponent, GifPlayerProps, GifPlayerRef, } from './components/GifPlayer';
|
|
3
|
-
export {
|
|
4
|
-
export type { CreateGifOptions, GifController, GifLoadStats
|
|
3
|
+
export { clearGifResourceCache, createGifController, setWorkerPoolSize, } from './utils';
|
|
4
|
+
export type { CreateGifOptions, GifController, GifLoadStats } from './utils';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getResourceFetchMs(response: Response, fallbackMs: number): number;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { LoadedGif, GifLoadStats } from './types';
|
|
2
2
|
interface LoadGifResourceOptions {
|
|
3
3
|
skipPending?: boolean;
|
|
4
|
+
useWorker?: boolean;
|
|
5
|
+
workerConcurrency?: number;
|
|
4
6
|
}
|
|
5
7
|
interface LoadGifResult {
|
|
6
8
|
gif: LoadedGif;
|
|
7
9
|
stats: GifLoadStats;
|
|
8
10
|
}
|
|
9
11
|
export declare function loadGifResource(src: string, options?: LoadGifResourceOptions): Promise<LoadGifResult>;
|
|
10
|
-
export declare function acquireGifResource(src: string): void;
|
|
11
|
-
export declare function releaseGifResource(src: string): void;
|
|
12
|
+
export declare function acquireGifResource(src: string, _useWorker?: boolean): void;
|
|
13
|
+
export declare function releaseGifResource(src: string, _useWorker?: boolean): void;
|
|
12
14
|
export declare function clearGifResourceCache(): void;
|
|
13
15
|
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LoadedGif } from './types';
|
|
2
|
+
interface DecodeResult {
|
|
3
|
+
gif: LoadedGif;
|
|
4
|
+
decodeTimeMs: number;
|
|
5
|
+
queueWaitMs: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_WORKER_CONCURRENCY: number;
|
|
8
|
+
export declare function clampWorkerConcurrency(size?: number): number;
|
|
9
|
+
export declare function setWorkerPoolSize(size?: number): void;
|
|
10
|
+
export declare function decodeGifInWorker(buffer: ArrayBuffer, concurrency?: number): Promise<DecodeResult>;
|
|
11
|
+
export {};
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { createGifController } from './gifController';
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export type { GifLoadStatsLine, GifLoadStatsMode, GifLoadStatsView } from './loadStats';
|
|
2
|
+
export { clearGifResourceCache } from './gifResourceManager';
|
|
3
|
+
export { setWorkerPoolSize } from './gifWorkerPool';
|
|
5
4
|
export type { CreateGifOptions, GifController, GifLoadStats } from './types';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { GifLoadStats } from './types';
|
|
2
|
-
export declare function getTotalLoadTimeMs(stats: GifLoadStats): number;
|
|
3
2
|
export type GifLoadStatsMode = 'fresh' | 'pending' | 'cache';
|
|
4
3
|
export interface GifLoadStatsLine {
|
|
5
4
|
label: string;
|
|
@@ -11,8 +10,6 @@ export interface GifLoadStatsView {
|
|
|
11
10
|
totalMs: number;
|
|
12
11
|
}
|
|
13
12
|
export declare function getGifLoadStatsView(stats: GifLoadStats): GifLoadStatsView;
|
|
14
|
-
export declare function formatGifLoadStats(stats: GifLoadStats): string;
|
|
15
13
|
export declare function formatLoadTimeMs(ms: number): string;
|
|
16
14
|
export declare function getDebugDensity(width: number, height: number): 'compact' | 'medium' | 'full';
|
|
17
15
|
export declare function formatGifLoadStatsCompact(stats: GifLoadStats): string;
|
|
18
|
-
export declare function getGifLoadStatsLineLabel(line: GifLoadStatsLine, mode: GifLoadStatsMode): string;
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -5,14 +5,14 @@ export interface LoadedGif {
|
|
|
5
5
|
height: number;
|
|
6
6
|
}
|
|
7
7
|
export interface GifLoadStats {
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
/**
|
|
8
|
+
/** 浏览器侧实际网络/缓存读取耗时(Resource Timing) */
|
|
9
|
+
networkTimeMs: number;
|
|
10
|
+
/** Worker 池排队,或 pending 跟随方等待进行中的加载 */
|
|
11
|
+
queueWaitMs: number;
|
|
12
|
+
/** 本次实际 decode 耗时 */
|
|
11
13
|
decodeTimeMs: number;
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
/** 等待进行中的 decode 阶段,仅 pending */
|
|
15
|
-
pendingWaitDecodeMs: number;
|
|
14
|
+
/** 墙钟总耗时(含主线程争用等未分项统计的等待) */
|
|
15
|
+
totalMs: number;
|
|
16
16
|
fromCache: boolean;
|
|
17
17
|
fromPending: boolean;
|
|
18
18
|
}
|
|
@@ -23,6 +23,9 @@ export interface CreateGifOptions {
|
|
|
23
23
|
onPause?: () => void;
|
|
24
24
|
onLoaded?: (stats: GifLoadStats) => void;
|
|
25
25
|
skipPending?: boolean;
|
|
26
|
+
useWorker?: boolean;
|
|
27
|
+
/** Worker 池并发数,仅 useWorker=true 时生效,默认 min(hardwareConcurrency, 4) */
|
|
28
|
+
workerConcurrency?: number;
|
|
26
29
|
}
|
|
27
30
|
export interface GifController {
|
|
28
31
|
play: () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@libshub/gif-tools",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "基于 Canvas 的 React GIF 播放组件,支持 Worker 解码、资源缓存与加载性能统计",
|
|
5
5
|
"module": "./dist/gif-tools.es.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"publishConfig": {
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"dev": "vite",
|
|
29
29
|
"build": "vite build && tsc -p tsconfig.lib.json",
|
|
30
|
+
"build:example": "vite build --config vite.example.config.ts",
|
|
31
|
+
"preview:example": "vite preview --config vite.example.config.ts",
|
|
30
32
|
"lint": "eslint .",
|
|
31
33
|
"preview": "vite preview"
|
|
32
34
|
},
|