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

punter.js

v5.0.0

Published

A teaching-focused JavaScript game engine for the browser

Downloads

4,424

Readme

punter.js

Tests npm license

A teaching-focused JavaScript game engine for the browser.

Created by the Orca Scan team, Punter.js powers OrcaCam and was designed to help students and new developers learn JavaScript by making games. Its deliberately simple design keeps the important parts visible instead of hiding them behind frameworks and abstractions.

Game programming terms

  • Canvas - an HTML element you draw on with JavaScript. Every punter.js game lives inside one <canvas>
  • Sprite - a game object (player, enemy, coin). It has a position, can move, animate and collide
  • Scene - one screen of your game (menu, level, game over). Sprites and logic are set up inside a scene
  • Game loop - code that runs 60 times per second to move sprites, check collisions and redraw the screen

Examples

This project includes a few example games which you can play and edit in the browser at https://punterjs.org. Or, you can download and run the examples locally using:

git clone https://github.com/orca-scan/punter.js.git
cd punter.js
npm install
npm start

This starts a local server on port 4000 and opens the games index in your browser.

Quick Start

<canvas id="game"></canvas>
<script src="punter.js"></script>
<script>

  // load assets and bind to a canvas (must be called first)
  punter.setup({
    canvas: '#game',
    debug: false, // set true to see FPS, bounds boxes, sprite labels
    images: {
      player: 'images/player.png',
      enemy: 'images/enemy.svg'
    },
    sounds: {
      jump: 'sounds/jump.mp3'
    }
  });

  // define a scene - create sprites and register update/draw handlers inside
  punter.scene('level1', function () {

    // create a sprite using the 'player' image loaded above
    var player = punter.createSprite({
      id: 'player',   // optional - auto-generated if omitted; must be unique if provided
      image: 'player', // key from config.images
      x: 50,           // pixels from left edge of canvas
      y: 50            // pixels from top edge of canvas
    });

    // runs ~60 times/sec - move things, read input, check collisions
    punter.on('update', function () {
      if (punter.isKeyDown('right')) player.moveX(2);
    });

    // runs once per frame after sprites draw - use for score text, HUD, overlays
    punter.on('draw', function (ctx) {
      // ctx is the canvas 2D context
    });
  });

  // go() a specific scene - can be called immediately
  punter.go('level1');
</script>

1. Setup

Call punter.setup(config) once at the start. It loads images and sounds ahead of time, sets up the canvas, and fires the ready event when everything's finished loading. Config options are:

Option | Type | Description :--------|:---------------|:-------------------------------------------------------- canvas | string/element | CSS selector or a <canvas> element debug | boolean | Shows FPS counter, sprite bounds, and position labels images | object | { key: imageUrl } map - preloaded before ready fires sounds | object | { key: soundUrl } map - decoded before ready fires

The images and sounds options are just name-to-file lookups - you pick a short name (like player) and point it at the actual file (like images/player.png), then refer to it by that short name everywhere else in your code.

2. Scenes

A scene is a named function that sets up update/draw handlers and creates sprites - think of it as "everything that happens on this particular screen of the game."

punter.scene('menu', function () {
  // set up sprites and handlers here
});

punter.go('menu'); // destroys sprites from the previous scene, then switches and starts the loop

scene() just registers a scene under a name - it doesn't run anything yet. go() is what actually switches to it and starts things moving. You'll usually have several scenes (a menu, a level, a game-over screen) and use go() to jump between them, e.g. punter.go('gameOver') when the player loses.

punter.go() can be called before setup() has finished loading - it queues the scene name and runs it automatically once assets are ready, so you don't need to wait on the ready event just to call go(). It always tears down every sprite from the current scene and clears input state before running the new scene's handler - scenes don't need to clean up after themselves, the engine does it for you.

3. Sprites

A sprite is any game object with a position - the player, an enemy, a coin, a wall. Tell it how to look (an image, a vector draw function, or both) and where to place it (x, y). Give it an id if you need to look it up later - otherwise the engine generates one for you.

var enemy = punter.createSprite({
  id: 'enemy1',            // optional - auto-generated if omitted; useful when you need punter.getSprite('enemy1') later
  image: 'enemy',          // matches a key from config.images (or an array of keys for animation)
  vector: null,            // optional draw function - see "Vector sprites" below
  x: 100, y: 200,          // position (number or '%' string, e.g. '50%')
  w: 32, h: 32,            // optional size (auto-detected from image if omitted; required for vector-only)
  preserveAspect: true,    // maintain image aspect ratio (default: true)
  collidable: true,        // computes bounding box for collisions (default: true)
  boundsMode: 'pixel',     // 'pixel' = edge-aware collisions from opaque pixels, 'rect' = full sprite w/h
  repeatX: false,          // tile horizontally across the canvas (default: false)
  repeatY: false,          // tile vertically across the canvas (default: false)
  clipHeight: null,        // clip sprite to this height in pixels (null = no clip)
  clipFrom: 'bottom',      // clip from 'top' or 'bottom' (default: 'bottom')
  outline: 'red'           // optional debug outline color
});

x and y are the sprite's position in pixels, measured from the top-left corner of the canvas - so x: 0, y: 0 is the top-left, and increasing x moves right, increasing y moves down. Once created, enemy is a normal JavaScript object you keep a reference to and call methods on, like enemy.moveX(5).

Vector sprites

Not every sprite needs an image file. Pass a vector function to draw the sprite with canvas code - useful for simple shapes, procedural graphics, or games that don't use image files at all. The engine calls it each frame with (ctx, w, h), where ctx is the canvas context already translated to the sprite's top-left corner.

// a triangle drawn with code instead of a PNG
var ship = punter.createSprite({
  id: 'ship',
  x: 100, y: 200,
  w: 36, h: 36,
  vector: function (ctx, w, h) {
    ctx.strokeStyle = '#00ffcc';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(w / 2, 0);    // tip
    ctx.lineTo(w, h);         // bottom-right
    ctx.lineTo(0, h);         // bottom-left
    ctx.closePath();
    ctx.stroke();
  }
});

When both image and vector are set, the image draws first and the vector function draws on top - useful for overlays like health bars or damage effects. Vector-only sprites default to boundsMode: 'rect' since there is no image to scan.

sprite methods

Method | Description :------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------- sprite.moveX(dx) | Move right (+dx) or left (-dx) by pixels sprite.moveY(dy) | Move down (+dy) or up (-dy) by pixels sprite.center(offsetX?, offsetY?) | Centre on canvas; use centerX() / centerY() for one axis sprite.animate(delayMs) | Advance animation frame; requires image to be an array sprite.bounce(range, speed) | Smooth up-and-down float anchored to creation position sprite.blink(ms?, durationMs?) | Flash on/off every ms ms (default 130); auto-stops after durationMs if given, otherwise blinks indefinitely; call blink(0) to stop early sprite.scroll(options) | Scroll and optionally loop ({ speedX, loop: true }) or respawn ({ speedX, respawnAfter: ms, offset: n }) sprite.rotate(amount) | Add amount (radians) to sprite.angle; vector functions can read this.angle directly sprite.isCollidingWith(other) | true if collision areas overlap; in pixel mode transparent corners are ignored, other can be a sprite or a plain { x, y, w, h } rect sprite.destroy() | Remove from engine; further draw() calls are silently ignored

sprite properties

Property | Description :----------------|:----------------------------------------------------------------- sprite.visible | true if any part of the sprite is within canvas bounds sprite.seen | true once the sprite has been visible at least once. Read/write sprite.actualW | Drawn width in pixels, same as w sprite.actualH | Drawn height in pixels, accounting for clipHeight if set

4. Input

Input is how your game reads the keyboard and mouse/touch. Use punter.isKeyDown() inside your update handler to check which keys are held, and punter.pointer to read mouse/touch position and taps.

punter.on('update', function () {

  // keyboard - pass one or more key names (true if any are held)
  if (punter.isKeyDown('left', 'a'))  player.moveX(-3);
  if (punter.isKeyDown('right', 'd')) player.moveX(3);
  if (punter.isKeyDown('up', 'w'))    player.moveY(-3);
  if (punter.isKeyDown('down', 's'))  player.moveY(3);
  if (punter.isKeyDown('space'))      punter.playSound('jump');

  // use + for combos where all keys must be held at once
  if (punter.isKeyDown('shift+a'))    player.moveX(-6);

  // mouse/touch - clicked is true for one frame after a tap or click
  if (punter.pointer.clicked) {
    player.x = punter.pointer.x;
    player.y = punter.pointer.y;
  }

  // swipe detection - fires once on release (good for mobile)
  if (punter.pointer.swipedLeft)  player.moveX(-5);
  if (punter.pointer.swipedRight) player.moveX(5);
});

isKeyDown() takes friendly key names like 'left', 'space', 'a' etc. Pass multiple names to check if any of them are held (OR logic), or join names with + for combos that require all keys at once (AND logic). The pointer properties work on both desktop (mouse) and mobile (touch) automatically.

key names

Name | Key :----------------------------------|:------------------------------ 'left' 'right' 'up' 'down' | Arrow keys 'space' | Spacebar 'enter' | Enter 'escape' / 'esc' | Escape 'tab' | Tab 'backspace' | Backspace 'shift' 'ctrl' 'alt' | Modifier keys 'a''z' | Letter keys (case-insensitive)

keyboard

Property/Method | Description :-------------------------------|:------------------------------------------------------------------ punter.isKeyDown('left', 'a') | true while any of the given keys is held punter.isKeyDown('shift+a') | true while all keys in the combo are held punter.clearInput() | Resets all key and pointer states (called automatically on go())

pointer (mouse & touch)

Property | Description :------------------------------|:--------------------------------------------------------------- punter.isPointerDown(button) | true while a button is held ('left', 'right', 'middle') punter.pointer.x | Current pointer X in canvas pixels punter.pointer.y | Current pointer Y in canvas pixels punter.pointer.clicked | true for one frame after a tap or click (not set by swipes) punter.pointer.down | true while the primary button or finger is held punter.pointer.swiped | true for one frame after a swipe punter.pointer.swipedUp | true if the swipe direction was up punter.pointer.swipedDown | true if the swipe direction was down punter.pointer.swipedLeft | true if the swipe direction was left punter.pointer.swipedRight | true if the swipe direction was right punter.pointer.swipeDistance | Distance of the swipe in canvas pixels

5. Sound

punter.playSound('jump', { volume: 0.8, loop: false, speed: 1 });
punter.stopSound('jump');

playSound takes the short name you gave a sound in setup()'s sounds option, plus optional settings - volume (0 to 1), loop (repeat forever or not), and speed (1 is normal, 2 is double speed, etc.).

Short sound effects layer automatically — up to 3 instances of the same sound can overlap at once (useful for rapid-fire collisions). The 4th call evicts the oldest. Sounds played with { loop: true } restart instead of stacking, as does any call with { restart: true }.

6. Events

punter.on(eventName, fn) Events are how you plug your own code into the engine's game loop - you give it a function, and the engine calls that function at the right moment. Register update and draw inside a scene function so they're set up fresh each time the scene starts. The ready, resize, and go events can be registered anywhere.

Event name | When it fires :----------|:------------------------------------------------------------------------ ready | Once, after all assets are loaded. Not needed just to call go() update | ~60 times/sec (fixed timestep) - put game logic here draw | Once per animation frame, after sprites auto-draw - use for HUD/overlays resize | Whenever the canvas resizes go | Whenever punter.go() switches scenes

// these can be registered anywhere - they are not scene-specific
punter.on('ready', function () {
  // all images and sounds are loaded - safe to start
  punter.go('level1');
});

punter.on('resize', function () {
  // canvas resized - reposition any manually-placed elements
  player.center();
});

punter.on('go', function (sceneName) {
  // fires after every scene switch - useful for analytics or shared teardown
});

// update and draw must go inside a scene
punter.scene('level1', function () {

  var player = punter.createSprite({ image: 'player', x: 50, y: 50 });
  var enemy = punter.createSprite({ image: 'enemy', x: 200, y: 100 });
  var score = 0;

  punter.on('update', function () {
    // runs ~60 times/sec - move things, read input, check collisions
    if (punter.isKeyDown('right')) player.moveX(3);
    if (player.isCollidingWith(enemy)) punter.go('gameOver');
  });

  punter.on('draw', function (ctx) {
    // runs after sprites draw - ctx is the canvas 2D context
    // use for score text, HUD, overlays; sprites draw themselves automatically
    ctx.fillStyle = 'white';
    ctx.fillText('Score: ' + score, 10, 20);
  });
});

Gotchas for Beginners

  • Sprite IDs must be unique - reusing one throws an error
  • repeatX and repeatY are mutually exclusive on a single sprite
  • Percentage strings ('50%') for x/y/w/h are resolved relative to canvas size
  • Don't manually resize the canvas - the engine owns that via its own resize() handling
  • punter.setup() must be called before createSprite, playSound, pause etc

How the Game Loop Works

Once you call punter.go(), the engine runs a loop 60 times every second. Each loop does this:

  1. Runs your update code (move things, check collisions)
  2. Clears the screen
  3. Draws all sprites
  4. Runs your draw code (score, HUD)
  5. Starts again

Each update always moves the game forward by the same amount of time - so your game runs at the same speed on fast and slow devices. Without this, a slow phone would make everything move in slow motion.

Use punter.pause() and punter.resume() to freeze and unfreeze the loop - good for a pause menu.

punter.redraw() redraws the screen without running any game logic - useful when the game is paused.


Technical details

You do not need to understand this section to build a game. These are some of the details Punter.js handles for you:

  • Content-trimmed + edge-aware collision data. getBounds does a per-pixel alpha scan once per image key, then caches both a tight content box and a compact occupancy mask. Runtime collision still starts with a fast AABB overlap, but pixel mode adds a lightweight edge-aware check so transparent corners are less likely to register as hits.
  • A real fixed-timestep loop. The loop runs update on a 60Hz accumulator, decoupled from draw, with a 100ms-per-frame cap to avoid a spiral of death after a tab is backgrounded. Game logic behaves the same on a 60Hz and 144Hz screen - most hand-rolled canvas code skips this and gets frame-rate-dependent movement.
  • Pointer, touch, and stylus are unified. One keys/pointer input model backed by the Pointer Events API (with mouse/touch fallback for older browsers), with coordinate mapping that accounts for canvas CSS scaling (getBoundingClientRect + scale factor). Covers mouse, touchscreen, and stylus/pen with no extra game code. Tap vs swipe is classified on release using a distance threshold scaled to canvas width — pointer.clicked fires for taps, the swiped* properties fire for swipes, and they are mutually exclusive.
  • DPR-aware canvas buffer. The canvas backing buffer is sized at internalResolution × devicePixelRatio (capped at 2x) so art stays sharp on retina screens. All game coordinates - sprite positions, movement, pointer, and custom canvas drawing - use logical pixels; the engine handles the physical scaling internally.

None of this is exotic, it's just the stuff that's easy to get wrong when you write it yourself.

Responsive Canvas

The canvas always matches the real screen's aspect ratio - landscape screens get a landscape canvas, portrait screens get a portrait canvas. There's no portrait lock and no letterboxing (black bars).

Internally, resize() uses a 375×667 reference resolution purely as a starting point for arithmetic: it picks whichever axis (width or height) that reference matches the current orientation on, holds that axis fixed, and computes the other axis from the screen's actual ratio. The result is an internal resolution whose aspect ratio equals the screen's aspect ratio exactly, every time - 375×667 never appears in the final canvas size except by coincidence.

The backing buffer is additionally scaled by devicePixelRatio (capped at 2x) for crisp rendering on retina displays. Sprites automatically reposition/resize on resize - you don't need to do anything manually.

Debug Mode

Set debug: true in setup() (or punter.debug = true later) to see:

  • FPS, frame count, canvas size, and orientation (bottom-right overlay)
  • Position/size labels under each sprite

To see collision outlines on a specific sprite, set outline: 'red' in its options — this works independently of debug mode and is useful for checking why collisions aren't firing.

This is genuinely useful while learning - turning it on lets you see the invisible collision boxes and frame rate, which makes it much easier to understand why something isn't colliding the way you expect.

Debug mode, device type, and orientation are also reflected as data-punter-* attributes on <html> (e.g. data-punter-device, data-punter-orientation, data-punter-scene) and as --punter-vpw / --punter-vph CSS custom properties, if you want to hook into them from your own CSS.

Other engine properties

punter exposes a handful of read-only properties and helpers beyond what's covered above:

Property | Description :----------------------|:-------------------------------------------------- punter.currentScene | name of the currently active scene punter.getSprite(id) | get a sprite by id, null if not found punter.width | logical pixel width of the canvas punter.height | logical pixel height of the canvas punter.frame | total frames since the loop started (never resets) punter.canvas | the <canvas> element punter.paused | true if the loop is paused punter.isMobile | true on mobile devices punter.orientation | 'portrait' or 'landscape'

License

MIT License. Built by the Orca Scan Team.