Render the Go board and connect it to the core #42
|
@ -1,6 +1,6 @@
|
|||
|
||||
SOURCES = $(shell find ../core -name "*.rs")
|
||||
dist/core.d.ts: $(SOURCES)
|
||||
dist/index.ts: $(SOURCES)
|
||||
mkdir -p dist
|
||||
typeshare ../core --lang=typescript --output-file=dist/core.d.ts
|
||||
typeshare ../core --lang=typescript --output-file=dist/index.ts
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
"name": "core-types",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"types": "dist/core.d.ts",
|
||||
"types": "dist/index.ts",
|
||||
"main": "dist/index.ts",
|
||||
"scripts": {
|
||||
"build": "make",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const assertNever = (_: never) => {};
|
|
@ -1,25 +1,239 @@
|
|||
export class GoBoard extends HTMLCanvasElement {
|
||||
static get observedAttributes() {
|
||||
return [];
|
||||
import { BoardElement, Color, Size, CoreRequest } from "core-types";
|
||||
import { assertNever } from "../assertNever";
|
||||
|
||||
const MARGIN = 20;
|
||||
const BOARD_WIDTH = 800;
|
||||
const BOARD_HEIGHT = 800;
|
||||
|
||||
type Pixel = { x: number; y: number };
|
||||
type Coordinate = { column: number; row: number };
|
||||
|
||||
export interface GoBoardProps {
|
||||
board: BoardElement;
|
||||
onClick: (_: CoreRequest) => void;
|
||||
}
|
||||
|
||||
export class GoBoard {
|
||||
private board: BoardElement;
|
||||
private pen: Pen;
|
||||
private cursorLocation: Coordinate | null;
|
||||
canvas: HTMLCanvasElement;
|
||||
|
||||
constructor({ board, onClick }: GoBoardProps) {
|
||||
this.board = board;
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.canvas.classList.add("board");
|
||||
this.canvas.width = BOARD_WIDTH;
|
||||
this.canvas.height = BOARD_HEIGHT;
|
||||
|
||||
this.pen = new Pen(
|
||||
this.canvas.width,
|
||||
this.canvas.height,
|
||||
MARGIN,
|
||||
this.board.size.width,
|
||||
this.board.size.height
|
||||
);
|
||||
|
||||
this.cursorLocation = null;
|
||||
|
||||
this.canvas.onmousemove = (event) => {
|
||||
const bounds = this.canvas.getBoundingClientRect();
|
||||
const coordinate = {
|
||||
x: event.clientX - bounds.x,
|
||||
y: event.clientY - bounds.y,
|
||||
};
|
||||
|
||||
let address = this.pen.address(coordinate);
|
||||
|
||||
if (this.cursorLocation != address) {
|
||||
this.cursorLocation = this.pen.address(coordinate);
|
||||
this.renderBoard();
|
||||
}
|
||||
};
|
||||
|
||||
this.canvas.onclick = (_) => {
|
||||
if (this.cursorLocation) {
|
||||
const intersection =
|
||||
this.board.spaces[boardAddress(this.board.size, this.cursorLocation)];
|
||||
switch (intersection.type) {
|
||||
case "Unplayable":
|
||||
break;
|
||||
case "Empty":
|
||||
onClick(intersection.content);
|
||||
break;
|
||||
case "Filled":
|
||||
break;
|
||||
default:
|
||||
assertNever(intersection);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
setBoard(board: BoardElement) {
|
||||
this.board = board;
|
||||
|
||||
this.pen = new Pen(
|
||||
this.canvas.width,
|
||||
this.canvas.height,
|
||||
MARGIN,
|
||||
this.board.size.width,
|
||||
this.board.size.height
|
||||
);
|
||||
this.renderBoard();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.setAttribute("width", "500");
|
||||
this.setAttribute("height", "500");
|
||||
this.classList.add("go-board");
|
||||
const ctx = this.getContext("2d");
|
||||
renderBoard() {
|
||||
const ctx = this.canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
alert("could not get the canvas context");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
ctx.fillStyle = "rgb(128, 128, 128)";
|
||||
ctx.fillRect(10, 10, 50, 50);
|
||||
|
||||
ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
|
||||
ctx.fillRect(30, 30, 50, 50);
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.beginPath();
|
||||
for (var col = 0; col < this.board.size.width; col++) {
|
||||
ctx.moveTo(MARGIN + col * this.pen.hspaceBetween, MARGIN);
|
||||
ctx.lineTo(
|
||||
MARGIN + col * this.pen.hspaceBetween,
|
||||
MARGIN + (this.board.size.height - 1) * this.pen.vspaceBetween
|
||||
);
|
||||
}
|
||||
for (var row = 0; row < this.board.size.height; row++) {
|
||||
ctx.moveTo(MARGIN, MARGIN + row * this.pen.vspaceBetween);
|
||||
ctx.lineTo(
|
||||
MARGIN + (this.board.size.width - 1) * this.pen.hspaceBetween,
|
||||
MARGIN + row * this.pen.vspaceBetween
|
||||
);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
|
||||
this.pen.starPoint(ctx, { column: 3, row: 3 });
|
||||
this.pen.starPoint(ctx, { column: 3, row: 9 });
|
||||
this.pen.starPoint(ctx, { column: 3, row: 15 });
|
||||
this.pen.starPoint(ctx, { column: 9, row: 3 });
|
||||
this.pen.starPoint(ctx, { column: 9, row: 9 });
|
||||
this.pen.starPoint(ctx, { column: 9, row: 15 });
|
||||
this.pen.starPoint(ctx, { column: 15, row: 3 });
|
||||
this.pen.starPoint(ctx, { column: 15, row: 9 });
|
||||
this.pen.starPoint(ctx, { column: 15, row: 15 });
|
||||
|
||||
col = 0;
|
||||
row = 0;
|
||||
for (let idx = 0; idx < this.board.spaces.length; idx++) {
|
||||
const space = this.board.spaces[idx];
|
||||
switch (space.type) {
|
||||
case "Filled":
|
||||
this.pen.stone(ctx, { column: col, row: row }, space.content.color);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
col = col + 1;
|
||||
if (col == this.board.size.width) {
|
||||
col = 0;
|
||||
row = row + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.cursorLocation) {
|
||||
this.pen.ghostStone(ctx, this.cursorLocation, Color.White);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Pen {
|
||||
margin: number;
|
||||
hspaceBetween: number;
|
||||
vspaceBetween: number;
|
||||
|
||||
constructor(
|
||||
width: number,
|
||||
height: number,
|
||||
margin: number,
|
||||
columns: number,
|
||||
rows: number
|
||||
) {
|
||||
this.margin = margin;
|
||||
this.hspaceBetween = (width - margin * 2) / (columns - 1);
|
||||
this.vspaceBetween = (height - margin * 2) / (rows - 1);
|
||||
}
|
||||
|
||||
starPoint(ctx: CanvasRenderingContext2D, addr: Coordinate) {
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 1.0);";
|
||||
ctx.beginPath();
|
||||
const pixel = this.position(addr);
|
||||
ctx.moveTo(pixel.x, pixel.y);
|
||||
ctx.arc(pixel.x, pixel.y, 5, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ghostStone(ctx: CanvasRenderingContext2D, addr: Coordinate, color: Color) {
|
||||
switch (color) {
|
||||
case Color.White:
|
||||
ctx.fillStyle = "rgba(230, 230, 230, 0.5)";
|
||||
break;
|
||||
case Color.Black:
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
|
||||
break;
|
||||
}
|
||||
|
||||
this.drawStone(ctx, addr);
|
||||
}
|
||||
|
||||
stone(ctx: CanvasRenderingContext2D, addr: Coordinate, color: Color) {
|
||||
switch (color) {
|
||||
case Color.White:
|
||||
ctx.fillStyle = "rgb(230, 230, 230)";
|
||||
break;
|
||||
case Color.Black:
|
||||
ctx.fillStyle = "rgb(0, 0, 0)";
|
||||
break;
|
||||
}
|
||||
|
||||
this.drawStone(ctx, addr);
|
||||
}
|
||||
|
||||
drawStone(ctx: CanvasRenderingContext2D, addr: Coordinate) {
|
||||
ctx.beginPath();
|
||||
const radius = this.hspaceBetween / 2 - 2;
|
||||
const pixel = this.position(addr);
|
||||
ctx.moveTo(pixel.x, pixel.y);
|
||||
ctx.arc(pixel.x, pixel.y, radius, 0, 2.0 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
position(addr: Coordinate): Pixel {
|
||||
return {
|
||||
x: this.margin + addr.column * this.hspaceBetween,
|
||||
y: this.margin + addr.row * this.vspaceBetween,
|
||||
};
|
||||
}
|
||||
|
||||
address(pixel: Pixel): Coordinate | null {
|
||||
if (
|
||||
Math.round(pixel.x) < this.margin ||
|
||||
Math.round(pixel.y) < this.margin
|
||||
) {
|
||||
return null;
|
||||
} else {
|
||||
return {
|
||||
column: Math.round(
|
||||
(Math.round(pixel.x) - this.margin) / this.hspaceBetween
|
||||
),
|
||||
row: Math.round(
|
||||
(Math.round(pixel.y) - this.margin) / this.vspaceBetween
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const boardAddress = (size: Size, coordinate: Coordinate): number =>
|
||||
coordinate.column + size.width * coordinate.row;
|
||||
|
|
|
@ -13,7 +13,10 @@ body {
|
|||
display: grid;
|
||||
}
|
||||
|
||||
.go-board {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
canvas {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.board {
|
||||
background-color: rgb(150, 150, 150);
|
||||
}
|
||||
|
|
|
@ -1,28 +1,62 @@
|
|||
import { GoBoard } from "./components/Board";
|
||||
import { CoreRequest, CoreResponse } from "core-types";
|
||||
import { CoreApi, initCore } from "./coreApi";
|
||||
// import { assertNever } from "./assertNever";
|
||||
|
||||
window.customElements.define("go-board", GoBoard, { extends: "canvas" });
|
||||
class UIState {
|
||||
private currentView: GoBoard | null;
|
||||
private rootElement: HTMLElement;
|
||||
coreApi: CoreApi;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"go-board": GoBoard;
|
||||
constructor(coreApi: CoreApi, root: HTMLElement) {
|
||||
this.currentView = null;
|
||||
this.rootElement = root;
|
||||
this.coreApi = coreApi;
|
||||
|
||||
if (!root) {
|
||||
console.log("root element not found");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
processResponse(response: CoreResponse) {
|
||||
switch (response.type) {
|
||||
case "PlayingFieldView":
|
||||
if (this.currentView) {
|
||||
this.currentView.setBoard(response.content.board);
|
||||
} else {
|
||||
this.currentView = new GoBoard({
|
||||
board: response.content.board,
|
||||
onClick: async (request: CoreRequest) => {
|
||||
const response = await this.coreApi.dispatch(request);
|
||||
this.processResponse(response);
|
||||
},
|
||||
});
|
||||
this.rootElement?.appendChild(this.currentView.canvas);
|
||||
this.currentView.renderBoard();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log("impossible branch: ", response);
|
||||
alert("impossible branch");
|
||||
// assertNever(response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
let coreApi = await initCore();
|
||||
let response = await coreApi.playingField();
|
||||
console.log("playing field response: ", response);
|
||||
|
||||
let response = await coreApi.dispatch({ type: "PlayingField" });
|
||||
const root = document.getElementById("root");
|
||||
|
||||
if (!root) {
|
||||
alert("could not retrieve the app root container");
|
||||
console.log("root element not present");
|
||||
return;
|
||||
}
|
||||
|
||||
const board = document.createElement("canvas", { is: "go-board" });
|
||||
console.log("constructed board: ", board);
|
||||
root.appendChild(board);
|
||||
const uiState = new UIState(coreApi, root);
|
||||
uiState.processResponse(response);
|
||||
};
|
||||
|
||||
main();
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
}
|
||||
},
|
||||
"kifu/pwa": {
|
||||
"name": "kifu-pwa",
|
||||
"version": "1.0.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
|
Loading…
Reference in New Issue