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

@vibedgames/gamepad

v0.1.1

Published

On-screen touch controls for browser games — framework-agnostic core + DOM and Phaser adapters

Downloads

257

Readme

@vibedgames/gamepad

On-screen touch controls for browser games — a floating analog joystick plus action buttons. Framework-agnostic core, with drop-in adapters that wire the input and render the overlay for you: a Phaser adapter (@vibedgames/gamepad/phaser) and a DOM adapter (@vibedgames/gamepad/dom) for everything else — Three.js, raw canvas, plain pages.

Install

npm install @vibedgames/gamepad

Phaser quickstart

import { attachVirtualGamepad } from "@vibedgames/gamepad/phaser";

// in your scene's create():
this.gamepad = attachVirtualGamepad(this, {
  // a "rest" button (no position) — any finger that isn't the stick fires:
  buttons: [{ id: "fire" }],
  onFirstTouch: () => this.hint?.remove(),
});

// in your scene's update():
this.gamepad.update(); // reconcile stale touches + redraw the overlay

const stick = this.gamepad.getStick();
if (stick.active && !stick.inDeadZone) {
  this.steer(stick.angle, stick.magnitude); // radians, 0–1 thrust
}
if (this.gamepad.isButtonDown("fire")) this.shoot();

The mouse is ignored on purpose, so a desktop build keeps whatever controls it already had — the gamepad is purely the touch overlay. isTouch flips true the first time a finger lands.

DOM quickstart (Three.js, canvas, anything)

Same controller, rendered as positioned DOM elements in a pointer-events: none overlay — it never steals taps from your HUD. Touches on interactive elements (button, a, inputs, [data-gamepad-ignore]) are left to the page.

import { attachDomGamepad } from "@vibedgames/gamepad/dom";

const gamepad = attachDomGamepad({
  visible: "coarse", // pre-show buttons on touch devices (default: after first touch)
  buttons: [
    {
      id: "jump",
      label: "JUMP",
      position: (v) => ({ x: v.width - 72 - v.inset.right, y: v.height - 72 - v.inset.bottom }),
    },
  ],
});

// in your game loop:
gamepad.update();
const stick = gamepad.getStick();
if (gamepad.justPressed("jump")) jump();

Give your game surface touch-action: none so the browser delivers moves instead of scrolling. setTint takes a CSS color here ("#22d3ee").

Fixed buttons (e.g. a bomb button)

Give a button a position resolver to pin it on-screen; it re-anchors on resize. The resolver's viewport includes safe-area insets (inset.top/right/ bottom/left — notch and home indicator, zeros elsewhere; requires viewport-fit=cover in your viewport meta tag on iOS). Use onButtonDown or justPressed(id) for edge-triggered actions (place a bomb once per tap) and isButtonDown for held input.

attachVirtualGamepad(this, {
  buttons: [
    {
      id: "bomb",
      label: "💣",
      position: ({ width, height, inset }) => ({
        x: width - 80 - inset.right,
        y: height - 80 - inset.bottom,
      }),
      radius: 48,
    },
  ],
  onButtonDown: (id) => {
    if (id === "bomb") this.placeBomb();
  },
});

Buttons with a label render it (text in the DOM adapter, a Text object in Phaser). Pass visible: "coarse" to either adapter to pre-show fixed buttons on touch-capable devices instead of waiting for the first touch — an invisible button is undiscoverable.

Grid movement

For tile-based games, snap the stick to a direction:

import { stickDirection4 } from "@vibedgames/gamepad/phaser";

const dir = stickDirection4(this.gamepad.getStick()); // "up" | "down" | "left" | "right" | null
if (dir) this.step(dir);

Rendering

The Phaser adapter draws the joystick + fixed buttons into a screen-fixed Graphics object. Tune it with render: { depth, tint, blendMode }, recolor at runtime with setTint(0xff00aa), or pass render: false and draw it yourself from gamepad.pad (see getStickGeometry, getStick, getButtonLayout).

Framework-agnostic core

The VirtualGamepad class has no engine dependency — feed it raw pointer events in screen-space (CSS pixel) coordinates and read the state back. Most non-Phaser games want attachDomGamepad instead; drive the core directly only for custom event routing or a custom renderer.

import { VirtualGamepad, safeAreaInset } from "@vibedgames/gamepad";

const pad = new VirtualGamepad({ buttons: [{ id: "jump" }] });
pad.setViewport(window.innerWidth, window.innerHeight, safeAreaInset());

// clientX/clientY, NOT offsetX or canvas.width — the pad works in CSS pixels,
// and a HiDPI canvas's buffer size is larger than its on-screen size.
window.addEventListener("pointerdown", (e) => pad.pointerDown(e.pointerId, e.clientX, e.clientY));
window.addEventListener("pointermove", (e) => pad.pointerMove(e.pointerId, e.clientX, e.clientY));
window.addEventListener("pointerup", (e) => pad.pointerUp(e.pointerId));
window.addEventListener("pointercancel", (e) => pad.pointerUp(e.pointerId));

// each frame:
pad.nextFrame(); // publish justPressed/justReleased edges
const stick = pad.getStick();
const jumping = pad.isButtonDown("jump");

Touch up events can go missing (finger slides off the canvas, tab switch) — call pad.reconcile(liveIds) each frame with the pointer ids you know are down, or pad.reset() on blur, like the adapters do.

Touch routing

Each pointerDown is routed in order:

  1. Fixed buttons — a touch inside a button's circle presses it.
  2. The stick — the first free touch anchors the floating stick.
  3. A "rest" button (a button with no position) — catches everything else.

This covers both the twin-stick model (stick + a rest "fire" button: any second finger shoots) and the d-pad-style model (stick + a fixed action button).

Physical controllers

PhysicalGamepad reads real controllers (the Gamepad API, standard mapping, first connected pad wins) with the same read API as the virtual pad, so one code path serves both. Bind your game's action ids to physical buttons and poll once per frame:

import { PhysicalGamepad, stickDirection4 } from "@vibedgames/gamepad";

const pad = new PhysicalGamepad({
  bindings: { jump: ["a"], dash: ["b", "rb"] },
  onConnect: () => showToast("🎮 controller connected"),
});

// in your game loop:
pad.update();
const dir = stickDirection4(pad.getStick()); // left stick; getStick("right") too
const jumped = vpad.justPressed("jump") || pad.justPressed("jump");

Raw button names ("a", "rt", "up", …) work without a binding, and buttonValue("rt") exposes analog trigger values for driving games. isPadConnected() is a standalone check for UI ("show controller hints only when a pad is plugged in"). Everything is poll-based — no listeners, no DOM — and the pad source is injectable (poll) for headless tests.

Related

  • @vibedgames/multiplayer — real-time multiplayer for browser games (framework-agnostic core + React hooks). Pairs naturally: recolor the joystick/buttons per player with setTint.

License

MIT