A 3D chart preview player for rhythm games like Clone Hero. Renders chart files as an interactive video-like preview using THREE.js.
npm install chart-previewA 3D chart preview player for rhythm games like Clone Hero. Renders chart files as an interactive video-like preview using THREE.js.
- 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
``bash`
npm install chart-preview
The simplest way to use chart-preview is with the Web Component and URL-based loading:
`html
`
That's it! The component handles fetching, parsing, texture loading, and rendering.
Load directly from a URL to a .sng file:
`typescript
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%
});
`
When you've already fetched the .sng file:
`typescript
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",
});
`
When loading from a folder or file picker:
`typescript
// 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",
});
`
For maximum control, you can pre-process the data yourself:
`typescript
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,
});
`
`typescript
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:
[attr.volume]="volume"
(player-statechange)="onStateChange($event)"
(player-error)="onError($event)"
>
,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class ChartPreviewComponent {
@ViewChild("player") player: ElementRef
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);
}
}
`
`tsx
import { useRef, useEffect } from "react";
import type { ChartPreviewPlayer } from "chart-preview";
import "chart-preview";
function ChartPreview({ chartUrl, instrument, difficulty }) {
const playerRef = useRef
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
}
`
`vue
:volume="volume"
@player-statechange="onStateChange"
@player-error="onError"
/>
`
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
`typescript`
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 |
The library supports multiple simultaneous players on the same page:
`html
`
The library uses a shared AudioContext internally to support many players without hitting browser limits.
`typescript`
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;
}
`typescript`
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;
}
`typescript`
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;
}
`typescript`
interface ChartPreviewPlayerConfig {
parsedChart: ParsedChart;
textures: Awaited
audioFiles: Uint8Array[];
instrument: Instrument;
difficulty: Difficulty;
startDelayMs: number;
audioLengthMs: number;
initialSeekPercent?: number;
}
| 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 |
| Value | Description |
| ---------- | ----------- |
| 'expert' | Expert |'hard'
| | Hard |'medium'
| | Medium |'easy'
| | Easy |
For advanced use cases, you can use the ChartPreview class directly:
`typescript
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();
`
The library exports helper utilities for advanced use cases:
`typescript
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");
`
`bashInstall dependencies
npm install
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 handlingMIT