npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

minimal-engine-lib

v0.1.3

Published

`minimal-engine-lib` is a project with engine library version.

Readme

Minimal Engine Library

minimal-engine-lib is a project with engine library version.

Table of Contents

Assumptions

High Level

AI Prompt:

I want to write a simple 2D game engine for a PC browser.
Canvas on full screen. It fits the window. Animations.
Updates for physics and renders for graphics. Input system.
Music and sounds. That's all, procedurally. Divided into modules.
So that the game itself can be added to this framework. Do you understand?
The config is state-of-the-art, minimal. The script builds everything and hosts it.
Typescipt, html, styles, and code are all separate files.

⬆ Table of Contents

Requirements

A minimal, modern 2D engine skeleton in TypeScript for PC browsers, based on Canvas (fullscreen, responsive), with:

  • a game loop (update + render, separated),
  • animations,
  • input system (keyboard / mouse),
  • audio (music + SFX),
  • modular structure,
  • clean architecture (engine separated from game logic),
  • modern build system (Vite / ES modules),
  • separate files: HTML, CSS, TS,
  • a dev script → builds and hosts.

Basically, something like a lightweight, custom mini-Unity / Phaser, but procedural and minimalist.

⬆ Table of Contents

🔥 Architecture

/src
  core.ts
  loop.ts
  renderer.ts
  input.ts
  audio.ts
  index.ts
package.json
tsconfig.json
vite.config.ts

⬆ Table of Contents

🚀 Stack

(the simplest and modern)

  • TypeScript
  • Vite (dev server + build)
  • ES modules
  • Zero frameworks

⬆ Table of Contents

📦 Installation

pnpm create vite@latest minimal-engine-lib -- --template vanilla-ts
cd minimal-engine-lib
pnpm install
pnpm run dev

There is a need to remove template files not needed for library.

⬆ Table of Contents

⚙️ Configuration

(Generated from vite)

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": [
      "ES2022",
      "DOM",
      "DOM.Iterable"
    ],
    "types": [
      "vite/client"
    ],
    "skipLibCheck": true,
    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": false,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": false,
    "declaration": true,
    "outDir": "dist",
    "declarationDir": "dist",
    "emitDeclarationOnly": true,
    "rootDir": "src",
    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": false,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": [
    "src"
  ]
}

Vite handles bundling.

package.json

{
  "name": "minimal-engine-lib",
  "version": "0.1.1",
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "exports": {
    ".": {
      "import": "./dist/minimal-engine.es.js",
      "require": "./dist/minimal-engine.cjs.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "vite build && tsc --emitDeclarationOnly",
    "dev": "vite"
  },
  "devDependencies": {
    "typescript": "^5.9.3",
    "vite": "^8.0.0-beta.16"
  }
}

vite.config.ts

import { defineConfig } from "vite";

export default defineConfig({
    build: {
        lib: {
            entry: "./src/index.ts",      // your engine entry file
            name: "MinimalEngine",        // UMD global variable name
            fileName: (format) => `minimal-engine.${format}.js`,
            formats: ["es", "cjs", "umd"] // bundle in ES, CommonJS, and UMD
        },
        minify: true,                  // <- keep code readable
        sourcemap: true,                // optional, generates source maps for debugging
        rollupOptions: {
            external: [],                 // any external dependencies you don't want to bundle
        }
    }
});

⬆ Table of Contents

Implementation

Game Loop

Classic fixed update + render.
✔ constant interval physics
✔ smooth render

loop.ts

export class GameLoop {
  private lastTime = 0;
  private accumulator = 0;
  private readonly fixedDelta = 1 / 60;

  constructor(
    private update: (dt: number) => void,
    private render: (alpha: number) => void
  ) {}

  start() {
    requestAnimationFrame(this.tick);
  }

  private tick = (time: number) => {
    if (!this.lastTime) {
      this.lastTime = time;
    }

    const delta = Math.min((time - this.lastTime) / 1000, 0.25);
    this.lastTime = time;
    this.accumulator += delta;

    while (this.accumulator >= this.fixedDelta) {
      this.update(this.fixedDelta);
      this.accumulator -= this.fixedDelta;
    }

    const alpha = this.accumulator / this.fixedDelta;
    this.render(alpha);
    requestAnimationFrame(this.tick);
  };
}

⬆ Table of Contents

Renderer

Simple fullscreen canvas renderer with resize handling.
✔ auto-resize
✔ context management
✔ clear utility

renderer.ts

export class Renderer {
    public ctx: CanvasRenderingContext2D;
    public canvas: HTMLCanvasElement;

    constructor() {
        const canvas = document.getElementById("canvas") as HTMLCanvasElement;;
        if (!canvas) throw new Error("Canvas not found on page!")
        this.canvas = canvas
        this.ctx = this.canvas.getContext("2d")!;

        window.addEventListener("resize", this.resize);
        this.resize();
    }

    resize = () => {
        const dpr = window.devicePixelRatio || 1;

        this.canvas.width = window.innerWidth * dpr;
        this.canvas.height = window.innerHeight * dpr;

        this.canvas.style.width = window.innerWidth + "px";
        this.canvas.style.height = window.innerHeight + "px";

        this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };

    clear() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
}

⬆ Table of Contents

Input

Simple keyboard input handling with Set.
✔ key tracking
✔ clean API
✔ lightweight

input.ts

export class Input {
  private keys = new Set<string>();

  constructor() {
    window.addEventListener("keydown", (e) => {
      if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.key)) {
        e.preventDefault();
      }
      this.keys.add(e.key);
    });
    window.addEventListener("keyup", (e) => this.keys.delete(e.key));
  }

  isDown(key: string) {
    return this.keys.has(key);
  }
}

⬆ Table of Contents

Audio

  • Minimal Web Audio manager with looping music support and volume control
    • SFX supportload() + play() for short sounds.
    • Looping musicplayMusic() with optional volume.
    • Volume controlsetMusicVolume().
    • Safe browser autoplay handling – resumes AudioContext on first user interaction.
    • Single music track at a time – clean and minimal.
    • Fully modular, fits your engine architecture.

audio.ts

export class AudioManager {
    private context: AudioContext;
    private buffers = new Map<string, AudioBuffer>();
    private unlocked = false;
    private musicSource: AudioBufferSourceNode | null = null;
    private musicGain: GainNode;

    constructor() {
        this.context = new AudioContext();
        this.musicGain = this.context.createGain();
        this.musicGain.connect(this.context.destination);

        const unlock = async () => {
            if (this.context.state === "suspended") {
                await this.context.resume();
            }
            this.unlocked = true;
            window.removeEventListener("click", unlock);
            window.removeEventListener("keydown", unlock);
        };

        window.addEventListener("click", unlock, { once: true });
        window.addEventListener("keydown", unlock, { once: true });
    }

    async load(name: string, url: string) {
        const res = await fetch(url);
        const arrayBuffer = await res.arrayBuffer();
        const audioBuffer = await this.context.decodeAudioData(arrayBuffer);
        this.buffers.set(name, audioBuffer);
    }

    play(name: string) {
        if (!this.unlocked) return;
        const buffer = this.buffers.get(name);
        if (!buffer) return;

        const source = this.context.createBufferSource();
        source.buffer = buffer;
        source.connect(this.context.destination);
        source.start();
    }

    playMusic(name: string, volume = 1) {
        if (!this.unlocked) return;
        const buffer = this.buffers.get(name);
        if (!buffer) return;

        this.stopMusic();

        this.musicGain.gain.value = volume;

        const source = this.context.createBufferSource();
        source.buffer = buffer;
        source.loop = true;
        source.connect(this.musicGain);
        source.start();
        this.musicSource = source;
    }

    stopMusic() {
        if (this.musicSource) {
            this.musicSource.stop();
            this.musicSource.disconnect();
            this.musicSource = null;
        }
    }

    setMusicVolume(volume: number) {
        this.musicGain.gain.value = volume;
    }

    async playMusicAfterGesture(name: string, volume = 1) {
        if (this.context.state === "suspended") {
            await this.context.resume();
        }
        this.unlocked = true;
        this.playMusic(name, volume);
    }
}

⬆ Table of Contents

Index

index.ts

export * from "./core";
export * from "./loop";
export * from "./renderer";
export * from "./input";
export * from "./audio";

⬆ Table of Contents

Publish

Step-by-step, ready-to-run command sequence for your first-time npm publish of minimal-engine-lib. This assumes you’re using pnpm and have an npm account.

Build the library

Run the Vite build script you already have:

pnpm run build
  • Output goes to dist/
  • Files: minimal-engine.esm.js, minimal-engine.cjs.js, minimal-engine.umd.js
  • Source maps: optional, already enabled

Test locally (optional but recommended)

pnpm pack
# This creates a tarball like minimal-engine-lib-0.1.0.tgz
# You can install it in another project to make sure it works:
pnpm add ./minimal-engine-lib-0.1.0.tgz

Login to npm

pnpm login

Enter your npm username, password, and email.


Publish to npm

Add .env file (add to .gitignore).
Put your token there, npm shows it only once after generating it.

NPM_TOKEN=your_token_here

Add file .npmrc.

registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
export NPM_TOKEN=<your-token>
pnpm publish --access public

Use --access public if your package name is scoped (e.g., @yourname/minimal-engine-lib).


Future updates

When you make changes:

pnpm version patch   # or minor/major
pnpm run build
pnpm publish --access public

Each publish must have a unique version.

⬆ Table of Contents