view mrjunejune/src/public/raylib.js @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents 65e5a5b89a4e
children
line wrap: on
line source

/**
 * I Stole this from https://github.com/tsoding/c3-demo/blob/main/raylib.js and added few functions for functionality.
 * It seems to be from raylib.com/examples
 */
function make_environment(env) {
  return new Proxy(env, {
    get(_target, prop, _receiver) {
      if (env[prop] !== undefined) {
        return env[prop].bind(env);
      }
      return (...args) => {
        throw new Error(`NOT IMPLEMENTED: ${prop} ${args}`);
      };
    },
  });
}

let iota = 0;
const LOG_ALL = iota++; // Display all logs
const LOG_TRACE = iota++; // Trace logging, intended for internal use only
const LOG_DEBUG = iota++; // Debug logging, used for internal debugging, it should be disabled on release builds
const LOG_INFO = iota++; // Info logging, used for program execution info
const LOG_WARNING = iota++; // Warning logging, used on recoverable failures
const LOG_ERROR = iota++; // Error logging, used on unrecoverable failures
const LOG_FATAL = iota++; // Fatal logging, used to abort program: exit(EXIT_FAILURE)
const LOG_NONE = iota++; // Disable logging

class RaylibJs {
  #FONT_SCALE_MAGIC = 0.65;

  #reset() {
    this.previous = undefined;
    this.wasm = undefined;
    this.ctx = undefined;
    this.dt = undefined;
    this.targetFPS = 60;
    this.entryFunction = undefined;
    this.prevPressedKeyState = new Set();
    this.currentPressedKeyState = new Set();
    this.currentMouseWheelMoveState = 0;
    /* -1 since 0 is left click, 1 middle, 2 is right click */
    this.currentIsMouseButtonPressed = -1;
    this.currentMousePosition = { x: 0, y: 0 };
    this.images = [];
    this.quit = false;
  }

  constructor() {
    this.#reset();
  }

  stop() {
    this.quit = true;
  }

  async start({ wasmPath, canvasId }) {
    if (this.wasm !== undefined) {
      console.error("The game is already running. Please stop() it first.");
      return;
    }

    const canvas = document.getElementById(canvasId);
    this.ctx = canvas.getContext("2d");
    if (this.ctx === null) {
      throw new Error("Could not create 2d canvas context");
    }

    this.wasm = await WebAssembly.instantiateStreaming(fetch(wasmPath), {
      env: make_environment(this),
    });

    const keyDown = (e) => {
      this.currentPressedKeyState.add(glfwKeyMapping[e.code]);
    };
    const keyUp = (e) => {
      this.currentPressedKeyState.delete(glfwKeyMapping[e.code]);
    };
    const wheelMove = (e) => {
      this.currentMouseWheelMoveState = Math.sign(-e.deltaY);
    };
    const mouseMove = (e) => {
      this.currentMousePosition = { x: e.clientX, y: e.clientY };
    };
    const mouseClick = (e) => {
      this.currentIsMouseButtonPressed = e.button;
    };
    const touchStarted = () => {
      this.currentIsMouseButtonPressed = 0;
    };
    const touchOrClickEnded = () => {
      this.currentIsMouseButtonPressed = -1;
    };
    window.addEventListener("keydown", keyDown);
    window.addEventListener("keyup", keyUp);
    window.addEventListener("wheel", wheelMove);
    window.addEventListener("mousemove", mouseMove);
    window.addEventListener("mousedown", mouseClick);
    window.addEventListener("mouseup", touchOrClickEnded);

    /* For phones */
    window.addEventListener("touchstart", touchStarted);
    window.addEventListener("touchend", touchOrClickEnded);
    window.addEventListener("touchcancel", touchOrClickEnded);

    this.wasm.instance.exports.main();
    const next = (timestamp) => {
      if (this.quit) {
        this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
        window.removeEventListener("keydown", keyDown);
        this.#reset();
        return;
      }
      this.dt = (timestamp - this.previous) / 1000.0;
      this.previous = timestamp;
      this.entryFunction();
      window.requestAnimationFrame(next);
    };
    window.requestAnimationFrame((timestamp) => {
      this.previous = timestamp;
      window.requestAnimationFrame(next);
    });
  }

  InitWindow(width, height, title_ptr) {
    this.ctx.canvas.width = width;
    this.ctx.canvas.height = height;
    const buffer = this.wasm.instance.exports.memory.buffer;
    document.title = cstr_by_ptr(buffer, title_ptr);
  }

  WindowShouldClose() {
    return false;
  }

  SetTargetFPS(fps) {
    console.log(
      `The game wants to run at ${fps} FPS, but in Web we gonna just ignore it.`,
    );
    this.targetFPS = fps;
  }

  GetScreenWidth() {
    return this.ctx.canvas.width;
  }

  GetScreenHeight() {
    return this.ctx.canvas.height;
  }

  GetFrameTime() {
    // TODO: This is a stopgap solution to prevent sudden jumps in dt when the user switches to a differen tab.
    // We need a proper handling of Target FPS here.
    return Math.min(this.dt, 1.0 / this.targetFPS);
  }

  BeginDrawing() {}

  EndDrawing() {
    this.prevPressedKeyState.clear();
    this.prevPressedKeyState = new Set(this.currentPressedKeyState);
    this.currentMouseWheelMoveState = 0.0;
  }

  DrawCircleV(center_ptr, radius, color_ptr) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const [x, y] = new Float32Array(buffer, center_ptr, 2);
    const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4);
    const color = color_hex_unpacked(r, g, b, a);
    this.ctx.beginPath();
    this.ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
    this.ctx.fillStyle = color;
    this.ctx.fill();
  }

  ClearBackground(color_ptr) {
    this.ctx.fillStyle = getColorFromMemory(
      this.wasm.instance.exports.memory.buffer,
      color_ptr,
    );
    this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
  }

  // RLAPI void DrawText(const char *text, int posX, int posY, int fontSize, Color color);       // Draw text (using default font)
  DrawText(text_ptr, posX, posY, fontSize, color_ptr) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const text = cstr_by_ptr(buffer, text_ptr);
    const color = getColorFromMemory(buffer, color_ptr);
    fontSize *= this.#FONT_SCALE_MAGIC;
    this.ctx.fillStyle = color;
    // TODO: since the default font is part of Raylib the css that defines it should be located in raylib.js and not in index.html
    this.ctx.font = `${fontSize}px grixel`;

    const lines = text.split("\n");
    for (var i = 0; i < lines.length; i++) {
      this.ctx.fillText(lines[i], posX, posY + fontSize + i * fontSize);
    }
  }

  // RLAPI void DrawRectangle(int posX, int posY, int width, int height, Color color);                        // Draw a color-filled rectangle
  DrawRectangle(posX, posY, width, height, color_ptr) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const color = getColorFromMemory(buffer, color_ptr);
    this.ctx.fillStyle = color;
    this.ctx.fillRect(posX, posY, width, height);
  }

  DrawRectangleV(position_ptr, size_ptr, color_ptr) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const color = getColorFromMemory(buffer, color_ptr);
    const position = new Float32Array(buffer, position_ptr, 2);
    const size = new Float32Array(buffer, size_ptr, 2);
    this.ctx.fillStyle = color;
    this.ctx.fillRect(position[0], position[1], size[0], size[1]);
  }

  IsKeyPressed(key) {
    return (
      !this.prevPressedKeyState.has(key) && this.currentPressedKeyState.has(key)
    );
  }
  IsKeyDown(key) {
    return this.currentPressedKeyState.has(key);
  }
  GetMouseWheelMove() {
    return this.currentMouseWheelMoveState;
  }
  IsGestureDetected() {
    return false;
  }
  IsMouseButtonPressed(key) {
    return this.currentIsMouseButtonPressed == key;
  }

  TextFormat(...args) {
    // TODO: Implement printf style formatting for TextFormat
    return args[0];
  }

  TraceLog(logLevel, text_ptr, ...args) {
    // TODO: Implement printf style formatting for TraceLog
    const buffer = this.wasm.instance.exports.memory.buffer;
    const text = cstr_by_ptr(buffer, text_ptr);
    switch (logLevel) {
      case LOG_ALL:
        console.log(`ALL: ${text} ${args}`);
        break;
      case LOG_TRACE:
        console.log(`TRACE: ${text} ${args}`);
        break;
      case LOG_DEBUG:
        console.log(`DEBUG: ${text} ${args}`);
        break;
      case LOG_INFO:
        console.log(`INFO: ${text} ${args}`);
        break;
      case LOG_WARNING:
        console.log(`WARNING: ${text} ${args}`);
        break;
      case LOG_ERROR:
        console.log(`ERROR: ${text} ${args}`);
        break;
      case LOG_FATAL:
        throw new Error(`FATAL: ${text}`);
      case LOG_NONE:
        console.log(`NONE: ${text} ${args}`);
        break;
    }
  }

  GetMousePosition(result_ptr) {
    const bcrect = this.ctx.canvas.getBoundingClientRect();
    const x = this.currentMousePosition.x - bcrect.left;
    const y = this.currentMousePosition.y - bcrect.top;

    const buffer = this.wasm.instance.exports.memory.buffer;
    new Float32Array(buffer, result_ptr, 2).set([x, y]);
  }

  CheckCollisionPointRec(point_ptr, rec_ptr) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const [x, y] = new Float32Array(buffer, point_ptr, 2);
    const [rx, ry, rw, rh] = new Float32Array(buffer, rec_ptr, 4);
    return x >= rx && x <= rx + rw && y >= ry && y <= ry + rh;
  }

  Fade(result_ptr, color_ptr, alpha) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const [r, g, b, _] = new Uint8Array(buffer, color_ptr, 4);
    const newA = Math.max(0, Math.min(255, 255.0 * alpha));
    new Uint8Array(buffer, result_ptr, 4).set([r, g, b, newA]);
  }

  DrawRectangleRec(rec_ptr, color_ptr) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const [x, y, w, h] = new Float32Array(buffer, rec_ptr, 4);
    const color = getColorFromMemory(buffer, color_ptr);
    this.ctx.fillStyle = color;
    this.ctx.fillRect(x, y, w, h);
  }

  DrawRectangleLinesEx(rec_ptr, lineThick, color_ptr) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const [x, y, w, h] = new Float32Array(buffer, rec_ptr, 4);
    const color = getColorFromMemory(buffer, color_ptr);
    this.ctx.strokeStyle = color;
    this.ctx.lineWidth = lineThick;
    this.ctx.strokeRect(
      x + lineThick / 2,
      y + lineThick / 2,
      w - lineThick,
      h - lineThick,
    );
  }

  MeasureText(text_ptr, fontSize) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const text = cstr_by_ptr(buffer, text_ptr);
    fontSize *= this.#FONT_SCALE_MAGIC;
    this.ctx.font = `${fontSize}px grixel`;
    return this.ctx.measureText(text).width;
  }

  TextSubtext(text_ptr, position, length) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const text = cstr_by_ptr(buffer, text_ptr);
    const subtext = text.substring(position, length);

    var bytes = new Uint8Array(buffer, 0, subtext.length + 1);
    for (var i = 0; i < subtext.length; i++) {
      bytes[i] = subtext.charCodeAt(i);
    }
    bytes[subtext.length] = 0;

    return bytes;
  }

  // RLAPI Texture2D LoadTexture(const char *fileName);
  LoadTexture(result_ptr, filename_ptr) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const filename = cstr_by_ptr(buffer, filename_ptr);

    var result = new Uint32Array(buffer, result_ptr, 5);
    var img = new Image();
    const isLocalhost = window.location.hostname === "localhost";
    const baseUrl = isLocalhost
      ? "http://localhost:6969/"
      : `https://${window.location.hostname}/`;
    img.src = `${baseUrl}${filename}`;
    this.images.push(img);

    result[0] = this.images.indexOf(img);
    // TODO: get the true width and height of the image
    result[1] = 256; // width
    result[2] = 256; // height
    result[3] = 1; // mipmaps
    result[4] = 7; // format PIXELFORMAT_UNCOMPRESSED_R8G8B8A8

    return result;
  }

  // RLAPI void DrawTexture(Texture2D texture, int posX, int posY, Color tint);
  DrawTexture(texture_ptr, posX, posY, color_ptr) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const [id, width, height, mipmaps, format] = new Uint32Array(
      buffer,
      texture_ptr,
      5,
    );
    // // TODO: implement tinting for DrawTexture
    // const tint = getColorFromMemory(buffer, color_ptr);

    this.ctx.drawImage(this.images[id], posX, posY);
  }

  // TODO: codepoints are not implemented
  LoadFontEx(
    result_ptr,
    fileName_ptr /*, fontSize, codepoints, codepointCount*/,
  ) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const fileName = cstr_by_ptr(buffer, fileName_ptr);
    // TODO: dynamically generate the name for the font
    // Support more than one custom font
    const font = new FontFace("myfont", `url(${fileName})`);
    document.fonts.add(font);
    font.load();
  }

  GenTextureMipmaps() {}
  SetTextureFilter() {}

  MeasureTextEx(result_ptr, font, text_ptr, fontSize, spacing) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const text = cstr_by_ptr(buffer, text_ptr);
    const result = new Float32Array(buffer, result_ptr, 2);
    this.ctx.font = fontSize + "px myfont";
    const metrics = this.ctx.measureText(text);
    result[0] = metrics.width;
    result[1] = fontSize;
  }

  DrawTextEx(font, text_ptr, position_ptr, fontSize, spacing, tint_ptr) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const text = cstr_by_ptr(buffer, text_ptr);
    const [posX, posY] = new Float32Array(buffer, position_ptr, 2);
    const tint = getColorFromMemory(buffer, tint_ptr);
    this.ctx.fillStyle = tint;
    this.ctx.font = fontSize + "px myfont";
    this.ctx.fillText(text, posX, posY + fontSize);
  }

  GetRandomValue(min, max) {
    return min + Math.floor(Math.random() * (max - min + 1));
  }

  ColorFromHSV(result_ptr, hue, saturation, value) {
    const buffer = this.wasm.instance.exports.memory.buffer;
    const result = new Uint8Array(buffer, result_ptr, 4);

    // Red channel
    let k = (5.0 + hue / 60.0) % 6;
    let t = 4.0 - k;
    k = t < k ? t : k;
    k = k < 1 ? k : 1;
    k = k > 0 ? k : 0;
    result[0] = Math.floor((value - value * saturation * k) * 255.0);

    // Green channel
    k = (3.0 + hue / 60.0) % 6;
    t = 4.0 - k;
    k = t < k ? t : k;
    k = k < 1 ? k : 1;
    k = k > 0 ? k : 0;
    result[1] = Math.floor((value - value * saturation * k) * 255.0);

    // Blue channel
    k = (1.0 + hue / 60.0) % 6;
    t = 4.0 - k;
    k = t < k ? t : k;
    k = k < 1 ? k : 1;
    k = k > 0 ? k : 0;
    result[2] = Math.floor((value - value * saturation * k) * 255.0);

    result[3] = 255;
  }

  raylib_js_set_entry(entry) {
    this.entryFunction =
      this.wasm.instance.exports.__indirect_function_table.get(entry);
  }
}

const glfwKeyMapping = {
  Space: 32,
  Quote: 39,
  Comma: 44,
  Minus: 45,
  Period: 46,
  Slash: 47,
  Digit0: 48,
  Digit1: 49,
  Digit2: 50,
  Digit3: 51,
  Digit4: 52,
  Digit5: 53,
  Digit6: 54,
  Digit7: 55,
  Digit8: 56,
  Digit9: 57,
  Semicolon: 59,
  Equal: 61,
  KeyA: 65,
  KeyB: 66,
  KeyC: 67,
  KeyD: 68,
  KeyE: 69,
  KeyF: 70,
  KeyG: 71,
  KeyH: 72,
  KeyI: 73,
  KeyJ: 74,
  KeyK: 75,
  KeyL: 76,
  KeyM: 77,
  KeyN: 78,
  KeyO: 79,
  KeyP: 80,
  KeyQ: 81,
  KeyR: 82,
  KeyS: 83,
  KeyT: 84,
  KeyU: 85,
  KeyV: 86,
  KeyW: 87,
  KeyX: 88,
  KeyY: 89,
  KeyZ: 90,
  BracketLeft: 91,
  Backslash: 92,
  BracketRight: 93,
  Backquote: 96,
  //  GLFW_KEY_WORLD_1   161 /* non-US #1 */
  //  GLFW_KEY_WORLD_2   162 /* non-US #2 */
  Escape: 256,
  Enter: 257,
  Tab: 258,
  Backspace: 259,
  Insert: 260,
  Delete: 261,
  ArrowRight: 262,
  ArrowLeft: 263,
  ArrowDown: 264,
  ArrowUp: 265,
  PageUp: 266,
  PageDown: 267,
  Home: 268,
  End: 269,
  CapsLock: 280,
  ScrollLock: 281,
  NumLock: 282,
  PrintScreen: 283,
  Pause: 284,
  F1: 290,
  F2: 291,
  F3: 292,
  F4: 293,
  F5: 294,
  F6: 295,
  F7: 296,
  F8: 297,
  F9: 298,
  F10: 299,
  F11: 300,
  F12: 301,
  F13: 302,
  F14: 303,
  F15: 304,
  F16: 305,
  F17: 306,
  F18: 307,
  F19: 308,
  F20: 309,
  F21: 310,
  F22: 311,
  F23: 312,
  F24: 313,
  F25: 314,
  NumPad0: 320,
  NumPad1: 321,
  NumPad2: 322,
  NumPad3: 323,
  NumPad4: 324,
  NumPad5: 325,
  NumPad6: 326,
  NumPad7: 327,
  NumPad8: 328,
  NumPad9: 329,
  NumpadDecimal: 330,
  NumpadDivide: 331,
  NumpadMultiply: 332,
  NumpadSubtract: 333,
  NumpadAdd: 334,
  NumpadEnter: 335,
  NumpadEqual: 336,
  ShiftLeft: 340,
  ControlLeft: 341,
  AltLeft: 342,
  MetaLeft: 343,
  ShiftRight: 344,
  ControlRight: 345,
  AltRight: 346,
  MetaRight: 347,
  ContextMenu: 348,
  //  GLFW_KEY_LAST   GLFW_KEY_MENU
};

function cstrlen(mem, ptr) {
  let len = 0;
  while (mem[ptr] != 0) {
    len++;
    ptr++;
  }
  return len;
}

function cstr_by_ptr(mem_buffer, ptr) {
  const mem = new Uint8Array(mem_buffer);
  const len = cstrlen(mem, ptr);
  const bytes = new Uint8Array(mem_buffer, ptr, len);
  return new TextDecoder().decode(bytes);
}

function color_hex_unpacked(r, g, b, a) {
  r = r.toString(16).padStart(2, "0");
  g = g.toString(16).padStart(2, "0");
  b = b.toString(16).padStart(2, "0");
  a = a.toString(16).padStart(2, "0");
  return "#" + r + g + b + a;
}

function color_hex(color) {
  const r = ((color >> (0 * 8)) & 0xff).toString(16).padStart(2, "0");
  const g = ((color >> (1 * 8)) & 0xff).toString(16).padStart(2, "0");
  const b = ((color >> (2 * 8)) & 0xff).toString(16).padStart(2, "0");
  const a = ((color >> (3 * 8)) & 0xff).toString(16).padStart(2, "0");
  return "#" + r + g + b + a;
}

function getColorFromMemory(buffer, color_ptr) {
  const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4);
  return color_hex_unpacked(r, g, b, a);
}