diff --git a/src/board/Board.ts b/src/board/Board.ts index 1f9957e..59df929 100644 --- a/src/board/Board.ts +++ b/src/board/Board.ts @@ -265,6 +265,10 @@ class Board { return this.canvas.toDataURL(); } + toImageData() { + return this.ctx.getImageData(0, 0, this.width, this.height).data; + } + toImgElement() { const dataUrl = this.toImgUrl(); diff --git a/src/encoders/GIF.ts b/src/encoders/GIF.ts index 906c457..8e4e3fe 100644 --- a/src/encoders/GIF.ts +++ b/src/encoders/GIF.ts @@ -2,8 +2,14 @@ import GIFLib from "gif.js"; class GIF { private gif: GIFLib; + private frameTime: number; - constructor(width: number, height: number, loop: boolean) { + constructor( + width: number, + height: number, + loop: boolean, + frameTime: number = 1000 + ) { this.gif = new GIFLib({ workers: 2, quality: 10, @@ -11,6 +17,7 @@ class GIF { height, repeat: loop ? 0 : -1, }); + this.frameTime = frameTime; } add( @@ -19,9 +26,10 @@ class GIF { | CanvasRenderingContext2D | WebGLRenderingContext | ImageData, - delay: number + + frames: number ) { - this.gif.addFrame(frame, { delay }); + this.gif.addFrame(frame, { delay: frames * this.frameTime }); } render(): Promise { diff --git a/src/encoders/MP4.ts b/src/encoders/MP4.ts index 8318b06..81d698b 100644 --- a/src/encoders/MP4.ts +++ b/src/encoders/MP4.ts @@ -3,35 +3,54 @@ import * as HME from "h264-mp4-encoder"; class MP4 { private hme: Promise; private encoder: HME.H264MP4Encoder | null = null; + private frameTime: number; - constructor(width: number, height: number) { + constructor( + private width: number, + private height: number, + frameTime: number = 1000 + ) { this.hme = HME.createH264MP4Encoder(); - this.setup(width, height); + this.frameTime = frameTime; } async setup(width: number, height: number) { this.encoder = await this.hme; this.encoder.width = width; this.encoder.height = height; + this.encoder.frameRate = 1000 / this.frameTime; + this.encoder.quantizationParameter = 10; this.encoder.initialize(); } - // add(frame: CanvasImageSource | string, delay: number) { - // // this.encoder?.addFrameRgba() - // } + async add(data: Uint8ClampedArray, frames: number) { + if (this.encoder === null) { + await this.setup(this.width, this.height); + } - // async render(): Promise { - // const blob = await this.video.complete(); + while (frames--) { + this.encoder?.addFrameRgba(data); + } + } - // const timestamp = Date.now(); + async render(): Promise { + this.encoder?.finalize(); - // const file = new File([blob], `board_${timestamp}.webm`, { - // type: "video/webm", - // lastModified: timestamp, - // }); + const uint8Array = this.encoder?.FS.readFile( + this.encoder.outputFilename + ) as Uint8Array; - // return file; - // } + const timestamp = Date.now(); + + const file = new File([uint8Array], `board_${timestamp}.mp4`, { + type: "video/mp4", + lastModified: timestamp, + }); + + this.encoder?.delete(); + + return file; + } } export default MP4; diff --git a/src/encoders/WebM.ts b/src/encoders/WebM.ts index b8a9d34..5176aaf 100644 --- a/src/encoders/WebM.ts +++ b/src/encoders/WebM.ts @@ -4,7 +4,7 @@ import WebMWriter from "webm-writer"; class WebM { private video: WebMWriter; - constructor() { + constructor(private frameTime: number = 1000) { this.video = new WebMWriter({ quality: 0.8, fileWriter: null, @@ -14,8 +14,8 @@ class WebM { }); } - add(frame: CanvasImageSource | string, delay: number) { - this.video.addFrame(frame, delay); + add(frame: CanvasImageSource | string, frames: number) { + this.video.addFrame(frame, frames * this.frameTime); } async render(): Promise { diff --git a/src/encoders/createAnimation.ts b/src/encoders/createAnimation.ts index 6848ea1..559738f 100644 --- a/src/encoders/createAnimation.ts +++ b/src/encoders/createAnimation.ts @@ -3,32 +3,43 @@ import Board from "../board/Board"; import Game from "../game/Game"; import GIF from "./GIF"; import WebM from "./WebM"; -// import MP4 from "./MP4"; +import MP4 from "./MP4"; -const MOVE_TIME = 1000; +const getData = (board: Board, encoder: GIF | WebM | MP4) => { + return encoder instanceof GIF + ? board.toImgElement() + : encoder instanceof MP4 + ? board.toImageData() + : board.canvas; +}; const createAnimation = async ( pgn: string, style: Style, size: number = 720, - format: "GIF" | "WebM" + format: "GIF" | "WebM" | "MP4" ) => { const game = new Game().loadPGN(pgn); - const board = new Board(8).setStyle(style).setSize(size).hideBorder(); - const animation = - format === "GIF" ? new GIF(board.width, board.height, true) : new WebM(); + const board = new Board(8).setStyle(style).setSize(size).showBorder(); + const encoder = + format === "GIF" + ? new GIF(board.width, board.height, true) + : format === "MP4" + ? new MP4(board.width, board.height) + : new WebM(); + const header = game.getHeader(); await board.titleFrame(header); board.render(); - animation.add(format === "GIF" ? board.toImgElement() : board.canvas, 5000); + + // @ts-ignore + await encoder.add(getData(board, encoder), 5); await board.frame(game.getBoardData(), header); board.render(); - animation.add( - format === "GIF" ? board.toImgElement() : board.canvas, - MOVE_TIME - ); + // @ts-ignore + await encoder.add(getData(board, encoder), 1); while (true) { const move = game.next(); @@ -39,13 +50,11 @@ const createAnimation = async ( await board.frame(game.getBoardData(), header, move); board.render(); - animation.add( - format === "GIF" ? board.toImgElement() : board.canvas, - MOVE_TIME - ); + // @ts-ignore + await encoder.add(getData(board, encoder), 1); } - return await animation.render(); + return await encoder.render(); }; export default createAnimation; diff --git a/src/main.ts b/src/main.ts index 05f5208..276a7a6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -46,7 +46,7 @@ const play = async (board: Board, pgn: string | null, interval: number) => { }; const createDownloadLink = async (pgn: string, style: Style) => { - const file = await createAnimation(pgn, style, 720, "WebM"); + const file = await createAnimation(pgn, style, 720, "MP4"); const link = document.createElement("a"); link.innerText = "DOWNLOAD"; link.setAttribute("href", URL.createObjectURL(file)); @@ -57,7 +57,7 @@ const createDownloadLink = async (pgn: string, style: Style) => { console.log(createDownloadLink.name); const main = async () => { - const style = styles.lila; + const style = styles.calm; // window.location.hash = // "#QiBEdWtlIEthcmwgLyBDb3VudCBJc291YXJkCkQgMTg1OC4/Py4/PwpFIFBhcmlzClIgMS0wClMgUGFyaXMgRlJBClcgUGF1bCBNb3JwaHkKCmU0IGU1IE5mMyBkNiBkNCBCZzQgZHhlNSBCeGYzIFF4ZjMgZHhlNSBCYzQgTmY2IFFiMyBRZTcgTmMzIGM2IEJnNSBiNSBOeGI1IGN4YjUgQnhiNSsgTmJkNyBPLU8tTyBSZDggUnhkNyBSeGQ3IFJkMSBRZTYgQnhkNysgTnhkNyBRYjgrIE54YjggUmQ4Iw=="; @@ -65,17 +65,16 @@ const main = async () => { // const hash = window.location.hash; // const pgn = hash === "" ? null : decompressPGN(hash.slice(1)); const pgn = pgns[pgns.length - 12]; - const board = new Board(8).setStyle(style).setSize(720).hideBorder(); + const board = new Board(8).setStyle(style).setSize(720).showBorder(); $app?.appendChild(board.canvas); const interval = 1000; play(board, pgn, interval); - // createDownloadLink(pgns[2], style).then((link) => { - // document.body.appendChild(link); - // console.log("Animation created!"); - // }); + createDownloadLink(pgns[2], style).then((link) => { + document.body.appendChild(link); + }); }; WebFont.load({