Fix GIF rendering artifacts related to patching
This commit is contained in:
parent
61192f91d9
commit
79bb8e76ce
|
@ -1,3 +1,4 @@
|
|||
import { log } from '@converse/headless';
|
||||
import { getOpenPromise } from '@converse/openpromise';
|
||||
import { parseGIF, decompressFrames } from 'gifuct-js';
|
||||
|
||||
|
@ -36,8 +37,11 @@ export default class ConverseGif {
|
|||
this.gif_el = el.querySelector('img');
|
||||
this.canvas = el.querySelector('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
// It's good practice to pre-render to an offscreen canvas
|
||||
|
||||
// Offscreen canvas with full gif
|
||||
this.offscreenCanvas = document.createElement('canvas');
|
||||
// Offscreen canvas for patches
|
||||
this.patchCanvas = document.createElement('canvas');
|
||||
|
||||
this.ctx_scaled = false;
|
||||
this.frames = [];
|
||||
|
@ -49,6 +53,7 @@ export default class ConverseGif {
|
|||
this.start = null;
|
||||
this.hovering = null;
|
||||
this.frameImageData = null;
|
||||
this.disposal_restore_from_idx = null;
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
@ -70,10 +75,10 @@ export default class ConverseGif {
|
|||
|
||||
// Show the first frame
|
||||
this.frame_idx = 0;
|
||||
this.putFrame(this.frame_idx);
|
||||
this.renderImage();
|
||||
|
||||
if (this.options.autoplay) {
|
||||
const delay = (this.frames[this.frame_idx]?.delay ?? 0);
|
||||
const delay = this.frames[this.frame_idx]?.delay ?? 0;
|
||||
setTimeout(() => this.play(), delay);
|
||||
}
|
||||
}
|
||||
|
@ -110,14 +115,14 @@ export default class ConverseGif {
|
|||
* `frame_delay` parameters can also be passed in. The `timestamp`
|
||||
* parameter comes from `requestAnimationFrame`.
|
||||
*
|
||||
* The purpose of this method is to call `putFrame` with the right delay
|
||||
* The purpose of this method is to call `renderImage` with the right delay
|
||||
* in order to render the GIF animation.
|
||||
*
|
||||
* Note, this method will cause the *next* upcoming frame to be rendered,
|
||||
* not the current one.
|
||||
*
|
||||
* This means `this.frame_idx` will be incremented before calling `this.putFrame`, so
|
||||
* `putFrame(0)` needs to be called *before* this method, otherwise the
|
||||
* This means `this.frame_idx` will be incremented before calling `this.renderImage`, so
|
||||
* `renderImage(0)` needs to be called *before* this method, otherwise the
|
||||
* animation will incorrectly start from frame #1 (this is done in `initPlayer`).
|
||||
*
|
||||
* @param { DOMHighResTimeStamp } timestamp - The timestamp as returned by `requestAnimationFrame`
|
||||
|
@ -132,7 +137,7 @@ export default class ConverseGif {
|
|||
return;
|
||||
}
|
||||
if (timestamp - previous_timestamp < frame_delay) {
|
||||
this.hovering ? this.drawPauseIcon() : this.putFrame(this.frame_idx);
|
||||
this.hovering ? this.drawPauseIcon() : this.renderImage();
|
||||
// We need to wait longer
|
||||
requestAnimationFrame((ts) => this.onAnimationFrame(ts, previous_timestamp, frame_delay));
|
||||
return;
|
||||
|
@ -142,8 +147,8 @@ export default class ConverseGif {
|
|||
return;
|
||||
}
|
||||
this.frame_idx = next_frame;
|
||||
this.putFrame(this.frame_idx);
|
||||
const delay = (this.frames[this.frame_idx]?.delay || 8);
|
||||
this.renderImage();
|
||||
const delay = this.frames[this.frame_idx]?.delay || 8;
|
||||
requestAnimationFrame((ts) => this.onAnimationFrame(ts, timestamp, delay));
|
||||
}
|
||||
|
||||
|
@ -195,21 +200,13 @@ export default class ConverseGif {
|
|||
|
||||
drawError () {
|
||||
this.ctx.fillStyle = 'black';
|
||||
this.ctx.fillRect(
|
||||
0,
|
||||
0,
|
||||
this.options.width ? this.options.width : this.lsd.width,
|
||||
this.options.height ? this.options.height : this.lsd.height
|
||||
);
|
||||
this.ctx.fillRect(0, 0, this.options.width, this.options.height);
|
||||
this.ctx.strokeStyle = 'red';
|
||||
this.ctx.lineWidth = 3;
|
||||
this.ctx.moveTo(0, 0);
|
||||
this.ctx.lineTo(
|
||||
this.options.width ? this.options.width : this.lsd.width,
|
||||
this.options.height ? this.options.height : this.lsd.height
|
||||
);
|
||||
this.ctx.moveTo(0, this.options.height ? this.options.height : this.lsd.height);
|
||||
this.ctx.lineTo(this.options.width ? this.options.width : this.lsd.width, 0);
|
||||
this.ctx.lineTo(this.options.width, this.options.height);
|
||||
this.ctx.moveTo(0, this.options.height);
|
||||
this.ctx.lineTo(this.options.width, 0);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
|
@ -224,41 +221,97 @@ export default class ConverseGif {
|
|||
this.el.requestUpdate();
|
||||
}
|
||||
|
||||
manageDisposal (i) {
|
||||
if (i <= 0) return;
|
||||
|
||||
const offscreenContext = this.offscreenCanvas.getContext('2d');
|
||||
const disposal = this.frames[i - 1].disposalType;
|
||||
/*
|
||||
* Disposal method indicates the way in which the graphic is to
|
||||
* be treated after being displayed.
|
||||
*
|
||||
* Values : 0 - No disposal specified. The decoder is
|
||||
* not required to take any action.
|
||||
* 1 - Do not dispose. The graphic is to be left
|
||||
* in place.
|
||||
* 2 - Restore to background color. The area used by the
|
||||
* graphic must be restored to the background color.
|
||||
* 3 - Restore to previous. The decoder is required to
|
||||
* restore the area overwritten by the graphic with
|
||||
* what was there prior to rendering the graphic.
|
||||
*
|
||||
* Importantly, "previous" means the frame state
|
||||
* after the last disposal of method 0, 1, or 2.
|
||||
*/
|
||||
if (i > 1) {
|
||||
if (disposal === 3) {
|
||||
// eslint-disable-next-line no-eq-null
|
||||
if (this.disposal_restore_from_idx != null) {
|
||||
offscreenContext.putImageData(this.frames[this.disposal_restore_from_idx].data, 0, 0);
|
||||
}
|
||||
} else {
|
||||
this.disposal_restore_from_idx = i - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (disposal === 2) {
|
||||
// Restore to background color
|
||||
// Browser implementations historically restore to transparent; we do the same.
|
||||
// http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079
|
||||
offscreenContext.clearRect(
|
||||
this.last_frame.dims.left,
|
||||
this.last_frame.dims.top,
|
||||
this.last_frame.dims.width,
|
||||
this.last_frame.dims.height
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a gif frame at a specific index inside the canvas.
|
||||
* @param {number|string} i - The frame index
|
||||
* @param {boolean} show_pause_on_hover - The frame index
|
||||
*/
|
||||
putFrame (i, show_pause_on_hover = true) {
|
||||
renderImage (show_pause_on_hover = true) {
|
||||
if (!this.frames.length) return;
|
||||
|
||||
let i = this.frame_idx;
|
||||
i = parseInt(i.toString(), 10);
|
||||
if (i > this.frames.length - 1 || i < 0) {
|
||||
i = 0;
|
||||
}
|
||||
|
||||
const frame = this.frames[i];
|
||||
const dims = frame.dims;
|
||||
this.manageDisposal(i);
|
||||
|
||||
const frame = this.frames[i];
|
||||
const patchContext = this.patchCanvas.getContext('2d');
|
||||
const offscreenContext = this.offscreenCanvas.getContext('2d');
|
||||
const dims = frame.dims;
|
||||
if (
|
||||
!this.frameImageData ||
|
||||
dims.width != this.frameImageData.width ||
|
||||
dims.height != this.frameImageData.height
|
||||
) {
|
||||
this.offscreenCanvas.width = dims.width;
|
||||
this.offscreenCanvas.height = dims.height;
|
||||
this.frameImageData = this.offscreenCanvas.getContext('2d').createImageData(dims.width, dims.height);
|
||||
this.patchCanvas.width = dims.width;
|
||||
this.patchCanvas.height = dims.height;
|
||||
this.frameImageData = patchContext.createImageData(dims.width, dims.height);
|
||||
}
|
||||
|
||||
// set the patch data as an override
|
||||
this.frameImageData.data.set(frame.patch);
|
||||
// draw the patch back over the canvas
|
||||
patchContext.putImageData(this.frameImageData, 0, 0);
|
||||
|
||||
this.offscreenCanvas.getContext('2d').putImageData(this.frameImageData, 0, 0);
|
||||
this.ctx.globalCompositeOperation = 'copy';
|
||||
this.ctx.drawImage(this.offscreenCanvas, dims.left, dims.top);
|
||||
offscreenContext.drawImage(this.patchCanvas, dims.left, dims.top);
|
||||
|
||||
const imageData = offscreenContext.getImageData(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height);
|
||||
this.ctx.putImageData(imageData, 0, 0);
|
||||
this.ctx.drawImage(this.canvas, 0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
if (show_pause_on_hover && this.hovering) {
|
||||
this.drawPauseIcon();
|
||||
}
|
||||
|
||||
this.last_frame = frame;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -278,20 +331,16 @@ export default class ConverseGif {
|
|||
}
|
||||
|
||||
drawPauseIcon () {
|
||||
if (!this.playing) {
|
||||
return;
|
||||
}
|
||||
// Clear the potential play button by re-rendering the current frame
|
||||
this.putFrame(this.frame_idx, false);
|
||||
if (!this.playing) return;
|
||||
|
||||
this.ctx.globalCompositeOperation = 'source-over';
|
||||
// Clear the potential play button by re-rendering the current frame
|
||||
this.renderImage(false);
|
||||
|
||||
// Draw dark overlay
|
||||
this.ctx.fillStyle = 'rgb(0, 0, 0, 0.25)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
const icon_size = this.canvas.height * 0.1;
|
||||
|
||||
// Draw bars
|
||||
this.ctx.lineWidth = this.canvas.height * 0.04;
|
||||
this.ctx.beginPath();
|
||||
|
@ -318,10 +367,7 @@ export default class ConverseGif {
|
|||
if (this.playing) return;
|
||||
|
||||
// Clear the potential pause button by re-rendering the current frame
|
||||
this.putFrame(this.frame_idx, false);
|
||||
|
||||
this.ctx.globalCompositeOperation = 'source-over';
|
||||
|
||||
this.renderImage(false);
|
||||
// Draw dark overlay
|
||||
this.ctx.fillStyle = 'rgb(0, 0, 0, 0.25)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
@ -375,7 +421,11 @@ export default class ConverseGif {
|
|||
promise.resolve(h.response);
|
||||
};
|
||||
h.onprogress = (e) => e.lengthComputable && this.doShowProgress(e.loaded, e.total, true);
|
||||
h.onerror = () => this.showError();
|
||||
h.onerror = (e) => {
|
||||
log.error(e);
|
||||
this.showError();
|
||||
};
|
||||
|
||||
h.send();
|
||||
return promise;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue