This commit is contained in:
Maciej Caderek
2022-02-17 03:36:40 +01:00
parent 3095c3b55e
commit 10cea708f0
25 changed files with 326 additions and 115 deletions

View File

@@ -3,12 +3,15 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"
/>
<link
rel="stylesheet"
href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css"
/>
<title>shortcastle</title>
<title>SHORTCASTLE</title>
</head>
<body class="dark">
<div id="root"></div>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -331,6 +331,10 @@ class Board {
}
toImageData() {
return this.ctx.getImageData(0, 0, this.width, this.height);
}
toClampedArray() {
return this.ctx.getImageData(0, 0, this.width, this.height).data;
}

View File

@@ -1,6 +1,6 @@
import { Coords } from "../../types";
const BASE_FONT_SIZE = 20;
const BASE_FONT_SIZE = 24;
const FONT_FAMILY = "Fira Mono";
const drawCoords = (

View File

@@ -8,9 +8,9 @@ import MP4 from "./MP4";
const getData = (board: Board, encoder: GIF | WebM | MP4) => {
return encoder instanceof GIF
? board.toImgElement()
: encoder instanceof MP4
? board.toImageData()
: encoder instanceof MP4
? board.toClampedArray()
: board.canvas;
};
@@ -18,7 +18,8 @@ const createAnimation = async (
pgn: string,
boardConfig: BoardConfig,
format: "GIF" | "WebM" | "MP4",
size: Size
size: Size,
includeTitleScreen: boolean
) => {
const game = new Game().loadPGN(pgn);
const board = new Board({ ...boardConfig, size: sizeToPX[size] });
@@ -31,11 +32,13 @@ const createAnimation = async (
const header = game.header;
await board.titleFrame(header);
board.render();
if (includeTitleScreen) {
await board.titleFrame(header);
board.render();
// @ts-ignore
await encoder.add(getData(board, encoder), 4);
// @ts-ignore
await encoder.add(getData(board, encoder), 4);
}
for (let ply = 0; ply < game.length; ply++) {
const position = game.getPosition(ply);

View File

@@ -179,6 +179,19 @@ class Game {
};
}
getTitle({ anonymous }: { anonymous: boolean }) {
const header = this.header;
const w = anonymous ? "Anonymous" : header.WhitePretty;
const b = anonymous ? "Anonymous" : header.BlackPretty;
return (
`${w} vs ${b}` +
(header.Event ? ` | ${header.Event}` : "") +
(header.Round ? `, Round ${header.Round}` : "") +
(header.DatePretty ? ` | ${header.DatePretty}` : "")
);
}
get pgn() {
return this.game.pgn();
}

View File

@@ -2,9 +2,9 @@ type Result =
| { error: false; pgn: string; side: "w" | "b" }
| { error: true; errorType: "INCORRECT_LINK" | "SERVER_ERROR" };
const importFromLichess = async (link: string): Promise<Result> => {
const [first, second] = link
.replace(/^https:\/\/(www\.)*lichess\.org\/*/, "")
const importFromLichess = async (url: URL): Promise<Result> => {
const [first, second] = url.pathname
.replace(/^\//, "")
.split("/")
.map((x) => x.trim());
@@ -24,7 +24,10 @@ const importFromLichess = async (link: string): Promise<Result> => {
return {
error: false,
pgn,
side: String(second).startsWith("black") ? "b" : "w",
side:
String(second).startsWith("black") || url.hash.startsWith("black")
? "b"
: "w",
};
}
@@ -32,8 +35,16 @@ const importFromLichess = async (link: string): Promise<Result> => {
};
const importFromLink = async (link: string): Promise<Result> => {
if (/^https:\/\/(www\.)*lichess\.org/.test(link)) {
return importFromLichess(link);
let url;
try {
url = new URL(link);
} catch {
return { error: true, errorType: "INCORRECT_LINK" };
}
if (/^(www\.)*lichess\.org/.test(url.hostname)) {
return importFromLichess(url);
}
return { error: true, errorType: "INCORRECT_LINK" };

View File

@@ -103,11 +103,15 @@ const main = async () => {
await player.load(game);
setState("activeTab", "game");
document.title = `SHORTCASTLE - ${game.getTitle({ anonymous: false })}`;
},
async loadFEN(fen: string) {
const game = new Game().loadFEN(fen);
setState({ pgn: "", fen, moves: game.getMoves(), ply: 0, game });
window.location.hash = `v1/fen/${state.fen}`;
await player.load(game);
document.title = `SHORTCASTLE - FEN ${fen}`;
},
async importPGN(link: string) {
const result = await importFromLink(link);
@@ -135,7 +139,8 @@ const main = async () => {
state.pgn,
state.boardConfig,
state.gameConfig.format,
state.gameConfig.animationSize
state.gameConfig.animationSize,
state.gameConfig.titleScreen
);
download(data, "game", state.gameConfig.format.toLowerCase());
},
@@ -165,59 +170,61 @@ const main = async () => {
/* Register events */
const keyMapping: { [key: string]: () => void } = {
ArrowLeft: handlers.prev,
ArrowRight: handlers.next,
ArrowUp: handlers.first,
ArrowDown: handlers.last,
" ": handlers.togglePlay,
b: handlers.toggleBorder,
f: handlers.flip,
e: handlers.toggleExtraInfo,
};
if (!state.mobile) {
const keyMapping: { [key: string]: () => void } = {
ArrowLeft: handlers.prev,
ArrowRight: handlers.next,
ArrowUp: handlers.first,
ArrowDown: handlers.last,
" ": handlers.togglePlay,
b: handlers.toggleBorder,
f: handlers.flip,
e: handlers.toggleExtraInfo,
};
document.addEventListener("keydown", (e) => {
const target = e.target as HTMLElement | null;
document.addEventListener("keydown", (e) => {
const target = e.target as HTMLElement | null;
if (
keyMapping[e.key] &&
target?.nodeName !== "INPUT" &&
target?.nodeName !== "TEXTAREA"
) {
keyMapping[e.key]();
}
});
if (
keyMapping[e.key] &&
target?.nodeName !== "INPUT" &&
target?.nodeName !== "TEXTAREA"
) {
keyMapping[e.key]();
}
});
const preventDefaults = (e: Event) => {
e.preventDefault();
e.stopPropagation();
};
const preventDefaults = (e: Event) => {
e.preventDefault();
e.stopPropagation();
};
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
document.addEventListener(eventName, preventDefaults, false);
});
["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);
}
});
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);
}
});
} else {
const hammer = new Hammer.Manager(board.canvas);
hammer.add(new Hammer.Swipe());
hammer.add(new Hammer.Pinch());
hammer.add(new Hammer.Press({ time: 500 }));
hammer.add(new Hammer.Tap({ taps: 1 }));
const hammer = new Hammer.Manager(board.canvas);
hammer.add(new Hammer.Swipe());
hammer.add(new Hammer.Pinch());
hammer.add(new Hammer.Press({ time: 500 }));
hammer.add(new Hammer.Tap({ taps: 1 }));
hammer.on("swiperight", handlers.next);
hammer.on("swipeleft", handlers.prev);
hammer.on("swipeup", handlers.first);
hammer.on("swipedown", handlers.last);
hammer.on("pinchin", handlers.showBorder);
hammer.on("pinchout", handlers.hideBorder);
hammer.on("tap", handlers.next);
hammer.on("press", handlers.flip);
hammer.on("swiperight", handlers.next);
hammer.on("swipeleft", handlers.prev);
hammer.on("swipeup", handlers.first);
hammer.on("swipedown", handlers.last);
hammer.on("pinchin", handlers.showBorder);
hammer.on("pinchout", handlers.hideBorder);
hammer.on("tap", handlers.next);
hammer.on("press", handlers.flip);
}
};
/* Boot */

View File

@@ -3,12 +3,14 @@ import { createStore } from "solid-js/store";
import Game from "./game/Game";
import { BoardConfig, GameConfig } from "./types";
const mobile = isMobile();
const boardConfig: BoardConfig = {
size: 1024,
tiles: 8,
boardStyle: "calm",
piecesStyle: "tatiana",
showBorder: true,
showBorder: !mobile,
showExtraInfo: true,
showMaterial: true,
showMoveIndicator: true,
@@ -48,7 +50,7 @@ const initialState: State = {
fen: "",
moves: [],
ply: 0,
mobile: isMobile(),
mobile,
activeTab: "load",
};

View File

@@ -26,16 +26,17 @@ body {
}
.dark {
background-color: #232831;
background-color: #313742;
background-image: url(/img/pattern.png);
color: #ddd;
background-size: 12rem;
color: rgb(212, 221, 224);
--logo-url: url(/img/logo.svg);
--color-btn: rgb(0, 173, 136);
--color-btn-light: rgb(0, 207, 162);
--color-tab: #899399;
--color-tab-light: #a9b4bd;
--color-bg-block: #0e0e13;
--color-bg-block: #17171f;
--color-bg-input: #20242a;
--color-border-input: #2d323a;
--color-highlight: #ffffff22;
@@ -43,26 +44,30 @@ body {
--color-text-contrast: #0e0e13;
--color-text-input: #acbddb;
--color-text-dimmed: #677794;
--color-scrollbar: rgb(0, 59, 47);
--color-scrollbar-track: #ffffff22;
}
.light {
background-color: #c1ced4;
background-color: #b2bcc0;
background-image: url(/img/pattern-light.png);
color: #222;
color: rgb(29, 31, 32);
--logo-url: url(/img/logo-dark.svg);
--color-btn: rgb(0, 148, 116);
--color-btn-light: rgb(0, 114, 89);
--color-tab: #5d6468;
--color-tab-light: #3e4346;
--color-bg-block: #f1f1f1;
--color-bg-input: #fcfcfc;
--color-bg-block: #dddddd;
--color-bg-input: #eeeeee;
--color-border-input: #7f8999;
--color-highlight: #00000022;
--color-text: rgb(46, 54, 58);
--color-text-contrast: #fff;
--color-text-input: #46494e;
--color-text-dimmed: #767980;
--color-scrollbar: rgb(133, 184, 173);
--color-scrollbar-track: #00000022;
}
.upload {
@@ -106,6 +111,7 @@ textarea {
border: solid 1px var(--color-border-input);
color: var(--color-text-input);
outline: none;
resize: none;
}
input:focus,
@@ -116,7 +122,7 @@ textarea:focus {
h2 {
color: var(--color-text);
text-align: left;
font-size: 1.8rem;
font-size: 1.5rem;
margin: 2.5rem 0 1.5rem 0;
font-weight: 500;
}
@@ -134,8 +140,29 @@ hr {
border-top: solid 1px var(--color-highlight);
}
a,
a:visited,
a:active {
color: var(--color-btn);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.invisible {
opacity: 0;
}
.board-box {
height: 100vh;
grid-area: board;
padding: var(--header-margin) 0 2rem 0;
}
.board {
/* box-shadow: 0 0 30px rgba(0, 0, 0, 0.5); */
border: solid 1rem var(--color-bg-block);
border-radius: 5px;
max-width: 100%;
max-height: 100%;
@@ -159,13 +186,6 @@ hr {
height: 100vh;
}
.board-box {
/* background: rgba(255, 166, 0, 0.1); */
height: 100vh;
grid-area: board;
padding: var(--header-margin) 0 2rem 0;
}
@media screen and (max-width: 1024px) {
.layout {
grid-template-columns: 1fr;

View File

@@ -2,14 +2,20 @@
grid-area: moves;
padding: 20px;
padding-top: var(--header-margin);
min-width: 360px;
min-width: 375px;
height: 100vh;
}
@media screen and (max-width: 1024px) {
.game-box {
height: auto;
}
}
.game-tabs {
height: 100%;
display: grid;
grid-template-rows: 38px 1fr 84px;
grid-template-rows: 38px 195px 1fr 84px;
}
.game-tabs__btn {

View File

@@ -1,6 +1,7 @@
import { Component, Switch, Match } from "solid-js";
import Moves from "./Moves";
import Controls from "./Controls";
import Info from "./Info";
import Load from "./Load";
import { Handlers } from "../../types";
import "./GameTabs.css";
@@ -33,6 +34,7 @@ const GameTabs: Component<{ moves: readonly string[]; handlers: Handlers }> = (
</div>
<Switch>
<Match when={state.activeTab === "game"}>
<Info handlers={props.handlers}></Info>
<Moves moves={props.moves} handlers={props.handlers} />
<Controls handlers={props.handlers} />
</Match>

View File

@@ -5,7 +5,7 @@
top: 0;
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: 2fr 1fr;
font-size: 1.8rem;
}

View File

@@ -0,0 +1,54 @@
.info-box {
grid-area: controls;
padding: 0 20px 20px 20px;
}
.info {
background: var(--color-bg-block);
padding: 30px 20px;
font-size: 1.5rem;
text-align: left;
}
.info__players {
position: relative;
line-height: 2rem;
margin-bottom: 2rem;
padding: 0 1rem;
}
.info__rating {
font-family: "Fira Code", monospace;
color: var(--color-text-dimmed);
position: absolute;
right: 1rem;
}
.info__color {
border-radius: 1rem;
padding: 0;
border: solid 2px var(--color-tab);
width: 1.4rem;
height: 1.4rem;
margin-right: 0.8rem;
}
.info__color--white {
background-color: #eee;
}
.info__color--black {
background-color: #111;
}
.info__event {
margin-bottom: 1rem;
}
.info__event,
.info__site {
font-size: 1.3rem;
line-height: 1.5rem;
color: var(--color-text);
padding: 0 1rem;
}

View File

@@ -0,0 +1,55 @@
import { Component, Show } from "solid-js";
import { Handlers } from "../../types";
import { state } from "../../state";
import "./Info.css";
import isSafeLink from "../../utils/isSafeLink";
const Info: Component<{ handlers: Handlers }> = () => {
return (
<div class="info">
<div className="info__players">
<p>
<button className="info__color info__color--white"></button>
{state.game.header.WhitePretty}{" "}
<span className="info__rating">
{state.game.header.WhiteElo ?? "????"}
</span>
</p>
<p>
<button className="info__color info__color--black"></button>
{state.game.header.BlackPretty}{" "}
<span className="info__rating">
{state.game.header.BlackElo ?? "????"}
</span>
</p>
</div>
<div className="info__event">
<Show when={state.game.header.Event}>
<p>{state.game.header.Event}</p>
</Show>
<Show when={state.game.header.Round}>
<p>Round {state.game.header.Round}</p>
</Show>
</div>
<div className="info__site">
<Show when={state.game.header.Site}>
<p>
<Show
when={isSafeLink(state.game.header.Site)}
fallback={state.game.header.Site}
>
<a href={state.game.header.Site ?? ""}>
{state.game.header.Site?.replace(/^https:\/\//, "")}
</a>
</Show>
</p>
</Show>
<Show when={state.game.header.DatePretty}>
<p>{state.game.header.DatePretty}</p>
</Show>
</div>
</div>
);
};
export default Info;

View File

@@ -3,7 +3,7 @@
padding: 20px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
grid-row-end: span 2;
grid-row-end: span 3;
}
.load__pgn-input {

View File

@@ -2,6 +2,11 @@
font-size: 1.4rem;
font-family: "Fira Mono";
text-align: left;
background-color: var(--color-bg-input);
}
.moves__turn {
text-align: center;
}
.move {

View File

@@ -1,4 +1,4 @@
import { Component, For, createEffect } from "solid-js";
import { Component, For, Show, createEffect } from "solid-js";
import chunk_ from "@arrows/array/chunk_";
import { Handlers } from "../../types";
import Scrollable from "./reusable/Scrollable";
@@ -16,6 +16,11 @@ const Moves: Component<{ moves: readonly string[]; handlers: Handlers }> = (
return (
<Scrollable class="moves">
<Show when={props.moves.length === 0}>
<p class="moves__turn">
{state.game.getPosition(0).turn === "w" ? "White" : "Black"} to move.
</p>
</Show>
<For each={chunk_(2, props.moves as string[])}>
{(move, i) => {
const [white, black] = move as [string, string];

View File

@@ -3,7 +3,13 @@
grid-area: setup;
padding: 20px;
padding-top: var(--header-margin);
min-width: 360px;
min-width: 375px;
}
@media screen and (max-width: 1024px) {
.setup-box {
height: auto;
}
}
.setup {

View File

@@ -43,7 +43,7 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
"options__button--active": state.boardConfig.showBorder,
}}
onClick={props.handlers.toggleBorder}
title="BORDER"
title={state.boardConfig.showBorder ? "HIDE BORDER" : "SHOW BORDER"}
>
<i class="las la-expand"></i>
</button>
@@ -53,7 +53,11 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
"options__button--active": state.boardConfig.showExtraInfo,
}}
onClick={props.handlers.toggleExtraInfo}
title="EXTRA INFO"
title={
state.boardConfig.showExtraInfo
? "HIDE EXTRA INFO"
: "SHOW EXTRA INFO"
}
>
<i class="las la-info-circle"></i>
</button>
@@ -63,7 +67,11 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
"options__button--active": state.gameConfig.titleScreen,
}}
onClick={props.handlers.toggleTitleScreen}
title="TITLE SCREEN"
title={
state.gameConfig.titleScreen
? "EXCLUDE TITLE SCREEN"
: "INCLUDE TITLE SCREEN"
}
>
<i class="las la-heading"></i>
</button>
@@ -74,7 +82,7 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
"options__button--active": state.boardConfig.anonymous,
}}
onClick={props.handlers.toggleAnonymous}
title="ANONYMOUS"
title="TOGGLE ANONYMOUS"
>
<i class="las la-user-secret"></i>
</button>
@@ -116,7 +124,7 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
</button>
</div>
<Show when={!state.mobile}>
<h3>Image</h3>
<hr class="invisible" />
<button
classList={{
share__size: true,
@@ -215,19 +223,9 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
<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 title = state.game.getTitle({
anonymous: state.boardConfig.anonymous,
});
const md = `[${title}](${window.location.href})`;
@@ -241,7 +239,7 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
</div>
<Show when={!state.mobile}>
<div class="share__animation">
<h3>Animation</h3>
<hr className="invisible" />
<button
classList={{
share__size: true,

View File

@@ -1,7 +1,7 @@
.scrollable {
background: var(--color-bg-block);
height: auto;
padding: 40px 20px;
padding: 20px 10px 20px 20px;
height: 100%;
display: flex;
overflow: auto;
@@ -11,6 +11,7 @@
overflow-y: auto;
overflow-x: hidden;
padding: 0;
padding-right: 10px;
width: 100%;
}
@@ -20,10 +21,11 @@
}
.scrollable__content::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
background-color: var(--color-scrollbar-track);
/* box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5); */
}
.scrollable__content::-webkit-scrollbar-thumb {
background-color: rgb(0, 59, 47);
outline: 1px solid rgb(0, 59, 47);
background-color: var(--color-scrollbar);
outline: 1px solid var(--color-scrollbar);
}

View File

@@ -6,6 +6,7 @@ const download = (data: string | Blob, name: string, ext: string) => {
link.download = `${name}_${Date.now()}.${ext}`;
link.target = "_blank";
link.click();
URL.revokeObjectURL(url);
};
export default download;

14
src/utils/isSafeLink.ts Normal file
View File

@@ -0,0 +1,14 @@
const isSafeLink = (text: string | null) => {
if (text === null) {
return false;
}
try {
const url = new URL(text);
return url.protocol === "https:";
} catch {
return false;
}
};
export default isSafeLink;