Skip to content

Repository files navigation

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-playerid="player"></chart-preview-player><scripttype="module">constplayer=document.getElementById("player");awaitplayer.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 componentconstplayer=document.querySelector("chart-preview-player");awaitplayer.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:

constresponse=awaitfetch("https://files.enchor.us/abc123.sng");constsngData=newUint8Array(awaitresponse.arrayBuffer());awaitplayer.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 selectionconstfiles=[{fileName: "notes.chart",data: chartFileData},{fileName: "song.ogg",data: audioFileData},{fileName: "guitar.ogg",data: guitarAudioData},];awaitplayer.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 fileconstparsedChart=parseChartFile(chartData,"chart",modifiers);// 2. Load textures (cache and reuse for same instrument type)consttextures=awaitChartPreview.loadTextures(getInstrumentType("guitar"),{animationsEnabled: areAnimationsSupported(),});// 3. Load the chartawaitplayer.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";importtype{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],})exportclassChartPreviewComponent{
@ViewChild("player")player: ElementRef<ChartPreviewPlayer>;volume=50;asyncloadChart(chartUrl: string,instrument: string,difficulty: string){awaitthis.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";importtype{ChartPreviewPlayer}from"chart-preview";import"chart-preview";functionChartPreview({ chartUrl, instrument, difficulty }){constplayerRef=useRef<ChartPreviewPlayer>(null);useEffect(()=>{constplayer=playerRef.current;if(!player||!chartUrl)return;player.loadFromUrl({url: chartUrl, instrument, difficulty });consthandleError=(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-playerref={playerRef}volume="50"/>;}

Vue

<template>
<chart-preview-playerref="player":volume="volume"@player-statechange="onStateChange"@player-error="onError"
/>
</template>
<script setup>import { ref, onMounted, onUnmounted } from"vue";import"chart-preview";constplayer=ref(null);constvolume=ref(50);asyncfunctionloadChart(url, instrument, difficulty) {awaitplayer.value.loadFromUrl({ url, instrument, difficulty });}functiononStateChange(event) {console.log("State:", event.detail.state);}functiononError(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

AttributeTypeDefaultDescription
volumestring"50"Initial volume (0-100)

Properties

PropertyTypeDescription
statePlayerStateCurrent player state
isPlayingbooleanWhether currently playing
volumenumberCurrent volume (0-100)
currentTimeMsnumberCurrent playback position in ms
durationMsnumberTotal duration in ms
isFullscreenbooleanWhether in fullscreen mode

Methods

MethodDescription
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

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

Player States

typePlayerState=|"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

KeyAction
SpacePlay/Pause
Seek backward 5s
Seek forward 5s
Volume up 10%
Volume down 10%
MToggle mute
FToggle fullscreen
EscapeExit fullscreen

Multiple Instances

The library supports multiple simultaneous players on the same page:

<chart-preview-playerid="player1"></chart-preview-player><chart-preview-playerid="player2"></chart-preview-player><chart-preview-playerid="player3"></chart-preview-player><scripttype="module">constplayers=document.querySelectorAll("chart-preview-player");// Each player can load a different chartawaitplayers[0].loadFromUrl({url: "chart1.sng",instrument: "guitar",difficulty: "expert",});awaitplayers[1].loadFromUrl({url: "chart2.sng",instrument: "drums",difficulty: "hard",});awaitplayers[2].loadFromUrl({url: "chart3.sng",instrument: "bass",difficulty: "medium",});// All can play simultaneouslyplayers.forEach((p)=>p.play());</script>

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

Configuration Types

LoadFromUrlConfig

interfaceLoadFromUrlConfig{/** 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

interfaceLoadFromSngFileConfig{/** 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

interfaceLoadFromChartFilesConfig{/** 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)

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

Supported Instruments & Difficulties

Instruments

ValueDescription
'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

ValueDescription
'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 performanceconsttextures=awaitChartPreview.loadTextures(getInstrumentType("guitar"),{animationsEnabled: areAnimationsSupported(),// or set to false to always use static textures});// Create previewconstpreview=awaitChartPreview.create({
parsedChart,
textures,
audioFiles,instrument: "guitar",difficulty: "expert",startDelayMs: 0,audioLengthMs: 180000,container: document.getElementById("container"),});// Control playbackawaitpreview.togglePaused();awaitpreview.seek(0.5);preview.volume=0.8;// Listen to eventspreview.on("progress",(percent)=>console.log(`${percent*100}%`));preview.on("end",()=>console.log("Ended"));// Clean uppreview.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 fileconstsngData=awaitfetchSngFile("https://example.com/chart.sng");constfiles=awaitextractSngFile(sngData);// Find specific filesconstchartFile=findChartFile(files);// Returns .chart or .mid fileconstaudioFiles=findAudioFiles(files);// Returns audio file data// Check if a file is a video (to exclude from processing)constnonVideoFiles=files.filter((f)=>!isVideoFile(f.fileName));// Prepare all data for playbackconstpreparedData=awaitprepareChartData(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

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages