/
githubmirror
/
pixijs
Обзор
Документация
Войти
/
githubmirror
/
pixijs
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
src/gif/GifSource.ts
225 строк
7 KB
Matt Karl
fix: allow canvas source options for gif asset (#11635)
26 авг 2025, 15:21
Не верифицирован
26 авг 2025, 15:21
b354257
Код
Авторство
О чём код?
import { decompressFrames, type ParsedFrame, parseGIF } from 'gifuct-js'; import { DOMAdapter } from '../environment/adapter'; import { CanvasSource, type CanvasSourceOptions } from '../rendering/renderers/shared/texture/sources/CanvasSource'; import { Texture } from '../rendering/renderers/shared/texture/Texture'; /** * Represents a single frame of a GIF. Includes image and timing data. * @category gif * @advanced */ interface GifFrame { /** Image data for the current frame */ texture: Texture<CanvasSource>; /** The start of the current frame, in milliseconds */ start: number; /** The end of the current frame, in milliseconds */ end: number; } /** * Options when constructing from buffer * @category gif * @advanced */ interface GifBufferOptions extends Omit<CanvasSourceOptions, 'resource'> { /** FPS to use when the GIF animation doesn't define any delay between frames */ fps?: number; } /** * Resource provided to GifSprite instances. This is very similar to using a shared * Texture between Sprites. This source contains all the frames and animation needed * to support playback. * @category gif * @advanced */ class GifSource { /** Width of the animation */ public readonly width: number; /** Height of the animation */ public readonly height: number; /** The total time to play the animation in milliseconds */ public readonly duration: number; /** Animation frames */ public readonly frames: GifFrame[]; /** Textures */ public readonly textures: Texture<CanvasSource>[]; /** Total number of frames in the animation */ public readonly totalFrames: number; /** * @param frames - Array of GifFrame instances. */ constructor(frames: GifFrame[]) { // #if _DEBUG if (!frames || !frames.length) throw new Error('Invalid frames'); // #endif // All frames are the same size, get the first frame's size const [{ texture: { width, height } }] = frames; this.width = width; this.height = height; this.frames = frames; this.textures = this.frames.map((frame) => frame.texture); this.totalFrames = this.frames.length; this.duration = this.frames[this.totalFrames - 1].end; } /** Destroy animation data and don't use after this */ public destroy() { for (const texture of this.textures) { texture.destroy(true); } for (const frame of this.frames) { frame.texture = null; } this.frames.length = 0; this.textures.length = 0; Object.assign(this, { frames: null, textures: null, width: 0, height: 0, duration: 0, totalFrames: 0, }); } /** * Create an animated GIF animation from a GIF image's ArrayBuffer. The easiest way to get * the buffer is to use Assets. * @example * import { GifSource, GifSprite } from 'pixi.js/gif'; * * const buffer = await fetch('./file.gif').then(res => res.arrayBuffer()); * const source = GifSource.from(buffer); * const sprite = new GifSprite(source); * @param buffer - GIF image arraybuffer from Assets. * @param options - Optional options to use when building from buffer. */ public static from(buffer: ArrayBuffer, options?: GifBufferOptions): GifSource { if (!buffer || buffer.byteLength === 0) { throw new Error('Invalid buffer'); } // fix https://github.com/matt-way/gifuct-js/issues/30 const validateAndFix = (gif: any): void => { let currentGce = null; for (const frame of gif.frames) { currentGce = frame.gce ?? currentGce; // fix loosing graphic control extension for same frames if ('image' in frame && !('gce' in frame)) { frame.gce = currentGce; } } }; const gif = parseGIF(buffer); validateAndFix(gif); const gifFrames = decompressFrames(gif, true); const frames: GifFrame[] = []; const animWidth = gif.lsd.width; const animHeight = gif.lsd.height; // Temporary canvases required for compositing frames const canvas = DOMAdapter.get().createCanvas(animWidth, animHeight); const context = canvas.getContext('2d', { willReadFrequently: true }); const patchCanvas = DOMAdapter.get().createCanvas(); const patchContext = patchCanvas.getContext('2d'); let time = 0; let previousFrame: ImageData | null = null; // Some GIFs have a non-zero frame delay, so we need to calculate the fallback const { fps = 30, ...canvasSourceOptions } = options ?? {}; const defaultDelay = 1000 / fps; // Precompute each frame and store as ImageData for (let i = 0; i < gifFrames.length; i++) { // Some GIF's omit the disposalType, so let's assume clear if missing const { disposalType = 2, delay = defaultDelay, patch, dims: { width, height, left, top }, } = gifFrames[i] as ParsedFrame; patchCanvas.width = width; patchCanvas.height = height; patchContext.clearRect(0, 0, width, height); const patchData = patchContext.createImageData(width, height); patchData.data.set(patch); patchContext.putImageData(patchData, 0, 0); if (disposalType === 3) { previousFrame = context.getImageData(0, 0, animWidth, animHeight); } context.drawImage(patchCanvas as CanvasImageSource, left, top); const imageData = context.getImageData(0, 0, animWidth, animHeight); if (disposalType === 2) { context.clearRect(0, 0, animWidth, animHeight); } else if (disposalType === 3) { context.putImageData(previousFrame as ImageData, 0, 0); } // Create new texture const resource = DOMAdapter.get().createCanvas( imageData.width, imageData.height ) as HTMLCanvasElement; const resourceContext = resource.getContext('2d'); resourceContext.putImageData(imageData, 0, 0); frames.push({ start: time, end: time + delay, texture: new Texture({ source: new CanvasSource({ resource, ...canvasSourceOptions, }), }), }); time += delay; } // clear the canvases canvas.width = canvas.height = 0; patchCanvas.width = patchCanvas.height = 0; return new GifSource(frames); } } export { GifBufferOptions, GifFrame, GifSource };