This commit is contained in:
Maciej Caderek
2022-02-12 23:16:01 +01:00
parent 840545886d
commit 6274236ac7
13 changed files with 227 additions and 177 deletions

11
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"h264-mp4-encoder": "^1.0.12", "h264-mp4-encoder": "^1.0.12",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"i": "^0.3.7", "i": "^0.3.7",
"is-mobile": "^3.0.0",
"npm": "^8.4.0", "npm": "^8.4.0",
"webfontloader": "^1.6.28", "webfontloader": "^1.6.28",
"webm-writer": "^1.0.0" "webm-writer": "^1.0.0"
@@ -1078,6 +1079,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-mobile": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-3.0.0.tgz",
"integrity": "sha512-UruBjgykgDJ5zRpJ8Zgh9ZUe4jQaHD8klX9FlkOt0oPyu3FpwJpxHvKg4+lhJOWGxSrMKsRuPFk60xeltvyliQ=="
},
"node_modules/is-what": { "node_modules/is-what": {
"version": "4.1.7", "version": "4.1.7",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.7.tgz", "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.7.tgz",
@@ -4471,6 +4477,11 @@
"has": "^1.0.3" "has": "^1.0.3"
} }
}, },
"is-mobile": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-3.0.0.tgz",
"integrity": "sha512-UruBjgykgDJ5zRpJ8Zgh9ZUe4jQaHD8klX9FlkOt0oPyu3FpwJpxHvKg4+lhJOWGxSrMKsRuPFk60xeltvyliQ=="
},
"is-what": { "is-what": {
"version": "4.1.7", "version": "4.1.7",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.7.tgz", "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.7.tgz",

View File

@@ -26,6 +26,7 @@
"h264-mp4-encoder": "^1.0.12", "h264-mp4-encoder": "^1.0.12",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"i": "^0.3.7", "i": "^0.3.7",
"is-mobile": "^3.0.0",
"npm": "^8.4.0", "npm": "^8.4.0",
"webfontloader": "^1.6.28", "webfontloader": "^1.6.28",
"webm-writer": "^1.0.0" "webm-writer": "^1.0.0"

View File

@@ -35,7 +35,6 @@ class Board {
private style: Style = boards.standard; private style: Style = boards.standard;
private header: { [key: string]: string | undefined } = {}; private header: { [key: string]: string | undefined } = {};
private borderVisible: boolean = true;
private lastPosition: Position | null = null; private lastPosition: Position | null = null;
private background: HTMLCanvasElement | null = null; private background: HTMLCanvasElement | null = null;
private currentScreen: "title" | "move" = "move"; private currentScreen: "title" | "move" = "move";
@@ -100,7 +99,7 @@ class Board {
this.tempCanvas.height = height; this.tempCanvas.height = height;
} }
const tempBorderWidth = this.borderVisible ? this.size / 32 : 0; const tempBorderWidth = this.cfg.showBorder ? this.size / 32 : 0;
const tempInnerSize = this.size - tempBorderWidth * 2; const tempInnerSize = this.size - tempBorderWidth * 2;
this.squareSize = Math.floor(tempInnerSize / this.cfg.tiles); this.squareSize = Math.floor(tempInnerSize / this.cfg.tiles);
this.innerSize = this.squareSize * this.cfg.tiles; this.innerSize = this.squareSize * this.cfg.tiles;
@@ -132,21 +131,21 @@ class Board {
} }
hideBorder() { hideBorder() {
this.borderVisible = false; this.cfg.showBorder = false;
this.setSize(this.size); this.setSize(this.size);
this.refresh(); this.refresh();
return this; return this;
} }
showBorder() { showBorder() {
this.borderVisible = true; this.cfg.showBorder = true;
this.setSize(this.size); this.setSize(this.size);
this.refresh(); this.refresh();
return this; return this;
} }
toggleBorder() { toggleBorder() {
this.borderVisible = !this.borderVisible; this.cfg.showBorder = !this.cfg.showBorder;
this.setSize(this.size); this.setSize(this.size);
this.refresh(); this.refresh();
return this; return this;
@@ -188,8 +187,8 @@ class Board {
ctx, ctx,
this.innerSize, this.innerSize,
this.innerSize, this.innerSize,
this.borderVisible ? this.borderWidth : 0, this.cfg.showBorder ? this.borderWidth : 0,
(this.borderVisible ? this.borderWidth : 0) + this.margin, (this.cfg.showBorder ? this.borderWidth : 0) + this.margin,
background background
); );
@@ -208,7 +207,7 @@ class Board {
} }
} }
if (this.borderVisible && this.cfg.showCoords) { if (this.cfg.showBorder && this.cfg.showCoords) {
drawCoords( drawCoords(
ctx, ctx,
coords, coords,
@@ -217,7 +216,7 @@ class Board {
this.cfg.flipped, this.cfg.flipped,
this.borderWidth, this.borderWidth,
this.size, this.size,
this.borderVisible, this.cfg.showBorder,
this.margin this.margin
); );
} }
@@ -254,7 +253,7 @@ class Board {
); );
} }
if (!this.borderVisible && this.cfg.showCoords) { if (!this.cfg.showBorder && this.cfg.showCoords) {
drawCoords( drawCoords(
this.tempCtx, this.tempCtx,
this.style.coords, this.style.coords,
@@ -263,7 +262,7 @@ class Board {
this.cfg.flipped, this.cfg.flipped,
this.borderWidth, this.borderWidth,
this.size, this.size,
this.borderVisible, this.cfg.showBorder,
this.margin this.margin
); );
} }

View File

@@ -29,10 +29,10 @@ const drawExtraInfo = async (
ctx.fillStyle = style.coords.onBorder; ctx.fillStyle = style.coords.onBorder;
if (data.White) { {
const w = drawText( const w = drawText(
ctx, ctx,
data.White, data.White ?? "White",
"Ubuntu", "Ubuntu",
fontSize, fontSize,
700, 700,
@@ -56,13 +56,10 @@ const drawExtraInfo = async (
); );
} }
if (data.Black) { {
const elo =
data.BlackElo && data.BlackElo !== "?" ? ` ${data.BlackElo}` : "";
const w = drawText( const w = drawText(
ctx, ctx,
data.Black, data.Black ?? "Black",
"Ubuntu", "Ubuntu",
fontSize, fontSize,
700, 700,
@@ -71,6 +68,9 @@ const drawExtraInfo = async (
"left" "left"
); );
const elo =
data.BlackElo && data.BlackElo !== "?" ? ` ${data.BlackElo}` : "";
drawText( drawText(
ctx, ctx,
elo, elo,

View File

@@ -1,17 +1,28 @@
import { BoardConfig } from "../types"; import { BoardConfig, Size } from "../types";
import Board from "../board/Board"; import Board from "../board/Board";
import Game from "../game/Game"; import Game from "../game/Game";
import sizeToPX from "./sizeToPX";
const createImage = async ( const createImage = async (
fen: string, fen: string,
pgn: string | null,
ply: number = 0,
boardConfig: BoardConfig, boardConfig: BoardConfig,
size: number size: Size
) => { ) => {
const game = new Game().loadFEN(fen); console.log({ fen, pgn, ply, size });
const board = new Board({ ...boardConfig, size }); const game = new Game();
const position = game.getPosition(0); if (pgn) {
await board.frame(position, {}); game.loadPGN(pgn);
} else {
game.loadFEN(fen);
}
const board = new Board({ ...boardConfig, size: sizeToPX[size] });
const position = game.getPosition(ply);
await board.frame(position, game.header);
board.render(); board.render();
return board.toImgUrl(); return board.toImgUrl();

9
src/encoders/sizeToPX.ts Normal file
View File

@@ -0,0 +1,9 @@
const sizeToPX = {
XS: 256,
S: 512,
M: 720,
L: 1024,
XL: 1440,
};
export default sizeToPX;

View File

@@ -76,8 +76,8 @@ class Game {
move: null, move: null,
end: 0, end: 0,
fen, fen,
check: false, check: this.game.in_check(),
mate: false, mate: this.game.in_checkmate(),
turn: this.game.turn(), turn: this.game.turn(),
material: this.materialInfo(this.game.board()), material: this.materialInfo(this.game.board()),
placement: this.getPlacement(this.game.fen()), placement: this.getPlacement(this.game.fen()),

View File

@@ -67,7 +67,12 @@ const main = async () => {
const player = new Player(board, gameConfig); const player = new Player(board, gameConfig);
const game = new Game().loadPGN(pgn); const game = new Game().loadPGN(pgn);
setState({ moves: game.getMoves() }); setState({
moves: game.getMoves(),
pgn,
ply: 0,
fen: game.getPosition(0).fen,
});
const handlers = { const handlers = {
prev() { prev() {
@@ -88,19 +93,24 @@ const main = async () => {
}, },
toggleBorder() { toggleBorder() {
board.toggleBorder(); board.toggleBorder();
setState("board", "showBorder", !state.board.showBorder);
}, },
showBorder() { showBorder() {
board.showBorder(); board.showBorder();
setState("board", "showBorder", true);
}, },
hideBorder() { hideBorder() {
board.hideBorder(); board.hideBorder();
setState("board", "showBorder", false);
}, },
toggleExtraInfo() { toggleExtraInfo() {
board.toggleExtraInfo(); board.toggleExtraInfo();
setState("board", "showExtraInfo", !state.board.showExtraInfo);
}, },
flip() { flip() {
board.flip(); board.flip();
setState("board", "flipped", !state.board.flipped);
}, },
togglePlay() { togglePlay() {
player.playing ? player.pause() : player.play(); player.playing ? player.pause() : player.play();
@@ -111,9 +121,11 @@ const main = async () => {
}, },
changeBoardStyle(style: BoardStyle) { changeBoardStyle(style: BoardStyle) {
board.setStyle(style); board.setStyle(style);
setState("board", "boardStyle", style);
}, },
changePiecesStyle(style: PiecesStyle) { changePiecesStyle(style: PiecesStyle) {
board.setPiecesStyle(style); board.setPiecesStyle(style);
setState("board", "piecesStyle", style);
}, },
async loadPGN(pgn: string) { async loadPGN(pgn: string) {
const game = new Game().loadPGN(pgn); const game = new Game().loadPGN(pgn);
@@ -126,13 +138,21 @@ const main = async () => {
await player.load(game); await player.load(game);
}, },
async downloadImage() { async downloadImage() {
const data = await createImage(state.fen, state.board, 1024); const data = await createImage(
state.fen,
state.pgn,
state.ply,
state.board,
state.game.picSize
);
download(data, "fen", "png"); download(data, "fen", "png");
}, },
}; };
// @ts-ignore // @ts-ignore
window.handlers = handlers; window.handlers = handlers;
// @ts-ignore
window.state = state;
/** /**
* RENDER * RENDER

View File

@@ -1,3 +1,4 @@
import isMobile from "is-mobile";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
import { BoardConfig, GameConfig } from "./types"; import { BoardConfig, GameConfig } from "./types";
@@ -32,6 +33,7 @@ export type State = {
fen: string; fen: string;
moves: string[]; moves: string[];
ply: number; ply: number;
mobile: boolean;
}; };
const initialState: State = { const initialState: State = {
@@ -41,6 +43,7 @@ const initialState: State = {
fen: "", fen: "",
moves: [], moves: [],
ply: 0, ply: 0,
mobile: isMobile(),
}; };
const [state, setState] = createStore(initialState); const [state, setState] = createStore(initialState);

View File

@@ -131,14 +131,16 @@ export type BoardConfig = {
flipped: boolean; flipped: boolean;
}; };
export type Size = "XS" | "S" | "M" | "L" | "XL";
export type GameConfig = { export type GameConfig = {
titleScreen: boolean; titleScreen: boolean;
fromPly: number | null; fromPly: number | null;
toPly: number | null; toPly: number | null;
loop: boolean; loop: boolean;
format: "GIF" | "MP4" | "WebM"; format: "GIF" | "MP4" | "WebM";
picSize: "XS" | "S" | "M" | "L" | "XL"; picSize: Size;
animationSize: "XS" | "S" | "M" | "L" | "XL"; animationSize: Size;
}; };
export type MaterialCount = { export type MaterialCount = {

View File

@@ -1,4 +1,4 @@
import { Component, createSignal } from "solid-js"; import { Component, createSignal, Show } from "solid-js";
import { Handlers } from "../../types"; import { Handlers } from "../../types";
import Scrollable from "./reusable/Scrollable"; import Scrollable from "./reusable/Scrollable";
import { state, setState } from "../../state"; import { state, setState } from "../../state";
@@ -83,60 +83,62 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
{copyId() === "fen-link" ? "Copied!" : "Copy link"} {copyId() === "fen-link" ? "Copied!" : "Copy link"}
</button> </button>
</div> </div>
<h3>Image</h3> <Show when={!state.mobile}>
<button <h3>Image</h3>
classList={{ <button
share__size: true, classList={{
"share__size--first": true, share__size: true,
"share__size--active": state.game.animationSize === "XS", "share__size--first": true,
}} "share__size--active": state.game.picSize === "XS",
onClick={() => setState("game", "animationSize", "XS")} }}
> onClick={() => setState("game", "picSize", "XS")}
XS >
</button> XS
<button </button>
classList={{ <button
share__size: true, classList={{
"share__size--active": state.game.animationSize === "S", share__size: true,
}} "share__size--active": state.game.picSize === "S",
onClick={() => setState("game", "animationSize", "S")} }}
> onClick={() => setState("game", "picSize", "S")}
S >
</button> S
<button </button>
classList={{ <button
share__size: true, classList={{
"share__size--active": state.game.animationSize === "M", share__size: true,
}} "share__size--active": state.game.picSize === "M",
onClick={() => setState("game", "animationSize", "M")} }}
> onClick={() => setState("game", "picSize", "M")}
M >
</button> M
<button </button>
classList={{ <button
share__size: true, classList={{
"share__size--active": state.game.animationSize === "L", share__size: true,
}} "share__size--active": state.game.picSize === "L",
onClick={() => setState("game", "animationSize", "L")} }}
> onClick={() => setState("game", "picSize", "L")}
L >
</button> L
<button </button>
classList={{ <button
share__size: true, classList={{
"share__size--last": true, share__size: true,
"share__size--active": state.game.animationSize === "XL", "share__size--last": true,
}} "share__size--active": state.game.picSize === "XL",
onClick={() => setState("game", "animationSize", "XL")} }}
> onClick={() => setState("game", "picSize", "XL")}
XL >
</button> XL
<button </button>
class="share__btn" <button
onClick={() => props.handlers.downloadImage()} class="share__btn"
> onClick={() => props.handlers.downloadImage()}
Save as image >
</button> Save as image
</button>
</Show>
</div> </div>
<div class="share__pgn"> <div class="share__pgn">
<h2>Game</h2> <h2>Game</h2>
@@ -149,88 +151,88 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
<button class="share__btn">Copy markdown</button> <button class="share__btn">Copy markdown</button>
</div> </div>
</div> </div>
<div class="share__animation"> <Show when={!state.mobile}>
<h3>Animation</h3> <div class="share__animation">
<button <h3>Animation</h3>
classList={{ <button
share__size: true, classList={{
"share__size--first": true, share__size: true,
"share__size--active": state.game.picSize === "XS", "share__size--first": true,
}} "share__size--active": state.game.animationSize === "XS",
onClick={() => setState("game", "picSize", "XS")} }}
> onClick={() => setState("game", "animationSize", "XS")}
XS >
</button> XS
<button </button>
classList={{ <button
share__size: true, classList={{
"share__size--active": state.game.picSize === "S", share__size: true,
}} "share__size--active": state.game.animationSize === "S",
onClick={() => setState("game", "picSize", "S")} }}
> onClick={() => setState("game", "animationSize", "S")}
S >
</button> S
<button </button>
classList={{ <button
share__size: true, classList={{
"share__size--active": state.game.picSize === "M", share__size: true,
}} "share__size--active": state.game.animationSize === "M",
onClick={() => setState("game", "picSize", "M")} }}
> onClick={() => setState("game", "animationSize", "M")}
M >
</button> M
<button </button>
classList={{ <button
share__size: true, classList={{
"share__size--active": state.game.picSize === "L", share__size: true,
}} "share__size--active": state.game.animationSize === "L",
onClick={() => setState("game", "picSize", "L")} }}
> onClick={() => setState("game", "animationSize", "L")}
L >
</button> L
<button </button>
classList={{ <button
share__size: true, classList={{
"share__size--last": true, share__size: true,
"share__size--active": state.game.picSize === "XL", "share__size--last": true,
}} "share__size--active": state.game.animationSize === "XL",
onClick={() => setState("game", "picSize", "XL")} }}
> onClick={() => setState("game", "animationSize", "XL")}
XL >
</button> XL
</button>
<button <button
classList={{ classList={{
share__format: true, share__format: true,
"share__format--first": true, "share__format--first": true,
"share__format--active": state.game.format === "GIF", "share__format--active": state.game.format === "GIF",
}} }}
onClick={() => setState("game", "format", "GIF")} onClick={() => setState("game", "format", "GIF")}
> >
GIF GIF
</button> </button>
<button <button
classList={{ classList={{
share__format: true, share__format: true,
"share__format--active": state.game.format === "MP4", "share__format--active": state.game.format === "MP4",
}} }}
onClick={() => setState("game", "format", "MP4")} onClick={() => setState("game", "format", "MP4")}
> >
MP4 MP4
</button> </button>
<button <button
classList={{ classList={{
share__format: true, share__format: true,
"share__format--last": true, "share__format--last": true,
"share__format--active": state.game.format === "WebM", "share__format--active": state.game.format === "WebM",
}} }}
onClick={() => setState("game", "format", "WebM")} onClick={() => setState("game", "format", "WebM")}
> >
WebM WebM
</button> </button>
<button class="share__create-animation">Save animation</button>
<button class="share__create-animation">Save animation</button> </div>
</div> </Show>
</Scrollable> </Scrollable>
); );
}; };

View File

@@ -1,9 +0,0 @@
const sizeToPX = {
XS: "256",
S: "512",
M: "720",
L: "1024",
XL: "1440",
};
export default sizeToPX;

View File

@@ -1,9 +1,10 @@
const download = (data: string | Blob, name: string, ext: string) => { const download = (data: string, name: string, ext: string) => {
const url = typeof data === "string" ? data : URL.createObjectURL(data); const url = typeof data === "string" ? data : URL.createObjectURL(data);
const link = document.createElement("a"); const link = document.createElement("a");
link.href = url; link.href = url;
link.download = `${name}_${Date.now()}.${ext}`; link.download = `${name}_${Date.now()}.${ext}`;
link.target = "_blank";
link.click(); link.click();
}; };