diff --git a/src/main.tsx b/src/main.tsx index 99ba28d..929a6ad 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,4 @@ import WebFont from "webfontloader"; -// import * as Hammer from "hammerjs"; import { render } from "solid-js/web"; import { BoardStyle } from "./types"; @@ -17,7 +16,6 @@ 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"; import importFromLink from "./imports/importFromLink"; import isFEN from "./utils/isFEN"; import isPGN from "./utils/isPGN"; @@ -230,6 +228,10 @@ const main = async () => { setState("boardConfig", "sounds", !state.boardConfig.sounds); saveConfig("board"); }, + toggleSpeech() { + setState("boardConfig", "speech", !state.boardConfig.speech); + saveConfig("board"); + }, }; /* Render the page */ @@ -332,32 +334,29 @@ const main = async () => { 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 })); - // 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 */ -WebFont.load({ - google: { - families: ["Ubuntu:500,700", "Fira Mono:500"], - }, - custom: { - families: ["Chess"], - urls: ["/fonts.css"], - }, - active: main, -}); +Promise.all([ + new Promise((resolve) => + WebFont.load({ + google: { + families: ["Ubuntu:500,700", "Fira Mono:500"], + }, + custom: { + families: ["Chess"], + urls: ["/fonts.css"], + }, + active: () => resolve(null), + }) + ).catch(() => null), + new Promise((resolve) => { + if (speechSynthesis.getVoices().length > 0) { + resolve(null); + } else { + window.speechSynthesis.onvoiceschanged = resolve; + } + }).catch(() => null), +]).then(() => main()); diff --git a/src/player/Player.ts b/src/player/Player.ts index 55bade3..0d4c9c6 100644 --- a/src/player/Player.ts +++ b/src/player/Player.ts @@ -3,6 +3,7 @@ import Board from "../board/Board"; import Game from "../game/Game"; import { setState, state } from "../state"; import sfx from "./sfx"; +import Speech, { sanToText } from "./speach"; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -11,11 +12,14 @@ class Player { private game: Game = new Game(); private ply: number = 0; private callback: (playing: boolean, ply: number) => void = () => {}; + private speech: Speech; public playing: boolean = false; private firstRender: Promise; constructor(private board: Board, private config: GameConfig) { + this.speech = new Speech(); + this.firstRender = this.board .frame(this.game.getPosition(0), this.game.header) .then((_) => this.board.render()); @@ -109,6 +113,10 @@ class Player { await this.board.frame(position, this.game.header); this.board.render(); + if (state.boardConfig.speech) { + this.speech.say(sanToText(position.move?.san as string)); + } + if (this.ply > 0 && state.boardConfig.sounds) { if (position.mate) { sfx.snap.play(); diff --git a/src/player/speach.ts b/src/player/speach.ts new file mode 100644 index 0000000..8401d02 --- /dev/null +++ b/src/player/speach.ts @@ -0,0 +1,66 @@ +const words: { [key: string]: string } = { + x: " takes ", + "+": " check!", + "#": " mate!", + "=": " promotes to a ", + P: "pawn ", + R: "rook ", + B: "bishop ", + N: "knight ", + Q: "queen ", + K: "king ", + "O-O": "short castle", + "O-O-O": "long castle", +}; + +const config = { + volume: 50, + rate: 2, + lang: "en-US", +}; + +const sanToText = (move: string) => { + if (move === "O-O" || move === "O-O-O") { + move = words[move]; + } else { + // Handles special cases like R2a6 ore Nbd2 + const special = move.match(/[a-h1-8][a-h][1-8]/); + + if (special) { + move = move.replace( + special[0], + `${special[0][0]} ${special[0][1]}${special[0][2]}` + ); + } + + move = move + .split("") + .map((x) => words[x] ?? x) + .join("") + .replace(/\s{2,}/g, " "); + } + + return move; +}; + +class Speach { + private voice: SpeechSynthesisVoice | undefined; + constructor() { + const voices = speechSynthesis.getVoices(); + this.voice = voices.find((voice) => voice.lang === config.lang); + } + + say(text: string) { + const info = new SpeechSynthesisUtterance(text); + info.volume = config.volume / 100; + info.lang = config.lang; + if (this.voice) { + info.voice = this.voice; + } + info.rate = 1 + config.rate / 10; + speechSynthesis.speak(info); + } +} + +export { sanToText }; +export default Speach; diff --git a/src/state.ts b/src/state.ts index ba71fab..178b35d 100644 --- a/src/state.ts +++ b/src/state.ts @@ -21,6 +21,7 @@ const initialBoardConfig: BoardConfig = { showCoords: true, showShadows: false, sounds: true, + speech: false, flipped: false, }; @@ -73,8 +74,6 @@ const initialState: State = { refreshHash: true, }; -console.log(initialState); - const [state, setState] = createStore(initialState); export { state, setState }; diff --git a/src/types.ts b/src/types.ts index 7eafd5e..cbdbea1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -90,6 +90,7 @@ export type BoardConfig = { showCoords: boolean; showShadows: boolean; sounds: boolean; + speech: boolean; flipped: boolean; }; @@ -173,6 +174,7 @@ export type Handlers = { downloadImage: () => Promise; downloadAnimation: () => Promise; toggleSound(): void; + toggleSpeech(): void; }; export type Header = { diff --git a/src/ui/components/Header.css b/src/ui/components/Header.css index f07edb2..6ae5f13 100644 --- a/src/ui/components/Header.css +++ b/src/ui/components/Header.css @@ -24,7 +24,7 @@ } .header__options { - padding: 0 2rem; + padding: 0 2rem 0; text-align: right; } @@ -36,8 +36,7 @@ color: var(--color-text); padding-top: 1rem; height: 100%; - margin-left: 10px; - /* background-color: aqua; */ + margin-left: 0.5rem; text-align: center; } diff --git a/src/ui/components/Header.tsx b/src/ui/components/Header.tsx index 35a9b50..63ad2cd 100644 --- a/src/ui/components/Header.tsx +++ b/src/ui/components/Header.tsx @@ -1,9 +1,9 @@ import { Component, createSignal } from "solid-js"; import { Handlers } from "../../types"; -// import { state, setState } from "../../state"; +import { state, setState } from "../../state"; import "./Header.css"; -const Header: Component<{ handlers: Handlers }> = () => { +const Header: Component<{ handlers: Handlers }> = (props) => { const [darkMode, setDarkMode] = createSignal(true); return ( @@ -15,6 +15,36 @@ const Header: Component<{ handlers: Handlers }> = () => { {/*
{}}>
*/} +
{ + props.handlers.toggleSound(); + }} + title={state.boardConfig.sounds ? "MUTE" : "SOUND ON"} + > + +
+
{ + props.handlers.toggleSpeech(); + }} + title={state.boardConfig.speech ? "SPEECH OFF" : "SPEECH ON"} + > + +
{ diff --git a/src/ui/components/Share.tsx b/src/ui/components/Share.tsx index be3cfaa..8e5128f 100644 --- a/src/ui/components/Share.tsx +++ b/src/ui/components/Share.tsx @@ -81,17 +81,6 @@ const Share: Component<{ handlers: Handlers; class?: string }> = (props) => { > -