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 { getOpenPromise } from '@converse/openpromise';
|
||||||
import { parseGIF, decompressFrames } from 'gifuct-js';
|
import { parseGIF, decompressFrames } from 'gifuct-js';
|
||||||
|
|
||||||
|
@ -36,8 +37,11 @@ export default class ConverseGif {
|
||||||
this.gif_el = el.querySelector('img');
|
this.gif_el = el.querySelector('img');
|
||||||
this.canvas = el.querySelector('canvas');
|
this.canvas = el.querySelector('canvas');
|
||||||
this.ctx = this.canvas.getContext('2d');
|
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');
|
this.offscreenCanvas = document.createElement('canvas');
|
||||||
|
// Offscreen canvas for patches
|
||||||
|
this.patchCanvas = document.createElement('canvas');
|
||||||
|
|
||||||
this.ctx_scaled = false;
|
this.ctx_scaled = false;
|
||||||
this.frames = [];
|
this.frames = [];
|
||||||
|
@ -49,6 +53,7 @@ export default class ConverseGif {
|
||||||
this.start = null;
|
this.start = null;
|
||||||
this.hovering = null;
|
this.hovering = null;
|
||||||
this.frameImageData = null;
|
this.frameImageData = null;
|
||||||
|
this.disposal_restore_from_idx = null;
|
||||||
|
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
@ -70,10 +75,10 @@ export default class ConverseGif {
|
||||||
|
|
||||||
// Show the first frame
|
// Show the first frame
|
||||||
this.frame_idx = 0;
|
this.frame_idx = 0;
|
||||||
this.putFrame(this.frame_idx);
|
this.renderImage();
|
||||||
|
|
||||||
if (this.options.autoplay) {
|
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);
|
setTimeout(() => this.play(), delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,14 +115,14 @@ export default class ConverseGif {
|
||||||
* `frame_delay` parameters can also be passed in. The `timestamp`
|
* `frame_delay` parameters can also be passed in. The `timestamp`
|
||||||
* parameter comes from `requestAnimationFrame`.
|
* 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.
|
* in order to render the GIF animation.
|
||||||
*
|
*
|
||||||
* Note, this method will cause the *next* upcoming frame to be rendered,
|
* Note, this method will cause the *next* upcoming frame to be rendered,
|
||||||
* not the current one.
|
* not the current one.
|
||||||
*
|
*
|
||||||
* This means `this.frame_idx` will be incremented before calling `this.putFrame`, so
|
* This means `this.frame_idx` will be incremented before calling `this.renderImage`, so
|
||||||
* `putFrame(0)` needs to be called *before* this method, otherwise the
|
* `renderImage(0)` needs to be called *before* this method, otherwise the
|
||||||
* animation will incorrectly start from frame #1 (this is done in `initPlayer`).
|
* animation will incorrectly start from frame #1 (this is done in `initPlayer`).
|
||||||
*
|
*
|
||||||
* @param { DOMHighResTimeStamp } timestamp - The timestamp as returned by `requestAnimationFrame`
|
* @param { DOMHighResTimeStamp } timestamp - The timestamp as returned by `requestAnimationFrame`
|
||||||
|
@ -132,7 +137,7 @@ export default class ConverseGif {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (timestamp - previous_timestamp < frame_delay) {
|
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
|
// We need to wait longer
|
||||||
requestAnimationFrame((ts) => this.onAnimationFrame(ts, previous_timestamp, frame_delay));
|
requestAnimationFrame((ts) => this.onAnimationFrame(ts, previous_timestamp, frame_delay));
|
||||||
return;
|
return;
|
||||||
|
@ -142,8 +147,8 @@ export default class ConverseGif {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.frame_idx = next_frame;
|
this.frame_idx = next_frame;
|
||||||
this.putFrame(this.frame_idx);
|
this.renderImage();
|
||||||
const delay = (this.frames[this.frame_idx]?.delay || 8);
|
const delay = this.frames[this.frame_idx]?.delay || 8;
|
||||||
requestAnimationFrame((ts) => this.onAnimationFrame(ts, timestamp, delay));
|
requestAnimationFrame((ts) => this.onAnimationFrame(ts, timestamp, delay));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,21 +200,13 @@ export default class ConverseGif {
|
||||||
|
|
||||||
drawError () {
|
drawError () {
|
||||||
this.ctx.fillStyle = 'black';
|
this.ctx.fillStyle = 'black';
|
||||||
this.ctx.fillRect(
|
this.ctx.fillRect(0, 0, this.options.width, this.options.height);
|
||||||
0,
|
|
||||||
0,
|
|
||||||
this.options.width ? this.options.width : this.lsd.width,
|
|
||||||
this.options.height ? this.options.height : this.lsd.height
|
|
||||||
);
|
|
||||||
this.ctx.strokeStyle = 'red';
|
this.ctx.strokeStyle = 'red';
|
||||||
this.ctx.lineWidth = 3;
|
this.ctx.lineWidth = 3;
|
||||||
this.ctx.moveTo(0, 0);
|
this.ctx.moveTo(0, 0);
|
||||||
this.ctx.lineTo(
|
this.ctx.lineTo(this.options.width, this.options.height);
|
||||||
this.options.width ? this.options.width : this.lsd.width,
|
this.ctx.moveTo(0, this.options.height);
|
||||||
this.options.height ? this.options.height : this.lsd.height
|
this.ctx.lineTo(this.options.width, 0);
|
||||||
);
|
|
||||||
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.stroke();
|
this.ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,41 +221,97 @@ export default class ConverseGif {
|
||||||
this.el.requestUpdate();
|
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.
|
* 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;
|
if (!this.frames.length) return;
|
||||||
|
|
||||||
|
let i = this.frame_idx;
|
||||||
i = parseInt(i.toString(), 10);
|
i = parseInt(i.toString(), 10);
|
||||||
if (i > this.frames.length - 1 || i < 0) {
|
if (i > this.frames.length - 1 || i < 0) {
|
||||||
i = 0;
|
i = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const frame = this.frames[i];
|
this.manageDisposal(i);
|
||||||
const dims = frame.dims;
|
|
||||||
|
|
||||||
|
const frame = this.frames[i];
|
||||||
|
const patchContext = this.patchCanvas.getContext('2d');
|
||||||
|
const offscreenContext = this.offscreenCanvas.getContext('2d');
|
||||||
|
const dims = frame.dims;
|
||||||
if (
|
if (
|
||||||
!this.frameImageData ||
|
!this.frameImageData ||
|
||||||
dims.width != this.frameImageData.width ||
|
dims.width != this.frameImageData.width ||
|
||||||
dims.height != this.frameImageData.height
|
dims.height != this.frameImageData.height
|
||||||
) {
|
) {
|
||||||
this.offscreenCanvas.width = dims.width;
|
this.patchCanvas.width = dims.width;
|
||||||
this.offscreenCanvas.height = dims.height;
|
this.patchCanvas.height = dims.height;
|
||||||
this.frameImageData = this.offscreenCanvas.getContext('2d').createImageData(dims.width, dims.height);
|
this.frameImageData = patchContext.createImageData(dims.width, dims.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the patch data as an override
|
// set the patch data as an override
|
||||||
this.frameImageData.data.set(frame.patch);
|
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);
|
offscreenContext.drawImage(this.patchCanvas, dims.left, dims.top);
|
||||||
this.ctx.globalCompositeOperation = 'copy';
|
|
||||||
this.ctx.drawImage(this.offscreenCanvas, 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) {
|
if (show_pause_on_hover && this.hovering) {
|
||||||
this.drawPauseIcon();
|
this.drawPauseIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.last_frame = frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -278,20 +331,16 @@ export default class ConverseGif {
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPauseIcon () {
|
drawPauseIcon () {
|
||||||
if (!this.playing) {
|
if (!this.playing) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Clear the potential play button by re-rendering the current frame
|
|
||||||
this.putFrame(this.frame_idx, false);
|
|
||||||
|
|
||||||
this.ctx.globalCompositeOperation = 'source-over';
|
// Clear the potential play button by re-rendering the current frame
|
||||||
|
this.renderImage(false);
|
||||||
|
|
||||||
// Draw dark overlay
|
// Draw dark overlay
|
||||||
this.ctx.fillStyle = 'rgb(0, 0, 0, 0.25)';
|
this.ctx.fillStyle = 'rgb(0, 0, 0, 0.25)';
|
||||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
const icon_size = this.canvas.height * 0.1;
|
const icon_size = this.canvas.height * 0.1;
|
||||||
|
|
||||||
// Draw bars
|
// Draw bars
|
||||||
this.ctx.lineWidth = this.canvas.height * 0.04;
|
this.ctx.lineWidth = this.canvas.height * 0.04;
|
||||||
this.ctx.beginPath();
|
this.ctx.beginPath();
|
||||||
|
@ -318,10 +367,7 @@ export default class ConverseGif {
|
||||||
if (this.playing) return;
|
if (this.playing) return;
|
||||||
|
|
||||||
// Clear the potential pause button by re-rendering the current frame
|
// Clear the potential pause button by re-rendering the current frame
|
||||||
this.putFrame(this.frame_idx, false);
|
this.renderImage(false);
|
||||||
|
|
||||||
this.ctx.globalCompositeOperation = 'source-over';
|
|
||||||
|
|
||||||
// Draw dark overlay
|
// Draw dark overlay
|
||||||
this.ctx.fillStyle = 'rgb(0, 0, 0, 0.25)';
|
this.ctx.fillStyle = 'rgb(0, 0, 0, 0.25)';
|
||||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
@ -375,7 +421,11 @@ export default class ConverseGif {
|
||||||
promise.resolve(h.response);
|
promise.resolve(h.response);
|
||||||
};
|
};
|
||||||
h.onprogress = (e) => e.lengthComputable && this.doShowProgress(e.loaded, e.total, true);
|
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();
|
h.send();
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user