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

vibeg-kit

v1.1.0

Published

Make web games that play on every screen and device: a full-window canvas framed for any shape, keyboard, mouse, gamepad and touch through one input API, on-screen controls, device-aware prompts, menus, sound and music. The device layer of the vibeg.lol g

Readme

vibeg-kit

Make web games that play on every screen and every device: a computer with keyboard, mouse or gamepad, a phone held upright or sideways, a tablet, an ultra-wide monitor. vibeg-kit is the device layer the vibeg.lol games are built on:

  • Screen: a full-window canvas at the device's pixel ratio, framed for any shape (no letterbox), a HUD layer that scales with the view, a frame-rate guard for slow phones.
  • Input: keyboard, mouse, gamepad and touch through one API (input.pressed('A')), an analog stick, the pointer, every finger, the wheel.
  • Touch: gestures on the game itself (tap, hold, halves, swipes, flicks, drags) and on-screen sticks and buttons where a game needs them.
  • Prompts: "Press Space to start" / "Tap to start" / "Press Ⓐ to start", switching by itself when the player picks up another device.
  • Menus, sound and music, save / load, pause when the tab is hidden.

It works with any engine. createGame is the Three.js version; createCore is the same without a renderer, for a 2D canvas, Phaser, PixiJS, Babylon or your own.

npm install vibeg-kit three      # three only for createGame

The package is plain ES modules with CSS imports: use it with a bundler that handles them (Vite, esbuild, Parcel, webpack with a CSS loader). The examples use Vite.

Quick start (Three.js)

import * as THREE from 'three';
import { createGame } from 'vibeg-kit';

const hh = createGame({ clearColor: 0x87ceeb, touch: { tap: 'A' } });
const { renderer, input, hud } = hh;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(40, 1.5, 0.1, 200);
camera.position.set(0, 2, 10);
hh.onResize(() => hh.fitCamera(camera, { fov: 40, aspect: 1.5 }));

hud.innerHTML = `<div class="msg">${hh.prompt('jump', ['A', 'TAP'])}</div>`;
hh.run((dt) => {
  if (input.pressed('A')) jump();
  if (input.pressed('START')) pause();
  renderer.render(scene, camera);
});
hh.onHide(() => pause());

index.html needs only a HUD element and the script:

<body><div id="hud"></div><script type="module" src="./src/main.js"></script></body>

Quick start (any engine)

import { createCore } from 'vibeg-kit/core';

const canvas = document.createElement('canvas');
const hh = createCore({ canvas, touch: { swipe: true } }); // the kit sizes the canvas
const g = canvas.getContext('2d');
let x = 0;
hh.run((dt) => {
  x += hh.input.stick.x * 300 * dt;
  g.setTransform(hh.view.pixelRatio, 0, 0, hh.view.pixelRatio, 0, 0); // draw in CSS px
  g.clearRect(0, 0, hh.view.width, hh.view.height);
  g.fillRect(hh.view.width / 2 + x, hh.view.height / 2, 40, 40);
});

An engine that sizes its own canvas passes sizeCanvas: false and resizes in onSize (see "Engine adapters").

Rules for games that work on the web and on phones

These are the rules the vibeg.lol games follow. A coding agent building a game with the kit should keep to them too.

  • Fill the screen, any shape. Frame the scene in hh.onResize (hh.view, hh.fitCamera, hh.fitBox): wide screens see more of the world, tall phones get more above and below or their own framing. Never letterbox, never stretch; build scenery wide and tall enough for a 21:9 monitor and a tall phone. Spawn things where the view is, not at fixed numbers.
  • Never touch the window yourself. No resize, keyboard, mouse, pointer or touch listeners, no setPixelRatio, no innerWidth. Read input through hh.input; the view through hh.view.
  • One loop. Everything happens in hh.run((dt) => ...), moved by dt (seconds). No setInterval, setTimeout or own requestAnimationFrame loop.
  • Every device feels native. The keys a PC player expects (below), the mouse wherever pointing fits, and on a phone the gestures a mobile game of that kind uses: tap, hold, swipe, drag. Use on-screen sticks and buttons only where the game needs them, and as few as possible.
  • Conventions. A confirms, jumps or fires; B goes back; START pauses. Menus also take a click or tap: start with A, START or a tap; resume with START or a tap; play again with A or a tap.
  • Name buttons only through the kit. Never write "Press Space" or "Tap" into the HUD: hh.prompt, hh.label and hh.text name the right thing for the device in use.
  • HUD that fits a phone. Size it with calc(N * var(--kit-u)), anchor pieces to the edges, keep it out of var(--kit-bottom), var(--kit-top) and var(--kit-pause) (the touch controls and the corner buttons), and cap wide boxes with max-width: calc(100vw - 24px) so they fit a 390 px wide phone. Text at least 18 px on a phone. Change the DOM only when a value changes.
  • Pause when the player leaves: hh.onHide(() => { if (playing) pause(); }).
  • Fast on a mid-range phone: 60 fps, at most 200 draw calls and 300k triangles per frame (?perf shows them, red over budget). Nothing created per frame; pools and instancing for repeated things.
  • Self-contained. Bundle everything (npm install three, not a CDN link); load assets with relative paths.

Screen

createGame(options) (Three.js) and createCore(options) (any engine) return hh, the game's handle.

| Option | | |---|---| | touch | the touch layout (see "Touch") | | keys | changes to the key map: { KeyZ: 'B' }, null removes a key | | maxPixelRatio | highest devicePixelRatio used (default 2) | | stickDpad | false: the gamepad's left stick does not press the d-pad buttons (read input.stick / input.padStick) | | wheel | true: the mouse wheel over the game never scrolls the page (for games that use input.wheel) | | clearColor, antialias | createGame only | | canvas, sizeCanvas, onSize, frame | createCore only (see "Engine adapters") |

  • The canvas fills the window (or the iframe it runs in) at devicePixelRatio, at most maxPixelRatio. A frame-rate guard lowers the resolution a step after two slow seconds.
  • hh.view: { width, height, aspect, portrait, pixelRatio, insets, unit, top, pause, controls, swipe } in CSS px.
    • insets: the parts covered by touch controls (in portrait with on-screen controls, insets.bottom is the strip they take).
    • unit, top, pause: the HUD variables below as numbers, for layout done in JavaScript.
    • controls: the on-screen controls' rectangles, { kind, button, mode, l, t, r, b } (kind 'pad', 'stick', 'button' or 'pause'); empty when they are not shown.
    • swipe: the swipe distance in px.
  • hh.onResize(fn): fn(view) now and whenever the size changes or the touch controls appear or go. Frame the camera there.
  • hh.freeArea(): the part of the view beside the touch controls, { l, t, r, b, w, h, cx, cy }.
  • hh.covered(gap = 0): the rectangles the kit's own buttons cover (the touch controls and, on touch screens, both top corners), each grown by gap. Keep HUD and the important part of the scene out of them.
  • hh.width, hh.height: the view size.

Framing a camera (Three.js)

  • hh.fitCamera(camera, opts) keeps a design frame fully visible in the free area and shows more of the world around it:
    • Perspective: { fov, aspect } is the frame (the vertical fov you designed for, at that aspect). Wider views see more to the sides, taller views more above and below. Past maxFov (75) the fov is capped and the call returns how much farther the camera must move to keep the frame (1 otherwise): camera.position.z = DIST * hh.fitCamera(camera, { fov: 40 }).
    • Orthographic: { width, height } in world units around the camera's centre line.
    • insets: false uses the whole view.
  • hh.fitBox(camera, box, opts) makes a box (a board, an arena, a table) as big as the screen allows, clear of HUD boxes and the kit's controls. The camera keeps its position and direction; the field of view (or the orthographic frustum) and a shift of the image change.
    camera.position.set(0, 20, 14); camera.lookAt(0, 0, 0);
    hh.onResize(() => {
      const score = hud.querySelector('#score').getBoundingClientRect();
      hh.fitBox(camera, new THREE.Box3(new THREE.Vector3(-6, 0, -4), new THREE.Vector3(6, 1, 4)), { avoid: [score] });
    });
    box is a THREE.Box3, { min, max } or a list of points. Options: area ({ l, t, r, b }, default the free area less margin), avoid (rectangles: { l, t, r, b } or DOMRects), margin (default 8 * view.unit), gap (default 6 * view.unit), covered: false to ignore the kit's controls. Returns { s, cx, cy, rect }: px per unit, the box's centre on screen and its bounding rectangle on screen (place HUD next to it).
  • For another engine, the same math is exported as plain functions: perspectiveFit(view, opts), orthographicFit(view, opts) and createFit(points) with fitCameraNumbers (fit a shape given in camera "tan space", [x / depth, y / depth], into a rectangle around others). createFit gives solve(area, avoid) (the biggest fit, { s, cx, cy }), room(fit, area, avoid, 'x' | 'y') (how far it can slide), rect(fit), overlaps(fit, rect) and extremeX(fit, y0, y1, side) (the shape's leftmost or rightmost x between two rows, to stand HUD right beside it). A game that picks between camera poses or HUD layouts (Tank Brigade) uses these directly.

HUD

hh.hud is a full-window layer over the canvas (pointer-events: none; elements with data-kit-btn or data-kit-item take clicks and taps). Size and place everything with the kit's CSS variables:

  • --kit-u: 1px on a 720x480 view, scaled with the view, between 0.75px and 1.6px. Write sizes as calc(24 * var(--kit-u)).
  • --kit-bottom: the height the touch controls take at the bottom (portrait).
  • --kit-pause: the width the touch pause button takes at the top right.
  • --kit-top: on touch screens, the height of the buttons in the top corners (the kit's pause button at the right; on vibeg.lol the site's close button at the left; 0 elsewhere). HUD that spans the top starts below it: padding-top: max(calc(34 * var(--kit-u)), var(--kit-top)).

URL switches: ?perf shows fps, resolution, draw calls and triangles (red over budget); ?touch forces touch mode and lets the mouse act as a finger; ?keyboard forces keyboard mode; ?mute turns sound off; ?cover hides prompts and controls for a cover screenshot (hh.cover is true, html.kit-cover is set).

Input

hh.input:

  • input.down(b), input.pressed(b) (this frame), input.released(b).
  • input.presses: every button pressed this frame, in the order they went down. Two presses within one frame (an arrow and A) are both there, in order.
  • input.dpad.x / .y: -1, 0 or 1 (y is -1 for up).
  • input.stick { x, y }, -1..1 per axis: the touch stick, else the gamepad's left stick (dead zone rescaled; past half-way it also presses the d-pad unless stickDpad: false), else the d-pad and keys. The axes are independent; normalise the vector yourself for free movement. input.padStick: the gamepad's stick alone.
  • input.pointer: the mouse, or the first finger on the game. x, y in CSS px; nx, ny in -1..1 (ny up) for raycaster.setFromCamera({ x: p.nx, y: p.ny }, camera); down, pressed, released; dx, dy (movement this frame); startX, startY (where the press began); active (over the game); type ('mouse' or 'touch').
  • input.pointers: every finger on the game (not on a control), and the mouse while its button is down: the same fields plus id. A lifted finger stays one frame with released set. For two-finger controls (a plunger while the flippers are held, a floating stick).
  • input.wheel { x, y }: the mouse wheel or touchpad scroll this frame, in px (y > 0 scrolls down). Pass wheel: true so the page does not scroll too.
  • input.item: the menu entry (data-kit-item) clicked or tapped this frame, or -1 (see "Menus").
  • hh.drag(opts): relative drag, "move by the finger's movement, not to the finger", so the finger never covers what it moves:
    const drag = hh.drag({ gain: 1.5, min: -6, max: 6 }); // axis 'x' | 'y', types 'touch' | 'mouse' | 'any'
    // every frame:
    ship.x = drag.apply(ship.x, 1 / pxPerUnit); // unchanged when nothing drags

Buttons: UP DOWN LEFT RIGHT A B X Y L R START SELECT TAP.

| Button | Keyboard | Gamepad (standard mapping) | |---|---|---| | D-pad | Arrows, WASD | D-pad, left stick | | A | Space, Z, J | 0 (bottom) | | B | X, K, Backspace | 1 (right) | | X / Y | C / V | 2 / 3 | | START | Esc, Enter, P | 9 | | SELECT | Shift | 8 | | L / R | Q / E | 4 / 5 and triggers 6 / 7 |

TAP is a click or tap on the game itself (anywhere that is not an on-screen control). The kit calls preventDefault on its keys (never with Ctrl, Cmd or Alt held), so the page never scrolls. A press shorter than one frame still counts as one pressed frame.

Touch

On-screen controls show only while the player uses touch: from the start on phones and tablets, or once a finger touches the screen on a hybrid laptop; a key or gamepad press hides them. Prefer gestures on the game itself to virtual buttons when they fit the game.

| touch option | What it does (touch, and mouse clicks too) | |---|---| | tap: 'A' | A tap or click anywhere presses A, held while down (one-button games). | | halves: ['LEFT', 'RIGHT'] | The left half of the screen presses the first button, the right half the second. halfLabels: ['◀', '▶'] draws faint labels. | | swipe: true | Swipes (and mouse drags) press UP / DOWN / LEFT / RIGHT for one frame; a finger that keeps moving swipes again. A short tap presses tap (or the half's button) and TAP, on release. | | swipe: { up: 'A', left: 'LEFT', ... } | Only the directions named press a button; dpad: true maps all four to the d-pad. | | swipe: { dpad: true, repeat: false } | One swipe per touch, however far the finger goes (puzzle moves, lane changes). | | swipe: { ..., flick: true } | A quick flick shorter than a swipe still swipes. distance sets the swipe distance (px). | | left: [...], right: [...] | On-screen controls at the bottom left and bottom right. | | pause: false | No pause button (on by default: START, top right). |

Controls in left / right: 'stick' (analog, input.stick), 'dpad' (4-way), 'dpad8' (8-way), 'dpad-x' (left / right only), and buttons: 'A' or { button: 'A', label: 'FIRE', color: '#e8453c' }. The first button of a cluster is the main one (biggest, nearest the corner); up to four per cluster. Every finger is tracked on its own, so a stick and a button work together.

  • hh.setTouch(config) swaps the layout at runtime (menus vs play).
  • hh.setButtonLabel('X', 'FULL') changes an on-screen button's text (and the prompts that name it) in place, without redrawing it.
  • hh.rotateHint = true shows a "turn sideways" hint on phones held upright, for a game that really needs a wide screen.

Prompts

Helpers that return HTML with one variant per device; CSS shows the one for the device in use and switches by itself. Put them in the HUD with innerHTML.

  • hh.prompt(action, buttons = 'A', { hold, pill, asPill }): a whole sentence. keyboard Press <kbd>Space</kbd> to start; gamepad Press Ⓐ to start; touch Tap to start (when the list has TAP or the tap-anywhere button), Tap [FIRE] to start (an on-screen button), Swipe ↑ to jump / Tap left to ... (gestures), otherwise a tappable pill that says the action. asPill: true always makes the pill on touch.
  • hh.label(button): one button inside your own sentence. hh.label('DPAD') gives Arrows / D-pad / Swipe.
  • hh.text({ keyboard, touch, gamepad }): free text per device (touch and gamepad fall back to keyboard).
  • hh.hint(button): only the key or pad button, nothing on touch (for something that is a tappable button itself).
  • hh.labelText(button): plain text for the current device.
  • hh.device: 'keyboard', 'touch' or 'gamepad'; hh.onDeviceChange(fn).

Chips and pills are tappable and clickable: they press their button. Any HUD element with data-kit-btn="A" does the same.

Menus (vibeg-kit/menu)

Title, options and pause menus that work the same with keys, gamepad, mouse and touch.

import { createMenu } from 'vibeg-kit/menu';

const menu = createMenu(hh, { sound });   // sound: optional, plays the menu sounds
const SOUND = [
  { id: 'sfx', label: 'Sound', on: () => sound.sfxOn, set: (on) => sound.setSfx(on) },
  { id: 'music', label: 'Music', on: () => sound.musicOn, set: (on) => sound.setMusic(on) },
];

function pause() {
  hud.innerHTML = `<div class="panel"><h2>PAUSED</h2><div class="menu"></div>${hh.prompt('resume', ['START', 'TAP'])}</div>`;
  menu.open(hud.querySelector('.menu'), [{ id: 'resume', label: 'Resume' }, ...SOUND, { id: 'quit', label: 'Quit to title', sound: 'back' }], {
    back: 'resume', start: 'resume', outside: 'resume', center: 'resume',
  });
}

hh.run(() => {
  if (paused) {
    const id = menu.update();
    if (id === 'resume') resume();
    else if (id === 'quit') toTitle();
  }
});
  • Entries: { id, label } (an action), { id, label, on, set } (On / Off: "Sound: On"), { id, label, values, index, set } (a choice: "Board ◀ 4×4 ▶"). label may be a function returning HTML (for a toggle it gets the state, for a choice the value). Optional per entry: sound (a key of sounds, a sound name, or false), action(), className, wrap(html) (HTML around the entry, for things beside it such as arrow buttons with data-kit-btn="LEFT" and "RIGHT"; the entry stays the element with data-kit-item).
  • UP / DOWN move the highlight (wrapping), A or a click picks, LEFT / RIGHT flip an On / Off entry or step through a choice. Labels change in place.
  • open(el, entries, opts): sel (the entry highlighted, index or id), back / start / outside (what B, START and a tap or click beside the entries do: an entry id, which acts as picking it, or a function), center (an entry to keep in the middle of the free area: put Resume there, so a tap in the middle of the screen resumes), panel (the element center moves, default the container's parent).
  • update() once per frame returns the id of the entry picked or flipped, or null. Every press of a frame counts, in order: DOWN then A picks the entry below. A tap beside the entries is never A (with touch: { tap: 'A' } a tap presses both).
  • close(), refresh() (read every label again), center(), sel, id, active.
  • Options: sound, sounds ({ move, select, back, toggle }, e.g. 'pack:move' on vibeg.lol), itemClass / selClass (default kit-item / kit-sel), words (['On', 'Off']).
  • Each entry is an element with data-kit-item="<index>". You can also build your own: input.item is the index clicked or tapped this frame.

Sound (vibeg-kit/sound)

Effects and music with Web Audio.

import { createSound } from 'vibeg-kit/sound';
const sound = createSound(hh, { sfx: ['jump', 'coin', 'hit'], music: 'theme' });
sound.play('coin', { volume: 0.8, rate: 1.1, pan: 0, delay: 0 });
const engine = sound.loop('engine');
engine.set(throttle * 0.6, 0.8 + speed); // every frame; set(0) fades it out and stops it
sound.startMusic(); sound.stopMusic(); sound.duck(true);   // duck: quieter under menus
sound.setSfx(false); sound.setMusic(false);               // saved with hh.save
  • Files: public/sfx/<name>.wav (short, mono) and public/music/<name>.ogg plus <name>.m4a for Safari and iPhones, which cannot decode Ogg (tools/music-m4a.sh <game folder> makes it with ffmpeg).
  • On vibeg.lol the free sound pack plays by name with nothing to copy: sfx: ['pack:coin', 'pack:jump'], music: 'pack:adventure', then sound.play('pack:coin'). The names are in the developer docs.
  • Browsers start audio only inside a key press, click or tap. The kit makes its AudioContext at the player's first one; files are fetched and decoded before that, so the first sound is ready at once. Inside a player frame that may autoplay (the vibeg.lol player) it starts at load. Gamepad buttons do not count. Audio stops while the tab is hidden.
  • Loops: set(volume, rate) starts the loop and glides to the values; set(0) or stop() fades it out; loop.playing; sound.stopLoops() stops them all. Nothing to track.
  • Sounds the game makes itself:
    • sound.add(name, audioBuffer) or sound.add(name, float32Samples, { sampleRate }).
    • sound.synth(name, seconds, (ac) => { ...nodes connected to ac.destination }) renders Web Audio nodes once, ahead of time (an OfflineAudioContext); resolves true when sound.play(name) can play it.
    • Live audio through the kit's mixer: sound.onStart((context, output) => ...) runs once the AudioContext exists; connect to output to follow the effects volume and the Sound switch. sound.context, sound.output.
  • Clocks: sound.musicTime (seconds into the music loop, for things on the beat), sound.time (the audio clock).
  • sound.ready, sound.sfxOn, sound.musicOn, sound.has(name). ?mute turns everything off.

Online rooms (vibeg-kit/online, vibeg.lol only)

The themed front end of vibeg.lol's online rooms for Three.js games, on the platform SDK (window.vibeg, served at /_vibeg/sdk.js on every game host). Load the SDK in index.html (<script src="/_vibeg/sdk.js"></script>) and declare the game's multiplayer in vibeg.json ("multiplayer": { "players": [2, 4], "referee": { "kind": "race" } }). Players sign in on vibeg.lol to play online. Away from vibeg.lol there is no SDK: the kit shows "Online play works on vibeg.lol." and calls onExit.

import { createOnline } from 'vibeg-kit/online';
const online = createOnline(hh, {
  game: 'my-game',                 // the site slug, for invite links
  title: 'ONLINE RACE',            // the lobby card's heading
  rules: '3 laps · up to 4 players',
  start: 'race',                   // "Start the race"
  players: [{ name: 'Red', color: '#e8453c' }, ...], // per slot: the colours
  option: { label: 'Circuit', values: ['Bay', 'Canyon'], start: 0 }, // optional, the host picks
  quickChat: true,                 // optional: the platform's quick chat button while in a room
  onMessage(type, msg) {},         // 'go', 's', 'e', 'fin', 'ko', 'claim', 'results', 'join', 'left', ...
  onExit(reason) {},               // left the room: back to the title
});
online.setOption(circuit);         // the option a room opens on (or the host changes it)
if (online.invited) online.join(online.invited); else online.create();
// or online.quickJoin(): an open room with a free place, else a new one on the option picked
// every frame: online.update();  send: online.send({ t: 's', x, y })
  • The kit draws the lobby and results card (room code, the players' usernames, the host's option, Start / Invite friends / Leave) and handles its input (A start, Y invite, B leave, ← → the option), and shows toasts (online.toast(text, color)).
  • Messages: { t: 's', ...numbers } (state, up to 25 a second) and { t: 'e', k: 'name', ...numbers } (events) go to the other players, cleaned to finite numbers under keys of 1-4 lower-case letters and digits; { t: 'fin' }, { t: 'ko', by } go to the referee; { t: 'claim', n } takes shared thing n (the first claim wins).
  • Rooms: online.create() opens a new room, online.join(code) joins one (an invite link: online.invited), online.quickJoin() puts the player in an open room of the game that takes strangers and has a free place, or opens a new one (the manifest's "public": false always opens a new one). A room this player opens starts on the option picked before (option.start, online.setOption(n)).
  • Quick chat: quickChat: true (or { key: 'KeyT' }, the key that opens it; T by default) shows the platform's quick chat button (fixed phrases and emotes, vibeg.quickChat.picker) while the player is in a room: in the bottom right corner, above the on-screen controls there. online.chatArea is its rectangle while it shows (keep HUD clear of it). Taps and keys on it never reach the game.
  • online.me, host, isHost, roster, mode, option, results, nameOf(id), colorOf(id). A dropped connection reconnects by itself within 30 s.
  • Theme the card on .kit-online (--kit-ol-accent, --kit-ol-panel, --kit-ol-ink).
  • A game on any engine can use the SDK directly (vibeg.create(), room.send(), vibeg.lobby(room)); see the developer docs.

Other

  • hh.run(update): the loop; update(dt) once per frame, dt in seconds, capped at 1/20.
  • hh.onHide(fn): the tab was hidden, or (on computers) the player clicked outside the game. Pause there.
  • hh.onGesture(fn): fn(event) inside every key press, click and touch, where browsers allow audio to start.
  • hh.save(key, value), hh.load(key, fallback): localStorage as JSON, failures ignored.
  • hh.stats: { frames, fps, calls, triangles, maxCalls, maxTriangles } (draw calls with the Three.js part). window.__kit is a read-only handle for automated tests (device, stats, view, input).

Engine adapters

createCore takes what an engine needs to plug in; createGame is built this way:

const hh = createCore({
  canvas: engine.canvas,
  sizeCanvas: false,                       // the engine sizes its canvas
  onSize: (view) => engine.resize(view.width, view.height, view.pixelRatio),
  frame: {
    schedule: (tick) => engine.onFrame(tick), // default: requestAnimationFrame
    before: () => engine.resetStats(),
    measure: () => ({ calls: engine.drawCalls, triangles: engine.triangles }),
    budget: { drawCalls: 200, triangles: 300000 },
  },
});

Exports

| Import | | |---|---| | vibeg-kit | createGame, BUDGET (Three.js), and everything below except online | | vibeg-kit/three | createGame, BUDGET | | vibeg-kit/core | createCore, BUTTONS, createDrag, freeArea, perspectiveFit, orthographicFit, createFit, fitCameraNumbers, swipeDistance | | vibeg-kit/menu | createMenu, menuEvents | | vibeg-kit/sound | createSound | | vibeg-kit/online | createOnline, roomFromUrl, inviteLink | | vibeg-kit/kit.css, vibeg-kit/online.css | the styles (imported by the modules above) |

In the vibeg repository

The first-party games in games/ use the kit as an npm workspace package ("vibeg-kit": "*"). npm run test:games [id ...] (from the repo root) builds each game and plays it with Playwright: desktop with the keyboard and the mouse, phone emulation upright and sideways with real touch events, and the draw-call budget; the steps per game are in games/<id>/e2e.mjs (games/sky-hopper/e2e.mjs is the example). npm test runs the kit's unit tests (test/kit.test.ts). games/sky-hopper is the reference game: framing per screen shape, a HUD in --kit-u, tap anywhere to flap, the kit's menus and sound.

License

MIT