Skip to content
Snippets Groups Projects
app.js 8.00 KiB
import jsQR from "./jsQR/";

var SCREEN_WIDTH = 256;
var SCREEN_HEIGHT = 240;

const nesCanvas = document.getElementById("nes-canvas");
canvas_ctx = nesCanvas.getContext("2d");
let image = canvas_ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

canvas_ctx.fillStyle = "black";
canvas_ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

const nesWorker = new Worker(new URL("nes.js", import.meta.url), {
  type: "module",
});

nesWorker.onmessage = (buffer) => {
  image.data.set(buffer["data"]);
};

function onAnimationFrame() {
  window.requestAnimationFrame(onAnimationFrame);

  canvas_ctx.putImageData(image, 0, 0);
}

onAnimationFrame();

var video = document.createElement("video");
var canvasElement = document.getElementById("canvas");
var canvas = canvasElement.getContext("2d", { willReadFrequently: true });
var loadingMessage = document.getElementById("loadingMessage");
var outputContainer = document.getElementById("output");
var outputData = document.getElementById("outputData");

const latencyDiv = document.getElementById("latency");
const latencyScan = document.getElementById("scan");

// buttons
const up = document.getElementById("up");
const down = document.getElementById("down");
const left = document.getElementById("left");
const right = document.getElementById("right");
const a = document.getElementById("a");
const b = document.getElementById("b");
const select = document.getElementById("select");
const start = document.getElementById("start");

let qr = {};
let pressedButtons = {};

// SO BAD AAAAAAA
up.onpointerdown = () => {
  pressedButtons.up = true;
  checkButtonStatus();
};

up.onpointerup = () => {
  pressedButtons.up = false;
  checkButtonStatus();
};

down.onpointerdown = () => {
  pressedButtons.down = true;
  checkButtonStatus();
};

down.onpointerup = () => {
  pressedButtons.down = false;
  checkButtonStatus();
};

left.onpointerdown = () => {
  pressedButtons.left = true;
  checkButtonStatus();
};

left.onpointerup = () => {
  pressedButtons.left = false;
  checkButtonStatus();
};

right.onpointerdown = () => {
  pressedButtons.right = true;
  checkButtonStatus();
};

right.onpointerup = () => {
  pressedButtons.right = false;
  checkButtonStatus();
};

a.onpointerdown = () => {
  pressedButtons.a = true;
  checkButtonStatus();
};

a.onpointerup = () => {
  pressedButtons.a = false;
  checkButtonStatus();
};

b.onpointerdown = () => {
  pressedButtons.b = true;
  checkButtonStatus();
};

b.onpointerup = () => {
  pressedButtons.b = false;
  checkButtonStatus();
};

start.onpointerdown = () => {
  pressedButtons.start = true;
  checkButtonStatus();
};

start.onpointerup = () => {
  pressedButtons.start = false;
  checkButtonStatus();
};

select.onpointerdown = () => {
  pressedButtons.select = true;
  checkButtonStatus();
};

select.onpointerup = () => {
  pressedButtons.select = false;
  checkButtonStatus();
};

function checkButtonStatus() {
  const pressed = {
    left: pressedButtons.left || qr.left,
    right: pressedButtons.right || qr.right,
    up: pressedButtons.up || qr.up,
    down: pressedButtons.down || qr.down,
    a: pressedButtons.a || qr.a,
    b: pressedButtons.b || qr.b,
    start: pressedButtons.start || qr.start,
    select: pressedButtons.select || qr.select,
  };

  up.style = pressed.up ? "background-color: red;" : "";
  down.style = pressed.down ? "background-color: red;" : "";
  left.style = pressed.left ? "background-color: red;" : "";
  right.style = pressed.right ? "background-color: red;" : "";
  a.style = pressed.a ? "background-color: red;" : "";
  b.style = pressed.b ? "background-color: red;" : "";
  start.style = pressed.start ? "background-color: red;" : "";
  select.style = pressed.select ? "background-color: red;" : "";

  nesWorker.postMessage({ keys: pressed });
}

let datas = {};

function renderData() {
  let html = "<table><tr><th>Data</th><th>Count</th><th>Buttons</th></tr>";
  const keys = Object.keys(datas);
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    const val = datas[key];

    html += `<tr><td>${key}</td><td>${val.count}</td><td>${val.buttons}</td></tr>`;
  }

  html += "</table>";

  outputData.innerHTML = html;
}

function drawLine(begin, end, color) {
  canvas.beginPath();
  canvas.moveTo(begin.x, begin.y);
  canvas.lineTo(end.x, end.y);
  canvas.lineWidth = 4;
  canvas.strokeStyle = color;
  canvas.stroke();
}

// Use facingMode: environment to attemt to get the front camera on phones
navigator.mediaDevices
  .getUserMedia({ video: { facingMode: "environment" } })
  .then(function (stream) {
    video.srcObject = stream;
    video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen
    video.play();
    requestAnimationFrame(tick);
  });

function tick() {
  loadingMessage.innerText = "⌛ Loading video...";
  if (video.readyState === video.HAVE_ENOUGH_DATA) {
    loadingMessage.hidden = true;
    canvasElement.hidden = false;
    outputContainer.hidden = false;

    canvasElement.height = video.videoHeight;
    canvasElement.width = video.videoWidth;
    canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
    var imageData = canvas.getImageData(
      0,
      0,
      canvasElement.width,
      canvasElement.height,
    );

    const start = window.performance.now();
    var code = jsQR(imageData.data, imageData.width, imageData.height, {
      inversionAttempts: "dontInvert",
    });
    const end = window.performance.now();
    const delta = end - start;

    latencyDiv.innerText = `Frame latency: ${delta}ms`;

    if (code?.data.length > 0) {
      // reset
      qr = {
        //a: qr.a, // don't reset a!
      };

      latencyScan.innerText = `Scan latency: ${delta}ms`;

      // Left
      if (
        code.data.includes("OP3%S") ||
        code.data.includes("OP9OS") ||
        code.data.includes("OOP8S") ||
        code.data.includes("OOI3S") ||
        code.data.includes("OP9+S") ||
        code.data.includes("OP8$S") ||
        code.data.includes("OP8$S") ||
        code.data.includes("OP98S")
      ) {
        qr.left = true;
      }

      // Right
      if (
        code.data.includes("OO$:S") ||
        code.data.includes("OPJMS") ||
        code.data.includes("OOZ6S") ||
        code.data.includes("OPZ6S") ||
        code.data.includes("OPCHS") ||
        code.data.includes("OPD S") ||
        code.data.includes("OOD S") ||
        code.data.includes("OPGTS") ||
        code.data.includes("OO PS") ||
        code.data.includes("OOYJS") ||
        code.data.includes("OOFAS") ||
        code.data.includes("OPH0S") ||
        code.data.includes("OPDGS") ||
        code.data.includes("OPDKS") ||
        code.data.includes("OPXWS") ||
        code.data.includes("OPU*S") ||
        code.data.includes("OOGTS") ||
        code.data.includes("OODKS")
      ) {
        qr.right = true;
      }

      // A button
      if (code.data.startsWith("ARC")) {
        qr.a = false;
      } else if (
        code.data.startsWith("A7H") ||
        code.data.startsWith("ANH") ||
        code.data.startsWith("A8+") ||
        code.data.startsWith("A8H") ||
        code.data.startsWith("A9H") ||
        code.data.startsWith("AO+")
      ) {
        qr.a = true;
      }

      drawLine(
        code.location.topLeftCorner,
        code.location.topRightCorner,
        "#FF3B58",
      );
      drawLine(
        code.location.topRightCorner,
        code.location.bottomRightCorner,
        "#FF3B58",
      );
      drawLine(
        code.location.bottomRightCorner,
        code.location.bottomLeftCorner,
        "#FF3B58",
      );
      drawLine(
        code.location.bottomLeftCorner,
        code.location.topLeftCorner,
        "#FF3B58",
      );

      if (!datas[code.data]) {
        let buttons = [];
        for (let i = 0; i < Object.keys(qr).length; i++) {
          const k = Object.keys(qr)[i];
          if (qr[k]) {
            buttons.push(k);
          }
        }

        datas[code.data] = { count: 1, buttons };
      } else {
        datas[code.data].count += 1;
      }

      renderData();
    }

    checkButtonStatus();
  }

  requestAnimationFrame(tick);
}