Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/GIF #43 #44

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/2d/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@hex-engine/core": "0.5.2",
"@hex-engine/inspector": "0.5.2",
"@types/matter-js": "^0.10.7",
"gifken": "^2.1.1",
"layout-bmfont-text": "^1.3.4",
"matter-js": "^0.14.2",
"mem": "^6.0.1",
Expand Down
155 changes: 155 additions & 0 deletions packages/2d/src/Components/GIF.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { AnimationAPI, AnimationFrame } from './Animation';
import { useType } from '@hex-engine/core';
import gifken, { Gif } from 'gifken';
import Preloader from '../Preloader';

interface GIFInterface extends AnimationAPI<HTMLImageElement> {
getGif(): Gif,
drawCurrentFrame(context: CanvasRenderingContext2D, x?: number, y?: number): void;
}

/**
* A component which enables you to play and manipulate gifs in Hex Engine.
* @example
* import someGifFile from "./your.gif";
*
* export default function MyGif() {
* useType(MyGif);
*
* const gif = useNewComponent(() => GIF({
* url: someGifFile,
* width: 200,
* height: 200,
* fps: 20,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible for GIFs to have frames with different lengths; one frame can be long and the other can be short. Does gifken support this?

If you change the code to use the Animation component, you can specify the duration of each frame, and it will handle advancing to the next frame at the right time for you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have find in the gifken documentation a property called delayCentiSeconds
I don't know if is exaclty what we want but i can test to set this property as duration when i will update the GIF component with the Animation component.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, sounds good! You can use https://ezgif.com/maker to make a GIF that has frames with differing durations.

* loop: true
* }));
*
* gif.play()
*
* useDraw((context) => {
* gif.drawCurrentFrame(context);
* });
* }
*/
export default function GIF(options: {
url: string,
width: number,
height: number,
fps?: number,
loop?: boolean
}): GIFInterface {
useType(GIF);
let gif: Gif = new Gif();
let frames: AnimationFrame<HTMLImageElement>[] = [];
let play: boolean = false;
let currentFrameIndex: number = 0;

const loadPromise = load(options.url).then(async arrayBuffer => {
gif = gifken.Gif.parse(arrayBuffer);
frames = await getFrames({
gif,
width: options.width,
height: options.height,
})

setInterval(() => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setInterval will keep playing forwards even if the game is paused from the developer inspector or the component is disabled. Could you instead use the Animation Component, which will only run when the game's frames advance?

if (frames.length - 1 > currentFrameIndex && play) {
currentFrameIndex ++;
}

if(frames.length - 1 <= currentFrameIndex && options.loop && play) {
currentFrameIndex = 0;
}
}, 1000 / (options.fps || 25))
})

Preloader.addTask(() => loadPromise);

return {
getGif() {
return gif;
},
drawCurrentFrame(context: CanvasRenderingContext2D, x: number = 0, y: number = 0) {
if(frames.length !== 0) {
context.drawImage(frames[currentFrameIndex].data, x, y);
}
},
frames: frames,
loop: options.loop || false,
get currentFrameIndex() {
return currentFrameIndex;
},
get currentFrame() {
return frames[currentFrameIndex];
},
get currentFrameCompletion() {
if(frames.length !== 0) {
return currentFrameIndex / frames.length;
}

return 1;
},
pause() {
play = false;
},
resume() {
this.play();
},
play() {
play = true;
},
restart() {
currentFrameIndex = 0;
play = true;
},
goToFrame(frameNumber: number) {
currentFrameIndex = frameNumber;
},
}
}

/**
* Load a gif.
* @param url - gif url
*/
async function load(url: string) {
return await fetch(url)
.then((res) => res.arrayBuffer());
}

/**
* Get all frames of a gif.
* For the moment, it must use a tmp canvas to get each frames due to a bug of gitken.
* See this issues: https://github.com/aaharu/gifken/issues/22
*/
async function getFrames({ gif, width, height }: {
gif: Gif,
width?: number,
height?: number,
}): Promise<AnimationFrame<HTMLImageElement>[]> {
const canvas: HTMLCanvasElement = document.createElement("canvas");
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D ;
canvas.width = width || gif.width;
canvas.height = height || gif.height;
ctx.clearRect(0, 0, gif.width, gif.height);

// get all frames as img
const tmpImgs: HTMLImageElement[] = await Promise.all(gif.split(false).map((splited) => {
return new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image(width || gif.width, height || gif.height);
img.onload = () => resolve(img);
img.onerror = (e) => reject(e);
img.src = gifken.GifPresenter.writeToDataUrl(splited.writeToArrayBuffer());
})
}));

// draw frame to the tmp canvas and create the final frames.
return tmpImgs.map((img: HTMLImageElement) => {
ctx.drawImage(img, 0, 0);
const newImg = new Image();
newImg.src = canvas.toDataURL("image/gif");
return new AnimationFrame<HTMLImageElement>(newImg, {
duration: 0,
});
})
}
2 changes: 2 additions & 0 deletions packages/2d/src/Components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Font from "./Font";
import FontMetrics from "./FontMetrics";
import Gamepad from "./Gamepad";
import Geometry from "./Geometry";
import GIF from "./GIF";
import Image from "./Image";
import ImageFilter from "./ImageFilter";
import Keyboard from "./Keyboard";
Expand Down Expand Up @@ -42,6 +43,7 @@ export {
FontMetrics,
Gamepad,
Geometry,
GIF,
Image,
ImageFilter,
Keyboard,
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4180,6 +4180,11 @@ brorand@^1.0.1:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=

browser-or-node@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/browser-or-node/-/browser-or-node-1.2.1.tgz#cd65172da6a7fd689c7a650d326bd2ad145419a7"
integrity sha512-sVIA0cysIED0nbmNOm7sZzKfgN1rpFmrqvLZaFWspaBAftfQcezlC81G6j6U2RJf4Lh66zFxrCeOsvkUXIcPWg==

browser-resolve@^1.11.3:
version "1.11.3"
resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
Expand Down Expand Up @@ -6713,6 +6718,13 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"

gifken@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/gifken/-/gifken-2.1.1.tgz#24ce88dbcb43f557bb17ae1e2d91d5633b71041b"
integrity sha512-q8uBypvYwFfoJhNm+4xjKkj5AZiF8PtXW2VsddS3YpCdXtLa6nJRVQ6Aj0R51iWv4dl1Y3qqe1t+wz9viNhfVQ==
dependencies:
browser-or-node "^1.2.1"

github-slugger@^1.0.0, github-slugger@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.2.1.tgz#47e904e70bf2dccd0014748142d31126cfd49508"
Expand Down