Skip to content

Commit

Permalink
fix: fixed gif images
Browse files Browse the repository at this point in the history
  • Loading branch information
matteobruni committed Jan 26, 2024
1 parent 4595c4c commit 24d92fa
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 130 deletions.
4 changes: 3 additions & 1 deletion shapes/image/src/GifUtils/ByteStream.ts
Expand Up @@ -75,7 +75,7 @@ export class ByteStream {
* @returns the sub blocks as binary
*/
readSubBlocksBin(): Uint8Array {
let size = 0,
let size = this.data[this.pos],
len = 0;

const emptySize = 0,
Expand All @@ -87,6 +87,8 @@ export class ByteStream {

const blockData = new Uint8Array(len);

size = this.data[this.pos++];

for (let i = 0; size !== emptySize; size = this.data[this.pos++]) {
for (let count = size; --count >= emptySize; blockData[i++] = this.data[this.pos++]) {
// do nothing
Expand Down
132 changes: 5 additions & 127 deletions shapes/image/src/ImageDrawer.ts
@@ -1,25 +1,9 @@
import {
type Container,
type ICoordinates,
type IShapeDrawData,
type IShapeDrawer,
errorPrefix,
} from "@tsparticles/engine";
import { type IImage, type IParticleImage, type ImageParticle, replaceImageColor } from "./Utils.js";
import { type Container, type IShapeDrawData, type IShapeDrawer, errorPrefix } from "@tsparticles/engine";
import { type IImage, type IParticleImage, type ImageParticle, drawGif, replaceImageColor } from "./Utils.js";
import type { ImageContainer, ImageEngine } from "./types.js";
import { DisposalMethod } from "./GifUtils/Enums/DisposalMethod.js";
import type { IImageShape } from "./IImageShape.js";

const origin: ICoordinates = {
x: 0,
y: 0,
},
defaultLoopCount = 0,
defaultFrame = 0,
half = 0.5,
initialTime = 0,
firstIndex = 0,
double = 2,
const double = 2,
defaultAlpha = 1,
sides = 12,
defaultRatio = 1;
Expand Down Expand Up @@ -55,7 +39,7 @@ export class ImageDrawer implements IShapeDrawer<ImageParticle> {
* @param data - the shape draw data
*/
draw(data: IShapeDrawData<ImageParticle>): void {
const { context, radius, particle, opacity, delta } = data,
const { context, radius, particle, opacity } = data,
image = particle.image,
element = image?.element;

Expand All @@ -66,113 +50,7 @@ export class ImageDrawer implements IShapeDrawer<ImageParticle> {
context.globalAlpha = opacity;

if (image.gif && image.gifData) {
const offscreenCanvas = new OffscreenCanvas(image.gifData.width, image.gifData.height),
offscreenContext = offscreenCanvas.getContext("2d");

if (!offscreenContext) {
throw new Error("could not create offscreen canvas context");
}

offscreenContext.imageSmoothingQuality = "low";
offscreenContext.imageSmoothingEnabled = false;

offscreenContext.clearRect(origin.x, origin.y, offscreenCanvas.width, offscreenCanvas.height);

if (particle.gifLoopCount === undefined) {
particle.gifLoopCount = image.gifLoopCount ?? defaultLoopCount;
}

let frameIndex = particle.gifFrame ?? defaultFrame;

const pos = { x: -image.gifData.width * half, y: -image.gifData.height * half },
frame = image.gifData.frames[frameIndex];

if (particle.gifTime === undefined) {
particle.gifTime = initialTime;
}

if (!frame.bitmap) {
return;
}

context.scale(radius / image.gifData.width, radius / image.gifData.height);

switch (frame.disposalMethod) {
case DisposalMethod.UndefinedA: // ! fall through
case DisposalMethod.UndefinedB: // ! fall through
case DisposalMethod.UndefinedC: // ! fall through
case DisposalMethod.UndefinedD: // ! fall through
case DisposalMethod.Replace:
offscreenContext.drawImage(frame.bitmap, frame.left, frame.top);

context.drawImage(offscreenCanvas, pos.x, pos.y);

offscreenContext.clearRect(origin.x, origin.y, offscreenCanvas.width, offscreenCanvas.height);

break;
case DisposalMethod.Combine:
offscreenContext.drawImage(frame.bitmap, frame.left, frame.top);

context.drawImage(offscreenCanvas, pos.x, pos.y);

break;
case DisposalMethod.RestoreBackground:
offscreenContext.drawImage(frame.bitmap, frame.left, frame.top);

context.drawImage(offscreenCanvas, pos.x, pos.y);

offscreenContext.clearRect(origin.x, origin.y, offscreenCanvas.width, offscreenCanvas.height);

if (!image.gifData.globalColorTable.length) {
offscreenContext.putImageData(
image.gifData.frames[firstIndex].image,
pos.x + frame.left,
pos.y + frame.top,
);
} else {
offscreenContext.putImageData(image.gifData.backgroundImage, pos.x, pos.y);
}

break;
case DisposalMethod.RestorePrevious:
{
const previousImageData = offscreenContext.getImageData(
origin.x,
origin.y,
offscreenCanvas.width,
offscreenCanvas.height,
);

offscreenContext.drawImage(frame.bitmap, frame.left, frame.top);

context.drawImage(offscreenCanvas, pos.x, pos.y);

offscreenContext.clearRect(origin.x, origin.y, offscreenCanvas.width, offscreenCanvas.height);
offscreenContext.putImageData(previousImageData, origin.x, origin.y);
}
break;
}

particle.gifTime += delta.value;

if (particle.gifTime > frame.delayTime) {
particle.gifTime -= frame.delayTime;

if (++frameIndex >= image.gifData.frames.length) {
if (--particle.gifLoopCount <= defaultLoopCount) {
return;
}

frameIndex = firstIndex;

// ? so apparently some GIFs seam to set the disposal method of the last frame wrong?...so this is a "fix" for that (clear after the last frame)
offscreenContext.clearRect(origin.x, origin.y, offscreenCanvas.width, offscreenCanvas.height);
}

particle.gifFrame = frameIndex;
}

context.scale(image.gifData.width / radius, image.gifData.height / radius);
drawGif(data);
} else if (element) {
const ratio = image.ratio,
pos = {
Expand Down
142 changes: 140 additions & 2 deletions shapes/image/src/Utils.ts
@@ -1,11 +1,28 @@
import { type IHsl, type Particle, errorPrefix, getLogger, getStyleFromHsl } from "@tsparticles/engine";
import {
type ICoordinates,
type IHsl,
type IShapeDrawData,
type Particle,
errorPrefix,
getLogger,
getStyleFromHsl,
} from "@tsparticles/engine";
import { decodeGIF, getGIFLoopAmount } from "./GifUtils/Utils.js";
import { DisposalMethod } from "./GifUtils/Enums/DisposalMethod.js";
import type { GIF } from "./GifUtils/Types/GIF.js";
import type { IImageShape } from "./IImageShape.js";

const stringStart = 0,
defaultLoopCount = 0,
defaultOpacity = 1;
defaultOpacity = 1,
origin: ICoordinates = {
x: 0,
y: 0,
},
defaultFrame = 0,
half = 0.5,
initialTime = 0,
firstIndex = 0;

/**
* The image interface, used for keeping useful data for drawing
Expand Down Expand Up @@ -236,3 +253,124 @@ export function replaceImageColor(
img.src = url;
});
}

/**
*
* @param data -
*/
export function drawGif(data: IShapeDrawData<ImageParticle>): void {
const { context, radius, particle, delta } = data,
image = particle.image;

if (!image?.gifData || !image.gif) {
return;
}

const offscreenCanvas = new OffscreenCanvas(image.gifData.width, image.gifData.height),
offscreenContext = offscreenCanvas.getContext("2d");

if (!offscreenContext) {
throw new Error("could not create offscreen canvas context");
}

offscreenContext.imageSmoothingQuality = "low";
offscreenContext.imageSmoothingEnabled = false;

offscreenContext.clearRect(origin.x, origin.y, offscreenCanvas.width, offscreenCanvas.height);

if (particle.gifLoopCount === undefined) {
particle.gifLoopCount = image.gifLoopCount ?? defaultLoopCount;
}

let frameIndex = particle.gifFrame ?? defaultFrame;

const pos = { x: -image.gifData.width * half, y: -image.gifData.height * half },
frame = image.gifData.frames[frameIndex];

if (particle.gifTime === undefined) {
particle.gifTime = initialTime;
}

if (!frame.bitmap) {
return;
}

context.scale(radius / image.gifData.width, radius / image.gifData.height);

switch (frame.disposalMethod) {
case DisposalMethod.UndefinedA: // ! fall through
case DisposalMethod.UndefinedB: // ! fall through
case DisposalMethod.UndefinedC: // ! fall through
case DisposalMethod.UndefinedD: // ! fall through
case DisposalMethod.Replace:
offscreenContext.drawImage(frame.bitmap, frame.left, frame.top);

context.drawImage(offscreenCanvas, pos.x, pos.y);

offscreenContext.clearRect(origin.x, origin.y, offscreenCanvas.width, offscreenCanvas.height);

break;
case DisposalMethod.Combine:
offscreenContext.drawImage(frame.bitmap, frame.left, frame.top);

context.drawImage(offscreenCanvas, pos.x, pos.y);

break;
case DisposalMethod.RestoreBackground:
offscreenContext.drawImage(frame.bitmap, frame.left, frame.top);

context.drawImage(offscreenCanvas, pos.x, pos.y);

offscreenContext.clearRect(origin.x, origin.y, offscreenCanvas.width, offscreenCanvas.height);

if (!image.gifData.globalColorTable.length) {
offscreenContext.putImageData(
image.gifData.frames[firstIndex].image,
pos.x + frame.left,
pos.y + frame.top,
);
} else {
offscreenContext.putImageData(image.gifData.backgroundImage, pos.x, pos.y);
}

break;
case DisposalMethod.RestorePrevious:
{
const previousImageData = offscreenContext.getImageData(
origin.x,
origin.y,
offscreenCanvas.width,
offscreenCanvas.height,
);

offscreenContext.drawImage(frame.bitmap, frame.left, frame.top);

context.drawImage(offscreenCanvas, pos.x, pos.y);

offscreenContext.clearRect(origin.x, origin.y, offscreenCanvas.width, offscreenCanvas.height);
offscreenContext.putImageData(previousImageData, origin.x, origin.y);
}
break;
}

particle.gifTime += delta.value;

if (particle.gifTime > frame.delayTime) {
particle.gifTime -= frame.delayTime;

if (++frameIndex >= image.gifData.frames.length) {
if (--particle.gifLoopCount <= defaultLoopCount) {
return;
}

frameIndex = firstIndex;

// ? so apparently some GIFs seam to set the disposal method of the last frame wrong?...so this is a "fix" for that (clear after the last frame)
offscreenContext.clearRect(origin.x, origin.y, offscreenCanvas.width, offscreenCanvas.height);
}

particle.gifFrame = frameIndex;
}

context.scale(image.gifData.width / radius, image.gifData.height / radius);
}

0 comments on commit 24d92fa

Please sign in to comment.