@libshub/gif-tools 1.0.3 → 1.0.7

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.
@@ -0,0 +1,7 @@
1
+ import { type RefObject } from 'react';
2
+ import type { GifLoadStats } from '../utils';
3
+ export declare function GifLoadStatsDebug({ stats, canvasRef, visible, }: {
4
+ stats: GifLoadStats;
5
+ canvasRef: RefObject<HTMLCanvasElement | null>;
6
+ visible: boolean;
7
+ }): import("react").JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import { type CSSProperties, type ForwardRefExoticComponent, type RefAttributes } from 'react';
2
+ import type { GifLoadStats } from '../utils';
2
3
  import './GifPlayer.css';
3
4
  export interface GifPlayerRef {
4
5
  play: () => void;
@@ -17,12 +18,11 @@ export interface GifPlayerProps {
17
18
  autoPlay?: boolean;
18
19
  showControls?: boolean;
19
20
  debug?: boolean;
20
- /** 未设置则无限循环;1 播放一次;N 播放 N 次后触发 onEnd */
21
21
  loopCount?: number;
22
22
  onPlay?: () => void;
23
23
  onPause?: () => void;
24
24
  onEnd?: () => void;
25
- onLoaded?: (decodeTimeMs: number) => void;
25
+ onLoaded?: (stats: GifLoadStats) => void;
26
26
  onError?: (error: Error) => void;
27
27
  }
28
28
  export type GifPlayerComponent = ForwardRefExoticComponent<GifPlayerProps & RefAttributes<GifPlayerRef>>;
@@ -1,2 +1,2 @@
1
- .gif-player{line-height:0;display:inline-block;position:relative;overflow:hidden}.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{color:#fff;pointer-events:none;background:#000000a6;border-radius:4px;padding:2px 8px;font-family:ui-monospace,monospace;font-size:12px;line-height:1.4;position:absolute;top:8px;left:8px}
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}
2
2
  /*$vite$:1*/
@@ -299,64 +299,122 @@ var c = (e, t) => () => (t || (e((t = { exports: {} }).exports, t), e = null), t
299
299
  });
300
300
  };
301
301
  })))(), h = /* @__PURE__ */ new Map(), g = /* @__PURE__ */ new Map(), _ = /* @__PURE__ */ new Map();
302
- async function v(e) {
303
- let t = await fetch(e, {
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
309
+ };
310
+ }
311
+ function y() {
312
+ return {
313
+ fetchTimeMs: 0,
314
+ decodeTimeMs: 0,
315
+ pendingWaitFetchMs: 0,
316
+ pendingWaitDecodeMs: 0,
317
+ fromCache: !0,
318
+ fromPending: !1
319
+ };
320
+ }
321
+ function b(e, t) {
322
+ return {
323
+ fetchTimeMs: e,
324
+ decodeTimeMs: t,
325
+ pendingWaitFetchMs: 0,
326
+ pendingWaitDecodeMs: 0,
327
+ fromCache: !1,
328
+ fromPending: !1
329
+ };
330
+ }
331
+ async function x(e, t) {
332
+ let n = performance.now(), r = await fetch(e, {
304
333
  mode: "cors",
305
334
  credentials: "omit"
306
335
  });
307
- if (!t.ok) throw Error(`Failed to load gif: ${t.status}`);
308
- let n = await t.arrayBuffer(), r = performance.now(), i = (0, m.parseGIF)(n), a = (0, m.decompressFrames)(i, !0), o = performance.now() - r;
309
- if (!a.length) throw Error("GIF has no frames");
336
+ 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");
310
341
  return {
311
342
  gif: {
312
- frames: a,
313
- width: i.lsd.width,
314
- height: i.lsd.height
343
+ frames: c,
344
+ width: s.lsd.width,
345
+ height: s.lsd.height
315
346
  },
316
- decodeTimeMs: o
347
+ fetchTimeMs: a,
348
+ decodeTimeMs: l
317
349
  };
318
350
  }
319
- async function y(e, t) {
351
+ async function S(e, t) {
320
352
  let n = h.get(e);
321
- if (n) return n.refCount += 1, {
353
+ if (n) return {
322
354
  gif: n.data,
323
- decodeTimeMs: 0
355
+ stats: y()
324
356
  };
325
357
  if (!t?.skipPending && g.has(e)) {
326
- let { gif: t } = await g.get(e);
327
- return h.get(e).refCount += 1, {
328
- gif: t,
329
- decodeTimeMs: 0
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, {
360
+ 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
+ }
330
374
  };
331
375
  }
332
376
  let r = (_.get(e) ?? 0) + 1;
333
377
  _.set(e, r);
334
- let i = v(e).then(({ gif: t, decodeTimeMs: n }) => _.get(e) === r ? (g.delete(e), h.set(e, {
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, {
335
383
  data: t,
336
- refCount: 0
384
+ refCount: 0,
385
+ fetchTimeMs: n,
386
+ decodeTimeMs: i
337
387
  }), {
338
388
  gif: t,
339
- decodeTimeMs: n
389
+ fetchTimeMs: n,
390
+ decodeTimeMs: i
340
391
  }) : {
341
392
  gif: t,
342
- decodeTimeMs: n
393
+ fetchTimeMs: n,
394
+ decodeTimeMs: i
343
395
  }).catch((t) => {
344
396
  throw _.get(e) === r && g.delete(e), t;
345
- });
346
- g.set(e, i);
347
- let { gif: a, decodeTimeMs: o } = await i;
348
- return _.get(e) === r && (h.get(e).refCount += 1), {
397
+ }), g.set(e, i);
398
+ let { gif: a, fetchTimeMs: o, decodeTimeMs: s } = await i.promise;
399
+ return {
349
400
  gif: a,
350
- decodeTimeMs: o
401
+ stats: b(o, s)
351
402
  };
352
403
  }
353
- function b(e) {
404
+ function C(e) {
405
+ let t = h.get(e);
406
+ t && (t.refCount += 1);
407
+ }
408
+ function w(e) {
354
409
  let t = h.get(e);
355
410
  t && (--t.refCount, t.refCount <= 0 && h.delete(e));
356
411
  }
412
+ function T() {
413
+ h.clear(), g.clear(), _.clear();
414
+ }
357
415
  //#endregion
358
416
  //#region src/utils/gifController.ts
359
- function x(e, t, n, r, i = {}) {
417
+ function E(e, t, n, r, i = {}) {
360
418
  let a = e.getContext("2d");
361
419
  if (!a) throw Error("Canvas 2d context unavailable");
362
420
  let o = a;
@@ -400,8 +458,8 @@ function x(e, t, n, r, i = {}) {
400
458
  function x() {
401
459
  b(), u = 0, d = 0, o.clearRect(0, 0, n, r), g(0);
402
460
  }
403
- function S() {
404
- b(), p = null, s.width = 0, s.height = 0, e.width = 0, e.height = 0;
461
+ function S(t) {
462
+ b(), p = null, s.width = 0, s.height = 0, t?.clearCanvas !== !1 && (e.width = 0, e.height = 0);
405
463
  }
406
464
  return g(0), {
407
465
  play: y,
@@ -412,58 +470,252 @@ function x(e, t, n, r, i = {}) {
412
470
  getCompletedLoops: () => d
413
471
  };
414
472
  }
415
- async function S(e, t, n = {}) {
416
- let { skipPending: r, onLoaded: i, ...a } = n, { gif: o, decodeTimeMs: s } = await y(t, { skipPending: r });
417
- i?.(s);
418
- let c = x(e, o.frames, o.width, o.height, a), l = c.destroy;
419
- return c.destroy = () => {
420
- l(), b(t);
421
- }, c;
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
478
+ };
479
+ }
480
+ //#endregion
481
+ //#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
516
+ };
517
+ }
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) {
523
+ return `${e.toFixed(1)}ms`;
524
+ }
525
+ function M(e, t) {
526
+ let n = Math.min(e, t);
527
+ return n < 130 ? "compact" : n < 220 ? "medium" : "full";
528
+ }
529
+ var N = {
530
+ fresh: "F",
531
+ pending: "P",
532
+ cache: "C"
533
+ };
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";
540
+ }
541
+ //#endregion
542
+ //#region src/components/GifLoadStatsDebug.tsx
543
+ var I = {
544
+ fresh: "FRESH",
545
+ pending: "PND",
546
+ cache: "CACHE"
547
+ };
548
+ function L(e, t) {
549
+ let [r, i] = a({
550
+ width: 0,
551
+ height: 0
552
+ });
553
+ return n(() => {
554
+ let n = e.current;
555
+ if (!n || !t) return;
556
+ let r = () => {
557
+ i({
558
+ width: n.clientWidth,
559
+ height: n.clientHeight
560
+ });
561
+ };
562
+ r();
563
+ let a = new ResizeObserver(r);
564
+ return a.observe(n), () => a.disconnect();
565
+ }, [e, t]), r;
566
+ }
567
+ function R({ label: e, valueMs: t }) {
568
+ return /* @__PURE__ */ s("div", {
569
+ className: "gif-player__debug-row",
570
+ children: [/* @__PURE__ */ o("span", {
571
+ className: "gif-player__debug-label",
572
+ children: e
573
+ }), /* @__PURE__ */ o("span", {
574
+ className: "gif-player__debug-value",
575
+ children: j(t)
576
+ })]
577
+ });
578
+ }
579
+ function z({ view: e, width: t, expanded: n, onToggle: r }) {
580
+ return /* @__PURE__ */ s("div", {
581
+ className: [
582
+ "gif-player__debug",
583
+ "gif-player__debug--full",
584
+ n && "gif-player__debug--expanded",
585
+ r && "gif-player__debug--collapsible"
586
+ ].filter(Boolean).join(" "),
587
+ "data-mode": e.mode,
588
+ style: { maxWidth: !n && t > 0 ? Math.min(152, Math.round(t * .52)) : void 0 },
589
+ role: r ? "button" : void 0,
590
+ tabIndex: r ? 0 : void 0,
591
+ title: r ? n ? "点击收起" : "点击展开详情" : void 0,
592
+ onClick: r,
593
+ onKeyDown: r ? (e) => {
594
+ (e.key === "Enter" || e.key === " ") && (e.preventDefault(), r());
595
+ } : void 0,
596
+ children: [
597
+ /* @__PURE__ */ o("span", {
598
+ className: "gif-player__debug-badge",
599
+ children: I[e.mode]
600
+ }),
601
+ /* @__PURE__ */ o("div", {
602
+ 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))
607
+ }),
608
+ /* @__PURE__ */ s("div", {
609
+ className: "gif-player__debug-total",
610
+ children: [/* @__PURE__ */ o("span", {
611
+ className: "gif-player__debug-label",
612
+ children: "Σ"
613
+ }), /* @__PURE__ */ o("span", {
614
+ className: "gif-player__debug-value",
615
+ children: j(e.totalMs)
616
+ })]
617
+ })
618
+ ]
619
+ });
620
+ }
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";
623
+ n(() => {
624
+ f(!1);
625
+ }, [e]), n(() => {
626
+ p || f(!1);
627
+ }, [p]);
628
+ let m = () => {
629
+ p && f((e) => !e);
630
+ };
631
+ return p && d ? /* @__PURE__ */ o(z, {
632
+ view: l,
633
+ width: i,
634
+ expanded: !0,
635
+ onToggle: m
636
+ }) : u === "compact" ? /* @__PURE__ */ o("div", {
637
+ className: "gif-player__debug gif-player__debug--compact gif-player__debug--collapsible",
638
+ "data-mode": l.mode,
639
+ role: "button",
640
+ tabIndex: 0,
641
+ title: "点击展开详情",
642
+ onClick: m,
643
+ onKeyDown: (e) => {
644
+ (e.key === "Enter" || e.key === " ") && (e.preventDefault(), m());
645
+ },
646
+ children: P(e)
647
+ }) : u === "medium" ? /* @__PURE__ */ o("div", {
648
+ className: "gif-player__debug gif-player__debug--medium gif-player__debug--collapsible",
649
+ "data-mode": l.mode,
650
+ role: "button",
651
+ tabIndex: 0,
652
+ title: "点击展开详情",
653
+ onClick: m,
654
+ onKeyDown: (e) => {
655
+ (e.key === "Enter" || e.key === " ") && (e.preventDefault(), m());
656
+ },
657
+ children: /* @__PURE__ */ s("div", {
658
+ className: "gif-player__debug-head",
659
+ children: [/* @__PURE__ */ o("span", {
660
+ className: "gif-player__debug-badge",
661
+ children: I[l.mode]
662
+ }), /* @__PURE__ */ o("span", {
663
+ className: "gif-player__debug-value gif-player__debug-value--total",
664
+ children: j(l.totalMs)
665
+ })]
666
+ })
667
+ }) : /* @__PURE__ */ o(z, {
668
+ view: l,
669
+ width: i,
670
+ expanded: !1
671
+ });
422
672
  }
423
673
  //#endregion
424
674
  //#region src/components/GifPlayer.tsx
425
- var C = 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) => {
426
- let C = i(null), w = i(null), T = i(g), E = i(_), D = i(v), O = i(y), k = i(b), [A, j] = a(c), [M, N] = a(!1), [P, F] = a(null), [I, L] = a(0), R = i(!1);
427
- T.current = g, E.current = _, D.current = v, O.current = y, k.current = b;
428
- let z = t(() => {
429
- w.current?.play(), j(!0);
430
- }, []), B = t(() => {
431
- w.current?.pause(), j(!1);
432
- }, []), V = t(() => {
433
- w.current?.isPlaying() ? B() : z();
434
- }, [B, z]), H = t(() => {
435
- w.current?.reset(), j(!1);
436
- }, []), U = t(() => {
437
- R.current = !0, L((e) => e + 1);
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);
438
680
  }, []), W = t(() => {
439
- let t = !1, n = C.current;
440
- if (!n) return () => {};
441
- let r = R.current;
442
- return R.current = !1, N(!1), j(!1), F(null), w.current?.destroy(), w.current = null, S(n, e, {
443
- skipPending: r,
681
+ T.current?.pause(), N(!1);
682
+ }, []), 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);
688
+ }, []), J = t(() => {
689
+ if (!e) return () => {};
690
+ let t = ++H.current, n = !1, r = S.current;
691
+ 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, {
694
+ skipPending: i,
444
695
  loopCount: d,
445
696
  onPlay: () => {
446
- j(!0), T.current?.();
697
+ t === H.current && N(!0), E.current?.();
447
698
  },
448
699
  onPause: () => {
449
- j(!1), E.current?.();
700
+ t === H.current && N(!1), O.current?.();
450
701
  },
451
702
  onEnd: () => {
452
- j(!1), D.current?.();
453
- },
454
- onLoaded: (e) => {
455
- F(e), O.current?.(e);
703
+ t === H.current && N(!1), k.current?.();
456
704
  }
457
- }).then((e) => {
458
- if (t) {
459
- e.destroy();
705
+ }).then(({ controller: r, stats: i }) => {
706
+ if (A.current?.(i), n || t !== H.current) {
707
+ r.destroy({ clearCanvas: !1 });
460
708
  return;
461
709
  }
462
- w.current = e, N(!0), c && (e.play(), j(!0));
710
+ C(e), T.current = r;
711
+ let a = r.destroy.bind(r);
712
+ r.destroy = (t) => {
713
+ a(t), w(e);
714
+ }, F(!0), L(i), c && (r.play(), N(!0));
463
715
  }).catch((e) => {
464
- t || (F(null), k.current?.(e instanceof Error ? e : Error(String(e))));
716
+ t === H.current && L(null), j.current?.(e instanceof Error ? e : Error(String(e)));
465
717
  }), () => {
466
- t = !0, w.current?.destroy(), w.current = null;
718
+ n = !0, t === H.current && (T.current?.destroy(), T.current = null);
467
719
  };
468
720
  }, [
469
721
  e,
@@ -471,19 +723,19 @@ var C = e(({ src: e, autoPlay: c = !0, showControls: l = !1, debug: u = !1, loop
471
723
  c
472
724
  ]);
473
725
  return r(x, () => ({
474
- play: z,
475
- pause: B,
476
- toggle: V,
477
- reset: H,
478
- reload: U,
479
- isPlaying: () => w.current?.isPlaying() ?? !1
726
+ play: U,
727
+ pause: W,
728
+ toggle: G,
729
+ reset: K,
730
+ reload: q,
731
+ isPlaying: () => T.current?.isPlaying() ?? !1
480
732
  }), [
481
- z,
482
- B,
483
- V,
484
- H,
485
- U
486
- ]), n(() => W(), [W, I]), /* @__PURE__ */ s("div", {
733
+ U,
734
+ W,
735
+ G,
736
+ K,
737
+ q
738
+ ]), n(() => J(), [J, R]), /* @__PURE__ */ s("div", {
487
739
  className: [
488
740
  "gif-player",
489
741
  l && "gif-player--show-controls",
@@ -492,26 +744,27 @@ var C = e(({ src: e, autoPlay: c = !0, showControls: l = !1, debug: u = !1, loop
492
744
  style: p,
493
745
  children: [
494
746
  /* @__PURE__ */ o("canvas", {
495
- ref: C,
747
+ ref: S,
496
748
  className: "gif-player__media",
497
749
  role: "img",
498
750
  style: {
499
751
  ...m === void 0 ? {} : { width: m },
500
752
  ...h === void 0 ? {} : { height: h },
501
- ...M ? {} : { visibility: "hidden" }
753
+ ...P ? {} : { visibility: "hidden" }
502
754
  }
503
755
  }),
504
- u && P !== null && /* @__PURE__ */ s("span", {
505
- className: "gif-player__debug",
506
- children: [P.toFixed(1), "ms"]
756
+ u && I !== null && P && /* @__PURE__ */ o(B, {
757
+ stats: I,
758
+ canvasRef: S,
759
+ visible: P
507
760
  }),
508
- l && M && /* @__PURE__ */ o("div", {
761
+ l && P && /* @__PURE__ */ o("div", {
509
762
  className: "gif-player__controls",
510
763
  children: /* @__PURE__ */ o("button", {
511
764
  type: "button",
512
765
  className: "gif-player__btn",
513
- onClick: V,
514
- children: A ? /* @__PURE__ */ s("svg", {
766
+ onClick: G,
767
+ children: M ? /* @__PURE__ */ s("svg", {
515
768
  viewBox: "0 0 24 24",
516
769
  "aria-hidden": "true",
517
770
  children: [/* @__PURE__ */ o("rect", {
@@ -537,6 +790,6 @@ var C = e(({ src: e, autoPlay: c = !0, showControls: l = !1, debug: u = !1, loop
537
790
  ]
538
791
  });
539
792
  });
540
- C.displayName = "GifPlayer";
793
+ V.displayName = "GifPlayer";
541
794
  //#endregion
542
- export { C as GifPlayer, S as createGifController };
795
+ export { V as GifPlayer, T as clearGifResourceCache, D as createGifController, A as formatGifLoadStats, j as formatLoadTimeMs, k as getGifLoadStatsView, O as getTotalLoadTimeMs };
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 } from './utils';
4
- export type { CreateGifOptions, GifController } from './utils';
3
+ export { createGifController, clearGifResourceCache, formatGifLoadStats, formatLoadTimeMs, getGifLoadStatsView, getTotalLoadTimeMs } from './utils';
4
+ export type { CreateGifOptions, GifController, GifLoadStats, GifLoadStatsLine, GifLoadStatsMode, GifLoadStatsView } from './utils';
@@ -1,2 +1,5 @@
1
- import type { CreateGifOptions, GifController } from './types';
2
- export declare function createGifController(canvas: HTMLCanvasElement, src: string, options?: CreateGifOptions): Promise<GifController>;
1
+ import type { CreateGifOptions, GifController, GifLoadStats } from './types';
2
+ export declare function createGifController(canvas: HTMLCanvasElement, src: string, options?: CreateGifOptions): Promise<{
3
+ controller: GifController;
4
+ stats: GifLoadStats;
5
+ }>;
@@ -1,13 +1,13 @@
1
- import type { LoadedGif } from './types';
1
+ import type { LoadedGif, GifLoadStats } from './types';
2
2
  interface LoadGifResourceOptions {
3
- /** reload 等场景:不复用进行中的请求,直接发起新 fetch */
4
3
  skipPending?: boolean;
5
4
  }
6
5
  interface LoadGifResult {
7
6
  gif: LoadedGif;
8
- decodeTimeMs: number;
7
+ stats: GifLoadStats;
9
8
  }
10
9
  export declare function loadGifResource(src: string, options?: LoadGifResourceOptions): Promise<LoadGifResult>;
10
+ export declare function acquireGifResource(src: string): void;
11
11
  export declare function releaseGifResource(src: string): void;
12
12
  export declare function clearGifResourceCache(): void;
13
13
  export {};
@@ -1,2 +1,5 @@
1
1
  export { createGifController } from './gifController';
2
- export type { CreateGifOptions, GifController } from './types';
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';
5
+ export type { CreateGifOptions, GifController, GifLoadStats } from './types';
@@ -0,0 +1,18 @@
1
+ import type { GifLoadStats } from './types';
2
+ export declare function getTotalLoadTimeMs(stats: GifLoadStats): number;
3
+ export type GifLoadStatsMode = 'fresh' | 'pending' | 'cache';
4
+ export interface GifLoadStatsLine {
5
+ label: string;
6
+ valueMs: number;
7
+ }
8
+ export interface GifLoadStatsView {
9
+ mode: GifLoadStatsMode;
10
+ lines: GifLoadStatsLine[];
11
+ totalMs: number;
12
+ }
13
+ export declare function getGifLoadStatsView(stats: GifLoadStats): GifLoadStatsView;
14
+ export declare function formatGifLoadStats(stats: GifLoadStats): string;
15
+ export declare function formatLoadTimeMs(ms: number): string;
16
+ export declare function getDebugDensity(width: number, height: number): 'compact' | 'medium' | 'full';
17
+ export declare function formatGifLoadStatsCompact(stats: GifLoadStats): string;
18
+ export declare function getGifLoadStatsLineLabel(line: GifLoadStatsLine, mode: GifLoadStatsMode): string;
@@ -4,20 +4,33 @@ export interface LoadedGif {
4
4
  width: number;
5
5
  height: number;
6
6
  }
7
+ export interface GifLoadStats {
8
+ /** 本次实际 fetch 耗时,仅 fresh 发起方有值 */
9
+ fetchTimeMs: number;
10
+ /** 本次实际 decode 耗时,仅 fresh 发起方有值 */
11
+ decodeTimeMs: number;
12
+ /** 等待进行中的 fetch 阶段,仅 pending */
13
+ pendingWaitFetchMs: number;
14
+ /** 等待进行中的 decode 阶段,仅 pending */
15
+ pendingWaitDecodeMs: number;
16
+ fromCache: boolean;
17
+ fromPending: boolean;
18
+ }
7
19
  export interface CreateGifOptions {
8
20
  loopCount?: number;
9
21
  onEnd?: () => void;
10
22
  onPlay?: () => void;
11
23
  onPause?: () => void;
12
- onLoaded?: (decodeTimeMs: number) => void;
13
- /** reload 等场景:不复用进行中的请求,直接发起新 fetch */
24
+ onLoaded?: (stats: GifLoadStats) => void;
14
25
  skipPending?: boolean;
15
26
  }
16
27
  export interface GifController {
17
28
  play: () => void;
18
29
  pause: () => void;
19
30
  reset: () => void;
20
- destroy: () => void;
31
+ destroy: (options?: {
32
+ clearCanvas?: boolean;
33
+ }) => void;
21
34
  isPlaying: () => boolean;
22
35
  getCompletedLoops: () => number;
23
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@libshub/gif-tools",
3
- "version": "1.0.3",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "module": "./dist/gif-tools.es.js",
6
6
  "types": "./dist/index.d.ts",