projang / 리체스 보드 시각화 버튼 추가

// ==UserScript==
// @name         리체스 보드 시각화 버튼 추가
// @version      1.3
// @description  리체스 사이트에서 보드를 시각화할수있는 버튼들을 추가합니다.

// @match        https://lichess.org/*
// @author       projang
// @license MIT
// @copyright 2021, projang (https://openuserjs.org/users/projang)
// @updateURL https://openuserjs.org/meta/projang/리체스_보드_시각화_버튼_추가.meta.js

// @grant       GM_addStyle
// @grant       GM_getResourceText

// @require     https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js
// ==/UserScript==

const url = window.location.href;

function init() {
  const pgnMenuElement = document.querySelector("div.pgn-options > div");
  if (!pgnMenuElement) throw new Error("pgn 정보가 없습니다!");
  pgnMenuElement.insertAdjacentHTML(
    "afterbegin",
    `<a data-icon="" class="text" id="drawheat">히트맵</a>`
  );
  document.querySelector("#drawheat").onclick = drawHeatmap;

  pgnMenuElement.insertAdjacentHTML(
    "afterbegin",
    `<a data-icon="" class="text" id="drawcolorheat">흑백 히트맵</a>`
  );
  document.querySelector("#drawcolorheat").onclick = drawColorHeatmap;
}
init();

function resetBoard() {
  console.log("reset board");
  const oldMenuButtons = document.querySelectorAll("div.pgn-options > div > a");
  const listeners = [...oldMenuButtons].map((e) => e.onclick);

  eval(document.querySelector("body>script:last-child").textContent);

  setTimeout(() => {
    const newMenuButtons = [
      ...document.querySelectorAll("div.pgn-options > div > a"),
    ];
    for (let i = 0; i < newMenuButtons.length; i++) {
      newMenuButtons[i].onclick = listeners[i];
    }
  }, 0);
}

function drawColorHeatmap() {
  const canvasId = "colorheatmap";

  if (!prepareChessArea(canvasId)) {
    resetBoard();
    return;
  }
  console.log("draw color heatmap");
  var canvas = document.getElementById(canvasId);
  var ctx = canvas.getContext("2d");
  var w = canvas.width;
  var cw = w / 8;
  const heatmap = getHeatmap();
  const max = Math.max(...heatmap.flat());
  const flipped = url.includes("black");
  for (var i = 0; i < 8; i++) {
    for (var j = 0; j < 8; j++) {
      const x = flipped ? 7 - j : j;
      const y = flipped ? 7 - i : i;
      const heat = heatmap[x][y];

      const w = convertRange(heat, [0, max], [0, 255]);
      const color = `rgb(${w},${w},${w})`;
      ctx.fillStyle = color;
      const size = cw;
      ctx.fillRect(j * cw, i * cw, size, size);
    }
  }
}

function drawHeatmap() {
  const canvasId = "heatmap";

  if (!prepareChessArea(canvasId)) {
    resetBoard();
    return;
  }
  console.log("draw heatmap");
  var canvas = document.getElementById(canvasId);
  var ctx = canvas.getContext("2d");
  var w = canvas.width;
  var cw = w / 8;
  const heatmap = getHeatmap();
  const max = Math.max(...heatmap.flat());
  const min = Math.min(...heatmap.flat());
  const flipped = url.includes("black");
  for (var i = 0; i < 8; i++) {
    for (var j = 0; j < 8; j++) {
      const darkColor = "#c1c18e";
      const lightColor = "#ececec";
      var fill = (i + j) % 2 ? darkColor : lightColor;
      ctx.fillStyle = fill;
      ctx.fillRect(j * cw, i * cw, cw, cw);
      ctx.fillStyle = "black";
      // if flipped, draw from bottom to top
      const x = flipped ? 7 - j : j;
      const y = flipped ? 7 - i : i;
      const heat = heatmap[x][y];
      const size = convertRange(heat, [min, max], [0, cw - cw / 4]);
      ctx.fillRect(
        j * cw + cw / 2 - size / 2,
        i * cw + cw / 2 - size / 2,
        size,
        size
      );
    }
  }
}
let heatmap;
function getHeatmap() {
  if (heatmap) return heatmap;
  const history = getChessHistory();
  const squareRecord = history.map((e) => e.to);
  // in squareRecord, each element is a string like "e2"
  // parse squareRecord to get x, y and save to heatmap
  heatmap = new Array(8).fill(0).map(() => new Array(8).fill(0));
  const test = {};
  squareRecord.forEach((e) => {
    test[e] = test[e] ? test[e] + 1 : 1;
    let [alphabet, x] = e.split("");
    const y = alphabet.charCodeAt(0) - 97;
    x--;
    heatmap[y][x]++;
  });
  // make test to desending order
  const test2 = Object.keys(test).map((e) => [e, test[e]]);
  test2.sort((a, b) => b[1] - a[1]);
  console.log(test2);
  // flip heatmap top to bottom
  heatmap = heatmap.map((e) => e.reverse());
  return heatmap;
}

function prepareChessArea(canvasId) {
  const cg_container = document.querySelector("cg-container");
  if (!cg_container) return false;
  const { offsetWidth: width } = cg_container;
  const html = `<canvas id="${canvasId}" class="cg-container">`;
  cg_container.outerHTML = html;
  document.getElementById(canvasId).width = document.getElementById(
    canvasId
  ).height = width;
  return true;
}

function convertRange(value, r1, r2) {
  return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0];
}

function getChessHistory() {
  const pgn = document.querySelector(".pgn").textContent;
  const chess = new Chess();
  chess.load_pgn(pgn);
  const history = chess.history({
    verbose: true,
  });
  return history;
}