website

Personal website
git clone https://github.com/michaelneuper/website.git
README | Files | Log | Refs

website/src/scripts/demos/computer/commands.js (3016B)


// M-x commands for the 8-bit computer (c, n and r with focus in its demo, or on
// nothing while it's on screen, or SPC c from anywhere). They open the 8-bit
// project first if its demo isn't showing.
//
// This module loads with the page, and the board (board.js) only when the
// project is first shown, so the state that outlives a mounted board is kept
// here: which board is on the page, and the coffee overclock.

import { reducedMotion, emit, on } from "../../util.js";
import { defcommand, completingRead, message, setSegment } from "../../emacs/index.js";
import { PROGRAMS } from "./machine.js";

let current = null;
const waiting = [];
let boost = 1;
let boostTimer = 0;

/** The board on the page, if one is mounted. */
export const currentBoard = () => current;

/** board.js calls this with a board's api when it mounts, and null when it goes. */
export function setCurrentBoard(api) {
  current = api;
  if (api) waiting.splice(0).forEach((resolve) => resolve());
}

/** How many times faster the clock runs, while a coffee is in effect. */
export const clockBoost = () => boost;

// The 8-bit project's demo, opened and scrolled to if it isn't on the page yet.
// Its code may still have to load, so this waits up to 5 seconds for it.
export async function showComputer() {
  if (!current) {
    const mounted = new Promise((resolve) => {
      waiting.push(resolve);
      setTimeout(resolve, 5000);
    });
    emit("project", "8-bit-computer");
    await mounted;
  }
  current?.board.scrollIntoView({ behavior: reducedMotion.matches ? "auto" : "smooth", block: "center" });
  return current;
}

const withComputer = (fn) => async () => {
  const c = current ?? (await showComputer());
  if (c) fn(c);
};
defcommand("computer-run", withComputer((c) => c.run()), { doc: "Run or pause the 8-bit computer", key: "c" });
defcommand("computer-step", withComputer((c) => c.step()), { doc: "Advance the clock by one cycle", key: "n" });
defcommand("computer-reset", withComputer((c) => c.reset()), { doc: "Reset the computer (RAM is kept)", key: "r" });
defcommand("computer-load-program", async () => {
  const key = await completingRead("Load program: ", Object.entries(PROGRAMS).map(([k, p]) => ({ value: k, ann: p.title })));
  if (!key) return;
  const c = await showComputer();
  c?.load(key);
  c?.run(true);
}, { doc: "Load one of the example programs into RAM", key: "SPC c l" });

// M-x coffee-refill: four times the clock for a while.
on("caffeine", ({ factor, seconds }) => {
  boost = factor;
  if (current) {
    if (current.halted()) current.reset();
    if (!current.running()) current.run(true);
  }
  let left = seconds;
  clearInterval(boostTimer);
  setSegment("misc", `${factor}× clock ${left}s`);
  boostTimer = setInterval(() => {
    left--;
    if (left > 0) return setSegment("misc", `${factor}× clock ${left}s`);
    clearInterval(boostTimer);
    boost = 1;
    setSegment("misc", "");
    message("The caffeine wore off. The clock is back to normal.");
  }, 1000);
});