Files
sharechess/src/board/Board.ts
2022-04-30 07:23:55 +02:00

511 lines
11 KiB
TypeScript

import {
BoardConfig,
CreateCanvas,
Header,
LoadImage,
Position,
Style,
BoardStyle,
} from "./../types";
// @ts-ignore
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";
import drawExtraInfo from "./layers/drawExtraInfo";
import boards from "./styles-board/boardStyles";
import isLink from "../utils/isLink";
import { PiecesStyle } from "./styles-pieces/piecesStyles";
import loadImageBrowser from "./loaders/loadImage";
const defaultConfig: BoardConfig = {
size: 720,
tiles: 8,
boardStyle: "standard",
piecesStyle: "tatiana",
showBorder: true,
showExtraInfo: true,
showMaterial: true,
showMoveIndicator: true,
showChecks: true,
showCoords: true,
showShadows: false,
flipped: 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 {
private cfg: BoardConfig = defaultConfig;
private _anonymous: boolean = false;
private scale: number = 1;
private borderScale: number = 1;
private size: number = 0;
private squareSize: number = 0;
private innerSize: number = 0;
private borderWidth: number = 0;
private margin: number = 0;
private style: Style = boards.standard;
private header: Header = defaultHeader;
private lastPosition: Position | null = null;
private background: HTMLCanvasElement | null = null;
private currentScreen: "title" | "move" = "move";
private ctx: CanvasRenderingContext2D;
private tempCtx: CanvasRenderingContext2D;
private tempCanvas: HTMLCanvasElement;
public canvas: HTMLCanvasElement;
constructor(
config: Partial<BoardConfig> = {},
private loadImage: LoadImage = loadImageBrowser,
private createCanvas: CreateCanvas = () => document.createElement("canvas")
) {
this.canvas = this.createCanvas();
this.tempCanvas = this.createCanvas();
const ctx = this.canvas.getContext("2d");
const tempCtx = this.tempCanvas.getContext("2d");
if (ctx === null || tempCtx === null) {
throw new Error("Cannot create canvas 2D context");
}
this.ctx = ctx;
this.tempCtx = tempCtx;
this.updateConfig(config, false);
}
async setCanvas(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
this.setSize(this.cfg.size);
await this.refresh();
}
async updateConfig(config: Partial<BoardConfig>, refresh: boolean = true) {
const cfg = { ...this.cfg, ...config };
this.cfg = cfg;
this.setSize(cfg.size);
this.setStyle(cfg.boardStyle, refresh);
if (refresh) {
await this.refresh();
}
}
set anonymous(value: boolean) {
this._anonymous = value;
this.refresh();
}
async refresh() {
await this.renderBackground();
if (this.currentScreen === "title") {
await this.titleFrame(this.header);
} else if (this.lastPosition !== null) {
await this.frame(this.lastPosition, this.header);
}
this.render();
}
setSize(size: number) {
this.size = size;
this.scale = size / 720;
this.margin = this.cfg.showExtraInfo ? Math.round(50 * this.scale) : 0;
const height = size + this.margin * 2;
if (this.canvas.width !== size || this.canvas.height !== height) {
this.canvas.width = size;
this.canvas.height = height;
this.tempCanvas.width = size;
this.tempCanvas.height = height;
}
const tempBorderWidth = this.cfg.showBorder
? (this.size / 32) * this.borderScale
: 0;
const tempInnerSize = this.size - tempBorderWidth * 2;
this.squareSize = Math.floor(tempInnerSize / this.cfg.tiles);
this.innerSize = this.squareSize * this.cfg.tiles;
this.borderWidth = (this.size - this.innerSize) / 2;
return this;
}
getSize() {
return this.size;
}
setBorderScale(scale: number) {
this.borderScale = scale;
}
setStyle(style: BoardStyle, refresh: boolean = true) {
this.style = boards[style];
this.cfg.boardStyle = style;
if (refresh) {
this.refresh();
}
return this;
}
setPiecesStyle(style: PiecesStyle) {
this.cfg.piecesStyle = style;
this.refresh();
return this;
}
flip() {
this.cfg.flipped = !this.cfg.flipped;
this.refresh();
return this;
}
flipWhite() {
if (!this.cfg.flipped) {
return;
}
this.cfg.flipped = false;
this.refresh();
return this;
}
flipBlack() {
if (this.cfg.flipped) {
return;
}
this.cfg.flipped = true;
this.refresh();
return this;
}
hideBorder() {
this.cfg.showBorder = false;
this.setSize(this.size);
this.refresh();
return this;
}
showBorder() {
this.cfg.showBorder = true;
this.setSize(this.size);
this.refresh();
return this;
}
toggleBorder() {
this.cfg.showBorder = !this.cfg.showBorder;
this.setSize(this.size);
this.refresh();
return this;
}
toggleExtraInfo() {
this.cfg.showExtraInfo = !this.cfg.showExtraInfo;
this.setSize(this.size);
this.refresh();
return this;
}
toggleShadows() {
this.cfg.showShadows = !this.cfg.showShadows;
this.refresh();
return this;
}
private getFinalHeader() {
return this._anonymous
? {
...this.header,
White: "Anonymous",
Black: "Anonymous",
WhitePretty: "Anonymous",
BlackPretty: "Anonymous",
Site: isLink(this.header.Site) ? null : this.header.Site,
}
: {
...this.header,
Site: isLink(this.header.Site)
? (this.header.Site as string).replace(/^http[s]*:\/\//, "")
: this.header.Site,
};
}
async titleFrame(header: Header) {
this.currentScreen = "title";
this.header = header;
await drawHeader(
this.tempCtx,
this.size,
this.scale,
this.margin,
this.style,
this.getFinalHeader(),
this.loadImage
);
}
async renderBackground() {
const canvas = this.createCanvas();
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
canvas.width = this.size;
canvas.height = this.size + this.margin * 2;
const { background, dark, light, border, coords } = this.style;
ctx.clearRect(0, 0, this.size, this.size);
ctx.fillStyle = "#FF0000";
ctx.fillRect(0, 0, this.size, this.size);
await drawRectangle(
ctx,
this.width,
this.height,
0,
0,
border,
this.loadImage
);
await drawRectangle(
ctx,
this.innerSize,
this.innerSize,
this.cfg.showBorder ? this.borderWidth : 0,
(this.cfg.showBorder ? this.borderWidth : 0) + this.margin,
background,
this.loadImage,
this.cfg.tiles
);
for (let rank = 0; rank < this.cfg.tiles; rank++) {
for (let file = 0; file < this.cfg.tiles; file++) {
const style =
(file % 2 === 0 && rank % 2 === 0) ||
(file % 2 !== 0 && rank % 2 !== 0)
? light
: dark;
const x = file * this.squareSize + this.borderWidth;
const y = rank * this.squareSize + this.borderWidth + this.margin;
await drawRectangle(
ctx,
this.squareSize,
this.squareSize,
x,
y,
style,
this.loadImage
);
}
}
if (this.cfg.showBorder && this.cfg.showCoords) {
drawCoords(
ctx,
coords,
this.squareSize,
this.cfg.tiles,
this.cfg.flipped,
this.borderWidth,
this.size,
this.cfg.showBorder,
this.margin
);
}
console.log("Background rendered!");
this.background = canvas;
}
async frame(position: Position | null, header?: Header) {
this.currentScreen = "move";
this.lastPosition = position;
this.header = header ?? this.header;
this.tempCtx.clearRect(0, 0, this.size, this.size);
if (this.background === null) {
await this.renderBackground();
}
this.tempCtx.drawImage((await this.background) as HTMLCanvasElement, 0, 0);
if (this.lastPosition?.move && this.cfg.showMoveIndicator) {
await drawMoveIndicators(
this.tempCtx,
this.lastPosition.move,
this.squareSize,
this.style,
this.borderWidth,
this.cfg.tiles,
this.cfg.flipped,
this.margin,
this.loadImage
);
}
if (!this.cfg.showBorder && this.cfg.showCoords) {
drawCoords(
this.tempCtx,
this.style.coords,
this.squareSize,
this.cfg.tiles,
this.cfg.flipped,
this.borderWidth,
this.size,
this.cfg.showBorder,
this.margin
);
}
if (!this.lastPosition) {
return;
}
await drawPieces(
this.tempCtx,
this.lastPosition,
this.squareSize,
this.borderWidth,
this.cfg.flipped,
this.margin,
this.cfg.piecesStyle,
this.cfg.showShadows,
this.loadImage
);
if (this.cfg.showExtraInfo && header) {
await drawExtraInfo(
this.tempCtx,
this.width,
this.height,
this.scale,
this.margin,
this.style,
this.getFinalHeader(),
this.cfg.flipped,
this.lastPosition
);
}
}
render() {
this.ctx.clearRect(0, 0, this.size, this.size);
this.ctx.drawImage(this.tempCanvas, 0, 0);
}
async renderStatic() {
this.ctx.clearRect(0, 0, this.size, this.size);
const { background, dark, light, border } = this.style;
drawRectangle(
this.ctx,
this.width,
this.height,
0,
0,
border,
this.loadImage
);
drawRectangle(
this.ctx,
this.innerSize,
this.innerSize,
this.cfg.showBorder ? this.borderWidth : 0,
(this.cfg.showBorder ? this.borderWidth : 0) + this.margin,
background,
this.loadImage,
this.cfg.tiles
);
for (let rank = 0; rank < this.cfg.tiles; rank++) {
for (let file = 0; file < this.cfg.tiles; file++) {
const style =
(file % 2 === 0 && rank % 2 === 0) ||
(file % 2 !== 0 && rank % 2 !== 0)
? light
: dark;
const x = file * this.squareSize + this.borderWidth;
const y = rank * this.squareSize + this.borderWidth + this.margin;
drawRectangle(
this.ctx,
this.squareSize,
this.squareSize,
x,
y,
style,
this.loadImage
);
}
}
}
toImgUrl() {
return this.canvas.toDataURL();
}
toImageData() {
return this.ctx.getImageData(0, 0, this.width, this.height);
}
toClampedArray() {
return this.ctx.getImageData(0, 0, this.width, this.height).data;
}
toImgElement() {
const dataUrl = this.toImgUrl();
const img = new Image();
img.classList.add("board");
img.src = dataUrl;
return img;
}
get width() {
return this.size;
}
get height() {
return this.size + this.margin * 2;
}
toGIF() {}
}
export default Board;