WIP
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { BoardConfig, PiecesStyle, Position } from "./../types";
|
||||
import { BoardConfig, Header, PiecesStyle, Position } from "./../types";
|
||||
import { Style, BoardStyle } from "../types";
|
||||
import drawRectangle from "./layers/drawRectangle";
|
||||
import drawCoords from "./layers/drawCoords";
|
||||
import drawMoveIndicators from "./layers/drawMoveIndicators";
|
||||
import drawPieces from "./layers/drawPieces";
|
||||
import drawHeader from "./layers/drawHeader.ts";
|
||||
import drawHeader from "./layers/drawHeader";
|
||||
import drawExtraInfo from "./layers/drawExtraInfo";
|
||||
import boards from "./styles-board";
|
||||
|
||||
@@ -20,6 +20,22 @@ const defaultConfig: BoardConfig = {
|
||||
showChecks: true,
|
||||
showCoords: true,
|
||||
flipped: false,
|
||||
anonymous: false,
|
||||
};
|
||||
|
||||
const defaultHeader: Header = {
|
||||
White: "White",
|
||||
Black: "Black",
|
||||
WhitePretty: "White",
|
||||
BlackPretty: "Black",
|
||||
WhiteElo: null,
|
||||
BlackElo: null,
|
||||
Date: null,
|
||||
DatePretty: null,
|
||||
Event: null,
|
||||
Round: null,
|
||||
Site: null,
|
||||
Result: null,
|
||||
};
|
||||
|
||||
class Board {
|
||||
@@ -34,7 +50,7 @@ class Board {
|
||||
private margin: number = 0;
|
||||
|
||||
private style: Style = boards.standard;
|
||||
private header: { [key: string]: string | undefined } = {};
|
||||
private header: Header = defaultHeader;
|
||||
private lastPosition: Position | null = null;
|
||||
private background: HTMLCanvasElement | null = null;
|
||||
private currentScreen: "title" | "move" = "move";
|
||||
@@ -158,7 +174,19 @@ class Board {
|
||||
return this;
|
||||
}
|
||||
|
||||
async titleFrame(header: { [key: string]: string | undefined }) {
|
||||
private getFinalHeader() {
|
||||
return this.cfg.anonymous
|
||||
? {
|
||||
...this.header,
|
||||
White: "Anonymous",
|
||||
Black: "Anonymous",
|
||||
WhitePretty: "Anonymous",
|
||||
BlackPretty: "Anonymous",
|
||||
}
|
||||
: this.header;
|
||||
}
|
||||
|
||||
async titleFrame(header: Header) {
|
||||
this.currentScreen = "title";
|
||||
this.header = header;
|
||||
|
||||
@@ -168,7 +196,7 @@ class Board {
|
||||
this.scale,
|
||||
this.margin,
|
||||
this.style,
|
||||
header
|
||||
this.getFinalHeader()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -224,13 +252,10 @@ class Board {
|
||||
this.background = canvas;
|
||||
}
|
||||
|
||||
async frame(
|
||||
position: Position | null,
|
||||
header: { [key: string]: string | undefined }
|
||||
) {
|
||||
async frame(position: Position | null, header?: Header) {
|
||||
this.currentScreen = "move";
|
||||
this.lastPosition = position;
|
||||
this.header = header;
|
||||
this.header = header ?? this.header;
|
||||
|
||||
this.tempCtx.clearRect(0, 0, this.size, this.size);
|
||||
|
||||
@@ -289,7 +314,7 @@ class Board {
|
||||
this.scale,
|
||||
this.margin,
|
||||
this.style,
|
||||
this.header,
|
||||
this.getFinalHeader(),
|
||||
this.cfg.flipped,
|
||||
this.lastPosition
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ const drawCoords = (
|
||||
) => {
|
||||
const scale = size / 1024;
|
||||
|
||||
if (scale <= 0.25) {
|
||||
if (scale <= 0.32) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Style, Position } from "./../../types";
|
||||
import { Style, Position, Header } from "./../../types";
|
||||
import drawText from "./drawText";
|
||||
|
||||
const chessFontMapping: { [key: string]: string } = {
|
||||
@@ -17,7 +17,7 @@ const drawExtraInfo = async (
|
||||
scale: number,
|
||||
margin: number,
|
||||
style: Style,
|
||||
data: { [key: string]: string | undefined },
|
||||
data: Header,
|
||||
flipped: boolean,
|
||||
position: Position
|
||||
) => {
|
||||
@@ -32,7 +32,7 @@ const drawExtraInfo = async (
|
||||
{
|
||||
const w = drawText(
|
||||
ctx,
|
||||
data.White ?? "White",
|
||||
data.White === "Anonymous" ? "White" : data.White,
|
||||
"Ubuntu",
|
||||
fontSize,
|
||||
700,
|
||||
@@ -41,8 +41,7 @@ const drawExtraInfo = async (
|
||||
"left"
|
||||
);
|
||||
|
||||
const elo =
|
||||
data.WhiteElo && data.WhiteElo !== "?" ? ` ${data.WhiteElo}` : "";
|
||||
const elo = data.WhiteElo ? ` ${data.WhiteElo}` : "";
|
||||
|
||||
drawText(
|
||||
ctx,
|
||||
@@ -59,7 +58,7 @@ const drawExtraInfo = async (
|
||||
{
|
||||
const w = drawText(
|
||||
ctx,
|
||||
data.Black ?? "Black",
|
||||
data.Black === "Anonymous" ? "Black" : data.Black,
|
||||
"Ubuntu",
|
||||
fontSize,
|
||||
700,
|
||||
@@ -68,8 +67,7 @@ const drawExtraInfo = async (
|
||||
"left"
|
||||
);
|
||||
|
||||
const elo =
|
||||
data.BlackElo && data.BlackElo !== "?" ? ` ${data.BlackElo}` : "";
|
||||
const elo = data.BlackElo ? ` ${data.BlackElo}` : "";
|
||||
|
||||
drawText(
|
||||
ctx,
|
||||
|
||||
@@ -1,53 +1,14 @@
|
||||
import { Style } from "./../../types";
|
||||
import { Header, Style } from "../../types";
|
||||
import drawRectangle from "./drawRectangle";
|
||||
import drawText from "./drawText";
|
||||
|
||||
const MONTHS = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
const [y, m, d] = date.split(".").map(Number);
|
||||
|
||||
const month = Number.isNaN(m) ? null : MONTHS[m - 1];
|
||||
const day = Number.isNaN(d) || month === null ? null : d;
|
||||
const year = Number.isNaN(y) ? null : y;
|
||||
|
||||
return month && day && year
|
||||
? `${month} ${day}, ${year}`
|
||||
: month && year
|
||||
? `${month} ${year}`
|
||||
: year
|
||||
? String(year)
|
||||
: "";
|
||||
};
|
||||
|
||||
const formatName = (name: string) => {
|
||||
return name
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.reverse()
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
const drawHeader = async (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
size: number,
|
||||
scale: number,
|
||||
margin: number,
|
||||
style: Style,
|
||||
data: { [key: string]: string | undefined }
|
||||
data: Header
|
||||
) => {
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
await drawRectangle(ctx, size, size + margin * 2, 0, 0, style.border);
|
||||
@@ -55,11 +16,11 @@ const drawHeader = async (
|
||||
const font = "Ubuntu";
|
||||
|
||||
const allSizes = [
|
||||
{ key: "White", line: 60 * scale, fontSize: 42 * scale, n: 0 },
|
||||
{ key: "Black", line: 60 * scale, fontSize: 42 * scale, n: 2 },
|
||||
{ key: "WhitePretty", line: 60 * scale, fontSize: 42 * scale, n: 0 },
|
||||
{ key: "BlackPretty", line: 60 * scale, fontSize: 42 * scale, n: 2 },
|
||||
{ key: "Event", line: 30 * scale, fontSize: 20 * scale, n: 4 },
|
||||
{ key: "Round", line: 30 * scale, fontSize: 20 * scale, n: 5 },
|
||||
{ key: "Date", line: 30 * scale, fontSize: 20 * scale, n: 7 },
|
||||
{ key: "DatePretty", line: 30 * scale, fontSize: 20 * scale, n: 7 },
|
||||
{ key: "Site", line: 30 * scale, fontSize: 20 * scale, n: 8 },
|
||||
];
|
||||
|
||||
@@ -67,16 +28,16 @@ const drawHeader = async (
|
||||
|
||||
const sizes = allSizes.filter(({ key }) => keys.has(key));
|
||||
|
||||
if (data.White && data.Black) {
|
||||
sizes.push({ key: "vs", line: 50, fontSize: 20, n: 1 });
|
||||
if (data.WhitePretty && data.BlackPretty) {
|
||||
sizes.push({ key: "vs", line: 50 * scale, fontSize: 20 * scale, n: 1 });
|
||||
}
|
||||
|
||||
if (data.Event || data.Round) {
|
||||
sizes.push({ key: "margin", line: 100, fontSize: 0, n: 3 });
|
||||
sizes.push({ key: "margin", line: 100 * scale, fontSize: 0, n: 3 });
|
||||
}
|
||||
|
||||
if (data.Date || data.Site) {
|
||||
const line = data.Event || data.Round ? 20 : 100;
|
||||
const line = data.Event || data.Round ? 20 * scale : 100 * scale;
|
||||
sizes.push({ key: "margin", line, fontSize: 0, n: 6 });
|
||||
}
|
||||
|
||||
@@ -101,17 +62,10 @@ const drawHeader = async (
|
||||
return;
|
||||
}
|
||||
|
||||
const item = data[key];
|
||||
const item = data[key as keyof Header];
|
||||
|
||||
if (item) {
|
||||
const text =
|
||||
key === "Date"
|
||||
? formatDate(item)
|
||||
: key === "Black" || key === "White"
|
||||
? formatName(item)
|
||||
: key === "Round"
|
||||
? `Round ${item}`
|
||||
: item;
|
||||
const text = key === "Round" ? `Round ${item}` : item;
|
||||
|
||||
const y = fromTop + line / 2;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BoardConfig } from "./../types";
|
||||
import { BoardConfig, Size } from "./../types";
|
||||
import Board from "../board/Board";
|
||||
import Game from "../game/Game";
|
||||
import sizeToPX from "./sizeToPX";
|
||||
import GIF from "./GIF";
|
||||
import WebM from "./WebM";
|
||||
import MP4 from "./MP4";
|
||||
@@ -16,10 +17,11 @@ const getData = (board: Board, encoder: GIF | WebM | MP4) => {
|
||||
const createAnimation = async (
|
||||
pgn: string,
|
||||
boardConfig: BoardConfig,
|
||||
format: "GIF" | "WebM" | "MP4"
|
||||
format: "GIF" | "WebM" | "MP4",
|
||||
size: Size
|
||||
) => {
|
||||
const game = new Game().loadPGN(pgn);
|
||||
const board = new Board(boardConfig);
|
||||
const board = new Board({ ...boardConfig, size: sizeToPX[size] });
|
||||
const encoder =
|
||||
format === "GIF"
|
||||
? new GIF(board.width, board.height, true)
|
||||
|
||||
@@ -10,7 +10,6 @@ const createImage = async (
|
||||
boardConfig: BoardConfig,
|
||||
size: Size
|
||||
) => {
|
||||
console.log({ fen, pgn, ply, size });
|
||||
const game = new Game();
|
||||
|
||||
if (pgn) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const sizeToPX = {
|
||||
XS: 256,
|
||||
XS: 360,
|
||||
S: 512,
|
||||
M: 720,
|
||||
L: 1024,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PieceType, PieceColor, BoardData, Position } from "../types";
|
||||
import { Chess, ChessInstance } from "chess.js";
|
||||
import { cleanPGN } from "./PGNHelpers";
|
||||
import { formatDate, formatName } from "../utils/formatters";
|
||||
|
||||
const MATERIAL_VALUE: Map<PieceType, number> = new Map([
|
||||
["q", 9],
|
||||
@@ -10,6 +11,13 @@ const MATERIAL_VALUE: Map<PieceType, number> = new Map([
|
||||
["p", 1],
|
||||
]);
|
||||
|
||||
const prepareHeaderEntry = (
|
||||
entry: string | undefined,
|
||||
ifEmpty: null | string = null
|
||||
) => {
|
||||
return !entry || entry === "?" ? ifEmpty : entry;
|
||||
};
|
||||
|
||||
class Game {
|
||||
private positions: Position[] = [];
|
||||
private game: ChessInstance = new Chess();
|
||||
@@ -149,7 +157,30 @@ class Game {
|
||||
}
|
||||
|
||||
get header() {
|
||||
return this.game.header();
|
||||
const header = this.game.header();
|
||||
|
||||
const white = prepareHeaderEntry(header.White, "Anonymous") as string;
|
||||
const black = prepareHeaderEntry(header.Black, "Anonymous") as string;
|
||||
const date = prepareHeaderEntry(header.Date);
|
||||
|
||||
return {
|
||||
White: white,
|
||||
Black: black,
|
||||
WhitePretty: formatName(white),
|
||||
BlackPretty: formatName(black),
|
||||
WhiteElo: prepareHeaderEntry(header.WhiteElo),
|
||||
BlackElo: prepareHeaderEntry(header.BlackElo),
|
||||
Date: date,
|
||||
DatePretty: date === null ? null : formatDate(date),
|
||||
Event: prepareHeaderEntry(header.Event),
|
||||
Round: prepareHeaderEntry(header.Round),
|
||||
Site: prepareHeaderEntry(header.Site),
|
||||
Result: prepareHeaderEntry(header.Result),
|
||||
};
|
||||
}
|
||||
|
||||
get pgn() {
|
||||
return this.game.pgn();
|
||||
}
|
||||
|
||||
getPosition(ply: number) {
|
||||
|
||||
207
src/main.tsx
207
src/main.tsx
@@ -1,78 +1,28 @@
|
||||
import { BoardConfig, BoardStyle, GameConfig, PiecesStyle } from "./types";
|
||||
import WebFont from "webfontloader";
|
||||
import * as Hammer from "hammerjs";
|
||||
import { render } from "solid-js/web";
|
||||
|
||||
import { BoardStyle, PiecesStyle } from "./types";
|
||||
|
||||
import Board from "./board/Board";
|
||||
import Game from "./game/Game";
|
||||
import pgns from "./test-data/pgns";
|
||||
import createAnimation from "./encoders/createAnimation";
|
||||
// import { decompressPGN } from "./game/PGNHelpers";
|
||||
import WebFont from "webfontloader";
|
||||
import Player from "./player/Player";
|
||||
import * as Hammer from "hammerjs";
|
||||
// import Moves from "./ui/Moves";
|
||||
// import Controls from "./ui/Controls";
|
||||
import App from "./ui/App";
|
||||
|
||||
import { state, setState } from "./state";
|
||||
|
||||
import { render } from "solid-js/web";
|
||||
import App from "./ui/App";
|
||||
import download from "./utils/download";
|
||||
import createImage from "./encoders/createImage";
|
||||
|
||||
const boardConfig: BoardConfig = {
|
||||
size: 1024,
|
||||
tiles: 8,
|
||||
boardStyle: "calm",
|
||||
piecesStyle: "tatiana",
|
||||
showBorder: true,
|
||||
showExtraInfo: true,
|
||||
showMaterial: true,
|
||||
showMoveIndicator: true,
|
||||
showChecks: true,
|
||||
showCoords: true,
|
||||
flipped: false,
|
||||
};
|
||||
|
||||
const gameConfig: GameConfig = {
|
||||
titleScreen: true,
|
||||
fromPly: null,
|
||||
toPly: null,
|
||||
loop: true,
|
||||
format: "GIF",
|
||||
picSize: "M",
|
||||
animationSize: "M",
|
||||
};
|
||||
|
||||
const createDownloadLink = async (pgn: string, boardConfig: BoardConfig) => {
|
||||
const file = await createAnimation(pgn, { ...boardConfig, size: 720 }, "MP4");
|
||||
const link = document.createElement("a");
|
||||
link.innerText = "DOWNLOAD";
|
||||
link.setAttribute("href", URL.createObjectURL(file));
|
||||
link.setAttribute("download", file.name);
|
||||
return link;
|
||||
};
|
||||
|
||||
console.log(createDownloadLink.name);
|
||||
import createAnimation from "./encoders/createAnimation";
|
||||
import readFile from "./utils/readFile";
|
||||
import download from "./utils/download";
|
||||
import { compressPGN } from "./game/PGNHelpers";
|
||||
import extractUrlData from "./persistance/extractUrlData";
|
||||
|
||||
const main = async () => {
|
||||
// window.location.hash =
|
||||
// "#QiBEdWtlIEthcmwgLyBDb3VudCBJc291YXJkCkQgMTg1OC4/Py4/PwpFIFBhcmlzClIgMS0wClMgUGFyaXMgRlJBClcgUGF1bCBNb3JwaHkKCmU0IGU1IE5mMyBkNiBkNCBCZzQgZHhlNSBCeGYzIFF4ZjMgZHhlNSBCYzQgTmY2IFFiMyBRZTcgTmMzIGM2IEJnNSBiNSBOeGI1IGN4YjUgQnhiNSsgTmJkNyBPLU8tTyBSZDggUnhkNyBSeGQ3IFJkMSBRZTYgQnhkNysgTnhkNyBRYjgrIE54YjggUmQ4Iw==";
|
||||
const board = new Board(state.boardConfig);
|
||||
const player = new Player(board, state.gameConfig);
|
||||
|
||||
// const hash = window.location.hash;
|
||||
// const pgn = hash === "" ? null : decompressPGN(hash.slice(1));
|
||||
const pgn = pgns[pgns.length - 1];
|
||||
// const pgn = pgns[2];
|
||||
const board = new Board(boardConfig);
|
||||
|
||||
// const interval = 1000;
|
||||
// play(board, gameConfig, pgn, interval);
|
||||
|
||||
const player = new Player(board, gameConfig);
|
||||
const game = new Game().loadPGN(pgn);
|
||||
|
||||
setState({
|
||||
moves: game.getMoves(),
|
||||
pgn,
|
||||
ply: 0,
|
||||
fen: game.getPosition(0).fen,
|
||||
});
|
||||
/* Register handlers */
|
||||
|
||||
const handlers = {
|
||||
prev() {
|
||||
@@ -93,24 +43,36 @@ const main = async () => {
|
||||
},
|
||||
toggleBorder() {
|
||||
board.toggleBorder();
|
||||
setState("board", "showBorder", !state.board.showBorder);
|
||||
setState("boardConfig", "showBorder", !state.boardConfig.showBorder);
|
||||
},
|
||||
|
||||
showBorder() {
|
||||
board.showBorder();
|
||||
setState("board", "showBorder", true);
|
||||
setState("boardConfig", "showBorder", true);
|
||||
},
|
||||
hideBorder() {
|
||||
board.hideBorder();
|
||||
setState("board", "showBorder", false);
|
||||
setState("boardConfig", "showBorder", false);
|
||||
},
|
||||
toggleExtraInfo() {
|
||||
board.toggleExtraInfo();
|
||||
setState("board", "showExtraInfo", !state.board.showExtraInfo);
|
||||
setState(
|
||||
"boardConfig",
|
||||
"showExtraInfo",
|
||||
!state.boardConfig.showExtraInfo
|
||||
);
|
||||
},
|
||||
toggleAnonymous() {
|
||||
setState("boardConfig", "anonymous", !state.boardConfig.anonymous);
|
||||
board.updateConfig({ anonymous: state.boardConfig.anonymous });
|
||||
},
|
||||
toggleTitleScreen() {
|
||||
setState("gameConfig", "titleScreen", !state.gameConfig.titleScreen);
|
||||
},
|
||||
flip() {
|
||||
console.log("FLIP");
|
||||
board.flip();
|
||||
setState("board", "flipped", !state.board.flipped);
|
||||
setState("boardConfig", "flipped", !state.boardConfig.flipped);
|
||||
},
|
||||
togglePlay() {
|
||||
player.playing ? player.pause() : player.play();
|
||||
@@ -121,20 +83,28 @@ const main = async () => {
|
||||
},
|
||||
changeBoardStyle(style: BoardStyle) {
|
||||
board.setStyle(style);
|
||||
setState("board", "boardStyle", style);
|
||||
setState("boardConfig", "boardStyle", style);
|
||||
},
|
||||
changePiecesStyle(style: PiecesStyle) {
|
||||
board.setPiecesStyle(style);
|
||||
setState("board", "piecesStyle", style);
|
||||
setState("boardConfig", "piecesStyle", style);
|
||||
},
|
||||
async loadPGN(pgn: string) {
|
||||
const game = new Game().loadPGN(pgn);
|
||||
setState({ pgn, fen: "", moves: game.getMoves() });
|
||||
setState({
|
||||
pgn: game.pgn,
|
||||
fen: "",
|
||||
moves: game.getMoves(),
|
||||
ply: 0,
|
||||
game,
|
||||
});
|
||||
window.location.hash = `v1/pgn/${compressPGN(game.pgn)}`;
|
||||
|
||||
await player.load(game);
|
||||
},
|
||||
async loadFEN(fen: string) {
|
||||
const game = new Game().loadFEN(fen);
|
||||
setState({ pgn: null, fen, moves: game.getMoves() });
|
||||
setState({ pgn: "", fen, moves: game.getMoves(), ply: 0, game });
|
||||
await player.load(game);
|
||||
},
|
||||
async downloadImage() {
|
||||
@@ -142,21 +112,24 @@ const main = async () => {
|
||||
state.fen,
|
||||
state.pgn,
|
||||
state.ply,
|
||||
state.board,
|
||||
state.game.picSize
|
||||
state.boardConfig,
|
||||
state.gameConfig.picSize
|
||||
);
|
||||
download(data, "fen", "png");
|
||||
},
|
||||
async downloadAnimation() {
|
||||
const data = await createAnimation(
|
||||
state.pgn,
|
||||
state.boardConfig,
|
||||
state.gameConfig.format,
|
||||
state.gameConfig.animationSize
|
||||
);
|
||||
download(data, "game", state.gameConfig.format.toLowerCase());
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
window.handlers = handlers;
|
||||
// @ts-ignore
|
||||
window.state = state;
|
||||
/* Render the page */
|
||||
|
||||
/**
|
||||
* RENDER
|
||||
**/
|
||||
render(
|
||||
() => <App handlers={handlers} state={state} />,
|
||||
document.getElementById("root") as HTMLElement
|
||||
@@ -165,31 +138,19 @@ const main = async () => {
|
||||
const $board = document.querySelector<HTMLImageElement>("#board");
|
||||
$board?.appendChild(board.canvas);
|
||||
|
||||
// const moves = new Moves($moves as HTMLElement, player).load(game.getMoves());
|
||||
// const controls = new Controls($controls as HTMLElement, handlers).load();
|
||||
/* Restore game from the url */
|
||||
|
||||
await player.load(game);
|
||||
const { pgn, fen } = extractUrlData();
|
||||
|
||||
// @ts-ignore
|
||||
window.load = async (pgn: string) => {
|
||||
const game = new Game().loadPGN(pgn);
|
||||
await player.load(game);
|
||||
// moves.load(game.getMoves());
|
||||
// controls.load();
|
||||
};
|
||||
await (pgn
|
||||
? handlers.loadPGN(pgn)
|
||||
: fen
|
||||
? handlers.loadFEN(fen)
|
||||
: handlers.loadFEN(
|
||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||
));
|
||||
|
||||
// @ts-ignore
|
||||
window.board = board;
|
||||
|
||||
document.addEventListener(
|
||||
"contextmenu",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
handlers.prev();
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
/* Register events */
|
||||
|
||||
const keyMapping: { [key: string]: () => void } = {
|
||||
ArrowLeft: handlers.prev,
|
||||
@@ -202,9 +163,31 @@ const main = async () => {
|
||||
e: handlers.toggleExtraInfo,
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", ({ key }) => {
|
||||
if (keyMapping[key]) {
|
||||
keyMapping[key]();
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
|
||||
if (
|
||||
keyMapping[e.key] &&
|
||||
target?.nodeName !== "INPUT" &&
|
||||
target?.nodeName !== "TEXTAREA"
|
||||
) {
|
||||
keyMapping[e.key]();
|
||||
}
|
||||
});
|
||||
|
||||
const preventDefaults = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||
document.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
document.addEventListener("drop", async (e) => {
|
||||
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
||||
const content = await readFile(e.dataTransfer.files[0]);
|
||||
handlers.loadPGN(content);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -222,12 +205,10 @@ const main = async () => {
|
||||
hammer.on("pinchout", handlers.hideBorder);
|
||||
hammer.on("tap", handlers.next);
|
||||
hammer.on("press", handlers.flip);
|
||||
|
||||
// createDownloadLink(pgn, boardConfig).then((link) => {
|
||||
// document.body.appendChild(link);
|
||||
// });
|
||||
};
|
||||
|
||||
/* Boot */
|
||||
|
||||
WebFont.load({
|
||||
google: {
|
||||
families: ["Ubuntu:500,700", "Fira Mono"],
|
||||
|
||||
27
src/persistance/extractUrlData.ts
Normal file
27
src/persistance/extractUrlData.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { decompressPGN } from "../game/PGNHelpers";
|
||||
|
||||
const HEADER_REGEX = /^#v\d+\/(pgn|fen)\//;
|
||||
|
||||
const extractUrlData = () => {
|
||||
const hash = window.location.hash;
|
||||
|
||||
if (!HEADER_REGEX.test(hash)) {
|
||||
return {
|
||||
pgn: "",
|
||||
fen: "",
|
||||
};
|
||||
}
|
||||
|
||||
const [rawVersion, format, ...chunks] = hash.split("/");
|
||||
|
||||
const version = Number((rawVersion.match(/\d+/g) ?? [])[0]);
|
||||
const data = chunks.join("/");
|
||||
|
||||
return {
|
||||
version,
|
||||
pgn: format === "pgn" ? decompressPGN(data) : "",
|
||||
fen: format === "fen" ? decodeURI(data) : "",
|
||||
};
|
||||
};
|
||||
|
||||
export default extractUrlData;
|
||||
16
src/state.ts
16
src/state.ts
@@ -1,5 +1,6 @@
|
||||
import isMobile from "is-mobile";
|
||||
import { createStore } from "solid-js/store";
|
||||
import Game from "./game/Game";
|
||||
import { BoardConfig, GameConfig } from "./types";
|
||||
|
||||
const boardConfig: BoardConfig = {
|
||||
@@ -14,6 +15,7 @@ const boardConfig: BoardConfig = {
|
||||
showChecks: true,
|
||||
showCoords: true,
|
||||
flipped: false,
|
||||
anonymous: false,
|
||||
};
|
||||
|
||||
const gameConfig: GameConfig = {
|
||||
@@ -27,9 +29,10 @@ const gameConfig: GameConfig = {
|
||||
};
|
||||
|
||||
export type State = {
|
||||
board: BoardConfig;
|
||||
game: GameConfig;
|
||||
pgn: string | null;
|
||||
boardConfig: BoardConfig;
|
||||
gameConfig: GameConfig;
|
||||
game: Game;
|
||||
pgn: string;
|
||||
fen: string;
|
||||
moves: string[];
|
||||
ply: number;
|
||||
@@ -37,9 +40,10 @@ export type State = {
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
board: boardConfig,
|
||||
game: gameConfig,
|
||||
pgn: null,
|
||||
boardConfig,
|
||||
gameConfig,
|
||||
game: new Game(),
|
||||
pgn: "",
|
||||
fen: "",
|
||||
moves: [],
|
||||
ply: 0,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default [
|
||||
"r1bqr1k1/pppnppbp/3p1np1/8/1P6/1B2PN2/PBPP1PPP/RN1Q1RK1 w - - 6 8",
|
||||
];
|
||||
@@ -1,26 +0,0 @@
|
||||
const pgns = [
|
||||
`[Event "Hoogovens Group A"]
|
||||
[Site "Wijk aan Zee NED"]
|
||||
[Date "1999.01.20"]
|
||||
[EventDate "1999.01.16"]
|
||||
[Round "4"]
|
||||
[Result "1-0"]
|
||||
[White "Garry Kasparov"]
|
||||
[Black "Veselin Topalov"]
|
||||
[ECO "B07"]
|
||||
[WhiteElo "2812"]
|
||||
[BlackElo "2700"]
|
||||
[PlyCount "87"]
|
||||
|
||||
1. e4 d6 2. d4 Nf6 3. Nc3 g6 4. Be3 Bg7 5. Qd2 c6 6. f3 b5
|
||||
7. Nge2 Nbd7 8. Bh6 Bxh6 9. Qxh6 Bb7 10. a3 e5 11. O-O-O Qe7
|
||||
12. Kb1 a6 13. Nc1 O-O-O 14. Nb3 exd4 15. Rxd4 c5 16. Rd1 Nb6
|
||||
17. g3 Kb8 18. Na5 Ba8 19. Bh3 d5 20. Qf4+ Ka7 21. Rhe1 d4
|
||||
22. Nd5 Nbxd5 23. exd5 Qd6 24. Rxd4 cxd4 25. Re7+ Kb6
|
||||
26. Qxd4+ Kxa5 27. b4+ Ka4 28. Qc3 Qxd5 29. Ra7 Bb7 30. Rxb7
|
||||
Qc4 31. Qxf6 Kxa3 32. Qxa6+ Kxb4 33. c3+ Kxc3 34. Qa1+ Kd2
|
||||
35. Qb2+ Kd1 36. Bf1 Rd2 37. Rd7 Rxd7 38. Bxc4 bxc4 39. Qxh8
|
||||
Rd3 40. Qa8 c3 41. Qa4+ Ke1 42. f4 f5 43. Kc1 Rd2 44. Qa7 1-0`,
|
||||
];
|
||||
|
||||
export default pgns;
|
||||
21
src/types.ts
21
src/types.ts
@@ -129,6 +129,7 @@ export type BoardConfig = {
|
||||
showChecks: boolean;
|
||||
showCoords: boolean;
|
||||
flipped: boolean;
|
||||
anonymous: boolean;
|
||||
};
|
||||
|
||||
export type Size = "XS" | "S" | "M" | "L" | "XL";
|
||||
@@ -199,6 +200,8 @@ export type Handlers = {
|
||||
showBorder(): void;
|
||||
hideBorder(): void;
|
||||
toggleExtraInfo(): void;
|
||||
toggleAnonymous(): void;
|
||||
toggleTitleScreen(): void;
|
||||
flip(): void;
|
||||
togglePlay(): void;
|
||||
goto(ply: number): void;
|
||||
@@ -206,5 +209,21 @@ export type Handlers = {
|
||||
changePiecesStyle: (style: PiecesStyle) => void;
|
||||
loadPGN: (pgn: string) => Promise<void>;
|
||||
loadFEN: (fen: string) => Promise<void>;
|
||||
downloadImage: () => void;
|
||||
downloadImage: () => Promise<void>;
|
||||
downloadAnimation: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type Header = {
|
||||
White: string;
|
||||
Black: string;
|
||||
WhitePretty: string;
|
||||
BlackPretty: string;
|
||||
WhiteElo: string | null;
|
||||
BlackElo: string | null;
|
||||
Date: string | null;
|
||||
DatePretty: string | null;
|
||||
Event: string | null;
|
||||
Round: string | null;
|
||||
Site: string | null;
|
||||
Result: string | null;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,21 @@ body {
|
||||
font-family: Ubuntu, sans-serif;
|
||||
}
|
||||
|
||||
button {
|
||||
.upload {
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.upload::before {
|
||||
content: "UPLOAD PGN FILE";
|
||||
visibility: visible;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button,
|
||||
.upload::before {
|
||||
padding: 10px;
|
||||
font-family: "Ubuntu";
|
||||
font-size: 1.5rem;
|
||||
@@ -32,7 +46,8 @@ button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
button:hover,
|
||||
.upload:hover::before {
|
||||
background: rgb(0, 207, 162);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -69,6 +84,11 @@ h3 {
|
||||
margin: 15px 0 10px 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
border-top: solid 1px #ffffff22;
|
||||
}
|
||||
|
||||
.dark {
|
||||
background-color: #191d24;
|
||||
/* background-image: url(src/ui/img/pattern.png); */
|
||||
|
||||
@@ -25,7 +25,7 @@ const prepareBoards = async () => {
|
||||
|
||||
for (const [key, style] of Object.entries(styles) as [BoardStyle, Style][]) {
|
||||
await board.updateConfig({ boardStyle: key });
|
||||
await board.frame(null, {});
|
||||
await board.frame(null);
|
||||
board.render();
|
||||
boards.push({
|
||||
key,
|
||||
@@ -51,12 +51,12 @@ const Boards: Component<{ handlers: Handlers }> = (props) => {
|
||||
<img
|
||||
class={
|
||||
"boards__ico" +
|
||||
(state.board.boardStyle === board.key
|
||||
(state.boardConfig.boardStyle === board.key
|
||||
? " boards__ico--active"
|
||||
: "")
|
||||
}
|
||||
onClick={() => {
|
||||
setState("board", "boardStyle", board.key);
|
||||
setState("boardConfig", "boardStyle", board.key);
|
||||
props.handlers.changeBoardStyle(board.key);
|
||||
}}
|
||||
src={board.img}
|
||||
|
||||
@@ -20,7 +20,7 @@ const GameTabs: Component<{ moves: readonly string[]; handlers: Handlers }> = (
|
||||
}
|
||||
onClick={() => setTab("moves")}
|
||||
>
|
||||
MOVES
|
||||
GAME
|
||||
</button>
|
||||
<button
|
||||
class={
|
||||
|
||||
@@ -28,3 +28,16 @@
|
||||
.load__pgn-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.load__pgn-file {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.load__pgn-file-info {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.load__pgn-file-info p {
|
||||
margin-top: 10px;
|
||||
color: #677794;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, createSignal } from "solid-js";
|
||||
import { Handlers } from "../../types";
|
||||
import readFile from "../../utils/readFile";
|
||||
import "./Load.css";
|
||||
|
||||
const Load: Component<{ handlers: Handlers; showMoves: () => void }> = (
|
||||
@@ -22,12 +23,15 @@ const Load: Component<{ handlers: Handlers; showMoves: () => void }> = (
|
||||
<button
|
||||
class="load__fen-btn"
|
||||
onClick={() => {
|
||||
props.handlers.loadFEN(fen());
|
||||
setFEN("");
|
||||
if (fen()) {
|
||||
props.handlers.loadFEN(fen());
|
||||
setFEN("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
LOAD FEN
|
||||
</button>
|
||||
<hr />
|
||||
<textarea
|
||||
class="load__pgn-input"
|
||||
name="load-pgn"
|
||||
@@ -39,13 +43,33 @@ const Load: Component<{ handlers: Handlers; showMoves: () => void }> = (
|
||||
<button
|
||||
class="load__pgn-btn"
|
||||
onClick={() => {
|
||||
props.handlers.loadPGN(pgn());
|
||||
setPGN("");
|
||||
props.showMoves();
|
||||
if (pgn()) {
|
||||
props.handlers.loadPGN(pgn());
|
||||
setPGN("");
|
||||
props.showMoves();
|
||||
}
|
||||
}}
|
||||
>
|
||||
LOAD PGN
|
||||
</button>
|
||||
<hr />
|
||||
<input
|
||||
class="upload load__pgn-file"
|
||||
type="file"
|
||||
accept="application/vnd.chess-pgn,application/x-chess-pgn,.pgn"
|
||||
onChange={async (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target?.files && target.files.length > 0) {
|
||||
const content = await readFile(target.files[0]);
|
||||
props.handlers.loadPGN(content);
|
||||
props.showMoves();
|
||||
}
|
||||
}}
|
||||
></input>
|
||||
<div className="load__pgn-file-info">
|
||||
<p>or</p>
|
||||
<p>drop the PGN file anywhere on the page</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, For } from "solid-js";
|
||||
import { Component, For, createEffect } from "solid-js";
|
||||
import chunk_ from "@arrows/array/chunk_";
|
||||
import { Handlers } from "../../types";
|
||||
import Scrollable from "./reusable/Scrollable";
|
||||
@@ -8,6 +8,10 @@ import { state } from "../../state";
|
||||
const Moves: Component<{ moves: readonly string[]; handlers: Handlers }> = (
|
||||
props
|
||||
) => {
|
||||
createEffect(() => {
|
||||
document.querySelector(`[data-ply="${state.ply}"]`)?.scrollIntoView();
|
||||
});
|
||||
|
||||
return (
|
||||
<Scrollable class="moves">
|
||||
<For each={chunk_(2, props.moves as string[])}>
|
||||
@@ -23,6 +27,7 @@ const Moves: Component<{ moves: readonly string[]; handlers: Handlers }> = (
|
||||
"move__ply--current": state.ply === i() * 2 + 1,
|
||||
}}
|
||||
onClick={() => props.handlers.goto(i() * 2 + 1)}
|
||||
data-ply={i() * 2 + 1}
|
||||
>
|
||||
{white}
|
||||
</span>
|
||||
@@ -32,6 +37,7 @@ const Moves: Component<{ moves: readonly string[]; handlers: Handlers }> = (
|
||||
"move__ply--current": state.ply === i() * 2 + 2,
|
||||
}}
|
||||
onClick={() => props.handlers.goto(i() * 2 + 2)}
|
||||
data-ply={i() * 2 + 2}
|
||||
>
|
||||
{black}
|
||||
</span>
|
||||
|
||||
@@ -19,12 +19,12 @@ const Pieces: Component<{ handlers: Handlers }> = (props) => {
|
||||
<img
|
||||
class={
|
||||
"pieces__ico" +
|
||||
(state.board.piecesStyle === item.key
|
||||
(state.boardConfig.piecesStyle === item.key
|
||||
? " pieces__ico--active"
|
||||
: "")
|
||||
}
|
||||
onClick={() => {
|
||||
setState("board", "piecesStyle", item.key);
|
||||
setState("boardConfig", "piecesStyle", item.key);
|
||||
props.handlers.changePiecesStyle(item.key);
|
||||
}}
|
||||
src={item.img}
|
||||
@@ -97,7 +97,7 @@ export default Pieces;
|
||||
// ? " boards__ico--active"
|
||||
// : "")
|
||||
// }
|
||||
// onClick={() => setState("board", "boardStyle", board.key)}
|
||||
// onClick={() => setState("boardConfig", "boardStyle", board.key)}
|
||||
// src={board.img}
|
||||
// title={board.name}
|
||||
// />
|
||||
|
||||
@@ -45,3 +45,40 @@
|
||||
margin-top: 0;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.options__button {
|
||||
margin: 3px;
|
||||
padding: 5px;
|
||||
font-size: 3rem;
|
||||
background: rgb(0, 173, 136);
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.options__button--active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.options__button:hover {
|
||||
background: rgb(0, 207, 162);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.options__button--last {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.options__button--first {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.rotatable {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Handlers } from "../../types";
|
||||
import Scrollable from "./reusable/Scrollable";
|
||||
import { state, setState } from "../../state";
|
||||
import "./Share.css";
|
||||
import download from "../../utils/download";
|
||||
|
||||
const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
const [copyId, setCopyId] = createSignal("");
|
||||
@@ -17,41 +18,66 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
<div className="share__view">
|
||||
<h2 class="header--first">Board options</h2>
|
||||
<button
|
||||
class="controls__button controls__button--first"
|
||||
onClick={props.handlers.flip}
|
||||
title="FLIP"
|
||||
classList={{
|
||||
options__button: true,
|
||||
"options__button--first": true,
|
||||
"options__button--active": true,
|
||||
}}
|
||||
>
|
||||
<i class="las la-sync"></i>
|
||||
<i
|
||||
classList={{
|
||||
rotated: state.boardConfig.flipped,
|
||||
las: true,
|
||||
"la-sync": true,
|
||||
rotatable: true,
|
||||
}}
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
class="controls__button"
|
||||
classList={{
|
||||
options__button: true,
|
||||
"options__button--active": state.boardConfig.showBorder,
|
||||
}}
|
||||
onClick={props.handlers.toggleBorder}
|
||||
title="BORDER"
|
||||
>
|
||||
<i class="las la-expand"></i>
|
||||
</button>
|
||||
<button
|
||||
class="controls__button"
|
||||
classList={{
|
||||
options__button: true,
|
||||
"options__button--active": state.boardConfig.showExtraInfo,
|
||||
}}
|
||||
onClick={props.handlers.toggleExtraInfo}
|
||||
title="EXTRA INFO"
|
||||
>
|
||||
<i class="las la-info-circle"></i>
|
||||
</button>
|
||||
<button
|
||||
class="controls__button"
|
||||
onClick={props.handlers.toggleExtraInfo}
|
||||
title="INCLUDE HEADER"
|
||||
classList={{
|
||||
options__button: true,
|
||||
"options__button--active": state.gameConfig.titleScreen,
|
||||
}}
|
||||
onClick={props.handlers.toggleTitleScreen}
|
||||
title="TITLE SCREEN"
|
||||
>
|
||||
<i class="las la-heading"></i>
|
||||
</button>
|
||||
<button
|
||||
class="controls__button controls__button--last"
|
||||
onClick={props.handlers.toggleExtraInfo}
|
||||
classList={{
|
||||
options__button: true,
|
||||
"options__button--last": true,
|
||||
"options__button--active": state.boardConfig.anonymous,
|
||||
}}
|
||||
onClick={props.handlers.toggleAnonymous}
|
||||
title="ANONYMOUS"
|
||||
>
|
||||
<i class="las la-user-secret"></i>
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="share__fen">
|
||||
<h2>Current position</h2>
|
||||
<input
|
||||
@@ -89,36 +115,36 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--first": true,
|
||||
"share__size--active": state.game.picSize === "XS",
|
||||
"share__size--active": state.gameConfig.picSize === "XS",
|
||||
}}
|
||||
onClick={() => setState("game", "picSize", "XS")}
|
||||
onClick={() => setState("gameConfig", "picSize", "XS")}
|
||||
>
|
||||
XS
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--active": state.game.picSize === "S",
|
||||
"share__size--active": state.gameConfig.picSize === "S",
|
||||
}}
|
||||
onClick={() => setState("game", "picSize", "S")}
|
||||
onClick={() => setState("gameConfig", "picSize", "S")}
|
||||
>
|
||||
S
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--active": state.game.picSize === "M",
|
||||
"share__size--active": state.gameConfig.picSize === "M",
|
||||
}}
|
||||
onClick={() => setState("game", "picSize", "M")}
|
||||
onClick={() => setState("gameConfig", "picSize", "M")}
|
||||
>
|
||||
M
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--active": state.game.picSize === "L",
|
||||
"share__size--active": state.gameConfig.picSize === "L",
|
||||
}}
|
||||
onClick={() => setState("game", "picSize", "L")}
|
||||
onClick={() => setState("gameConfig", "picSize", "L")}
|
||||
>
|
||||
L
|
||||
</button>
|
||||
@@ -126,9 +152,9 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--last": true,
|
||||
"share__size--active": state.game.picSize === "XL",
|
||||
"share__size--active": state.gameConfig.picSize === "XL",
|
||||
}}
|
||||
onClick={() => setState("game", "picSize", "XL")}
|
||||
onClick={() => setState("gameConfig", "picSize", "XL")}
|
||||
>
|
||||
XL
|
||||
</button>
|
||||
@@ -140,98 +166,156 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="share__pgn">
|
||||
<h2>Game</h2>
|
||||
<div class="double">
|
||||
<button class="share__btn">Copy PGN</button>
|
||||
<button class="share__btn">Copy link</button>
|
||||
</div>
|
||||
<div class="double">
|
||||
<button class="share__btn">Export PGN</button>
|
||||
<button class="share__btn">Copy markdown</button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={!state.mobile}>
|
||||
<div class="share__animation">
|
||||
<h3>Animation</h3>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--first": true,
|
||||
"share__size--active": state.game.animationSize === "XS",
|
||||
}}
|
||||
onClick={() => setState("game", "animationSize", "XS")}
|
||||
>
|
||||
XS
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--active": state.game.animationSize === "S",
|
||||
}}
|
||||
onClick={() => setState("game", "animationSize", "S")}
|
||||
>
|
||||
S
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--active": state.game.animationSize === "M",
|
||||
}}
|
||||
onClick={() => setState("game", "animationSize", "M")}
|
||||
>
|
||||
M
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--active": state.game.animationSize === "L",
|
||||
}}
|
||||
onClick={() => setState("game", "animationSize", "L")}
|
||||
>
|
||||
L
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--last": true,
|
||||
"share__size--active": state.game.animationSize === "XL",
|
||||
}}
|
||||
onClick={() => setState("game", "animationSize", "XL")}
|
||||
>
|
||||
XL
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__format: true,
|
||||
"share__format--first": true,
|
||||
"share__format--active": state.game.format === "GIF",
|
||||
}}
|
||||
onClick={() => setState("game", "format", "GIF")}
|
||||
>
|
||||
GIF
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__format: true,
|
||||
"share__format--active": state.game.format === "MP4",
|
||||
}}
|
||||
onClick={() => setState("game", "format", "MP4")}
|
||||
>
|
||||
MP4
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__format: true,
|
||||
"share__format--last": true,
|
||||
"share__format--active": state.game.format === "WebM",
|
||||
}}
|
||||
onClick={() => setState("game", "format", "WebM")}
|
||||
>
|
||||
WebM
|
||||
</button>
|
||||
<button class="share__create-animation">Save animation</button>
|
||||
<Show when={state.pgn}>
|
||||
<hr />
|
||||
<div class="share__pgn">
|
||||
<h2>Game</h2>
|
||||
<div class="double">
|
||||
<button
|
||||
class="share__btn"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(state.pgn);
|
||||
blinkCopy("pgn");
|
||||
}}
|
||||
>
|
||||
{copyId() === "pgn" ? "Copied!" : "Copy PGN"}
|
||||
</button>
|
||||
<button
|
||||
class="share__btn"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
blinkCopy("pgn-link");
|
||||
}}
|
||||
>
|
||||
{copyId() === "pgn-link" ? "Copied!" : "Copy link"}
|
||||
</button>
|
||||
</div>
|
||||
<div class="double">
|
||||
<button
|
||||
class="share__btn"
|
||||
onClick={() => {
|
||||
const data = new Blob([state.pgn], {
|
||||
type: "application/vnd.chess-pgn;charset=utf-8",
|
||||
});
|
||||
download(data, "pgn", "pgn");
|
||||
}}
|
||||
>
|
||||
Export PGN
|
||||
</button>
|
||||
<button
|
||||
class="share__btn"
|
||||
onClick={() => {
|
||||
const header = state.game.header;
|
||||
const w = state.boardConfig.anonymous
|
||||
? "Anonymous"
|
||||
: header.WhitePretty;
|
||||
const b = state.boardConfig.anonymous
|
||||
? "Anonymous"
|
||||
: header.BlackPretty;
|
||||
|
||||
const title =
|
||||
`${w} vs ${b}` +
|
||||
(header.Event ? ` | ${header.Event}` : "") +
|
||||
(header.Round ? `, Round ${header.Round}` : "") +
|
||||
(header.DatePretty ? ` | ${header.DatePretty}` : "");
|
||||
|
||||
const md = `[${title}](${window.location.href})`;
|
||||
|
||||
navigator.clipboard.writeText(md);
|
||||
blinkCopy("markdown");
|
||||
}}
|
||||
>
|
||||
{copyId() === "markdown" ? "Copied!" : "Copy markdown"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={!state.mobile}>
|
||||
<div class="share__animation">
|
||||
<h3>Animation</h3>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--first": true,
|
||||
"share__size--active": state.gameConfig.animationSize === "XS",
|
||||
}}
|
||||
onClick={() => setState("gameConfig", "animationSize", "XS")}
|
||||
>
|
||||
XS
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--active": state.gameConfig.animationSize === "S",
|
||||
}}
|
||||
onClick={() => setState("gameConfig", "animationSize", "S")}
|
||||
>
|
||||
S
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--active": state.gameConfig.animationSize === "M",
|
||||
}}
|
||||
onClick={() => setState("gameConfig", "animationSize", "M")}
|
||||
>
|
||||
M
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--active": state.gameConfig.animationSize === "L",
|
||||
}}
|
||||
onClick={() => setState("gameConfig", "animationSize", "L")}
|
||||
>
|
||||
L
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
"share__size--last": true,
|
||||
"share__size--active": state.gameConfig.animationSize === "XL",
|
||||
}}
|
||||
onClick={() => setState("gameConfig", "animationSize", "XL")}
|
||||
>
|
||||
XL
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__format: true,
|
||||
"share__format--first": true,
|
||||
"share__format--active": state.gameConfig.format === "GIF",
|
||||
}}
|
||||
onClick={() => setState("gameConfig", "format", "GIF")}
|
||||
>
|
||||
GIF
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__format: true,
|
||||
"share__format--active": state.gameConfig.format === "MP4",
|
||||
}}
|
||||
onClick={() => setState("gameConfig", "format", "MP4")}
|
||||
>
|
||||
MP4
|
||||
</button>
|
||||
<button
|
||||
classList={{
|
||||
share__format: true,
|
||||
"share__format--last": true,
|
||||
"share__format--active": state.gameConfig.format === "WebM",
|
||||
}}
|
||||
onClick={() => setState("gameConfig", "format", "WebM")}
|
||||
>
|
||||
WebM
|
||||
</button>
|
||||
<button
|
||||
class="share__create-animation"
|
||||
onClick={() => props.handlers.downloadAnimation()}
|
||||
>
|
||||
Save animation
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</Scrollable>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const download = (data: string, name: string, ext: string) => {
|
||||
const download = (data: string | Blob, name: string, ext: string) => {
|
||||
const url = typeof data === "string" ? data : URL.createObjectURL(data);
|
||||
|
||||
const link = document.createElement("a");
|
||||
|
||||
40
src/utils/formatters.ts
Normal file
40
src/utils/formatters.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
const MONTHS = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
const [y, m, d] = date.split(".").map(Number);
|
||||
|
||||
const month = Number.isNaN(m) ? null : MONTHS[m - 1];
|
||||
const day = Number.isNaN(d) || month === null ? null : d;
|
||||
const year = Number.isNaN(y) ? null : y;
|
||||
|
||||
return month && day && year
|
||||
? `${month} ${day}, ${year}`
|
||||
: month && year
|
||||
? `${month} ${year}`
|
||||
: year
|
||||
? String(year)
|
||||
: "";
|
||||
};
|
||||
|
||||
const formatName = (name: string) => {
|
||||
return name
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.reverse()
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
export { formatDate, formatName };
|
||||
15
src/utils/readFile.ts
Normal file
15
src/utils/readFile.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
const readFile = (file: Blob): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
resolve(reader.result as string);
|
||||
};
|
||||
|
||||
reader.onerror = reject;
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
};
|
||||
|
||||
export default readFile;
|
||||
Reference in New Issue
Block a user