diff --git a/packages/2d/package.json b/packages/2d/package.json index 01f483f0..03d85376 100644 --- a/packages/2d/package.json +++ b/packages/2d/package.json @@ -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", diff --git a/packages/2d/src/Components/GIF.ts b/packages/2d/src/Components/GIF.ts new file mode 100644 index 00000000..050170b6 --- /dev/null +++ b/packages/2d/src/Components/GIF.ts @@ -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 { + 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, + * 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[] = []; + 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(() => { + 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[]> { + 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((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(newImg, { + duration: 0, + }); + }) +} \ No newline at end of file diff --git a/packages/2d/src/Components/index.ts b/packages/2d/src/Components/index.ts index cb0c919d..e949a828 100644 --- a/packages/2d/src/Components/index.ts +++ b/packages/2d/src/Components/index.ts @@ -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"; @@ -42,6 +43,7 @@ export { FontMetrics, Gamepad, Geometry, + GIF, Image, ImageFilter, Keyboard, diff --git a/yarn.lock b/yarn.lock index e592e032..01c87177 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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"