This commit is contained in:
Maciej Caderek
2022-02-14 00:00:41 +01:00
parent 6274236ac7
commit e0b79a7071
26 changed files with 608 additions and 358 deletions

View File

@@ -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
);

View File

@@ -16,7 +16,7 @@ const drawCoords = (
) => {
const scale = size / 1024;
if (scale <= 0.25) {
if (scale <= 0.32) {
return;
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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)

View File

@@ -10,7 +10,6 @@ const createImage = async (
boardConfig: BoardConfig,
size: Size
) => {
console.log({ fen, pgn, ply, size });
const game = new Game();
if (pgn) {

View File

@@ -1,5 +1,5 @@
const sizeToPX = {
XS: 256,
XS: 360,
S: 512,
M: 720,
L: 1024,

View File

@@ -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) {

View File

@@ -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"],

View 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;

View File

@@ -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,

View File

@@ -1,3 +0,0 @@
export default [
"r1bqr1k1/pppnppbp/3p1np1/8/1P6/1B2PN2/PBPP1PPP/RN1Q1RK1 w - - 6 8",
];

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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); */

View File

@@ -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}

View File

@@ -20,7 +20,7 @@ const GameTabs: Component<{ moves: readonly string[]; handlers: Handlers }> = (
}
onClick={() => setTab("moves")}
>
MOVES
GAME
</button>
<button
class={

View File

@@ -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;
}

View File

@@ -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={() => {
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={() => {
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>
);
};

View File

@@ -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>

View File

@@ -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}
// />

View File

@@ -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);
}

View File

@@ -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,15 +166,67 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
</button>
</Show>
</div>
<Show when={state.pgn}>
<hr />
<div class="share__pgn">
<h2>Game</h2>
<div class="double">
<button class="share__btn">Copy PGN</button>
<button class="share__btn">Copy link</button>
<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">Export PGN</button>
<button class="share__btn">Copy markdown</button>
<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}>
@@ -158,36 +236,36 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
classList={{
share__size: true,
"share__size--first": true,
"share__size--active": state.game.animationSize === "XS",
"share__size--active": state.gameConfig.animationSize === "XS",
}}
onClick={() => setState("game", "animationSize", "XS")}
onClick={() => setState("gameConfig", "animationSize", "XS")}
>
XS
</button>
<button
classList={{
share__size: true,
"share__size--active": state.game.animationSize === "S",
"share__size--active": state.gameConfig.animationSize === "S",
}}
onClick={() => setState("game", "animationSize", "S")}
onClick={() => setState("gameConfig", "animationSize", "S")}
>
S
</button>
<button
classList={{
share__size: true,
"share__size--active": state.game.animationSize === "M",
"share__size--active": state.gameConfig.animationSize === "M",
}}
onClick={() => setState("game", "animationSize", "M")}
onClick={() => setState("gameConfig", "animationSize", "M")}
>
M
</button>
<button
classList={{
share__size: true,
"share__size--active": state.game.animationSize === "L",
"share__size--active": state.gameConfig.animationSize === "L",
}}
onClick={() => setState("game", "animationSize", "L")}
onClick={() => setState("gameConfig", "animationSize", "L")}
>
L
</button>
@@ -195,9 +273,9 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
classList={{
share__size: true,
"share__size--last": true,
"share__size--active": state.game.animationSize === "XL",
"share__size--active": state.gameConfig.animationSize === "XL",
}}
onClick={() => setState("game", "animationSize", "XL")}
onClick={() => setState("gameConfig", "animationSize", "XL")}
>
XL
</button>
@@ -205,18 +283,18 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
classList={{
share__format: true,
"share__format--first": true,
"share__format--active": state.game.format === "GIF",
"share__format--active": state.gameConfig.format === "GIF",
}}
onClick={() => setState("game", "format", "GIF")}
onClick={() => setState("gameConfig", "format", "GIF")}
>
GIF
</button>
<button
classList={{
share__format: true,
"share__format--active": state.game.format === "MP4",
"share__format--active": state.gameConfig.format === "MP4",
}}
onClick={() => setState("game", "format", "MP4")}
onClick={() => setState("gameConfig", "format", "MP4")}
>
MP4
</button>
@@ -224,15 +302,21 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
classList={{
share__format: true,
"share__format--last": true,
"share__format--active": state.game.format === "WebM",
"share__format--active": state.gameConfig.format === "WebM",
}}
onClick={() => setState("game", "format", "WebM")}
onClick={() => setState("gameConfig", "format", "WebM")}
>
WebM
</button>
<button class="share__create-animation">Save animation</button>
<button
class="share__create-animation"
onClick={() => props.handlers.downloadAnimation()}
>
Save animation
</button>
</div>
</Show>
</Show>
</Scrollable>
);
};

View File

@@ -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
View 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
View 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;