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

osu-renderer

v0.0.3

Published

A TypeScript library for simulating and rendering osu!standard replays in the browser using PixiJS.

Readme

osu-renderer

A TypeScript library for simulating and rendering osu!standard replays in the browser using PixiJS.

Install

npm install osu-renderer

Peer Dependencies

  • pixi.js ^8.0.0

Quick Start

import {
  simulateScore,
  createRenderer,
  updateSkinTextures,
} from "osu-renderer";

// 1. Simulate the replay against the beatmap
const simulation = simulateScore(replay, beatmap);

// 2. Load skin textures (provide a URL or data URL for each texture)
await updateSkinTextures({
  cursor: "/media/skins/default/cursor.png",
  hitcircle: "/media/skins/default/hitcircle.png",
  hitcircleoverlay: "/media/skins/default/hitcircleoverlay.png",
  approachcircle: "/media/skins/default/approachcircle.png",
  "spinner-bottom": "/media/skins/default/spinner-bottom.png",
  "spinner-middle": "/media/skins/default/spinner-middle.png",
  "spinner-top": "/media/skins/default/spinner-top.png",
  "spinner-approachcircle": "/media/skins/default/spinner-approachcircle.png",
  sliderb: "/media/skins/default/sliderb.png",
  sliderfollowcircle: "/media/skins/default/sliderfollowcircle.png",
  reversearrow: "/media/skins/default/reversearrow.png",
  sliderscorepoint: "/media/skins/default/sliderscorepoint.png",
  sliderstartcircle: "/media/skins/default/sliderstartcircle.png",
  sliderstartcircleoverlay: "/media/skins/default/sliderstartcircleoverlay.png",
  sliderendcircle: "/media/skins/default/sliderendcircle.png",
  sliderendcircleoverlay: "/media/skins/default/sliderendcircleoverlay.png",
  hit0: "/media/skins/default/hit0.png",
  hit50: "/media/skins/default/hit50.png",
  hit100: "/media/skins/default/hit100.png",
  hit300: "/media/skins/default/hit300.png",
});

// 3. Create the renderer
const renderer = await createRenderer({
  beatmap,
  simulation,
  replay, // optional — enables cursor and debug overlay
  width: 1280,
  height: 720,
  mediaPath: "/media",
});

// 4. Mount the canvas
document.body.appendChild(renderer.canvas);

// 5. Drive playback in your animation loop
function animate(time: number) {
  renderer.update(time); // time in ms matching the beatmap timeline
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

// 6. Clean up when done
renderer.destroy();

API

Simulation

simulateScore(replay, beatmap)

Processes replay frames against a beatmap and produces per-frame score state and resolved hit objects.

function simulateScore(replay: Replay, beatmap: StandardBeatmap): Simulation;

| Parameter | Type | Description | | --------- | ---------------------------------------------- | ---------------------- | | replay | Replay (from osu-classes) | The replay to simulate | | beatmap | StandardBeatmap (from osu-standard-stable) | The parsed beatmap |

Returns a Simulation:

type Simulation = {
  hitObjects: HitObject[]; // Resolved hit objects with results
  frames: SimulatedFrame[]; // Per-frame game state
};

SimulatedFrame

Each frame captures the full game state at a point in time:

type SimulatedFrame = {
  x: number; // Cursor X
  y: number; // Cursor Y
  time: number; // Timestamp (ms)
  score: number; // Cumulative score
  combo: number; // Current combo
  great: number; // 300 count
  good: number; // 100 count
  okay: number; // 50 count
  miss: number; // Miss count
  accuracy: number; // 0–1
  actions: Set<StandardAction>; // Buttons held this frame
  angle?: number; // Cursor angle (spinner)
  currentSpinnerRotation?: number; // Active spinner rotation (radians)
  activeSliderProgress?: number; // Active slider progress (0–1 per span)
};

HitObject

type HitObject = {
  x: number;
  y: number;
  time: number; // Start time (ms)
  resultTime: number; // Time the result was determined
  result: HitResult; // Great / Ok / Meh / Miss
  type: HitType; // Bitmask — circle, slider, spinner
  endTime?: number; // End time for sliders and spinners
  totalRotation?: number; // Total rotation for spinners (radians)
  slider?: SliderData; // Slider path and timing data
};

SliderData

type SliderData = {
  path: Coordinate[]; // Calculated path points
  repeats: number;
  duration: number; // Total duration (ms)
  velocity: number;
  tickPositions: { position: Coordinate; time: number }[];
  repeatPositions: { position: Coordinate; time: number }[];
  endPosition: Coordinate;
};

Coordinate

type Coordinate = { x: number; y: number };

isInside(cx, cy, hx, hy, hr)

Circle-point collision check. Returns true if the cursor at (cx, cy) is inside the hit object at (hx, hy) with radius hr.

function isInside(
  cx: number,
  cy: number,
  hx: number,
  hy: number,
  hr: number,
): boolean;

Renderer

createRenderer(config)

Creates a PixiJS-backed renderer that draws the beatmap, hit objects, and cursor.

function createRenderer(config: {
  beatmap: StandardBeatmap;
  simulation: Simulation;
  replay?: Replay; // Pass to enable cursor rendering and debug overlay
  width: number;
  height: number;
  mediaPath: string; // Root path for skin and beatmap assets
}): Promise<Renderer>;

Returns a Renderer:

type Renderer = {
  app: Application; // The underlying PixiJS Application
  canvas: HTMLCanvasElement; // The canvas element to mount
  update: (time: number) => void; // Advance to a point in time (ms)
  destroy: () => void; // Tear down the renderer and free resources
  setBackgroundDim: (dim: number) => void; // 0 = full brightness, 1 = fully dimmed
};

The renderer expects skin textures to be loaded before creation — call updateSkinTextures first.


Skin / Textures

updateSkinTextures(urls)

Loads skin textures from the provided URLs and updates the shared texture references used by the renderer. Each value can be any URL or data URL.

function updateSkinTextures(urls: SkinTextureUrls): Promise<void>;

SkinTextureUrls

type SkinTextureUrls = Record<keyof typeof textures, string>;

textures

The shared texture object containing all skin texture references. These are populated by updateSkinTextures and consumed by the renderer internally. You can also use them directly if building custom rendering on top of this library.

Keys: cursor, hitcircle, hitcircleoverlay, approachcircle, spinner-bottom, spinner-middle, spinner-top, spinner-approachcircle, sliderb, sliderfollowcircle, reversearrow, sliderscorepoint, sliderstartcircle, sliderstartcircleoverlay, sliderendcircle, sliderendcircleoverlay, hit0, hit50, hit100, hit300


Math Utilities

calcPreempt(AR)

Returns the approach duration in milliseconds for a given Approach Rate. This is how long before a hit object's time it becomes visible.

function calcPreempt(AR: number): number;
// AR 0 → 1800ms, AR 5 → 1200ms, AR 10 → 450ms

calcFade(AR)

Returns the fade-in duration in milliseconds for a given Approach Rate. Matches osu!lazer: capped at 400ms and only scaled down when the preempt drops below 450ms (e.g. AR > 10 via DT).

function calcFade(AR: number): number;
// AR 0..10 → 400ms (scales down for AR > 10)

calcAlpha(time, ar, hitObject)

Returns the current opacity (0–1) of a hit object based on the current time and Approach Rate.

function calcAlpha(time: number, ar: number, hitObject: HitObject): number;

calcObjectRadius(CS)

Returns the hit circle radius in osu! pixels for a given Circle Size.

function calcObjectRadius(CS: number): number;

calcCursorSize(CS)

Returns the cursor scale factor for a given Circle Size.

function calcCursorSize(CS: number): number;

lerp2D(t0, x0, y0, t1, x1, y1, t)

Linearly interpolates between two 2D points over time. Used internally for smooth cursor movement between replay frames.

function lerp2D(
  t0: number,
  x0: number,
  y0: number,
  t1: number,
  x1: number,
  y1: number,
  t: number,
): { x: number; y: number };