From 6eb5672351bf18163d5c9be02e7feb5ee895d976 Mon Sep 17 00:00:00 2001 From: Michael Pilosov Date: Sun, 5 Apr 2026 22:51:03 -0600 Subject: [PATCH] Add frontend, Makefile, and additional built-in games --- Makefile | 26 + games.py | 97 ++++ static/app.js | 658 ++++++++++++++++++++++++ static/gif.js | 3 + static/gif.worker.js | 3 + static/img/chesspieces/wikipedia/bB.png | Bin 0 -> 1405 bytes static/img/chesspieces/wikipedia/bK.png | Bin 0 -> 3009 bytes static/img/chesspieces/wikipedia/bN.png | Bin 0 -> 1875 bytes static/img/chesspieces/wikipedia/bP.png | Bin 0 -> 777 bytes static/img/chesspieces/wikipedia/bQ.png | Bin 0 -> 2648 bytes static/img/chesspieces/wikipedia/bR.png | Bin 0 -> 748 bytes static/img/chesspieces/wikipedia/wB.png | Bin 0 -> 2374 bytes static/img/chesspieces/wikipedia/wK.png | Bin 0 -> 2823 bytes static/img/chesspieces/wikipedia/wN.png | Bin 0 -> 2388 bytes static/img/chesspieces/wikipedia/wP.png | Bin 0 -> 1571 bytes static/img/chesspieces/wikipedia/wQ.png | Bin 0 -> 3812 bytes static/img/chesspieces/wikipedia/wR.png | Bin 0 -> 1097 bytes static/index.html | 111 ++++ static/style.css | 339 ++++++++++++ 19 files changed, 1237 insertions(+) create mode 100644 Makefile create mode 100644 static/app.js create mode 100644 static/gif.js create mode 100644 static/gif.worker.js create mode 100644 static/img/chesspieces/wikipedia/bB.png create mode 100644 static/img/chesspieces/wikipedia/bK.png create mode 100644 static/img/chesspieces/wikipedia/bN.png create mode 100644 static/img/chesspieces/wikipedia/bP.png create mode 100644 static/img/chesspieces/wikipedia/bQ.png create mode 100644 static/img/chesspieces/wikipedia/bR.png create mode 100644 static/img/chesspieces/wikipedia/wB.png create mode 100644 static/img/chesspieces/wikipedia/wK.png create mode 100644 static/img/chesspieces/wikipedia/wN.png create mode 100644 static/img/chesspieces/wikipedia/wP.png create mode 100644 static/img/chesspieces/wikipedia/wQ.png create mode 100644 static/img/chesspieces/wikipedia/wR.png create mode 100644 static/index.html create mode 100644 static/style.css diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..afa8c93 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +.PHONY: dev serve lint fmt help + +# Development server with auto-reload +dev: + uv run uvicorn chess_pressure.app:app --host 0.0.0.0 --port 8888 --reload + +# Production server +serve: + uv run chess-pressure + +# Lint +lint: fmt + uvx ruff check chess_pressure/ + +# Format +fmt: + uvx ruff format chess_pressure/ + uvx ruff check --fix chess_pressure/ + +help: + @echo "chess-pressure" + @echo "" + @echo " make dev dev server with reload (:8888)" + @echo " make serve production server (:8888)" + @echo " make lint ruff format + check" + @echo " make fmt ruff format + auto-fix" diff --git a/games.py b/games.py index 0d93803..ff092b2 100644 --- a/games.py +++ b/games.py @@ -96,6 +96,103 @@ c5 15. b3 Nc6 16. d5 Ne7 17. Be3 Ng6 18. Qd2 Nh7 19. a4 Nh4 20. Nxh4 Qxh4 Qd7 39. Qa7 Rc7 40. Qb6 Rb7 41. Ra8+ Kf7 42. Qa6 Qc7 43. Qc6 Qb6+ 44. Kf1 Rb8 45. Ra6 1-0""", }, + { + "id": "polgar-anand", + "name": "Polgar vs Anand (1999)", + "white": "Polgar", + "black": "Anand", + "pgn": """[Event "Dos Hermanas"] +[Site "Dos Hermanas"] +[Date "1999.04.??"] +[White "Judit Polgar"] +[Black "Viswanathan Anand"] +[Result "1-0"] + +1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6 6.Be3 e6 7.g4 e5 8.Nf5 g6 +9.g5 gxf5 10.exf5 d5 11.Qf3 d4 12.O-O-O Nbd7 13.Bd2 dxc3 14.Bxc3 Bg7 15.Rg1 +O-O 16.gxf6 Qxf6 17.Qe3 Kh8 18.f4 Qb6 19.Qg3 Qh6 20.Rd6 f6 21.Bd2 e4 22.Bc4 +b5 23.Be6 Ra7 24.Rc6 a5 25.Be3 Rb7 26.Bd5 Rb8 27.Rc7 b4 28.b3 Rb5 29.Bc6 Rxf5 +30.Rxc8 Rxc8 31.Bxd7 Rcc5 32.Bxf5 Rxf5 33.Rd1 Kg8 34.Qg2 1-0""", + }, + { + "id": "polgar-kasparov", + "name": "Polgar vs Kasparov (2002)", + "white": "Polgar", + "black": "Kasparov", + "pgn": """[Event "Russia vs Rest of the World"] +[Site "Moscow"] +[Date "2002.09.09"] +[White "Judit Polgar"] +[Black "Garry Kasparov"] +[Result "1-0"] + +1.e4 e5 2.Nf3 Nc6 3.Bb5 Nf6 4.O-O Nxe4 5.d4 Nd6 6.Bxc6 dxc6 7.dxe5 Nf5 +8.Qxd8+ Kxd8 9.Nc3 h6 10.Rd1+ Ke8 11.h3 Be7 12.Ne2 Nh4 13.Nxh4 Bxh4 14.Be3 +Bf5 15.Nd4 Bh7 16.g4 Be7 17.Kg2 h5 18.Nf5 Bf8 19.Kf3 Bg6 20.Rd2 hxg4+ 21.hxg4 +Rh3+ 22.Kg2 Rh7 23.Kg3 f6 24.Bf4 Bxf5 25.gxf5 fxe5 26.Re1 Bd6 27.Bxe5 Kd7 +28.c4 c5 29.Bxd6 cxd6 30.Re6 Rah8 31.Rexd6+ Kc8 32.R2d5 Rh3+ 33.Kg2 Rh2+ +34.Kf3 R2h3+ 35.Ke4 b6 36.Rc6+ Kb8 37.Rd7 Rh2 38.Ke3 Rf8 39.Rcc7 Rxf5 +40.Rb7+ Kc8 41.Rdc7+ Kd8 42.Rxg7 Kc8 1-0""", + }, + { + "id": "hou-caruana", + "name": "Hou Yifan vs Caruana (2017)", + "white": "Hou Yifan", + "black": "Caruana", + "pgn": """[Event "GRENKE Chess Classic"] +[Site "Karlsruhe"] +[Date "2017.04.15"] +[White "Hou Yifan"] +[Black "Fabiano Caruana"] +[Result "1-0"] + +1.e4 e5 2.Nf3 Nc6 3.Bb5 Nf6 4.O-O Nxe4 5.Re1 Nd6 6.Nxe5 Be7 7.Bf1 O-O 8.d4 +Nf5 9.Nf3 d5 10.c3 Bd6 11.Nbd2 Nce7 12.Qc2 c6 13.Bd3 g6 14.Nf1 f6 15.h3 Rf7 +16.Bd2 Bd7 17.Re2 c5 18.dxc5 Bxc5 19.Bf4 Rc8 20.Rae1 g5 21.Ng3 Nxg3 22.Bxg3 +a5 23.Qd2 a4 24.b4 axb3 25.axb3 Ng6 26.h4 gxh4 27.Nxh4 Nxh4 28.Bxh4 Qf8 +29.Qf4 Bd6 30.Qd4 Rd8 31.Re3 Bc8 32.b4 Kg7 33.Bb5 Bc7 34.Re8 Qd6 35.Bg3 Qb6 +36.Qd3 Bd7 37.Bxd7 Rdxd7 38.Qf5 Bxg3 39.Qg4+ Kh6 40.Qh3+ 1-0""", + }, + { + "id": "ju-lei", + "name": "Ju Wenjun vs Lei Tingjie, WCC G12 (2023)", + "white": "Ju Wenjun", + "black": "Lei Tingjie", + "pgn": """[Event "Women's World Championship"] +[Site "Shanghai/Chongqing"] +[Date "2023.07.22"] +[White "Ju Wenjun"] +[Black "Lei Tingjie"] +[Result "1-0"] + +1.d4 d5 2.Nf3 Nf6 3.e3 c5 4.dxc5 e6 5.b4 a5 6.c3 axb4 7.cxb4 b6 8.Bb5+ Bd7 +9.Bxd7+ Nbxd7 10.a4 bxc5 11.b5 Qc7 12.Bb2 Bd6 13.O-O O-O 14.Nbd2 Rfc8 15.Qc2 +c4 16.Bc3 Nc5 17.a5 Nb3 18.Bxf6 Nxa1 19.Bxa1 Qxa5 20.Qc3 Qxc3 21.Bxc3 Rcb8 +22.Nd4 e5 23.Nf5 Bf8 24.Bxe5 Rxb5 25.g4 g6 26.Nd4 Rb2 27.Nb1 Bg7 28.Bxg7 Kxg7 +29.Nc3 Ra5 30.Rd1 Rb6 31.Nde2 Rb3 32.Kg2 h6 33.Kf3 f6 34.Rc1 Kf7 35.Nf4 d4 +36.exd4 g5 37.Ne2 f5 38.gxf5 Rxf5+ 39.Ke3 g4 40.Nf4 Rb8 41.d5 Rf6 42.Rc2 Ra8 +43.Nb5 Rb6 44.Nd4 Ra3+ 45.Ke4 c3 46.Nfe2 Rb2 47.Kd3 Rb1 48.Nxc3 Rh1 49.f3 gxf3 +50.Nxf3 Rf1 51.Nd4 Ke7 52.Kc4 Rf4 53.Rb2 Rh4 54.Rb7+ Kf6 55.Rb2 Ra8 56.Kc5 Rh3 +57.Ncb5 Re3 58.d6 Ke5 59.Nc6+ Ke4 60.d7 Rd3 61.Nd6+ Kf4 62.Rb8 1-0""", + }, + { + "id": "shirov-polgar", + "name": "Shirov vs Polgar (1994)", + "white": "Shirov", + "black": "Polgar", + "pgn": """[Event "Buenos Aires Sicilian"] +[Site "Buenos Aires"] +[Date "1994.10.22"] +[White "Alexei Shirov"] +[Black "Judit Polgar"] +[Result "0-1"] + +1.e4 c5 2.Nf3 e6 3.d4 cxd4 4.Nxd4 Nc6 5.Nc3 d6 6.g4 a6 7.Be3 Nge7 8.Nb3 b5 +9.f4 Bb7 10.Qf3 g5 11.fxg5 Ne5 12.Qg2 b4 13.Ne2 h5 14.gxh5 Nf5 15.Bf2 Qxg5 +16.Na5 Ne3 17.Qg3 Qxg3 18.Nxg3 Nxc2+ 19.Kd1 Nxa1 20.Nxb7 b3 21.axb3 Nxb3 +22.Kc2 Nc5 23.Nxc5 dxc5 24.Be1 Nf3 25.Bc3 Nd4+ 26.Kd3 Bd6 27.Bg2 Be5 28.Kc4 +Ke7 29.Ra1 Nc6 0-1""", + }, ] diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..62e945f --- /dev/null +++ b/static/app.js @@ -0,0 +1,658 @@ +/* Chess Pressure — main application */ + +(function () { + "use strict"; + + // --- State --- + let gameData = null; // {headers, moves, frames, result} + let currentIndex = 0; // which frame we're viewing + let playing = false; + let playTimer = null; + let pressureMode = "equal"; // "equal" or "weighted" + let board = null; + let forkPoint = null; // index where fork happened, null if on original line + let originalData = null; // saved original game before fork + let forkedMoves = []; // UCI moves made after fork point + let showPieces = true; + let showBoard = false; + let selectedSquare = null; // tap-to-move: currently selected source square + let legalMoves = []; // legal moves from selected square + + const PLAY_SPEED = 800; // ms between auto-advance + + // --- Pressure colors --- + function pressureColor(value, maxAbs) { + if (value === 0 || maxAbs === 0) return "transparent"; + const intensity = Math.min(Math.abs(value) / maxAbs, 1); + const alpha = 0.15 + intensity * 0.55; + if (value > 0) return `rgba(50, 130, 220, ${alpha})`; // blue = white + return `rgba(220, 50, 50, ${alpha})`; // red = black + } + + // --- Pressure rendering (applied directly to board squares) --- + const FILES = "abcdefgh"; + + const PRESSURE_SCALE = { equal: 5, weighted: 15 }; + + function renderPressure(frame) { + const key = pressureMode === "weighted" ? "pressure_weighted" : "pressure"; + const pressure = frame[key]; + const maxAbs = PRESSURE_SCALE[pressureMode]; + + for (let rank = 1; rank <= 8; rank++) { + for (let fileIdx = 0; fileIdx < 8; fileIdx++) { + const sq = FILES[fileIdx] + rank; + const pyIdx = (rank - 1) * 8 + fileIdx; // python-chess square index + const el = document.querySelector(`[data-square="${sq}"]`); + if (!el) continue; + + const val = pressure[pyIdx]; + const isLight = (rank + fileIdx) % 2 === 1; + + if (!showBoard) { + // Pure pressure view — theme-aware neutral background + const isDark = document.documentElement.dataset.theme !== "light"; + const neutral = isDark ? [26, 26, 26] : [245, 245, 245]; + if (val === 0) { + el.style.backgroundColor = `rgb(${neutral[0]}, ${neutral[1]}, ${neutral[2]})`; + } else { + const intensity = Math.min(Math.abs(val) / maxAbs, 1); + const alpha = 0.3 + intensity * 0.7; + const tint = val > 0 ? [50, 130, 220] : [220, 50, 50]; + const base = neutral; + const r = Math.round(base[0] * (1 - alpha) + tint[0] * alpha); + const g = Math.round(base[1] * (1 - alpha) + tint[1] * alpha); + const b = Math.round(base[2] * (1 - alpha) + tint[2] * alpha); + el.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; + } + } else { + const base = isLight ? [192, 192, 192] : [120, 120, 120]; // grayscale + if (val === 0) { + el.style.backgroundColor = `rgb(${base[0]}, ${base[1]}, ${base[2]})`; + } else { + const intensity = Math.min(Math.abs(val) / maxAbs, 1); + const alpha = 0.2 + intensity * 0.6; + const tint = val > 0 ? [50, 130, 220] : [220, 50, 50]; + const r = Math.round(base[0] * (1 - alpha) + tint[0] * alpha); + const g = Math.round(base[1] * (1 - alpha) + tint[1] * alpha); + const b = Math.round(base[2] * (1 - alpha) + tint[2] * alpha); + el.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; + } + } + } + } + } + + // --- Move list --- + // --- Tap-to-move --- + function clearSelection() { + selectedSquare = null; + legalMoves = []; + document.querySelectorAll(".square-selected, .square-target").forEach((el) => { + el.classList.remove("square-selected", "square-target"); + }); + } + + function handleSquareClick(square) { + if (!gameData) return; + const frame = gameData.frames[currentIndex]; + const fen = frame.board.fen; + + if (selectedSquare) { + // Check if this square is a legal target + const uci = selectedSquare + square; + const isTarget = legalMoves.some((m) => m === uci || m === uci + "q"); + if (isTarget) { + // Make the move + const moveUci = legalMoves.find((m) => m.startsWith(uci)) || uci; + clearSelection(); + doMove(fen, moveUci); + return; + } + } + + // Select a new piece (or deselect) + if (square === selectedSquare) { + clearSelection(); + return; + } + + // Fetch legal moves for this square + fetch("/api/move", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fen, uci: square + square }), // dummy — we just need to check if piece exists + }).catch(() => {}); + + // Check if there's a piece here by seeing if any legal move starts from this square + // We need to ask the server for legal moves from this position + fetch(`/api/legal?fen=${encodeURIComponent(fen)}`).then(r => r.ok ? r.json() : null).then(data => { + if (!data) return; + const fromMoves = data.filter((m) => m.startsWith(square)); + if (fromMoves.length === 0) { + clearSelection(); + return; + } + clearSelection(); + selectedSquare = square; + legalMoves = fromMoves; + + // Highlight + const srcEl = document.querySelector(`[data-square="${square}"]`); + if (srcEl) srcEl.classList.add("square-selected"); + fromMoves.forEach((m) => { + const target = m.substring(2, 4); + const el = document.querySelector(`[data-square="${target}"]`); + if (el) el.classList.add("square-target"); + }); + }); + } + + function doMove(fen, uci) { + fetch("/api/move", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fen, uci }), + }) + .then((r) => r.ok ? r.json() : null) + .then((data) => { + if (!data) return; + if (forkPoint === null && currentIndex < gameData.frames.length - 1) { + originalData = JSON.parse(JSON.stringify(gameData)); + forkPoint = currentIndex; + gameData.moves = gameData.moves.slice(0, currentIndex); + gameData.frames = gameData.frames.slice(0, currentIndex + 1); + forkedMoves = []; + } + gameData.moves.push({ san: data.san, uci: data.uci, ply: currentIndex + 1 }); + gameData.frames.push(data.frame); + if (forkPoint !== null) forkedMoves.push(data.uci); + goTo(gameData.frames.length - 1); + updateForkUI(); + }); + } + + // --- Move list --- + function renderMoveList() { + const el = document.getElementById("move-list"); + if (!gameData || !gameData.moves.length) { + el.innerHTML = 'No moves'; + return; + } + let html = ""; + for (let i = 0; i < gameData.moves.length; i += 2) { + const num = Math.floor(i / 2) + 1; + const w = gameData.moves[i]; + const b = gameData.moves[i + 1]; + const wClass = currentIndex === i + 1 ? "active" : ""; + const bClass = b && currentIndex === i + 2 ? "active" : ""; + const wForked = forkPoint !== null && i >= forkPoint ? " forked" : ""; + const bForked = forkPoint !== null && i + 1 >= forkPoint ? " forked" : ""; + html += `
`; + html += `${num}.`; + html += `${w.san}`; + if (b) { + html += `${b.san}`; + } + html += `
`; + } + el.innerHTML = html; + + // Scroll active into view within the move list only (not the page) + const active = el.querySelector(".move.active"); + if (active) { + const container = el; + const top = active.offsetTop - container.offsetTop; + const bottom = top + active.offsetHeight; + if (top < container.scrollTop) { + container.scrollTop = top; + } else if (bottom > container.scrollTop + container.clientHeight) { + container.scrollTop = bottom - container.clientHeight; + } + } + + // Click handlers + el.querySelectorAll(".move[data-idx]").forEach((m) => { + m.addEventListener("click", () => goTo(parseInt(m.dataset.idx))); + }); + } + + // --- Navigation --- + function goTo(index) { + if (!gameData) return; + index = Math.max(0, Math.min(index, gameData.frames.length - 1)); + currentIndex = index; + const frame = gameData.frames[index]; + board.position(frame.board.fen, false); + renderPressure(frame); + renderMoveList(); + updateSlider(); + updateStatus(frame); + } + + function updateSlider() { + const slider = document.getElementById("slider"); + slider.max = gameData ? gameData.frames.length - 1 : 0; + slider.value = currentIndex; + } + + function updateStatus(frame) { + const el = document.getElementById("status-bar"); + const b = frame.board; + let text = b.turn === "w" ? "White to move" : "Black to move"; + if (b.is_checkmate) text = (b.turn === "w" ? "Black" : "White") + " wins by checkmate"; + else if (b.is_stalemate) text = "Stalemate"; + else if (b.is_check) text += " (check)"; + if (gameData.result && gameData.result !== "*") text += ` — ${gameData.result}`; + text += ` | Move ${b.fullmove}`; + el.textContent = text; + } + + function updateGameInfo() { + const el = document.getElementById("game-info"); + if (!gameData || !gameData.headers) { + el.textContent = ""; + return; + } + const h = gameData.headers; + const parts = []; + if (h.White) parts.push(`${h.White} vs ${h.Black || "?"}`); + if (h.Event) parts.push(h.Event); + if (h.Date) parts.push(h.Date); + el.textContent = parts.join(" — "); + } + + // --- Playback --- + function togglePlay() { + playing = !playing; + document.getElementById("btn-play").innerHTML = playing ? "▮▮" : "▶"; + if (playing) { + playTimer = setInterval(() => { + if (currentIndex >= gameData.frames.length - 1) { + togglePlay(); + return; + } + goTo(currentIndex + 1); + }, PLAY_SPEED); + } else { + clearInterval(playTimer); + } + } + + // --- Interactive moves --- + function onDrop(source, target) { + if (source === target) { + // Tap — treat as click-to-select/move + handleSquareClick(source); + return "snapback"; + } + clearSelection(); + const frame = gameData.frames[currentIndex]; + const fen = frame.board.fen; + let uci = source + target; + // Auto-promote to queen for pawn reaching last rank + const rank = target.charAt(1); + const piece = frame.board.fen.split(" ")[0]; // just for checking + if ((rank === "8" || rank === "1") && uci.length === 4) { + uci += "q"; + } + doMove(fen, uci); + return "snapback"; + } + + function updateForkUI() { + const el = document.getElementById("fork-controls"); + if (forkPoint !== null) { + el.style.display = "flex"; + document.getElementById("fork-point").textContent = Math.floor(forkPoint / 2) + 1; + } else { + el.style.display = "none"; + } + } + + function resetFork() { + if (!originalData) return; + gameData = originalData; + originalData = null; + forkPoint = null; + forkedMoves = []; + goTo(0); + updateForkUI(); + } + + // --- Load game --- + async function loadGame(gameId) { + const r = await fetch(`/api/games/${gameId}`); + if (!r.ok) return; + gameData = await r.json(); + forkPoint = null; + originalData = null; + forkedMoves = []; + currentIndex = 0; + goTo(0); + updateGameInfo(); + updateForkUI(); + } + + async function loadPGN(pgn) { + const r = await fetch("/api/parse", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ pgn }), + }); + if (!r.ok) { + alert("Failed to parse PGN"); + return; + } + gameData = await r.json(); + forkPoint = null; + originalData = null; + forkedMoves = []; + currentIndex = 0; + goTo(0); + updateGameInfo(); + updateForkUI(); + document.getElementById("game-select").value = ""; + } + + // --- Init --- + async function init() { + // Build board + board = Chessboard("board", { + draggable: true, + position: "start", + pieceTheme: "/static/img/chesspieces/wikipedia/{piece}.png", + onDrop: onDrop, + }); + + // Tap destination squares (onDrop only fires on pieces, not empty squares) + document.getElementById("board").addEventListener("click", (e) => { + if (!selectedSquare) return; + const sqEl = e.target.closest("[data-square]"); + if (sqEl) handleSquareClick(sqEl.dataset.square); + }); + + // Restore theme before first render + const saved = localStorage.getItem("theme"); + if (saved) document.documentElement.dataset.theme = saved; + + // Load game list + const r = await fetch("/api/games"); + const games = await r.json(); + const select = document.getElementById("game-select"); + games.forEach((g) => { + const opt = document.createElement("option"); + opt.value = g.id; + opt.textContent = g.name; + select.appendChild(opt); + }); + + // Load first game by default + if (games.length) { + await loadGame(games[0].id); + select.value = games[0].id; + } + + // Show board after first render (prevents tan flash) + document.getElementById("board").classList.add("ready"); + + // --- Event listeners --- + select.addEventListener("change", (e) => { + if (e.target.value === "__new") { + loadPGN('[White "You"]\n[Black "Opponent"]\n[Result "*"]\n\n*'); + } else if (e.target.value) { + loadGame(e.target.value); + } + }); + + document.getElementById("slider").addEventListener("input", (e) => { + goTo(parseInt(e.target.value)); + }); + + document.getElementById("btn-start").addEventListener("click", () => goTo(0)); + document.getElementById("btn-prev").addEventListener("click", () => goTo(currentIndex - 1)); + document.getElementById("btn-play").addEventListener("click", togglePlay); + document.getElementById("btn-next").addEventListener("click", () => goTo(currentIndex + 1)); + document.getElementById("btn-end").addEventListener("click", () => + goTo(gameData ? gameData.frames.length - 1 : 0) + ); + + document.querySelectorAll('input[name="pmode"]').forEach((r) => { + r.addEventListener("change", (e) => { + pressureMode = e.target.value; + if (gameData) renderPressure(gameData.frames[currentIndex]); + }); + }); + + document.getElementById("toggle-pieces").addEventListener("change", (e) => { + showPieces = e.target.checked; + document.getElementById("board").classList.toggle("hide-pieces", !showPieces); + }); + + document.getElementById("toggle-board").addEventListener("change", (e) => { + showBoard = e.target.checked; + if (gameData) renderPressure(gameData.frames[currentIndex]); + }); + + document.getElementById("theme-toggle").addEventListener("click", () => { + const html = document.documentElement; + html.dataset.theme = html.dataset.theme === "dark" ? "light" : "dark"; + localStorage.setItem("theme", html.dataset.theme); + if (gameData) renderPressure(gameData.frames[currentIndex]); + }); + + // PGN upload dialog + const dialog = document.getElementById("pgn-dialog"); + document.getElementById("btn-upload").addEventListener("click", () => dialog.showModal()); + document.getElementById("pgn-cancel").addEventListener("click", () => dialog.close()); + document.getElementById("pgn-file-btn").addEventListener("click", () => + document.getElementById("pgn-file").click() + ); + document.getElementById("pgn-file").addEventListener("change", (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (ev) => { + document.getElementById("pgn-input").value = ev.target.result; + }; + reader.readAsText(file); + } + }); + document.getElementById("pgn-submit").addEventListener("click", () => { + const pgn = document.getElementById("pgn-input").value.trim(); + if (pgn) { + loadPGN(pgn); + dialog.close(); + } + }); + + // Fork reset + document.getElementById("btn-reset-fork").addEventListener("click", resetFork); + + // GIF export + const exportDialog = document.getElementById("export-dialog"); + document.getElementById("btn-export").addEventListener("click", () => exportDialog.showModal()); + document.getElementById("export-cancel").addEventListener("click", () => exportDialog.close()); + document.getElementById("export-go").addEventListener("click", exportGif); + + // Keyboard shortcuts + document.addEventListener("keydown", (e) => { + if (e.target.tagName === "TEXTAREA" || e.target.tagName === "INPUT") return; + if (e.key === "ArrowLeft") goTo(currentIndex - 1); + else if (e.key === "ArrowRight") goTo(currentIndex + 1); + else if (e.key === "Home") goTo(0); + else if (e.key === "End") goTo(gameData ? gameData.frames.length - 1 : 0); + else if (e.key === " ") { e.preventDefault(); togglePlay(); } + }); + + // Responsive board resize + // Re-apply pressure after any board redraw (resize, scroll zoom, etc.) + window.addEventListener("resize", () => { + board.resize(); + if (gameData) renderPressure(gameData.frames[currentIndex]); + }); + + // Catch chessboard.js redraws that reset square colors (e.g. scroll-triggered resize) + const boardEl = document.getElementById("board"); + let repaintTimer = null; + const observer = new MutationObserver(() => { + if (repaintTimer) return; + repaintTimer = setTimeout(() => { + repaintTimer = null; + if (gameData) renderPressure(gameData.frames[currentIndex]); + }, 50); + }); + observer.observe(boardEl, { childList: true, subtree: true }); + + // Prevent board touch events from scrolling the page + boardEl.addEventListener("touchstart", (e) => { e.stopPropagation(); }, { passive: false }); + boardEl.addEventListener("touchmove", (e) => { e.preventDefault(); e.stopPropagation(); }, { passive: false }); + } + + // --- GIF Export --- + const EXPORT_SIZE = 480; + const SQ = EXPORT_SIZE / 8; + let pieceImages = {}; + + function loadPieceImages() { + const pieces = ["wK","wQ","wR","wB","wN","wP","bK","bQ","bR","bB","bN","bP"]; + const promises = pieces.map((p) => new Promise((resolve) => { + const img = new Image(); + img.onload = () => { pieceImages[p] = img; resolve(); }; + img.onerror = () => resolve(); + img.src = `/static/img/chesspieces/wikipedia/${p}.png`; + })); + return Promise.all(promises); + } + + function computeSquareColor(val, maxAbs, rank, fileIdx) { + const isDark = document.documentElement.dataset.theme !== "light"; + const isLight = (rank + fileIdx) % 2 === 1; + + if (!showBoard) { + const neutral = isDark ? [26, 26, 26] : [245, 245, 245]; + if (val === 0) return `rgb(${neutral[0]},${neutral[1]},${neutral[2]})`; + const intensity = Math.min(Math.abs(val) / maxAbs, 1); + const alpha = 0.3 + intensity * 0.7; + const tint = val > 0 ? [50, 130, 220] : [220, 50, 50]; + const r = Math.round(neutral[0] * (1 - alpha) + tint[0] * alpha); + const g = Math.round(neutral[1] * (1 - alpha) + tint[1] * alpha); + const b = Math.round(neutral[2] * (1 - alpha) + tint[2] * alpha); + return `rgb(${r},${g},${b})`; + } else { + const base = isLight ? [192, 192, 192] : [120, 120, 120]; + if (val === 0) return `rgb(${base[0]},${base[1]},${base[2]})`; + const intensity = Math.min(Math.abs(val) / maxAbs, 1); + const alpha = 0.2 + intensity * 0.6; + const tint = val > 0 ? [50, 130, 220] : [220, 50, 50]; + const r = Math.round(base[0] * (1 - alpha) + tint[0] * alpha); + const g = Math.round(base[1] * (1 - alpha) + tint[1] * alpha); + const b = Math.round(base[2] * (1 - alpha) + tint[2] * alpha); + return `rgb(${r},${g},${b})`; + } + } + + function drawFrame(ctx, frame) { + const key = pressureMode === "weighted" ? "pressure_weighted" : "pressure"; + const pressure = frame[key]; + const maxAbs = PRESSURE_SCALE[pressureMode]; + const fen = frame.board.fen.split(" ")[0]; + + // Draw squares + for (let row = 0; row < 8; row++) { + for (let col = 0; col < 8; col++) { + const rank = 8 - row; + const pyIdx = (rank - 1) * 8 + col; + const color = computeSquareColor(pressure[pyIdx], maxAbs, rank, col); + ctx.fillStyle = color; + ctx.fillRect(col * SQ, row * SQ, SQ, SQ); + } + } + + // Draw pieces if enabled + if (showPieces) { + const rows = fen.split("/"); + for (let row = 0; row < 8; row++) { + let col = 0; + for (const ch of rows[row]) { + if (ch >= "1" && ch <= "8") { + col += parseInt(ch); + } else { + const color = ch === ch.toUpperCase() ? "w" : "b"; + const pieceMap = { k:"K", q:"Q", r:"R", b:"B", n:"N", p:"P" }; + const key = color + pieceMap[ch.toLowerCase()]; + const img = pieceImages[key]; + if (img) { + ctx.drawImage(img, col * SQ, row * SQ, SQ, SQ); + } + col++; + } + } + } + } + } + + async function exportGif() { + if (!gameData || gameData.frames.length === 0) return; + + await loadPieceImages(); + + const speed = parseInt(document.getElementById("export-speed").value); + const progressEl = document.getElementById("export-progress"); + const barEl = document.getElementById("export-bar"); + const pctEl = document.getElementById("export-pct"); + const goBtn = document.getElementById("export-go"); + + progressEl.style.display = "flex"; + goBtn.disabled = true; + goBtn.textContent = "Exporting..."; + + const gif = new GIF({ + workers: 2, + quality: 10, + width: EXPORT_SIZE, + height: EXPORT_SIZE, + workerScript: "/static/gif.worker.js", + }); + + const canvas = document.createElement("canvas"); + canvas.width = EXPORT_SIZE; + canvas.height = EXPORT_SIZE; + const ctx = canvas.getContext("2d"); + + const total = gameData.frames.length; + for (let i = 0; i < total; i++) { + drawFrame(ctx, gameData.frames[i]); + gif.addFrame(ctx, { copy: true, delay: speed }); + const pct = Math.round(((i + 1) / total) * 50); + barEl.value = pct; + pctEl.textContent = pct + "%"; + } + + gif.on("progress", (p) => { + const pct = 50 + Math.round(p * 50); + barEl.value = pct; + pctEl.textContent = pct + "%"; + }); + + gif.on("finished", (blob) => { + barEl.value = 100; + pctEl.textContent = "100%"; + goBtn.disabled = false; + goBtn.textContent = "Export"; + + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "chess-pressure.gif"; + a.click(); + URL.revokeObjectURL(url); + + setTimeout(() => { progressEl.style.display = "none"; }, 1000); + }); + + gif.render(); + } + + document.addEventListener("DOMContentLoaded", init); +})(); diff --git a/static/gif.js b/static/gif.js new file mode 100644 index 0000000..2e4d204 --- /dev/null +++ b/static/gif.js @@ -0,0 +1,3 @@ +// gif.js 0.2.0 - https://github.com/jnordberg/gif.js +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GIF=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&this._events[type].length>m){this._events[type].warned=true;console.error("(node) warning: possible EventEmitter memory "+"leak detected. %d listeners added. "+"Use emitter.setMaxListeners() to increase limit.",this._events[type].length);if(typeof console.trace==="function"){console.trace()}}}return this};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.once=function(type,listener){if(!isFunction(listener))throw TypeError("listener must be a function");var fired=false;function g(){this.removeListener(type,g);if(!fired){fired=true;listener.apply(this,arguments)}}g.listener=listener;this.on(type,g);return this};EventEmitter.prototype.removeListener=function(type,listener){var list,position,length,i;if(!isFunction(listener))throw TypeError("listener must be a function");if(!this._events||!this._events[type])return this;list=this._events[type];length=list.length;position=-1;if(list===listener||isFunction(list.listener)&&list.listener===listener){delete this._events[type];if(this._events.removeListener)this.emit("removeListener",type,listener)}else if(isObject(list)){for(i=length;i-- >0;){if(list[i]===listener||list[i].listener&&list[i].listener===listener){position=i;break}}if(position<0)return this;if(list.length===1){list.length=0;delete this._events[type]}else{list.splice(position,1)}if(this._events.removeListener)this.emit("removeListener",type,listener)}return this};EventEmitter.prototype.removeAllListeners=function(type){var key,listeners;if(!this._events)return this;if(!this._events.removeListener){if(arguments.length===0)this._events={};else if(this._events[type])delete this._events[type];return this}if(arguments.length===0){for(key in this._events){if(key==="removeListener")continue;this.removeAllListeners(key)}this.removeAllListeners("removeListener");this._events={};return this}listeners=this._events[type];if(isFunction(listeners)){this.removeListener(type,listeners)}else if(listeners){while(listeners.length)this.removeListener(type,listeners[listeners.length-1])}delete this._events[type];return this};EventEmitter.prototype.listeners=function(type){var ret;if(!this._events||!this._events[type])ret=[];else if(isFunction(this._events[type]))ret=[this._events[type]];else ret=this._events[type].slice();return ret};EventEmitter.prototype.listenerCount=function(type){if(this._events){var evlistener=this._events[type];if(isFunction(evlistener))return 1;else if(evlistener)return evlistener.length}return 0};EventEmitter.listenerCount=function(emitter,type){return emitter.listenerCount(type)};function isFunction(arg){return typeof arg==="function"}function isNumber(arg){return typeof arg==="number"}function isObject(arg){return typeof arg==="object"&&arg!==null}function isUndefined(arg){return arg===void 0}},{}],2:[function(require,module,exports){var UA,browser,mode,platform,ua;ua=navigator.userAgent.toLowerCase();platform=navigator.platform.toLowerCase();UA=ua.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0];mode=UA[1]==="ie"&&document.documentMode;browser={name:UA[1]==="version"?UA[3]:UA[1],version:mode||parseFloat(UA[1]==="opera"&&UA[4]?UA[4]:UA[2]),platform:{name:ua.match(/ip(?:ad|od|hone)/)?"ios":(ua.match(/(?:webos|android)/)||platform.match(/mac|win|linux/)||["other"])[0]}};browser[browser.name]=true;browser[browser.name+parseInt(browser.version,10)]=true;browser.platform[browser.platform.name]=true;module.exports=browser},{}],3:[function(require,module,exports){var EventEmitter,GIF,browser,extend=function(child,parent){for(var key in parent){if(hasProp.call(parent,key))child[key]=parent[key]}function ctor(){this.constructor=child}ctor.prototype=parent.prototype;child.prototype=new ctor;child.__super__=parent.prototype;return child},hasProp={}.hasOwnProperty,indexOf=[].indexOf||function(item){for(var i=0,l=this.length;iref;i=0<=ref?++j:--j){results.push(null)}return results}.call(this);numWorkers=this.spawnWorkers();if(this.options.globalPalette===true){this.renderNextFrame()}else{for(i=j=0,ref=numWorkers;0<=ref?jref;i=0<=ref?++j:--j){this.renderNextFrame()}}this.emit("start");return this.emit("progress",0)};GIF.prototype.abort=function(){var worker;while(true){worker=this.activeWorkers.shift();if(worker==null){break}this.log("killing active worker");worker.terminate()}this.running=false;return this.emit("abort")};GIF.prototype.spawnWorkers=function(){var j,numWorkers,ref,results;numWorkers=Math.min(this.options.workers,this.frames.length);(function(){results=[];for(var j=ref=this.freeWorkers.length;ref<=numWorkers?jnumWorkers;ref<=numWorkers?j++:j--){results.push(j)}return results}).apply(this).forEach(function(_this){return function(i){var worker;_this.log("spawning worker "+i);worker=new Worker(_this.options.workerScript);worker.onmessage=function(event){_this.activeWorkers.splice(_this.activeWorkers.indexOf(worker),1);_this.freeWorkers.push(worker);return _this.frameFinished(event.data)};return _this.freeWorkers.push(worker)}}(this));return numWorkers};GIF.prototype.frameFinished=function(frame){var i,j,ref;this.log("frame "+frame.index+" finished - "+this.activeWorkers.length+" active");this.finishedFrames++;this.emit("progress",this.finishedFrames/this.frames.length);this.imageParts[frame.index]=frame;if(this.options.globalPalette===true){this.options.globalPalette=frame.globalPalette;this.log("global palette analyzed");if(this.frames.length>2){for(i=j=1,ref=this.freeWorkers.length;1<=ref?jref;i=1<=ref?++j:--j){this.renderNextFrame()}}}if(indexOf.call(this.imageParts,null)>=0){return this.renderNextFrame()}else{return this.finishRendering()}};GIF.prototype.finishRendering=function(){var data,frame,i,image,j,k,l,len,len1,len2,len3,offset,page,ref,ref1,ref2;len=0;ref=this.imageParts;for(j=0,len1=ref.length;j=this.frames.length){return}frame=this.frames[this.nextFrame++];worker=this.freeWorkers.shift();task=this.getTask(frame);this.log("starting frame "+(task.index+1)+" of "+this.frames.length);this.activeWorkers.push(worker);return worker.postMessage(task)};GIF.prototype.getContextData=function(ctx){return ctx.getImageData(0,0,this.options.width,this.options.height).data};GIF.prototype.getImageData=function(image){var ctx;if(this._canvas==null){this._canvas=document.createElement("canvas");this._canvas.width=this.options.width;this._canvas.height=this.options.height}ctx=this._canvas.getContext("2d");ctx.setFill=this.options.background;ctx.fillRect(0,0,this.options.width,this.options.height);ctx.drawImage(image,0,0);return this.getContextData(ctx)};GIF.prototype.getTask=function(frame){var index,task;index=this.frames.indexOf(frame);task={index:index,last:index===this.frames.length-1,delay:frame.delay,transparent:frame.transparent,width:this.options.width,height:this.options.height,quality:this.options.quality,dither:this.options.dither,globalPalette:this.options.globalPalette,repeat:this.options.repeat,canTransfer:browser.name==="chrome"};if(frame.data!=null){task.data=frame.data}else if(frame.context!=null){task.data=this.getContextData(frame.context)}else if(frame.image!=null){task.data=this.getImageData(frame.image)}else{throw new Error("Invalid frame")}return task};GIF.prototype.log=function(){var args;args=1<=arguments.length?slice.call(arguments,0):[];if(!this.options.debug){return}return console.log.apply(console,args)};return GIF}(EventEmitter);module.exports=GIF},{"./browser.coffee":2,events:1}]},{},[3])(3)}); +//# sourceMappingURL=gif.js.map diff --git a/static/gif.worker.js b/static/gif.worker.js new file mode 100644 index 0000000..269624e --- /dev/null +++ b/static/gif.worker.js @@ -0,0 +1,3 @@ +// gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null;this.getImagePixels();this.analyzePixels();if(this.globalPalette===true)this.globalPalette=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePalette();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPalette)this.writePalette();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither="FloydSteinberg";this.dither=dither};GIFEncoder.prototype.setGlobalPalette=function(palette){this.globalPalette=palette};GIFEncoder.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace("-serpentine",""),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j=0&&x1+x=0&&y1+y>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i=0){disp=dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPalette){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes("NETSCAPE2.0");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<0)cur_accum|=code<=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<>betashift;var betagamma=intbias<>3;var radiusbiasshift=6;var radiusbias=1<>3);var i,v;for(i=0;i>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(jlo){a=radpower[m++];if(jlo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i>intbiasshift-netbiasshift);if(biasdist>betashift;freq[i]-=betafreq;bias[i]+=betafreq<>1;for(j=previouscol+1;j>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i=0){if(i=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j;OAg&;29>pXtLFL9VGS_o&_2gy))=`i$DX3vw-9Gh)}RQ7JcwuWkdbRXt|A`xA>c4EW@MX> zj1K`bMy|`TiE&}qk z)5K?tT>s;C;y#C_DqWhf#67pwvT!jB9HSn^b1L&UY}D79cyZ!hpTfBbxfeETrsfTo zDW224pVh+TdWo{)C`Y)3gggu%T4E{j;}FCv7V#rpMSR#zUWI`}!j4;@#o3 zwq!Dyxf++t==OPfWGs*%U_4=RzH(`}WqAcL=rA~|k z8bl;wh$ODZe5|BZM;X(Y;=zPth-6yGt$ruZ0ZgQw$T8d!)h(;@rPja_mvT-ukYl(u zs?GS<5H=0`@|P$NBKsGCfS*QLz?~s(UwK`PycA^t`CY|i6-2asO<*4~(nwcPz*lwH zOyD`#=M@yu_Hk5blorehk!O_$wBqr2etHrF*_N1}&3aa7n1s2Jc$^=wp`pRbX0!Q? zf^xw&*cRJ_c;ARZQo5G7nMF*cQgUo;Oa=wE#Wo>cLTHTVxt7$`)ybx&CfU{1CA+)3 z%OcxgTWk|G@Ht(@yARWfFb5I~K9ahL$MeAh6&Q)t7fM>?%RDUH^ZtS+=Mk>cNx)rI zSjxSui+C~W>_5P$Ysv6ov9F$kz?Yr25HN<*lqbT}EQgl6svu9+Hr%txLo=goZiOOR&&?PZU1ak|MYyb7l^<6GG!O zUsX)rSl|~ve_;JO<3qd|+{5<{VFMxZqVewU3gVr_)x>+TsJ|bZn-3D#ibCe-dTAJ#4RquQl+UL@cWj+ls}qQ@x6~8_nh*F-!cKc!KyX@f7g~ z;u+#k#B;>oh$F-?G&xy9Q+6togUu26I0v6+5W^|NbOJGEvAMyqKU-C93njH_WNeCt*&$TPie{tCooB3#mtf7LOCfFK8;> zLZ3-&4stj7of;6YSj2aO+|}j}oF}i)8Rt-D9IFA9~G_GA#Y&zMRQ2aT?G3jCO<1$YN-Hcm_$1rNmBAD+);Xe%ck8oZgZpVBqMSKWM zVSdJQwJpSL#3qGX(M52%yNJtNVpxVUDcrNQ$ksOa*Z`ku9P1F<4#c_#v44+eu}9=% z=trP|Z1_lNO1_lNO2I1mAaAmM>MWmh600000 LNkvXXu0mjfilMA( literal 0 HcmV?d00001 diff --git a/static/img/chesspieces/wikipedia/bK.png b/static/img/chesspieces/wikipedia/bK.png new file mode 100644 index 0000000000000000000000000000000000000000..de9880ce6390d6a53464f1b028bff7be0fd0f131 GIT binary patch literal 3009 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 DNo%uI literal 0 HcmV?d00001 diff --git a/static/img/chesspieces/wikipedia/bN.png b/static/img/chesspieces/wikipedia/bN.png new file mode 100644 index 0000000000000000000000000000000000000000..e31a6d0224819791210e91218fc99b7cd3b848ed GIT binary patch literal 1875 zcmV-Z2dwysP)9wVNxLjD`@*iP6__$6VA8`tL&-X#RW zUP5D_3pg!{fgB^uc5!V9p(tNV|Gp5&;zh}#i3&mRJj2PgnS>Jx@#yvSfh=IOEEuS) zYKSmaaa{!A4~pxQ{4vmn@SiF{sCCuL0>==(p%PfhDZ)^~NW!o{6tLqymZi2z(Zy#! z%lA1-SS~L#j}it2f`CuC5C&X`M>(%N&&tYzef##^$h?u1g{eeo%(*%pyZ{SquvcgUrlKsIIPto}M0{~GzX|xd+VWPeTnQG71|}zqobjzsYw#CySp0~m(fM`;oP82 zZGH9i^^$O+D2*(Lii*-?NRv4b8X6j;u1}sk2@w$yzwz!f78Cxfe2Mkz*MkzBot>J_ zi$Os_*r&8Y@7}!|&wt@nJdE3Qsb2tBLnYGF(?RRS1y|{tH*doH{Jgkk%^Ga`oJRp) zC0tNHd~4?}Qpu;v0H3knLPDTMfpq8zX-;z>M=lnglY`8ICc2#t-6uxHO6 zAgiHI#6#j2ck0wB>5DX*&Di!WX?utl0V`cS{E8JTd}et{CMPGu?c29ucz77%;^MHa zln3!*eJ#vVSXWmkt;&ZE9m4ZIfrFgO7g}uByhWqYh>HnayLL@P=bp6X`BJ~~C=smv@?6opVFtPUR>}+XoJUKZDX=!QL z_9n}8C}!Edak8(JfX7~NQ$BXm?*>+LZc6k$(}ExzT4cYhRPPGX4GiQjkGTO)$&%e&+@8x)lE1V!?_D)&d?pcu*4X z)~#Do`hm1nvvgw`Jbd8K1owWrfWeB(TI_gm7lOb_FRVS3Lv+kw5|&?W)h)RHyN^1- zz2ZxcSyWWy@FcLVMvDOJnI#NYdORYyUnNzhWsnw~X^NdYcVgS$TI`2w2?u3sqz`jP z6?RgFqTCG?y{z8pux3U^hC{%;d-qC}3|d8H(4scW3F-pDUbxfi5PVpenWuQT8-)38?k({PSnyS@n8IqcN|mpqeVyP(DAH=daK^;M zM2CPUPMnb9go6E7Q+|Q%EOH)8Ar>A{U_GH+ep2WU;yo{~cs@;wDg``u?i|(-V*4w+ zi}x_$Fb^qCOlwkLh#!kGNxPYM0lyYFNysbbK%31*1>DcGc&`Z@RWm3BymaXj74U6d z#WQ#q(P1hemLJRY+f}>@xJ}-%a8pY|>ts|GXyH}BPkFLFWJ*fPJ!uJ9z#1OKi;*KT zTnUIp?-ZCHmfe{5c@%IR>->hN(U_K&7V1d9$D@Ex^5Xjms_K1;)2C0k zj|cA9vBM$Y@#DwoUie)e1$>6^55fLVBzWQEu3fukF|m#%9#*S$E-WkzW1I(g6fjn{ z!sj^Rvu3k-^T5DB!QkLvfyH9kLIlLP<|Bd*9XfRA(4j+z4jnq!;y-|v529HMk23%O N002ovPDHLkV1nn}h0*{3 literal 0 HcmV?d00001 diff --git a/static/img/chesspieces/wikipedia/bP.png b/static/img/chesspieces/wikipedia/bP.png new file mode 100644 index 0000000000000000000000000000000000000000..afa0c9d4459e4d04648ca413ba90690bfdd49a02 GIT binary patch literal 777 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 l`)^ z-n=PRR8&Y=QBY6-yLRn@ZQHiN`t|E!<;sAcp zw%QWGCeI%Jk3K#=;OpxPetv$>x@|x}z;l$ZV;L-qHWmf^#$AssUBz<`-r2+glP6CG z>_T(r&V{(RIEasrhgGXq!P>QJVbi8fuzmY>$jZus-Me?gzJ2>3CnpCEA3h96j~SgBFX6i=n){9A`ir z#HFPO&Xg|-=o{<6x~^WmD%Fi`z_y^^(b3Vi|D)|m85tRE>cCoCTcNtTS_%f25@~5^ zkdl%DD^{$K)@n0n&V)&mCP8Rus6?D)wJ3j&Wzh!LnCJt2q0d8y4oP)jU0A0YnVFgY z5%}M%^~l)wFE;yP_Y&ER2N4kw9-lmLse^4{OB)f}CJerjZ%cfnx1Ga5AJ=dbCQN`S zQ>IA5PM(V}NuxNxC#9lytNSe|u9X$^>eP$cv_ zapFW6GiHo5(Fn$G-M?kKu8Z3v9K5khZP~H~_UzdM`T6;9>eMMHEiHu$7cRi%%a@_9 zt`2V8xPgmpX*-6yjE4^&O1p_CPo7BQSZ{Byw4)dp7?9Z4*C$=a@8ul~?nv(3xdXRu z-GYXO2FWjO>n>ir2xVnuQdcf4EQGy#_d;rFDkLT*!i*U+ASfsZp8#grO1<3N+@D^% zcC8n;R!^Tk1yR)8$w^x<9N^hj52~uF;M}=$P*hYT1)7(a2S<(^k!D<+Z87ld>}=^@ z{6AhtVmZ7qY%M3<7~EyxB-PZ^B#CM?!QhR$efze!dGqFv?4;ia)~!l0yBZrCMHFP` z&Yh5)oGf)Y?5^ytyLH^dbz^vVIK;-r!kRT}aPoi?Cr-fi>(`}>UA}yIt(^`NLcH-L znk~eW9RXcC58P}Q0r4H>Prd9|A72ewX17`RufpI%>V9E(U(Fa!AN|zEPw1(lt$@F; z@GHhYPy@?IR2;DJ?WY!c2HX1rGpm>CXrVf5sSRb+rajbkz2f&{@Gp_Z({Vq(Iu^~i zKp6b}gH?=4!r<>@;qT1gd(Zf)?{3Bqg~7i%!h_FT;$gb>A(hD{n^f|VL_XuGj%cbg zoZ8?kyyUPnW0000+NjA?5IGAHvP4t z54%KWsa~*up#S25g4PyUJ-=B#vU8SfOkVC3u;J1>CCN>xZx>V_Sh(vMyUFU{q&A(T z%a1H=@4r>w>!lpyyYBwP?~T)?JD%6QuQd^xTrI;6v+VaXtCeeiy8Wc6`Rl~%4ZC%_?Pl1m|0#bk z^7R^V?ErQbHRi&f$MXc79yDCv;r+Zw?g6KO{)aj8(mb3yc@;lQyRI|;dChg!RlNKE z2Rz^KOH@NqfTL01M}LIQvs-Etth*oXd|r8f`Q??<_)3?oyAnC+!1AR*FSp!&`>Jlg ze_+iNk^RBzuU~#z^mB@p_ivL;)4i7N+qG`>cNAB`UCr7Rwbo5)mgnY` z%;g)qY$tQOpL?4le&-}t+^m}H?YFCJM3nTCehM|;g7cR zGA+F`RWxmJzeVFD=0;#pu4Yve;?#ZdEzbUm=b{7K&po<#$IEQ?)x!@1Zt*^=Q-AeL z=bfkBqwE`8`967bj@It5w>>Ys`)K2?z~f8Lnb+t}{M^W$ak}{Ubs@{GvO*EfV88O5 ze8_iV_VWU1rp*0s-6soA?ptd4F#E=>dx^Ko-DaPg7dgu!o*9yi7XDzq)Gu?~A%9jq ONWjz8&t;ucLK6U+JX?nV literal 0 HcmV?d00001 diff --git a/static/img/chesspieces/wikipedia/wB.png b/static/img/chesspieces/wikipedia/wB.png new file mode 100644 index 0000000000000000000000000000000000000000..70e0e14088f6ea09b84b728df6aaae6d4776ab4e GIT binary patch literal 2374 zcmV-M3Ay%(P)0+lPuTv^8oqt4P$%##owYTr~cn^%Wo0s4+fb z@R69NHsY(AsdGn^c#!>i%k*h9R34RH2x^(G6_wV1Y z7lDGA(wRFbxe<9FCu*4yWR=gwF z*~sLKcLlErR)-J}*9rv37`dL{dBH4w_7NQ&O@jvyrXxp=Na7ghY_k%S`Qx)JToex+ zWl9v0rUWKWo=m4tpRSk2;M}B1ljJ>`hc)&2L#Ixisw0}7#wg_G z=2A*Ziai=L+2%77dD*&n8#ivGs;a6w!r7spLu#{r{d&8IC!U4mIzr7j@bH*foIQKi zG1@{MtG~~jIYXT~b<&0WlI!p#S_9&5!X-z>VG52PDn`5Mf}vSB7UUA z>)pFIUA=lWs8~R0X(^?qrb^=2392k?!KNnSBEO0EhG01!u3fu!bmhtwY6!b`@5UOS zB#s-x<;$0;ZQHi`n&Cjdh}TB&a}|9`j9`(cuw~1Z#{%Lw6qK!+0dd^eK5^8jQTohfupb2c8V?g085t-jCjqMzHfYZqPoFMxy+z?RV(4j-RfEivEFb5COvuDqsBd=Rc zSO@Atoh;y2UKX$z4>4%aASx>>Yj70(e+Cg9s0($nfW=-FuLI{}fsrFe(xXR@C>(^7 z#@KOIGDgjljlC<$~ei!-q6t z#0V9E@S&YooAJL91p@{Qppuf3up%g&KF^LjckWRC{{2-1-th7ug6R~+#>Udcix)& zm@Zz9<)dxE*bw=%60v*t?tudb4$$q}x9RTPyE08_D$!*3@83^xadG-?)}JjOZo+(9 z@LTK2)?IR$%I3eU+D+-c#B-X6%|VD4WBC~G>km9*#tgZ2cgd0^O(M!-ozW)ZM8Asn z8mAQj%x`z>*x}j$f~79l|8?Xu6){_@XzS=MI7mN4( z_36{+ao*-eE~sqO)YPbV!)hO4lAi=jQ+?FYqeq)FMb-d-m*RA#7{?Uu&UAjbhd3o{}=Wq@8;9itLS$tb(6YhV4^DH0knU3Iz{yMAnbFF%* zM`UCqwQk)S`~2iS!uIXkOX3*ka1HkaNr|f1c8ulYZN9pgSXjUdpW8fttqvnaspc{$ zdw>@ZJTDqne`5vzB>1A>D?F+1#fzJ-3np5%J4fGSz9aZIt9I{nnO!J+T!W}{5U`BP zpzIJ{|9g{dCvsg0T=%Q&%Zuz=?C)izx`*K3x!D{M%oV&K_?_Sng87111q%hQ3l<69 z9(~727Q3I*Pc?LiQn_efo}l%;m)m`2OD077l$( zmL@`t8&fYt8+H?5AIyy#H|o4M!7O!5U8$Q4n_yphJBYcI=+&$?337<7*@;%+2 z%Pm^8hz=e+NJT|OuC1t4R8+|CM{;s%1#uwlc7$B}XU`gOUCgyw}M_WASY)7-gp>u}z@d9-lhLMix)6)PlH zuU<_nSFWVRix*QyMh4BAHH#)roJd244541Vdg-6{%VV6wHQa-HQ3hpU1Gbc1)PcHC zr`-?u1|Q)oe1`7;a?PPbhw2NmWjr{!wwk;~MIIIe_Uzds9qQ4ehinRXKrE?aH>q&K zk)pXvs$HkZ=kPDi;d;3Jm$IqbQudThC*mIX3ZLOSo&jQGHiLXqi^PxJT-C-0&Xo;x z##MaJJ)RBdyvM9yR%)!!%KU28e!_~6s&`nmuR`D9;{v|+t7g;5R)todlOL6h82oV0 zD7TyO48c4ewQAUzAFbNoFu)Jt{6lao=VSc@|IF;i`5AJxp@PE&M{7*xE&})U6pRkx zLp0yhkIN+6Yz^l+-r_oSKa$wDciGpq?E6prENVo~!*pW&2vRxcwiUTRg zkRXV%7AaE1+Pime0`OU^L=wjYnRI28U!=;`sZ)n;-@Z*LDJdMcZrvh$r>OKVCSCT@ zgy98NSXfvXJ$(3(AXclD1M=X(1O7L$3X>Tco3wpNBK#~>cgKz$Hu9v%&)c_eCqdfCa7CudlF+EqAd(j?U@hd3AyA<*B&LwTl|PZGlkhM^WO$Z{88 znCv2JV%7YPc&?`=o+~Qs+a-ZB zXUDskxw0G}b8a;Y66)jp+yDENT_>LhFzYiENz$JnG z`}ddUeqgw*5*HN}Wt~5NKAkvmf)W!G>FwLMw(~xJ{!B?pNp#`D1zNv;JvD9GR7>P~ ziw7!zq2h`0iw_?@-1`3gdvZrUe*8$UU%#eBixyG7eEHOgzY}Co#flYO+Q14GDrj%q zTB=kj>wyCY`1l~o-T7?Tut7W4ZlejjC?`E`+&HH<>rCXaj~+du)~#Dp?%cUE*vn_n zo;f}5>C>lnE7;G^kNWiK<2GrI#=LRk1{Et-OeJ!GQRMxZ;S(;@wryK__wF5iji`jr znKOq%LPG3FnKy5q-ErzqZ0qE4xM`@Oqj9r3mx~uK5?jTpqv@tMd8mY)#KiOD$wQ}3 zo%-rYYtMtdg6c|Q@cGlHPZ^GzGG&VG7&xqigoF&cmF&po&6`z?ND%R?ZqmXcDrQ)H z*?H>fx$^ugSFTX`^5yyDeSCaq=+L2d#~wU*ko^7q`QMrSq_bzwl3t?TL*vGcC4p}l zn(0LzJkw9oPMtY(hV&G91MrU0OJgN$-MW>356@n!RxR7=bF|gdOHQ6VsoKI3dc}wN z82*7l3l=PB$mHdCSZQz!xK@c0C3sR{`t<2G@|f2*j65kUW|h}lt5&6+jo-o1O|31QnnX#+P2 z^QDUo+>AjnIC)4qcI;>)uvf2MhF(i9h^tqxs!k+UudN+C(QxU7)Uy*Qim$BP(#!s8Kv71n}AD)p!2&k_4XDyZQ$; zju$%-t2F4}0)Y_he314M>Wl5_I3xDLt;KG{ie1Can>P(rD^Cz8=Z*}K$XSv|m>5=T znImmslO|1U`8GXB)Q*Atp^*g2>uuSxh4SamFUdnjyNb~%t$#`qBO@anZv5RP$&q~c z@PUsxY}haxfn&yu$@aL8_TZ~muU1zc(9#nb=>WMi0g$r4bLUQ`H}s#=?04i3 zkv3C87X0*R2Ur<~v-09$VPRINf|}TbdV?%veFqO7jFP3g&(sxoh=>&zE?h{*j~}-c z<{c4<0zKxVkY||Fa3v)ecFLPTn!ZPm9(+|G0f2q8(n*j3dGqGws}F@A+&tpG*w|Q_ zI&~_|nl+1K+O%mje*AbEF=7Pu?c0|Zh9V*&C^$IS>9{B;;b9FLG>DEKJ!(hf&Ye5? z`3iZ6_j-FnP1u%}$L8PBN<%FJPacU&^u0ixU%h(8S1QyGYK}TZT3VVNU=Q&(vrzwz{e3lWDX#yK;f8#&F#MQ&{rdH} zy#ra5#8n(^Zm@6I%_wz@%<$RU3G^2|cy<#<(gDFS z?A((lPq=y^_JevO#{1f~44eyZi?LuP5F4Vy05%gHVh+L>*$(IZD-ddnc3gJ^1R_ud z8E`5f1bFl24Tt<4HVvdB;6{M$j+{GpP7SiHP8u@3&jz+=(SqNoRzL_k?oXdS&D9m2 z6+M9?M~>ueYv|*HN+JwG0jp7?M*MTDR;_qN5NeO~K2|h#7rS@w=Ed;q*RR{P)~SMH zbDR^wO-t3heEBjhUAmO|_3OuNAA)QcdF)p7@WO=)7v^3a&7D1a_T-KO4@e5OdO40j zv|^zB4L1f)D!UCb18i3D;>CHxW>i!ZCj_eypKI5y&7BXtv#W8?7J}o@*?^Ue_O~2I zpcV*0us1GLs1WyRfPv5|Om&NN}yDaH%xPNp1Zwvpb1g*j&E$N+4|J5N5#0uZTFp;4>LuJeVf%piu3KC&O z4NLmVHfSSWBpeY)Ilyp`;a7%j49hI(Zzc6-sB8K10%3lO`7iQJF~t;9Ofkh2Q%v!& Z{12-kJ&=_`^N#=k002ovPDHLkV1l3JS(E?( literal 0 HcmV?d00001 diff --git a/static/img/chesspieces/wikipedia/wN.png b/static/img/chesspieces/wikipedia/wN.png new file mode 100644 index 0000000000000000000000000000000000000000..237250c164f652f0ea61f5e8f65c9b4cb83489b7 GIT binary patch literal 2388 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 V@B7wJo3#J{002ovPDHLkV1iHz`5piO literal 0 HcmV?d00001 diff --git a/static/img/chesspieces/wikipedia/wQ.png b/static/img/chesspieces/wikipedia/wQ.png new file mode 100644 index 0000000000000000000000000000000000000000..c3dfc15e556cbbeb546374aa9ee5e6bf121e929d GIT binary patch literal 3812 zcmVDQ0=PIA(>@2yjH>c6#|I_KOM z2qY*$2})3c5|p3>B`87hLxmk~>F`LyEzP}OB^7iyUk=LGb$FG-%Ec?qvtJE0cX*D& zD;!?#@DztDM3E`s@NNNG(6IfVXT^RE;Qhhji>?uL%i;3|*l;w%0@tfoui$|L2NcY; z0CIYiwktaPcZZKTe9GZ<4*%|c0NfdQ?Fh< zwQt`(Wr=I<8TtJY+HUW9X>#-q&*4&%XS4)`^XJbWoH})?I(P1zI&|oediL38Er^W7 zsbOSK)9JQt+o}r}E`$I)fBw8`)259DIu2i;?E(b~1Rr|nA@#^3kEqntR0(3M z`vJhKD4U+1uClVSLLmJ7^Ur!DnvN_BGa9z>ylmOBYU9R@A=wQZHmEXX%2?U0VcPu@ z00Z90kt5adN9trN)jO8>$pObLNa(BgRFkC&OjX_VU?&7qj5Y#lMSB z*-EZc(C8@T$Jw&<&Gr*|_0?CEShw5c1i%Ik{}18%_3Nt}H*SP>wMmmENkBT~5WrnN z+aKz&pbzyaJJ!`^hL4w}%TJy>In>YV*RRXd4_MUC34p9qpV*24k+;Eg7Q5+DA5${J z1BjMUWRe|zhq6gYNkQ1iy3`F#Q{kwwZR3LvKF}=MY#e3d1LR=QqD57aB1QZHSl&)y zVX`zeeZJ@PMh>T=1`sF0TW`IkiWMtn<<>-SjM@z(xOeYf1&iqwX(M@;a?TMF|Dkj~ zL<8_+SD)U<&2#Zn9=^8j(EwybeEs#;+Px%vf~~{Id}v24W(CZ(k^3-=OihP(Ql>(M z3ToN1W%6Z30Z}!j%JhJ&t?9Sh%%>peDxI{;*a7?TG6kE{G-+o_c+SB3!OTD+{u9&RkH$F^c(K;-&7_ub-5uF3NA>UX2I6j`_ zUaMBE)TvXaLgl&Uo+*D)9IHlIU1rao9dZIh0I-pDQlK-sqlK!eeuN?(s(`Ka5aY~xB;|) z5UNZ`NwMV#B+YX87MsQ5#f$G4f!s42$t@F3_?rn{>9~ii+XdhRqYFA|9nsa2Fw&Qs zzU4X*j+|+SJyADHuf6t~oDP3B#F&PB7dEgp!k!v$>z_ zx+($jh*5-?lS+gU7?gmcn&(mWGU<-SWBm+S%_@r};A3r`@C&P_Ek6lnl!@?2I zWZ7ZEhIy8AjX=%lgl=+ULU(bVZxKrXYgzBdgAYD<2Pg!!g(q2eX7lFFo}K0NAUisJ z`gFBv(eZ|Dro(iVWpPd%J$h6d=aPu7i49FPY0^X;J9g}jogLjx znVrAH>wLGS%vu`9?sG;`)mQaQAOOjBYFcvb4wt*a_muB_KHWJ$%4ps+LJ424ru zQ$x)E;y9=5j*2`xDRq@EUtZO!RZFui!?BV48VjH#;WJztD-%Z2VtZ~S^f_kC7}cRe z2bqBtmt3Vv71gItANA5pFKH7^tY5#LX)M?7A%Q%m%z#J_lWwJWBXo16=Vb=sfRbgrxMt0op^f3lkt6!NNpKt^ z_?1^)(boTyPd=&Iw{NeSHf^eR&WaT)>cn!(*k@iW%OHbnvu4e7UDQe4WT(;=ZPGS6 z*bPT`wc&{Yf4Md>A;OdYu;)sVOrDML=V4x8e_4YWV;I{JgBUr>7{`+3Gs+@^EOk&9 zbyBzS8Q#H0Cp-H15GU@w8L1)Stijr~YwJ;g4L4!J1buMF3%qpcQhhYzXh#zIPe1*n zM+t6wyNl!OWWtp0V3wwjhHnbx;>|Qun-h^K@IZN!w36@q|Jb0p4*}944-) zzXkB|#~=6fl*F0m2I`BicZr#$ZCZ;P! zFKn|CEZh5(p)4|fhya!V@cH`zz`t0p3dbzgExwi8wQHBoSy{JkopwVJPF%2Hfey}- ztuS%oMC}_KHENXJt?~5{c!Ig+9?vL4S!9r<4(g&#>ZT3aqD|UH2N8RjeLnzr*t(iH z@Z-Y5H%ROPQzQq6{2Vh*3)-M9;w`j|4#GEhINrhgr&q^FIU{qRd-v{Y#E21U+O%ov z{rBHjpMLtOw$;cy;A2Fx1YmYqFSgGqiwv^VL0!~I-Lyel_y)rOJwIN-%QUW`Aciic z#GXBS@_Z9P2b{Ymd>J&}m)`LP9=~O<&qmcaZ%Sg(XDz}j%%V=G2=H*UV$H&i!;Oy{ zigk?iG@KKb`JekdXVg)iFN&}MsRw(IdU5abRVW*co3IguNBdv#2H@}Q6v1_mF^iWP zLza|`u3ft-KBDH>#peSLJn(={$KW`|$RK8d+nx@xMv|1yi6SW}N&n@4?(>{-l;>oD zeATK|b$!&!(U1?baaA#u2MidXMvoq?eS)8T_L+3ylks0JUck$i;AlJUwf=AfV}Wnj zu%S-(CB=fY3_3b+;6T0Gl1#~NOUFs);4lAkpXV>W_@aI>d;!mkY2CWD-lRB#i!1}r z4Myn2H+;3@1;FMKAYAU5nVD+g!i9ReFR9E%M5Cz|K%3-~S0MVSTcq>u2*k;6(9{<(-5^mh z{BwL^gek)(2Ylk-6_?E|qAl9QM#O|c7j!~5@$DVx$-Gj+juf#l1yxY-@M{y zN%jP8|7uWH90b#|j4peOZu|yUpFo^aO5pZ~gnZi1SK?%m_B1;lzl6b0JIxE+{(N+m zvB!3p&T&pXVLE%>gd49JzGUv5GGz`MnH{E%Ri@5)rj7BYO`FNu%IK69FB`|*PW6Ij z-AOSWDQXsS@>2NWv66;M87^(u{%@a`FfzqV9jT_yLV=wBT3mt>l%ND9C_xEIP=XSa aWBNa`K;}@I(g!*K00005EZc&52L4cg2f>g*3l?hiePQw zAD|+l7Wo1BZr=;jp+{=HwcZ#u1xrm>5j=lKz0S=hEoi>6-fV#F?d`O*v_vZ_E0oLS{O$h!e%-t} zUNJ5&yW@KGAhz)y_Q^4hWlcOVmI~0p~Z-8B0T~w)59!-}@C7PI+P{ylTko8#Y=;&w|Aojgv47@@U+6wRl zZ_Wh>I8v`29f~F2<1Jr|fDeBd{cO0h! ze9h#W9sAmGTnaGW>_fyB1U^LKHU-!rc=XK8-6kGM&sO2L{wdhk*GEG`Lo_%zXq4~( zFM?d_$LlM^Zyi*$E)NB8&lp%R+Vh6i}j!h^?8wtk@y5!_NCc6N5s z#l?lOdmgmmK?`q7dbAo5?M(D1aCUY^H#awQb#)aAp#@DD9%*Nu_2DTb1-JG4PCOo` zL?RIip%u92ZA(v1A)%g4)9ExVE-q4`P@w(&{SPHHpao6s**oDmiWy7-XT`z6L1_0r zXhBm3IOFoS)YB?JoEc#OT3Ue993ThC0djyGAP2|+a)2Bl2gm_(fE*wP$N_SI93ThC z0djy&1!(XaY4-!f!p6P5z0d-LmbRGE=>VtXB&e;ejgrY^D1?^x^mFEv%K<(v+Z`Jl zqq(^`e~sTq5*pCbLOgagyl;YK!Ro-k06jcBgmwu-3!2`)--XN4i&IDoo~ldt@bED7 zWgXDe&caja$q8h`c+XlwVvZRyQ=TzXH-DxO;q9tL$Emo5ZI zmIPOK^}Z_s((v#w?d5KD3{W&*@tbZu>oTmwAIE_Sm4;&Ry7*l;dDc!qc50ebHA$2Fp{0KfhR z;VFJK5g>XmehRJ%<{ag!oP)7G60}am1fz~J<~h{<#b7WP3 + + + + + Chess Pressure + + + + +
+

Chess Pressure

+
+ +
+
+ +
+
+
+
+
+
+ +
+ + + + + +
+
+ + + + +
+
+
+ +
+
+ + +
+ +
+ +
+ + + +
+ +
+ +
+
+
+ + +
+

Export GIF

+ +

480 × 480px — respects current view settings

+ +
+ + +
+
+
+ + +
+

Upload PGN

+ +
+ + + + +
+
+
+ + + + + + + + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..0e917c4 --- /dev/null +++ b/static/style.css @@ -0,0 +1,339 @@ +/* --- Reset & Base --- */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --bg: #1a1a1a; + --bg2: #242424; + --fg: #e0e0e0; + --fg2: #999; + --border: #333; + --accent: #6ca; + --move-bg: #2a2a2a; + --move-active: #3a3a3a; + --fork-color: #f90; +} + +[data-theme="light"] { + --bg: #f5f5f5; + --bg2: #fff; + --fg: #1a1a1a; + --fg2: #666; + --border: #ddd; + --accent: #287; + --move-bg: #eee; + --move-active: #ddd; + --fork-color: #c60; +} + +html { font-size: 15px; } +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; + background: var(--bg); + color: var(--fg); + min-height: 100dvh; +} + +/* --- Header --- */ +header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 1.2rem; + border-bottom: 1px solid var(--border); +} +header h1 { + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.header-controls { display: flex; gap: 0.5rem; } + +#theme-toggle { + background: none; + border: 1px solid var(--border); + color: var(--fg); + padding: 0.3rem 0.6rem; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; +} +[data-theme="dark"] .icon-sun { display: none; } +[data-theme="light"] .icon-moon { display: none; } + +/* --- Layout --- */ +main { + display: flex; + gap: 1.2rem; + padding: 1.2rem; + max-width: 1100px; + margin: 0 auto; +} + +.board-panel { flex: 0 0 auto; } +.side-panel { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.8rem; + min-width: 0; +} + +/* --- Board --- */ +.board-wrap { + position: relative; + width: min(480px, calc(100vw - 2.4rem)); + aspect-ratio: 1; + touch-action: none; + -webkit-overflow-scrolling: auto; +} +#board { + width: 100%; + height: 100%; + touch-action: none; +} +/* Board square colors are set by renderPressure() in JS */ +.hide-pieces .piece-417db { opacity: 0 !important; } + +/* Hide board until first render to prevent tan flash */ +#board { visibility: hidden; } +#board.ready { visibility: visible; } + +/* Tap-to-move highlights */ +.square-selected { box-shadow: inset 0 0 0 3px rgba(255, 255, 100, 0.8) !important; } +.square-target { box-shadow: inset 0 0 0 3px rgba(100, 255, 100, 0.6) !important; } +.square-target::after { + content: ""; + display: block; + width: 30%; + height: 30%; + margin: 35%; + border-radius: 50%; + background: rgba(100, 255, 100, 0.4); +} + +/* --- Playback --- */ +.playback { + margin-top: 0.6rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} +#slider { + width: 100%; + accent-color: var(--accent); + touch-action: none; +} +.playback-buttons { + display: flex; + gap: 0.3rem; + justify-content: center; +} +.playback-buttons button { + background: var(--bg2); + border: 1px solid var(--border); + color: var(--fg); + padding: 0.4rem 0.8rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; +} +.playback-buttons button:hover { background: var(--move-active); } +#btn-next { transform: scaleX(1); } + +.pressure-mode { + display: flex; + gap: 1rem; + justify-content: center; + font-size: 0.8rem; + color: var(--fg2); +} +.pressure-mode label { cursor: pointer; } + +/* --- Game selector --- */ +.game-selector { + display: flex; + gap: 0.5rem; + min-width: 0; +} +.game-selector select { + flex: 1; + min-width: 0; + background: var(--bg2); + color: var(--fg); + border: 1px solid var(--border); + padding: 0.4rem; + border-radius: 4px; + font-size: 0.85rem; + overflow: hidden; + text-overflow: ellipsis; +} +.game-selector button, +.fork-controls button { + background: var(--bg2); + border: 1px solid var(--border); + color: var(--fg); + padding: 0.4rem 0.8rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; + white-space: nowrap; +} +.game-selector button:hover, +.fork-controls button:hover { background: var(--move-active); } + +/* --- Game info --- */ +.game-info { + font-size: 0.8rem; + color: var(--fg2); + padding: 0.4rem 0; + border-bottom: 1px solid var(--border); +} + +/* --- Move list --- */ +.move-list { + flex: 1; + overflow-y: auto; + font-size: 0.85rem; + line-height: 1.6; + max-height: 50vh; + padding: 0.4rem 0; +} +.move-row { + display: flex; + gap: 0.3rem; + padding: 0 0.3rem; +} +.move-num { + color: var(--fg2); + min-width: 2.2rem; + text-align: right; +} +.move { + padding: 0.1rem 0.4rem; + border-radius: 3px; + cursor: pointer; + min-width: 4rem; +} +.move:hover { background: var(--move-bg); } +.move.active { background: var(--move-active); font-weight: 600; } +.move.forked { border-left: 2px solid var(--fork-color); } + +/* --- Fork controls --- */ +.fork-controls { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.4rem 0; + font-size: 0.8rem; +} +.fork-badge { + color: var(--fork-color); + font-weight: 600; +} + +/* --- Export --- */ +.export-row { + padding: 0.4rem 0; +} +.export-row button { + background: var(--bg2); + border: 1px solid var(--border); + color: var(--fg); + padding: 0.4rem 0.8rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; + width: 100%; +} +.export-row button:hover { background: var(--move-active); } +dialog label { + display: block; + font-size: 0.85rem; + margin-bottom: 0.6rem; +} +dialog select { + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.3rem; + font-size: 0.85rem; +} +.export-info { + font-size: 0.8rem; + color: var(--fg2); + margin: 0.4rem 0; +} +.export-progress { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0.6rem 0; +} +.export-progress progress { + flex: 1; + height: 6px; + accent-color: var(--accent); +} +#export-pct { font-size: 0.8rem; color: var(--fg2); } + +/* --- Status bar --- */ +.status-bar { + font-size: 0.8rem; + color: var(--fg2); + padding: 0.4rem 0; + border-top: 1px solid var(--border); +} + +/* --- PGN Dialog --- */ +dialog { + background: var(--bg2); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1.5rem; + max-width: 500px; + width: 90vw; + margin: auto; + top: 50%; + transform: translateY(-50%); +} +dialog::backdrop { background: rgba(0,0,0,0.6); } +dialog h2 { font-size: 1rem; margin-bottom: 0.8rem; } +dialog textarea { + width: 100%; + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.6rem; + font-family: monospace; + font-size: 0.8rem; + resize: vertical; +} +.dialog-buttons { + display: flex; + gap: 0.5rem; + margin-top: 0.8rem; + justify-content: flex-end; +} +.dialog-buttons button { + background: var(--bg); + border: 1px solid var(--border); + color: var(--fg); + padding: 0.4rem 1rem; + border-radius: 4px; + cursor: pointer; +} +.dialog-buttons button:hover { background: var(--move-active); } + +/* --- Mobile --- */ +@media (max-width: 720px) { + main { + flex-direction: column; + padding: 0.8rem; + } + .board-wrap { width: 100%; } + .move-list { max-height: 30vh; } +}