fix: gif-player 真实播放(observer+画布重试) + 帧延迟单位修正(1/100s→ms)

This commit is contained in:
2026-07-22 16:38:07 +08:00
parent 47c4801f34
commit f0f5c915d4
2 changed files with 65 additions and 36 deletions

View File

@@ -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 原始分辨率(已超分到 720CSS 负责缩放到容器
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));
}
}
});

View File

@@ -1,3 +1,3 @@
<view class="gif-player">
<canvas type="2d" id="gifCanvas" class="canvas"></canvas>
<canvas wx:if="{{!failed}}" type="2d" id="gifCanvas" class="canvas"></canvas>
</view>