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

chart-preview

v1.3.0

Published

A 3D chart preview player for rhythm games like Clone Hero. Renders chart files as an interactive video-like preview using THREE.js.

Downloads

769

Readme

chart-preview

A 3D chart preview player for rhythm games like Clone Hero. Renders chart files as an interactive video-like preview using THREE.js.

Features

  • Renders .chart and .mid files as a 3D highway visualization
  • Supports 5-fret guitar, 6-fret (GHL) guitar, and drums
  • Plays audio files in sync with the visual preview
  • Video player-like controls (play, pause, seek, volume, fullscreen)
  • Keyboard shortcuts for easy control
  • Framework-agnostic - works with React, Angular, Vue, or vanilla JS
  • Web Component that can be dropped into any project
  • Multiple instance support - run several players simultaneously
  • Simple URL-based loading - just provide a URL to a .sng file
  • Animated note textures - supports animated WebP textures

Installation

npm install chart-preview

Quick Start

The simplest way to use chart-preview is with the Web Component and URL-based loading:

<chart-preview-player id="player"></chart-preview-player>

<script type="module">
  const player = document.getElementById("player");

  await player.loadFromUrl({
    url: "https://files.enchor.us/abc123.sng",
    instrument: "guitar",
    difficulty: "expert",
  });
</script>

That's it! The component handles fetching, parsing, texture loading, and rendering.

Usage Options

Option 1: URL-Based Loading (Simplest)

Load directly from a URL to a .sng file:

import "chart-preview"; // Registers the web component

const player = document.querySelector("chart-preview-player");

await player.loadFromUrl({
  url: "https://files.enchor.us/abc123.sng",
  instrument: "guitar",
  difficulty: "expert",
  initialSeekPercent: 0.25, // Optional: start at 25%
});

Option 2: Raw .sng File Loading

When you've already fetched the .sng file:

const response = await fetch("https://files.enchor.us/abc123.sng");
const sngData = new Uint8Array(await response.arrayBuffer());

await player.loadFromSngFile({
  sngFile: sngData,
  instrument: "guitar",
  difficulty: "expert",
});

Option 3: Individual Files Loading

When loading from a folder or file picker:

// From a file input or folder selection
const files = [
  { fileName: "notes.chart", data: chartFileData },
  { fileName: "song.ogg", data: audioFileData },
  { fileName: "guitar.ogg", data: guitarAudioData },
];

await player.loadFromChartFiles({
  files,
  instrument: "guitar",
  difficulty: "expert",
});

Option 4: Pre-Processed Data (Advanced)

For maximum control, you can pre-process the data yourself:

import {
  ChartPreview,
  ChartPreviewPlayer,
  getInstrumentType,
  areAnimationsSupported,
} from "chart-preview";
import { parseChartFile } from "scan-chart";

// 1. Parse your chart file
const parsedChart = parseChartFile(chartData, "chart", modifiers);

// 2. Load textures (cache and reuse for same instrument type)
const textures = await ChartPreview.loadTextures(getInstrumentType("guitar"), {
  animationsEnabled: areAnimationsSupported(),
});

// 3. Load the chart
await player.loadChart({
  parsedChart,
  textures,
  audioFiles: [audioData],
  instrument: "guitar",
  difficulty: "expert",
  startDelayMs: 0,
  audioLengthMs: 180000,
});

Framework Integration

Angular

import {
  Component,
  ViewChild,
  ElementRef,
  CUSTOM_ELEMENTS_SCHEMA,
} from "@angular/core";
import type { ChartPreviewPlayer } from "chart-preview";
import "chart-preview"; // Register web component

@Component({
  selector: "app-chart-preview",
  template: `
    <chart-preview-player
      #player
      [attr.volume]="volume"
      (player-statechange)="onStateChange($event)"
      (player-error)="onError($event)"
    >
    </chart-preview-player>
  `,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class ChartPreviewComponent {
  @ViewChild("player") player: ElementRef<ChartPreviewPlayer>;
  volume = 50;

  async loadChart(chartUrl: string, instrument: string, difficulty: string) {
    await this.player.nativeElement.loadFromUrl({
      url: chartUrl,
      instrument,
      difficulty,
    });
  }

  onStateChange(event: CustomEvent) {
    console.log("State:", event.detail.state);
  }

  onError(event: CustomEvent) {
    console.error("Error:", event.detail.error);
  }
}

React

import { useRef, useEffect } from "react";
import type { ChartPreviewPlayer } from "chart-preview";
import "chart-preview";

function ChartPreview({ chartUrl, instrument, difficulty }) {
  const playerRef = useRef<ChartPreviewPlayer>(null);

  useEffect(() => {
    const player = playerRef.current;
    if (!player || !chartUrl) return;

    player.loadFromUrl({ url: chartUrl, instrument, difficulty });

    const handleError = (e: CustomEvent) => console.error(e.detail.error);
    player.addEventListener("player-error", handleError);

    return () => {
      player.removeEventListener("player-error", handleError);
      player.dispose();
    };
  }, [chartUrl, instrument, difficulty]);

  return <chart-preview-player ref={playerRef} volume="50" />;
}

Vue

<template>
  <chart-preview-player
    ref="player"
    :volume="volume"
    @player-statechange="onStateChange"
    @player-error="onError"
  />
</template>

<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import "chart-preview";

const player = ref(null);
const volume = ref(50);

async function loadChart(url, instrument, difficulty) {
  await player.value.loadFromUrl({ url, instrument, difficulty });
}

function onStateChange(event) {
  console.log("State:", event.detail.state);
}

function onError(event) {
  console.error("Error:", event.detail.error);
}

onUnmounted(() => {
  player.value?.dispose();
});
</script>

Web Component API

<chart-preview-player>

A complete chart preview player with built-in controls.

Attributes

| Attribute | Type | Default | Description | | --------- | -------- | ------- | ---------------------- | | volume | string | "50" | Initial volume (0-100) |

Properties

| Property | Type | Description | | --------------- | ------------- | ------------------------------- | | state | PlayerState | Current player state | | isPlaying | boolean | Whether currently playing | | volume | number | Current volume (0-100) | | currentTimeMs | number | Current playback position in ms | | durationMs | number | Total duration in ms | | isFullscreen | boolean | Whether in fullscreen mode |

Methods

| Method | Description | | ---------------------------- | --------------------------------- | | loadFromUrl(config) | Load from a URL to a .sng file | | loadFromSngFile(config) | Load from raw .sng file data | | loadFromChartFiles(config) | Load from individual files | | loadChart(config) | Load from pre-processed data | | togglePlayPause() | Toggle play/pause | | play() | Start playback | | pause() | Pause playback | | seek(percent) | Seek to position (0-1) | | seekRelative(deltaMs) | Seek relative to current position | | setVolume(volume) | Set volume (0-100) | | toggleMute() | Toggle mute | | toggleFullscreen() | Toggle fullscreen mode | | dispose() | Clean up resources |

Events

| Event | Detail | Description | | -------------------- | --------------------------------- | ----------------- | | player-statechange | { state, previousState } | State changed | | player-progress | { percent, currentMs, totalMs } | Playback progress | | player-end | - | Playback ended | | player-error | { error } | Error occurred |

Player States

type PlayerState =
  | "idle" // No chart loaded
  | "loading" // Loading chart/audio
  | "ready" // Ready to play
  | "playing" // Currently playing
  | "paused" // Paused
  | "seeking" // Seeking
  | "ended" // Playback ended
  | "error"; // Error occurred

Keyboard Shortcuts

| Key | Action | | -------- | ----------------- | | Space | Play/Pause | | | Seek backward 5s | | | Seek forward 5s | | | Volume up 10% | | | Volume down 10% | | M | Toggle mute | | F | Toggle fullscreen | | Escape | Exit fullscreen |

Multiple Instances

The library supports multiple simultaneous players on the same page:

<chart-preview-player id="player1"></chart-preview-player>
<chart-preview-player id="player2"></chart-preview-player>
<chart-preview-player id="player3"></chart-preview-player>

<script type="module">
  const players = document.querySelectorAll("chart-preview-player");

  // Each player can load a different chart
  await players[0].loadFromUrl({
    url: "chart1.sng",
    instrument: "guitar",
    difficulty: "expert",
  });
  await players[1].loadFromUrl({
    url: "chart2.sng",
    instrument: "drums",
    difficulty: "hard",
  });
  await players[2].loadFromUrl({
    url: "chart3.sng",
    instrument: "bass",
    difficulty: "medium",
  });

  // All can play simultaneously
  players.forEach((p) => p.play());
</script>

The library uses a shared AudioContext internally to support many players without hitting browser limits.

Configuration Types

LoadFromUrlConfig

interface LoadFromUrlConfig {
  /** URL to the .sng file */
  url: string;
  /** The instrument to display */
  instrument: Instrument;
  /** The difficulty level to display */
  difficulty: Difficulty;
  /** Initial seek position (0-1). Defaults to 0 */
  initialSeekPercent?: number;
  /** AbortSignal to cancel the fetch operation */
  signal?: AbortSignal;
  /** Whether to enable animated textures. Defaults to true */
  animationsEnabled?: boolean;
}

LoadFromSngFileConfig

interface LoadFromSngFileConfig {
  /** Raw .sng file data */
  sngFile: Uint8Array;
  /** The instrument to display */
  instrument: Instrument;
  /** The difficulty level to display */
  difficulty: Difficulty;
  /** Initial seek position (0-1). Defaults to 0 */
  initialSeekPercent?: number;
  /** Whether to enable animated textures. Defaults to true */
  animationsEnabled?: boolean;
}

LoadFromChartFilesConfig

interface LoadFromChartFilesConfig {
  /** Array of files with their names and data */
  files: { fileName: string; data: Uint8Array }[];
  /** The instrument to display */
  instrument: Instrument;
  /** The difficulty level to display */
  difficulty: Difficulty;
  /** Initial seek position (0-1). Defaults to 0 */
  initialSeekPercent?: number;
  /** Whether to enable animated textures. Defaults to true */
  animationsEnabled?: boolean;
}

ChartPreviewPlayerConfig (Advanced)

interface ChartPreviewPlayerConfig {
  parsedChart: ParsedChart;
  textures: Awaited<ReturnType<typeof ChartPreview.loadTextures>>;
  audioFiles: Uint8Array[];
  instrument: Instrument;
  difficulty: Difficulty;
  startDelayMs: number;
  audioLengthMs: number;
  initialSeekPercent?: number;
}

Supported Instruments & Difficulties

Instruments

| Value | Description | | ----------------- | -------------------- | | 'guitar' | Lead Guitar (5-fret) | | 'guitarcoop' | Co-op Guitar | | 'rhythm' | Rhythm Guitar | | 'bass' | Bass Guitar | | 'drums' | Drums | | 'keys' | Keys | | 'guitarghl' | GHL Guitar (6-fret) | | 'guitarcoopghl' | GHL Co-op Guitar | | 'rhythmghl' | GHL Rhythm Guitar | | 'bassghl' | GHL Bass |

Difficulties

| Value | Description | | ---------- | ----------- | | 'expert' | Expert | | 'hard' | Hard | | 'medium' | Medium | | 'easy' | Easy |

Low-Level API

For advanced use cases, you can use the ChartPreview class directly:

import {
  ChartPreview,
  getInstrumentType,
  areAnimationsSupported,
} from "chart-preview";

// Load textures (cache for reuse)
// Optionally disable animations for better performance
const textures = await ChartPreview.loadTextures(getInstrumentType("guitar"), {
  animationsEnabled: areAnimationsSupported(), // or set to false to always use static textures
});

// Create preview
const preview = await ChartPreview.create({
  parsedChart,
  textures,
  audioFiles,
  instrument: "guitar",
  difficulty: "expert",
  startDelayMs: 0,
  audioLengthMs: 180000,
  container: document.getElementById("container"),
});

// Control playback
await preview.togglePaused();
await preview.seek(0.5);
preview.volume = 0.8;

// Listen to events
preview.on("progress", (percent) => console.log(`${percent * 100}%`));
preview.on("end", () => console.log("Ended"));

// Clean up
preview.dispose();

Helper Utilities

The library exports helper utilities for advanced use cases:

import {
  extractSngFile,
  fetchSngFile,
  prepareChartData,
  findChartFile,
  findAudioFiles,
  isVideoFile,
  areAnimationsSupported,
} from "chart-preview";

// Check if animated textures are supported (ImageDecoder API)
if (areAnimationsSupported()) {
  console.log("Animated note textures will be used");
}

// Fetch and extract a .sng file
const sngData = await fetchSngFile("https://example.com/chart.sng");
const files = await extractSngFile(sngData);

// Find specific files
const chartFile = findChartFile(files); // Returns .chart or .mid file
const audioFiles = findAudioFiles(files); // Returns audio file data

// Check if a file is a video (to exclude from processing)
const nonVideoFiles = files.filter((f) => !isVideoFile(f.fileName));

// Prepare all data for playback
const preparedData = await prepareChartData(files, "guitar", "expert");

Development

# Install dependencies
npm install

# Start dev server
npm run dev

# Build for production
npm run build

# Type check
npm run lint

Browser Compatibility

  • Chrome 80+
  • Firefox 75+
  • Safari 14+
  • Edge 80+

Requires support for:

  • Web Components (Custom Elements v1)
  • Web Audio API
  • WebGL

Animated Textures: Requires the ImageDecoder API (Chromium-based browsers only: Chrome, Edge, Opera). Use areAnimationsSupported() to check. Other browsers fall back to static textures.

Dependencies

  • three - 3D rendering
  • scan-chart - Chart parsing
  • parse-sng - .sng file extraction
  • eventemitter3 - Event handling

License

MIT