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

@safe-engine/sdl

v1.3.3

Published

safe-engine with webgl renderer support base on sdl3 api

Readme

@safe-engine/sdl (SafeX SDL Engine)

High-performance WebGL & Native SDL3 Hybrid Engine Core for 2D/3D Casual Games
Maintained under SGM Unified Engineering Discipline

License: ISC NPM Version Docs


Documentation & Project Tracking


Install

npm install @safe-engine/sdl

Dev

bun run dev

Resolution policy

Engine.start() accepts an optional fourth argument that controls how the logical game size is presented inside the actual window or browser viewport:

Engine.start('My Game', 720, 1280, 'letterbox')

Available policies are letterbox (default), overscan, stretch, and integer-scale. The active Engine.viewport exposes the resulting rendered rectangle, X/Y scale, and safe area.

Assets

Sprite and Label acquire cached native textures/fonts and release them when their node is destroyed. Labels only rebuild their cached text texture when the text or font changes.

import { AssetManager, TextureAtlas } from "./engine";

const group = AssetManager.createGroup()
  .addTexture("player", "res/Texture/player.png")
  .addFont("ui", "res/Font/LilitaOne-Regular.ttf", 24)
  .addAtlas("buttons", "res/Texture/button.png", {
    normal: { x: 0, y: 0, width: 220, height: 68 },
  });

group.preload(({ progress }) => {
  console.log(`Loading ${Math.round(progress * 100)}%`);
}).then(() => {
  console.log("Assets ready");

  const atlas = group.get<TextureAtlas>("buttons");
  if (!atlas) throw new Error("buttons atlas was not preloaded");
  sprite.setFrame(atlas, "normal");

  const sheet = AssetManager.acquireSpriteSheet(
    "res/Texture/player.png",
    32,
    32,
  );
  sprite.setFrame(sheet, "0");

  group.unload();
  sheet.release();
});

Audio

The global Audio manager provides music and sound-effect groups, mute and volume controls, looping, fades, and pooled sound voices. Use WAV files for assets that must work on native and web builds; browsers may support additional formats, but native playback currently uses SDL's WAV loader.

import { Audio } from "./engine";

// Reuses up to six simultaneous voices instead of allocating unbounded sounds.
const laser = Audio.createSound("res/Audio/laser.wav", {
  group: "sfx",
  maxVoices: 6,
  volume: 0.8,
});

laser.play();
laser.play({ volume: 0.5 });

// Music loops by default. Fade times are measured in seconds.
Audio.playMusic("res/Audio/theme.wav", { fadeIn: 1.5 });
Audio.stopMusic(0.75);

Audio.group("master").volume = 0.9;
Audio.group("music").volume = 0.6;
Audio.group("sfx").muted = true;
Audio.group("music").fadeTo(0.25, 1);

const voice = Audio.play("res/Audio/impact.wav");
voice?.fadeOut(0.3);

laser.release();

Audio.pause() and Audio.resume() control all playback without changing individual pause state. The engine also suspends audio automatically while the application is paused, backgrounded, or interrupted, and resumes only after all active lifecycle conditions have cleared.

Scene lifecycle

When a scene becomes active, the engine calls onLoad() followed by onEnter(). Replacing it calls onExit(), onUnload(), and then destroys its node tree. onPause() and onResume() follow app inactivity without unloading the scene.

onSaveProgress() is called automatically before the app enters the background and again if the OS terminates it.

class GameScene extends Scene {
  onSaveProgress(): void {
    saveGame(this.progress);
  }

  onLowMemory(): void {
    this.releaseOptionalCaches();
  }

  onUnload(): void {
    this.releaseSceneResources();
  }

  onOrientationChange(
    orientation: Orientation,
    width: number,
    height: number,
  ): void {
    this.layout(orientation, width, height);
  }
}

Scenes can also implement onBackground(), onForeground(), and onInterruption(active) for finer control over mobile transitions.

Player persistence

PersistenceJSON stores settings and level progress as a versioned JSON save. It uses the browser's localStorage by default and accepts any object with the same getItem, setItem, and removeItem interface.

import { PersistenceJSON } from "./engine";

interface PlayerSave {
  settings: { musicVolume: number; soundVolume: number };
  progress: { unlockedLevel: number; highScores: Record<string, number> };
}

const playerSave = new PersistenceJSON<PlayerSave>("player", {
  version: 2,
  defaults: () => ({
    settings: { musicVolume: 1, soundVolume: 1 },
    progress: { unlockedLevel: 1, highScores: {} },
  }),
  migrations: {
    // Each migration upgrades version N to N + 1.
    1: (old: any) => ({
      ...old,
      progress: { ...old.progress, highScores: {} },
    }),
  },
});

const player = playerSave.load();
player.progress.unlockedLevel = 2;
playerSave.save(player);

Loading an older save runs every migration in order and immediately rewrites the upgraded file. Invalid JSON, saves from newer app versions, and missing migrations throw rather than silently replacing player progress.

Resolution and safe areas

Engine.start() dimensions are the logical design resolution. Rendering keeps that coordinate system at every window or device size, scales uniformly, and letterboxes any remaining space. Browser rendering uses the device pixel ratio for a sharp drawing buffer without changing game coordinates.

const { logicalWidth, logicalHeight, safeArea, safeInsets } = Engine.viewport;

// Place interactive UI inside the notch/system-bar-safe logical rectangle.
hud.node.setPosition(
  safeArea.x + 24,
  safeArea.y + 24,
);

// Convert raw window/client coordinates when integrating a platform API.
const world = Engine.screenToWorld(clientX, clientY);
const insideGame = Engine.viewport.containsScreenPoint(clientX, clientY);

Scene touch callbacks already receive logical game coordinates. Use Engine.worldToScreen() for overlays that live outside the SDL/WebGL renderer.

Input dispatch

Interactive components receive pointer input automatically. Button performs sprite hit testing, captures a press until release, and invokes onClick without scene-level touch forwarding.

const button = node.addComponent(Button);
button.onClick = () => startGame();
button.inputPriority = 10;

Hit components are ordered by descending inputPriority. Equal priorities use reverse render order, so the visually topmost component receives input first. Calling event.stopPropagation() prevents lower-priority hit components and the scene touch callback from receiving that event. Buttons do this by default; set button.consumeInput = false to allow propagation.

UI

UI components use the existing node tree and logical coordinate system. UIContainer supports horizontal and vertical stack layout and padding, cross-axis alignment, flexible children, and anchor constraints. Panel, UIImage, NineSlice, ProgressBar, TextInput, Toggle, and ScrollView provide the standard retained widgets.

import {
  Label,
  Localization,
  Node,
  Panel,
  ProgressBar,
  TextInput,
  Toggle,
  UIContainer,
  UIElement,
  Widget,
} from "./engine";

Localization.add("en", { status: "Energy: {value}%" });
Localization.add("vi", { status: "Năng lượng: {value}%" });
Localization.use("en");

const hud = new Node("hud");
const panel = hud.addComponent(Panel);
panel.setSize(520, 180);
panel.direction = "vertical";
panel.padding = { top: 20, right: 20, bottom: 20, left: 20 };

const titleNode = panel.node!.addChild(new Node("title"));
titleNode.addComponent(UIElement).setSize(480, 48);
const title = titleNode.addComponent(Label);
title.setFont("res/Font/LilitaOne-Regular.ttf", 30);
title.setLocalized("status", { value: 75 });
title.wrapWidth = 480;
title.align = "center";
title.outlineWidth = 2;

const progressNode = panel.node!.addChild(new Node("progress"));
const progress = progressNode.addComponent(ProgressBar);
progress.setSize(480, 24).setValue(0.75);

const toggleNode = panel.node!.addChild(new Node("sound"));
const toggle = toggleNode.addComponent(Toggle);
toggle.setSize(72, 36);
toggle.onChange = (enabled) => console.log("Sound", enabled);

const nameNode = panel.node!.addChild(new Node("name"));
const nameInput = nameNode.addComponent(TextInput, {
  placeholder: "Player name",
  onChange: (value) => console.log("Name", value),
  onSubmit: (value) => console.log("Submit", value),
});
nameInput.setSize(480, 48);

const topBar = new Node("top-bar");
topBar.width = 320;
topBar.height = 72;
topBar.addComponent(Widget, { top: 16, left: 16, right: 16 });

// Disable a subtree without removing it from the hierarchy.
topBar.active = false;

Widget pins a node to the current safe-area border by default, so HUD controls stay inside phone notches/system bars and resize with any window size. Set inSafeArea: false to pin it against the full device window instead. Supplying both sides on an axis stretches the node on that axis; supplying one side keeps the node's existing size and pins it to that border. Set centerHorizon or centerVertical to center the node on that axis.

Set node.active = false to pause updates, rendering, input, and descendant traversal for that node's whole subtree until it is re-enabled.

Anchor values are normalized to the parent UI element. Equal minimum and maximum anchors pin an element to a point; different values stretch it between two points. Offsets then inset or move the anchored rectangle.

const child = new Node("full-size-child");
panel.node!.addChild(child);
const element = child.addComponent(UIElement);
element.setAnchors(0, 0, 1, 1);
element.offsetLeft = 16;
element.offsetTop = 16;
element.offsetRight = 16;
element.offsetBottom = 16;

ScrollView clips its descendants and uses its first child as the movable content root. Set contentWidth and contentHeight to define scroll limits. Labels support explicit newlines, word wrapping, horizontal and vertical alignment, color, opacity, outlines, and localization keys. TextInput focuses on pointer press, opens the platform text entry path, and emits onChange, onFocus, onBlur, and onSubmit callbacks.

Tweening

Tweens use seconds and advance with engine time, so they pause automatically when the app is paused or backgrounded. Numeric properties and nested colors can be animated directly.

import { Easing, Tween } from "./engine";

Tween.to(
  player.node,
  { x: 600, y: 240, rotation: 360, scaleX: 1.5, scaleY: 1.5 },
  0.8,
  {
    ease: Easing.cubicOut,
    delay: 0.1,
    onComplete: () => console.log("move complete"),
  },
);

Tween.sequence()
  .to(sprite, {
    opacity: 0.25,
    color: { r: 255, g: 96, b: 64 },
  }, 0.2, { ease: Easing.quadOut })
  .delay(0.15)
  .call(() => console.log("flash"))
  .to(sprite, {
    opacity: 1,
    color: { r: 255, g: 255, b: 255 },
  }, 0.3)
  .start();

Available callbacks are onStart, onUpdate, onComplete, and onStop. Calling stop() on a tween or sequence cancels it. Scene changes cancel all active tweens.