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

@harlamov/games

v1.0.48

Published

Tiny framework for projects made with Legend17 UI Editor

Readme

@harlamov/games

A lightweight runtime for browser-based 2D games built with PixiJS 8. The library creates and configures the PixiJS application, mounts the root stage, handles screen resizing, and provides shared services for audio, localization, and the game environment.

The library is designed for games built with the Legend17 UI Editor.

Create a new project with:

npm create @harlamov/game

Requirements

  • Node.js 20.19 or newer;
  • PixiJS 8.19.

Preparing the page

By default, the canvas is appended to the #pixi-container element:

<div id="pixi-container"></div>

The container must have a non-zero size:

html,
body,
#pixi-container {
	width: 100%;
	height: 100%;
	margin: 0;
	overflow: hidden;
}

Use the pixiContainer option to provide a different container. It accepts a CSS selector:

await startApp(application, {
	gameId: "example-game",
	pixiContainer: "#game"
});

It also accepts an HTMLElement:

const pixiContainer = document.querySelector<HTMLElement>("#game");

if (!pixiContainer) {
	throw new Error("Pixi container was not found.");
}

await startApp(application, {
	gameId: "example-game",
	pixiContainer
});

Starting a game

Create an App class with an asynchronous start() method and mount the game layers:

export class App {
	private readonly layers = new AppLayers();

	public async start(): Promise<void> {
		mountStage(this.layers);
	}
}

Pass the application module and game options to startApp in the entry point:

// src/index.ts
const [{ startApp }, application] = await Promise.all([
	import("@harlamov/games"),
	import("./core/App")
]);

await startApp(application, {
	gameId: "example-game"
});

mountStage() can only be called once. The root AppLayers container must implement onScreenResize(). The method is called immediately after the stage is mounted and whenever the container size, viewport, orientation, or fullscreen state changes.

Editor element orientation

Elements generated by the Legend17 UI Editor support three orientations:

type Orientation = "landscape" | "portrait" | "square";

Generated prefabs implement setOrientation(orientation). This method applies the properties configured for the selected orientation in the editor and passes the orientation to nested elements.

AppLayers manages the current orientation. Its onScreenResize() method determines the orientation from the aspect ratio, stores it in moduleLoader.orientation, and applies it to elements already on the stage:

class AppLayers extends Container {
	public onScreenResize(screenWidth: number, screenHeight: number): void {
		const aspectRatio = screenWidth / screenHeight;
		const orientation: Orientation = aspectRatio >= Math.sqrt(4 / 3)
			? "landscape"
			: aspectRatio <= Math.sqrt(3 / 4)
				? "portrait"
				: "square";

		if (moduleLoader.orientation !== orientation) {
			moduleLoader.orientation = orientation;
			this.applyOrientation(this.content, orientation);
		}
	}
}

applyOrientation() should recursively traverse child elements and call setOrientation() wherever that method is available. Preserve the current visible value so that applying an orientation does not change visibility managed by the game.

A prefab added after the most recent resize must be synchronized before it is shown:

import type { PrefabView } from "@harlamov/games";

type Screen = Container & PrefabView<Orientation>;

const screen = await app.managers.prefabs.get(ScreenClass);
screen.setOrientation(moduleLoader.orientation);
app.layers.screens.addChild(screen);

The PrefabView<Orientation> interface guarantees the presence of prepare() and setOrientation() at the TypeScript level.

Global API

The following global values are available after startApp() completes:

| Value | Purpose | | --- | --- | | app | The App instance and game services | | app.renderer | The PixiJS Application | | app.audio | Audio loading and playback | | app.environment | Game, user, device, and language environment | | locale | Localization | | mountStage | Root stage mounting | | delay | Delays in seconds using the shared PixiJS ticker | | trace | console.log with a library prefix | | removeHtmlPreloader | Removes #html-preloader or a custom element |

Example:

trace("Game started");
await delay(0.5);

Project types

The GlobalTypes interface supports declaration merging. Use it to restrict the allowed languages, localization keys, and sound identifiers, and to register the application type:

import type { App } from "./App";

type Language = "en" | "ru";
type LocaleKey =
	| "app.title"
	| "menu.play"
	| "inventory.coins";
type SoundId = "click" | "music" | "win";

declare module "@harlamov/games" {
	interface GlobalTypes {
		App: App;
		Language: Language;
		LocaleKey: LocaleKey;
		SoundId: SoundId;
	}
}

TypeScript will then validate arguments passed to locale.translate(), locale.setLanguage(), app.audio.playSound(), and related methods.

Audio

By default, sounds are loaded as MP3 files from assets/sounds. A sound ID is used as its file name:

assets/sounds/click.mp3
assets/sounds/music.mp3

Sounds can be preloaded or loaded lazily on first playback:

await app.audio.load(["click", "music"]);

await app.audio.playSound("click");
await app.audio.playLoop("music");

app.audio.stopLoop("music");
app.audio.mute();
app.audio.unmute();

Web Audio is unlocked automatically after the first pointerdown or keydown. Loops requested before audio is unlocked start after a permitted user interaction.

Configure the base path, file format, and retry count through startApp:

await startApp(application, {
	gameId: "example-game",
	audio: {
		basePath: "assets/audio",
		extension: "ogg",
		loadRetryCount: 2
	}
});

The path and format can also be resolved dynamically once before the first load:

await startApp(application, {
	gameId: "example-game",
	audio: {
		resolveBasePath: () => {
			return navigator.connection?.saveData
				? "assets/audio/low"
				: "assets/audio/high";
		},
		resolveExtension: () => {
			const audio = document.createElement("audio");
			return audio.canPlayType('audio/ogg; codecs="vorbis"')
				? "ogg"
				: "mp3";
		}
	}
});

The native AudioContext is available through app.audio.context, allowing the application to manage its lifecycle:

document.addEventListener("visibilitychange", () => {
	if (document.hidden) {
		void app.audio.context.suspend();
	} else {
		void app.audio.context.resume();
	}
});

suspend() preserves the positions of active sounds and loops. mute() has different semantics: it stops current playback, while unmute() starts the requested loops again.

Localization

Each language must be stored in a separate JSON file. The files may be located anywhere. A locale file is a flat object whose keys and values are strings.

For example, public/assets/locale/en.json:

{
	"app.title": "Example Game",
	"inventory.coins": "%count% $(count, [coin, coins])$",
	"inventory.leaves": "%count% leaves"
}

And public/assets/locale/ru.json:

{
	"app.title": "Пример игры",
	"inventory.coins": "%count% $(count, [монета, монеты, монет])$",
	"inventory.leaves": "лист$[а, ов]"
}

Do not use nested objects, arrays, comments, computed values, or executable code. The file must remain a plain object mapping keys to string values. Every language file must contain the same set of keys.

To use localization in the editor, provide a path to one of the locale files. Set the path through localeFilePath in the editor's .legend17 project file. The path is resolved relative to the project file:

{
	"localeFilePath": "../../public/assets/locale/en.json"
}

The editor reads this JSON file to obtain the available keys and their text values while working with views.

At game startup, load the selected language file through PixiJS Assets, register it, and make the language active:

import { Assets } from "pixi.js";

const language = app.environment.getLanguageFromQuery() ?? "en";
const localeData = await Assets.load(`assets/locale/${language}.json`);

locale.addLanguage(language, localeData);
locale.setLanguage(language);

locale.translate("app.title");
locale.translate("inventory.coins", { count: 5 });

Translation strings support the following syntax:

  • %name% — inserts a string or number;
  • $(name, [one, few, other])$ — selects a complete form using Intl.PluralRules;
  • $[suffix1, suffix2] — sequentially selects suffixes using the plural parameter;
  • a literal \\n in the stored string — inserts a line break.

Suffix example:

locale.translate("inventory.leaves", { plural: [1] }); // лист
locale.translate("inventory.leaves", { plural: [2] }); // листа
locale.translate("inventory.leaves", { plural: [5] }); // листов

Missing and empty translations return ......

Environment

app.environment.game.id;
app.environment.game.storagePath;
app.environment.designResolution;

app.environment.user.id;
app.environment.user.name;
app.environment.user.language;
app.environment.user.device.id;
app.environment.user.device.isMobile;
app.environment.user.device.hapticSupported;

Use app.environment.getLanguageFromQuery() to resolve a language from a query parameter. By default, it reads lang and normalizes regional language tags:

?lang=en-US -> en
?lang=ru_RU -> ru

The query parameter name can be changed:

app.environment.getLanguageFromQuery({
	queryParameter: "locale"
});

startApp options

| Option | Default | Description | | --- | --- | --- | | gameId | required | Unique game identifier | | baseStoragePath | harlamov.games | Storage namespace prefix | | supportedLanguages | ["en", "ru"] | Supported languages | | designResolution | 1920 × 1080 | Logical coordinate system | | user.name | Player 1 | Local player name | | user.id | 123456 | Local player identifier | | pixiContainer | #pixi-container | Container HTMLElement or CSS selector | | backgroundColor | #0e1822 | Canvas background color | | imageRendering | auto | CSS image-rendering value | | skipDetections | true | Disables PixiJS texture format detection | | texturePreference | { format: ["webp"] } | Preferred texture formats and resolutions | | assetLoadRetryCount | 3 | PixiJS Assets load retries | | audio | — | Audio manager options |

HTML preloader

When the page contains a static preloader with the html-preloader ID, remove it only after the in-game preloader has been displayed:

export class App {
	public async start(): Promise<void> {
		await this.loadInGamePreloader();
		this.showInGamePreloader();
		removeHtmlPreloader();

		await this.loadGameAssets();
	}
}

Calling removeHtmlPreloader() more than once is safe.

Standard type extensions

The package registers helpers for numbers, strings, and arrays when imported.

(1250).pretty();
String("hello").capitalize();
String("hello world").wrap(5);
String("123").isInteger();
String("example").toNumberHash();

[1, 2].pushOnce(2, 3);
[1, 2, 3].remove(2);

These extensions are added as non-enumerable prototype properties.

Contact

License

This library is distributed under the MIT License.