diff --git a/miniprogram/components/gif-player/gif-player.js b/miniprogram/components/gif-player/gif-player.js index 3d2114d..7bc29ce 100644 --- a/miniprogram/components/gif-player/gif-player.js +++ b/miniprogram/components/gif-player/gif-player.js @@ -4,8 +4,15 @@ const { GifReader } = require('../../utils/vendor/omggif.js'); Component({ properties: { - // GIF 地址(通常是 ex.gifUrl,原始 180px 动图) - src: { type: String, value: '' } + // GIF 地址(通常是 ex.gifUrl,超分后的 720px 动图) + src: { + type: String, + value: '', + // src 可能在组件 attached 之后才由父页面异步设上,用 observer 兜底重新加载 + observer(value) { + if (value) this._load(); + } + } }, data: { failed: false @@ -14,24 +21,37 @@ Component({ attached() { this._aborted = false; this._timer = null; + this._reader = null; + this._ctx = null; + this._rgba = null; + this._frame = 0; if (this.data.src) this._load(); }, detached() { - this._aborted = true; - if (this._timer) { clearTimeout(this._timer); this._timer = null; } + this._abort(); } }, methods: { + _abort() { + this._aborted = true; + if (this._timer) { clearTimeout(this._timer); this._timer = null; } + }, + // 下载 GIF 二进制并构造解码器(解耦于画布,便于画布未就绪时重试渲染) _load() { const src = this.data.src; - if (!src) return; + if (!src || this._aborted) return; + // 防重复:attached 与 observer 可能都触发;同一 src 只加载一次 + if (this._loadedSrc === src && this._reader) return; + this._loadedSrc = src; + this._abort(); // 清掉上一次可能的循环定时器 wx.request({ url: src, responseType: 'arraybuffer', success: (res) => { if (this._aborted) return; try { - this._play(res.data); + this._reader = new GifReader(new Uint8Array(res.data)); + this._start(); } catch (e) { console.error('[gif-player] decode fail', e); this.setData({ failed: true }); @@ -42,42 +62,51 @@ Component({ } }); }, - _play(buffer) { - const bytes = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : new Uint8Array(buffer); - const reader = new GifReader(bytes); - const numFrames = reader.numFrames(); - if (!numFrames) { this.setData({ failed: true }); return; } - const w = reader.width; - const h = reader.height; + // 等待 Canvas 2D 节点就绪(自定义组件里 attached 时可能还没布局好,最多重试 10 次) + _start(attempt = 0) { + if (this._aborted || !this._reader) return; const q = wx.createSelectorQuery().in(this); q.select('#gifCanvas').fields({ node: true, size: true }).exec((rs) => { - if (this._aborted || !rs || !rs[0] || !rs[0].node) return; + if (this._aborted) return; + if (!rs || !rs[0] || !rs[0].node) { + if (attempt < 10) setTimeout(() => this._start(attempt + 1), 60); + return; + } const canvas = rs[0].node; const ctx = canvas.getContext('2d'); - // 画布内部分辨率=GIF 原始分辨率;CSS 负责缩放到容器 + const w = this._reader.width; + const h = this._reader.height; + // 画布内部分辨率 = GIF 原始分辨率(已超分到 720);CSS 负责缩放到容器 canvas.width = w; canvas.height = h; - const rgba = new Uint8ClampedArray(w * h * 4); - let frame = 0; - const render = () => { - if (this._aborted) return; - try { - // decodeAndBlitFrameRGBA 会按 GIF 的 disposal 规则在 rgba 上累积绘制 - reader.decodeAndBlitFrameRGBA(frame, rgba); - const img = ctx.createImageData(w, h); - img.data.set(rgba); - ctx.putImageData(img, 0, 0); - } catch (e) { - console.error('[gif-player] frame fail', e); - return; - } - const info = reader.frameInfo(frame); - const delay = info && info.delay ? info.delay : 100; - frame = (frame + 1) % numFrames; - this._timer = setTimeout(render, Math.max(16, delay)); - }; - render(); + this._ctx = ctx; + this._rgba = new Uint8ClampedArray(w * h * 4); + this._frame = 0; + this._render(); }); + }, + _render() { + if (this._aborted || !this._reader || !this._ctx) return; + const r = this._reader; + const n = r.numFrames(); + if (!n) { this.setData({ failed: true }); return; } + try { + // decodeAndBlitFrameRGBA 按 GIF 的 disposal 规则在 rgba 上累积绘制 + r.decodeAndBlitFrameRGBA(this._frame, this._rgba); + const img = this._ctx.createImageData(r.width, r.height); + img.data.set(this._rgba); + this._ctx.putImageData(img, 0, 0); + } catch (e) { + console.error('[gif-player] frame fail', e); + this.setData({ failed: true }); + return; + } + const info = r.frameInfo(this._frame); + // omggif 的 delay 单位是 1/100 秒,需 ×10 转毫秒;缺失时给 100ms + const delay = (info && info.delay) ? info.delay * 10 : 100; + this._frame = (this._frame + 1) % n; + // 单帧 GIF 只画一次;多帧才循环。最小 40ms 避免极端快帧 + if (n > 1) this._timer = setTimeout(() => this._render(), Math.max(40, delay)); } } }); diff --git a/miniprogram/components/gif-player/gif-player.wxml b/miniprogram/components/gif-player/gif-player.wxml index af964a5..efd3643 100644 --- a/miniprogram/components/gif-player/gif-player.wxml +++ b/miniprogram/components/gif-player/gif-player.wxml @@ -1,3 +1,3 @@ - +