diff --git a/index.html b/index.html index d7ef81d..a564e18 100644 --- a/index.html +++ b/index.html @@ -3,12 +3,15 @@ - + - shortcastle + SHORTCASTLE
diff --git a/public/img/logo-dark.svg b/public/img/logo-dark.svg index 9dcf106..8de93c5 100644 --- a/public/img/logo-dark.svg +++ b/public/img/logo-dark.svg @@ -1,7 +1,7 @@ - - + + diff --git a/public/img/logo.svg b/public/img/logo.svg index c23b702..ae482d7 100644 --- a/public/img/logo.svg +++ b/public/img/logo.svg @@ -1,7 +1,7 @@ - - + + diff --git a/public/img/pattern-light.png b/public/img/pattern-light.png index 6a5f0d7..96ce5fe 100644 Binary files a/public/img/pattern-light.png and b/public/img/pattern-light.png differ diff --git a/src/board/Board.ts b/src/board/Board.ts index c7cd4d9..2572089 100644 --- a/src/board/Board.ts +++ b/src/board/Board.ts @@ -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; } diff --git a/src/board/layers/drawCoords.ts b/src/board/layers/drawCoords.ts index c9e27b1..6d117ef 100644 --- a/src/board/layers/drawCoords.ts +++ b/src/board/layers/drawCoords.ts @@ -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 = ( diff --git a/src/encoders/createAnimation.ts b/src/encoders/createAnimation.ts index f8ee71b..1972654 100644 --- a/src/encoders/createAnimation.ts +++ b/src/encoders/createAnimation.ts @@ -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); diff --git a/src/game/Game.ts b/src/game/Game.ts index 98609fa..1785a1a 100644 --- a/src/game/Game.ts +++ b/src/game/Game.ts @@ -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(); } diff --git a/src/imports/importFromLink.ts b/src/imports/importFromLink.ts index e2fbd6f..8823a2d 100644 --- a/src/imports/importFromLink.ts +++ b/src/imports/importFromLink.ts @@ -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 => { - const [first, second] = link - .replace(/^https:\/\/(www\.)*lichess\.org\/*/, "") +const importFromLichess = async (url: URL): Promise => { + const [first, second] = url.pathname + .replace(/^\//, "") .split("/") .map((x) => x.trim()); @@ -24,7 +24,10 @@ const importFromLichess = async (link: string): Promise => { 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 => { }; const importFromLink = async (link: string): Promise => { - 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" }; diff --git a/src/main.tsx b/src/main.tsx index 61985e2..0464d5e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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 */ diff --git a/src/state.ts b/src/state.ts index 19e69d3..539d7d4 100644 --- a/src/state.ts +++ b/src/state.ts @@ -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", }; diff --git a/src/ui/App.css b/src/ui/App.css index 5bf13aa..3e21d5a 100644 --- a/src/ui/App.css +++ b/src/ui/App.css @@ -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; diff --git a/src/ui/components/GameTabs.css b/src/ui/components/GameTabs.css index 4e1ac47..da829b7 100644 --- a/src/ui/components/GameTabs.css +++ b/src/ui/components/GameTabs.css @@ -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 { diff --git a/src/ui/components/GameTabs.tsx b/src/ui/components/GameTabs.tsx index 1865d22..2e8089a 100644 --- a/src/ui/components/GameTabs.tsx +++ b/src/ui/components/GameTabs.tsx @@ -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 }> = ( + diff --git a/src/ui/components/Header.css b/src/ui/components/Header.css index b3e85c8..7da4c43 100644 --- a/src/ui/components/Header.css +++ b/src/ui/components/Header.css @@ -5,7 +5,7 @@ top: 0; width: 100%; display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 2fr 1fr; font-size: 1.8rem; } diff --git a/src/ui/components/Info.css b/src/ui/components/Info.css new file mode 100644 index 0000000..e6eb62d --- /dev/null +++ b/src/ui/components/Info.css @@ -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; +} diff --git a/src/ui/components/Info.tsx b/src/ui/components/Info.tsx new file mode 100644 index 0000000..9efa4f1 --- /dev/null +++ b/src/ui/components/Info.tsx @@ -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 ( +
+
+

+ + {state.game.header.WhitePretty}{" "} + + {state.game.header.WhiteElo ?? "????"} + +

+

+ + {state.game.header.BlackPretty}{" "} + + {state.game.header.BlackElo ?? "????"} + +

+
+
+ +

{state.game.header.Event}

+
+ +

Round {state.game.header.Round}

+
+
+
+ +

+ + + {state.game.header.Site?.replace(/^https:\/\//, "")} + + +

+
+ +

{state.game.header.DatePretty}

+
+
+
+ ); +}; + +export default Info; diff --git a/src/ui/components/Load.css b/src/ui/components/Load.css index 0b752cd..16318c3 100644 --- a/src/ui/components/Load.css +++ b/src/ui/components/Load.css @@ -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 { diff --git a/src/ui/components/Moves.css b/src/ui/components/Moves.css index 7fb2f0d..6e69b17 100644 --- a/src/ui/components/Moves.css +++ b/src/ui/components/Moves.css @@ -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 { diff --git a/src/ui/components/Moves.tsx b/src/ui/components/Moves.tsx index 29d46ca..71a8461 100644 --- a/src/ui/components/Moves.tsx +++ b/src/ui/components/Moves.tsx @@ -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 ( + +

+ {state.game.getPosition(0).turn === "w" ? "White" : "Black"} to move. +

+
{(move, i) => { const [white, black] = move as [string, string]; diff --git a/src/ui/components/SetupTabs.css b/src/ui/components/SetupTabs.css index 2e7f989..81e7282 100644 --- a/src/ui/components/SetupTabs.css +++ b/src/ui/components/SetupTabs.css @@ -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 { diff --git a/src/ui/components/Share.tsx b/src/ui/components/Share.tsx index 6559179..3034480 100644 --- a/src/ui/components/Share.tsx +++ b/src/ui/components/Share.tsx @@ -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"} > @@ -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" + } > @@ -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" + } > @@ -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" > @@ -116,7 +124,7 @@ const Share: Component<{ handlers: Handlers }> = (props) => { -

Image

+