@libshub/gif-tools 1.0.8 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @libshub/gif-tools
2
2
 
3
- 基于 Canvas 的 React GIF 播放组件,使用 [gifuct-js](https://github.com/matt-way/gifuct-js) 解码,支持播放控制、循环次数、资源缓存与加载性能统计。
3
+ 基于 Canvas 的 React GIF 播放组件,使用 [gifuct-js](https://github.com/matt-way/gifuct-js) 解码,支持播放控制、循环次数、资源缓存、Worker 解码与加载性能统计。
4
4
 
5
5
  ## 演示
6
6
 
@@ -51,6 +51,8 @@ function App() {
51
51
  | `autoPlay` | `boolean` | `true` | 加载完成后自动播放 |
52
52
  | `showControls` | `boolean` | `false` | 显示播放/暂停按钮 |
53
53
  | `debug` | `boolean` | `false` | 在画面上叠加加载耗时调试信息 |
54
+ | `useWorker` | `boolean` | `false` | 在 Web Worker 中解码,减轻主线程压力 |
55
+ | `workerConcurrency` | `number` | `min(hardwareConcurrency, 4)` | Worker 池并发数,范围 1–16,仅 `useWorker=true` 时生效 |
54
56
  | `loopCount` | `number` | — | 循环次数,不传则按 GIF 自身循环或无限循环 |
55
57
  | `onLoaded` | `(stats: GifLoadStats) => void` | — | 加载完成 |
56
58
  | `onPlay` | `() => void` | — | 开始播放 |
@@ -68,23 +70,41 @@ import { GifPlayer, type GifPlayerRef } from '@libshub/gif-tools'
68
70
 
69
71
  const ref = useRef<GifPlayerRef>(null)
70
72
 
71
- ref.current?.play() // 播放
72
- ref.current?.pause() // 暂停
73
- ref.current?.toggle() // 切换播放/暂停
74
- ref.current?.reset() // 回到第一帧并暂停
75
- ref.current?.reload() // 重新加载(跳过 pending 复用,强制 fresh)
76
- ref.current?.isPlaying() // 是否正在播放
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)
77
101
  ```
78
102
 
79
103
  ### 完整示例
80
104
 
81
105
  ```tsx
82
106
  import { useRef } from 'react'
83
- import {
84
- GifPlayer,
85
- formatGifLoadStats,
86
- type GifPlayerRef,
87
- } from '@libshub/gif-tools'
107
+ import { GifPlayer, type GifPlayerRef } from '@libshub/gif-tools'
88
108
  import '@libshub/gif-tools/style.css'
89
109
 
90
110
  export default function Demo() {
@@ -103,9 +123,11 @@ export default function Demo() {
103
123
  style={{ borderRadius: 8 }}
104
124
  autoPlay
105
125
  showControls
126
+ useWorker
127
+ workerConcurrency={4}
106
128
  loopCount={2}
107
129
  debug
108
- onLoaded={(stats) => console.log(formatGifLoadStats(stats))}
130
+ onLoaded={(stats) => console.log(`loaded in ${stats.totalMs.toFixed(1)}ms`)}
109
131
  onPlay={() => console.log('play')}
110
132
  onPause={() => console.log('pause')}
111
133
  onEnd={() => console.log('end')}
@@ -118,36 +140,56 @@ export default function Demo() {
118
140
 
119
141
  ## 资源缓存
120
142
 
121
- 相同 `src` 的 GIF 在全局共享解码结果,多个 `GifPlayer` 实例不会重复 fetch / decode。
143
+ 相同 `src` 的 GIF 在全局共享解码结果(不论是否使用 Worker),多个 `GifPlayer` 实例不会重复 fetch / decode。
122
144
 
123
145
  ```tsx
124
146
  import { clearGifResourceCache } from '@libshub/gif-tools'
125
147
 
126
- // 清空全部缓存
127
148
  clearGifResourceCache()
128
149
  ```
129
150
 
130
- `onLoaded` 回调中的 `GifLoadStats` 可区分三种加载模式:
151
+ ## 加载统计 `GifLoadStats`
131
152
 
132
- - **fresh**:首次加载,包含 `fetchTimeMs` `decodeTimeMs`
133
- - **pending**:复用进行中的请求,包含 `pendingWaitFetchMs` 与 `pendingWaitDecodeMs`
134
- - **cache**:命中缓存,耗时均为 0
153
+ `onLoaded` 回调中的 `GifLoadStats` 可区分三种加载模式,并拆分 network / queue / decode 三阶段:
135
154
 
136
- ```tsx
137
- import { formatGifLoadStats, getGifLoadStatsView } from '@libshub/gif-tools'
155
+ | 字段 | 说明 |
156
+ |------|------|
157
+ | `networkTimeMs` | 网络或浏览器缓存读取耗时(Resource Timing) |
158
+ | `queueWaitMs` | Worker 池排队,或 pending 跟随方等待进行中的加载 |
159
+ | `decodeTimeMs` | 实际 decode 耗时 |
160
+ | `totalMs` | 墙钟总耗时(该实例从开始加载到就绪) |
161
+ | `fromCache` | 命中内存缓存 |
162
+ | `fromPending` | 等待同一 `src` 的进行中加载 |
138
163
 
139
- onLoaded={(stats) => {
140
- console.log(formatGifLoadStats(stats))
141
- // fresh
142
- // fetch 120.5ms
143
- // decode 45.2ms
144
- // total 165.7ms
164
+ | 模式 | 判断 | 典型字段 |
165
+ |------|------|----------|
166
+ | **fresh** | `!fromCache && !fromPending` | `networkTimeMs`、`queueWaitMs`、`decodeTimeMs` |
167
+ | **pending** | `fromPending` | `queueWaitMs`(其余为 0) |
168
+ | **cache** | `fromCache` | 均为 0 |
145
169
 
146
- const view = getGifLoadStatsView(stats)
147
- console.log(view.mode) // 'fresh' | 'pending' | 'cache'
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`)
148
188
  }}
149
189
  ```
150
190
 
191
+ > 并发场景下,各实例 `totalMs` 为各自墙钟时间;整批加载的总耗时应取最慢实例的 `totalMs`,而非简单累加。
192
+
151
193
  ## 底层 API
152
194
 
153
195
  不依赖 React 时,可直接在 Canvas 上创建控制器:
@@ -157,6 +199,8 @@ import { createGifController } from '@libshub/gif-tools'
157
199
 
158
200
  const canvas = document.querySelector('canvas')!
159
201
  const { controller, stats } = await createGifController(canvas, src, {
202
+ useWorker: true,
203
+ workerConcurrency: 4,
160
204
  loopCount: 3,
161
205
  onPlay: () => {},
162
206
  onPause: () => {},
@@ -181,13 +225,9 @@ controller.destroy()
181
225
 
182
226
  - `GifPlayerProps`、`GifPlayerRef`、`GifPlayerComponent`
183
227
  - `GifLoadStats`、`GifController`、`CreateGifOptions`
184
- - `GifLoadStatsView`、`GifLoadStatsLine`、`GifLoadStatsMode`
185
228
 
186
229
  **工具函数**
187
230
 
188
231
  - `createGifController` — 在 Canvas 上创建 GIF 控制器
189
232
  - `clearGifResourceCache` — 清空 GIF 资源缓存
190
- - `formatGifLoadStats` — 格式化加载统计(多行)
191
- - `formatLoadTimeMs` — 格式化毫秒数
192
- - `getGifLoadStatsView` — 获取结构化加载统计视图
193
- - `getTotalLoadTimeMs` — 获取总加载耗时
233
+ - `setWorkerPoolSize` — 设置全局 Worker 池并发数(1–16),不传参则恢复默认 `min(hardwareConcurrency, 4)`
@@ -18,6 +18,8 @@ export interface GifPlayerProps {
18
18
  autoPlay?: boolean;
19
19
  showControls?: boolean;
20
20
  debug?: boolean;
21
+ useWorker?: boolean;
22
+ workerConcurrency?: number;
21
23
  loopCount?: number;
22
24
  onPlay?: () => void;
23
25
  onPause?: () => void;
@@ -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:96px;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}
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*/
@@ -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
- })))(), h = /* @__PURE__ */ new Map(), g = /* @__PURE__ */ new Map(), _ = /* @__PURE__ */ new Map();
302
- function v(e, t, n) {
303
- return n === null || e >= n ? {
304
- pendingWaitFetchMs: 0,
305
- pendingWaitDecodeMs: t - e
306
- } : {
307
- pendingWaitFetchMs: n - e,
308
- pendingWaitDecodeMs: t - n
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
- function y() {
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
- fetchTimeMs: 0,
434
+ networkTimeMs: 0,
435
+ queueWaitMs: 0,
314
436
  decodeTimeMs: 0,
315
- pendingWaitFetchMs: 0,
316
- pendingWaitDecodeMs: 0,
437
+ totalMs: 0,
317
438
  fromCache: !0,
318
439
  fromPending: !1
319
440
  };
320
441
  }
321
- function b(e, t) {
442
+ function j(e, t, n, r, i) {
322
443
  return {
323
- fetchTimeMs: e,
324
- decodeTimeMs: t,
325
- pendingWaitFetchMs: 0,
326
- pendingWaitDecodeMs: 0,
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 x(e, t) {
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
- frames: c,
344
- width: s.lsd.width,
345
- height: s.lsd.height
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 S(e, t) {
352
- let n = h.get(e);
353
- if (n) return {
354
- gif: n.data,
355
- stats: y()
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 && g.has(e)) {
358
- let t = g.get(e), n = performance.now(), { gif: r, fetchTimeMs: i, decodeTimeMs: a } = await t.promise, { pendingWaitFetchMs: o, pendingWaitDecodeMs: s } = v(n, performance.now(), t.fetchDoneAt), c = h.get(e);
359
- return c ||= (h.set(e, {
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
- fetchTimeMs: i,
363
- decodeTimeMs: a
364
- }), h.get(e)), {
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 r = (_.get(e) ?? 0) + 1;
377
- _.set(e, r);
378
- let i = {
379
- fetchDoneAt: null,
380
- promise: void 0
381
- };
382
- i.promise = x(e, i).then(({ gif: t, fetchTimeMs: n, decodeTimeMs: i }) => _.get(e) === r ? (g.delete(e), h.set(e, {
383
- data: t,
384
- refCount: 0,
385
- fetchTimeMs: n,
386
- decodeTimeMs: i
387
- }), {
388
- gif: t,
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: a,
401
- stats: b(o, s)
511
+ gif: o,
512
+ stats: j(s, c, l, u, {})
402
513
  };
403
514
  }
404
- function C(e) {
405
- let t = h.get(e);
406
- t && (t.refCount += 1);
515
+ function F(e, t) {
516
+ let n = E.get(k(e));
517
+ n && (n.refCount += 1);
407
518
  }
408
- function w(e) {
409
- let t = h.get(e);
410
- t && (--t.refCount, t.refCount <= 0 && h.delete(e));
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 T() {
413
- h.clear(), g.clear(), _.clear();
523
+ function L() {
524
+ E.clear(), D.clear(), O.clear();
414
525
  }
415
526
  //#endregion
416
527
  //#region src/utils/gifController.ts
417
- function E(e, t, n, r, i = {}) {
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 D(e, t, n = {}) {
474
- let { skipPending: r, onLoaded: i, ...a } = n, { gif: o, stats: s } = await S(t, { skipPending: r });
475
- return i?.(s), {
476
- controller: E(e, o.frames, o.width, o.height, a),
477
- stats: s
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
- function O(e) {
483
- return e.fromCache ? 0 : e.fromPending ? e.pendingWaitFetchMs + e.pendingWaitDecodeMs : e.fetchTimeMs + e.decodeTimeMs;
484
- }
485
- function k(e) {
486
- return e.fromCache ? {
487
- mode: "cache",
488
- lines: [{
489
- label: "wait fetch",
490
- valueMs: 0
491
- }, {
492
- label: "wait decode",
493
- valueMs: 0
494
- }],
495
- totalMs: 0
496
- } : e.fromPending ? {
497
- mode: "pending",
498
- lines: [{
499
- label: "wait fetch",
500
- valueMs: e.pendingWaitFetchMs
501
- }, {
502
- label: "wait decode",
503
- valueMs: e.pendingWaitDecodeMs
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 A(e) {
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 M(e, t) {
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 N = {
628
+ var W = {
530
629
  fresh: "F",
531
630
  pending: "P",
532
631
  cache: "C"
533
632
  };
534
- function P(e) {
535
- let t = k(e), n = N[t.mode];
536
- return t.mode === "cache" ? `${n} · 0` : `${n} · ${j(t.totalMs)}`;
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 I = {
639
+ var K = {
544
640
  fresh: "FRESH",
545
641
  pending: "PND",
546
642
  cache: "CACHE"
547
643
  };
548
- function L(e, t) {
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 R({ label: e, valueMs: t }) {
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: j(t)
671
+ children: H(t)
576
672
  })]
577
673
  });
578
674
  }
579
- function z({ view: e, width: t, expanded: n, onToggle: r }) {
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: I[e.mode]
696
+ children: i
600
697
  }),
601
698
  /* @__PURE__ */ o("div", {
602
699
  className: "gif-player__debug-body",
603
- children: e.lines.map((t) => /* @__PURE__ */ o(R, {
604
- label: F(t, e.mode),
605
- valueMs: t.valueMs
606
- }, t.label))
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: j(e.totalMs)
712
+ children: H(e.totalMs)
616
713
  })]
617
714
  })
618
715
  ]
619
716
  });
620
717
  }
621
- function B({ stats: e, canvasRef: t, visible: r }) {
622
- let { width: i, height: c } = L(t, r), l = k(e), u = M(i, c), [d, f] = a(!1), p = u !== "full";
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(z, {
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: P(e)
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: I[l.mode]
758
+ children: K[l.mode]
662
759
  }), /* @__PURE__ */ o("span", {
663
760
  className: "gif-player__debug-value gif-player__debug-value--total",
664
- children: j(l.totalMs)
761
+ children: H(l.totalMs)
665
762
  })]
666
763
  })
667
- }) : /* @__PURE__ */ o(z, {
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 V = e(({ src: e, autoPlay: c = !0, showControls: l = !1, debug: u = !1, loopCount: d, className: f, style: p, width: m, height: h, onPlay: g, onPause: _, onEnd: v, onLoaded: y, onError: b }, x) => {
676
- let S = i(null), T = i(null), E = i(g), O = i(_), k = i(v), A = i(y), j = i(b), [M, N] = a(c), [P, F] = a(!1), [I, L] = a(null), [R, z] = a(0), V = i(!1), H = i(0);
677
- E.current = g, O.current = _, k.current = v, A.current = y, j.current = b;
678
- let U = t(() => {
679
- T.current?.play(), N(!0);
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?.isPlaying() ? W() : U();
684
- }, [W, U]), K = t(() => {
685
- T.current?.reset(), N(!1);
686
- }, []), q = t(() => {
687
- V.current = !0, z((e) => e + 1);
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 = ++H.current, n = !1, r = S.current;
787
+ let t = ++U.current, n = !1, r = w.current;
691
788
  if (!r) return () => {};
692
- let i = V.current;
693
- return V.current = !1, F(!1), N(!1), L(null), T.current?.destroy(), T.current = null, D(r, e, {
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
- loopCount: d,
792
+ useWorker: d,
793
+ workerConcurrency: f,
794
+ loopCount: p,
696
795
  onPlay: () => {
697
- t === H.current && N(!0), E.current?.();
796
+ t === U.current && M(!0), E.current?.();
698
797
  },
699
798
  onPause: () => {
700
- t === H.current && N(!1), O.current?.();
799
+ t === U.current && M(!1), D.current?.();
701
800
  },
702
801
  onEnd: () => {
703
- t === H.current && N(!1), k.current?.();
802
+ t === U.current && M(!1), O.current?.();
704
803
  }
705
804
  }).then(({ controller: r, stats: i }) => {
706
- if (A.current?.(i), n || t !== H.current) {
805
+ if (k.current?.(i), n || t !== U.current) {
707
806
  r.destroy({ clearCanvas: !1 });
708
807
  return;
709
808
  }
710
- C(e), T.current = r;
809
+ F(e, d), T.current = r;
711
810
  let a = r.destroy.bind(r);
712
811
  r.destroy = (t) => {
713
- a(t), w(e);
714
- }, F(!0), L(i), c && (r.play(), N(!0));
812
+ a(t), I(e, d);
813
+ }, P(!0), R(i), c && (r.play(), M(!0));
715
814
  }).catch((e) => {
716
- t === H.current && L(null), j.current?.(e instanceof Error ? e : Error(String(e)));
815
+ t === U.current && R(null), A.current?.(e instanceof Error ? e : Error(String(e)));
717
816
  }), () => {
718
- n = !0, t === H.current && (T.current?.destroy(), T.current = null);
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
- c
824
+ f
724
825
  ]);
725
- return r(x, () => ({
726
- play: U,
727
- pause: W,
728
- toggle: G,
729
- reset: K,
730
- reload: q,
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
- ]), n(() => J(), [J, R]), /* @__PURE__ */ s("div", {
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
- f
843
+ m
743
844
  ].filter(Boolean).join(" "),
744
- style: p,
845
+ style: h,
745
846
  children: [
746
847
  /* @__PURE__ */ o("canvas", {
747
- ref: S,
848
+ ref: w,
748
849
  className: "gif-player__media",
749
850
  role: "img",
750
851
  style: {
751
- ...m === void 0 ? {} : { width: m },
752
- ...h === void 0 ? {} : { height: h },
753
- ...P ? {} : { visibility: "hidden" }
852
+ ...g === void 0 ? {} : { width: g },
853
+ ..._ === void 0 ? {} : { height: _ },
854
+ ...N ? {} : { visibility: "hidden" }
754
855
  }
755
856
  }),
756
- u && I !== null && P && /* @__PURE__ */ o(B, {
757
- stats: I,
758
- canvasRef: S,
759
- visible: P
857
+ u && L !== null && N && /* @__PURE__ */ o(X, {
858
+ stats: L,
859
+ canvasRef: w,
860
+ visible: N
760
861
  }),
761
- l && P && /* @__PURE__ */ o("div", {
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: G,
767
- children: M ? /* @__PURE__ */ s("svg", {
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
- V.displayName = "GifPlayer";
894
+ Z.displayName = "GifPlayer";
794
895
  //#endregion
795
- export { V as GifPlayer, T as clearGifResourceCache, D as createGifController, A as formatGifLoadStats, j as formatLoadTimeMs, k as getGifLoadStatsView, O as getTotalLoadTimeMs };
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 { createGifController, clearGifResourceCache, formatGifLoadStats, formatLoadTimeMs, getGifLoadStatsView, getTotalLoadTimeMs } from './utils';
4
- export type { CreateGifOptions, GifController, GifLoadStats, GifLoadStatsLine, GifLoadStatsMode, GifLoadStatsView } from './utils';
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 {};
@@ -1,5 +1,4 @@
1
1
  export { createGifController } from './gifController';
2
- export { acquireGifResource, clearGifResourceCache, releaseGifResource } from './gifResourceManager';
3
- export { formatGifLoadStats, formatGifLoadStatsCompact, formatLoadTimeMs, getDebugDensity, getGifLoadStatsView, getTotalLoadTimeMs } from './loadStats';
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;
@@ -5,14 +5,14 @@ export interface LoadedGif {
5
5
  height: number;
6
6
  }
7
7
  export interface GifLoadStats {
8
- /** 本次实际 fetch 耗时,仅 fresh 发起方有值 */
9
- fetchTimeMs: number;
10
- /** 本次实际 decode 耗时,仅 fresh 发起方有值 */
8
+ /** 浏览器侧实际网络/缓存读取耗时(Resource Timing) */
9
+ networkTimeMs: number;
10
+ /** Worker 池排队,或 pending 跟随方等待进行中的加载 */
11
+ queueWaitMs: number;
12
+ /** 本次实际 decode 耗时 */
11
13
  decodeTimeMs: number;
12
- /** 等待进行中的 fetch 阶段,仅 pending */
13
- pendingWaitFetchMs: number;
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.8",
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": {
@@ -55,4 +55,4 @@
55
55
  "dependencies": {
56
56
  "gifuct-js": "^2.1.2"
57
57
  }
58
- }
58
+ }