chart-preview 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ # Changesets
2
+
3
+ Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4
+ with multi-package repos, or single-package repos to help you version and publish your code. You can
5
+ find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6
+
7
+ We have a quick list of common questions to get you started engaging with this project in
8
+ [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "public",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "ignore": []
11
+ }
@@ -0,0 +1,38 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ concurrency: ${{ github.workflow }}-${{ github.ref }}
9
+
10
+ jobs:
11
+ release:
12
+ name: Release
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: write
16
+ pull-requests: write
17
+ id-token: write
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Setup Node.js
23
+ uses: actions/setup-node@v4
24
+ with:
25
+ node-version: 20
26
+ cache: "npm"
27
+ registry-url: "https://registry.npmjs.org"
28
+
29
+ - name: Install Dependencies
30
+ run: npm ci
31
+
32
+ - name: Create Release Pull Request or Publish
33
+ uses: changesets/action@v1
34
+ with:
35
+ publish: npm run release
36
+ env:
37
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,6 @@
1
+ {
2
+ "recommendations": [
3
+ "esbenp.prettier-vscode",
4
+ "dbaeumer.vscode-eslint"
5
+ ]
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
3
+ "editor.formatOnSave": true,
4
+ "editor.codeActionsOnSave": {
5
+ "source.fixAll.eslint": "explicit"
6
+ },
7
+ "typescript.tsdk": "node_modules/typescript/lib"
8
+ }
package/README.md ADDED
@@ -0,0 +1,541 @@
1
+ # chart-preview
2
+
3
+ A 3D chart preview player for rhythm games like Clone Hero. Renders chart files as an interactive video-like preview using THREE.js.
4
+
5
+ ## Features
6
+
7
+ - Renders `.chart` and `.mid` files as a 3D highway visualization
8
+ - Supports 5-fret guitar, 6-fret (GHL) guitar, and drums
9
+ - Plays audio files in sync with the visual preview
10
+ - Video player-like controls (play, pause, seek, volume, fullscreen)
11
+ - Keyboard shortcuts for easy control
12
+ - Framework-agnostic - works with React, Angular, Vue, or vanilla JS
13
+ - **Web Component** that can be dropped into any project
14
+ - **Multiple instance support** - run several players simultaneously
15
+ - **Simple URL-based loading** - just provide a URL to a .sng file
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install chart-preview
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ The simplest way to use chart-preview is with the Web Component and URL-based loading:
26
+
27
+ ```html
28
+ <chart-preview-player id="player"></chart-preview-player>
29
+
30
+ <script type="module">
31
+ const player = document.getElementById("player");
32
+
33
+ await player.loadFromUrl({
34
+ url: "https://files.enchor.us/abc123.sng",
35
+ instrument: "guitar",
36
+ difficulty: "expert",
37
+ });
38
+ </script>
39
+ ```
40
+
41
+ That's it! The component handles fetching, parsing, texture loading, and rendering.
42
+
43
+ ## Usage Options
44
+
45
+ ### Option 1: URL-Based Loading (Simplest)
46
+
47
+ Load directly from a URL to a `.sng` file:
48
+
49
+ ```typescript
50
+ import "chart-preview"; // Registers the web component
51
+
52
+ const player = document.querySelector("chart-preview-player");
53
+
54
+ await player.loadFromUrl({
55
+ url: "https://files.enchor.us/abc123.sng",
56
+ instrument: "guitar",
57
+ difficulty: "expert",
58
+ initialSeekPercent: 0.25, // Optional: start at 25%
59
+ });
60
+ ```
61
+
62
+ ### Option 2: Raw .sng File Loading
63
+
64
+ When you've already fetched the `.sng` file:
65
+
66
+ ```typescript
67
+ const response = await fetch("https://files.enchor.us/abc123.sng");
68
+ const sngData = new Uint8Array(await response.arrayBuffer());
69
+
70
+ await player.loadFromSngFile({
71
+ sngFile: sngData,
72
+ instrument: "guitar",
73
+ difficulty: "expert",
74
+ });
75
+ ```
76
+
77
+ ### Option 3: Individual Files Loading
78
+
79
+ When loading from a folder or file picker:
80
+
81
+ ```typescript
82
+ // From a file input or folder selection
83
+ const files = [
84
+ { fileName: "notes.chart", data: chartFileData },
85
+ { fileName: "song.ogg", data: audioFileData },
86
+ { fileName: "guitar.ogg", data: guitarAudioData },
87
+ ];
88
+
89
+ await player.loadFromChartFiles({
90
+ files,
91
+ instrument: "guitar",
92
+ difficulty: "expert",
93
+ });
94
+ ```
95
+
96
+ ### Option 4: Pre-Processed Data (Advanced)
97
+
98
+ For maximum control, you can pre-process the data yourself:
99
+
100
+ ```typescript
101
+ import {
102
+ ChartPreview,
103
+ ChartPreviewPlayer,
104
+ getInstrumentType,
105
+ } from "chart-preview";
106
+ import { parseChartFile } from "scan-chart";
107
+
108
+ // 1. Parse your chart file
109
+ const parsedChart = parseChartFile(chartData, "chart", modifiers);
110
+
111
+ // 2. Load textures (cache and reuse for same instrument type)
112
+ const textures = await ChartPreview.loadTextures(getInstrumentType("guitar"));
113
+
114
+ // 3. Load the chart
115
+ await player.loadChart({
116
+ parsedChart,
117
+ textures,
118
+ audioFiles: [audioData],
119
+ instrument: "guitar",
120
+ difficulty: "expert",
121
+ startDelayMs: 0,
122
+ audioLengthMs: 180000,
123
+ });
124
+ ```
125
+
126
+ ## Framework Integration
127
+
128
+ ### Angular
129
+
130
+ ```typescript
131
+ import {
132
+ Component,
133
+ ViewChild,
134
+ ElementRef,
135
+ CUSTOM_ELEMENTS_SCHEMA,
136
+ } from "@angular/core";
137
+ import type { ChartPreviewPlayer } from "chart-preview";
138
+ import "chart-preview"; // Register web component
139
+
140
+ @Component({
141
+ selector: "app-chart-preview",
142
+ template: `
143
+ <chart-preview-player
144
+ #player
145
+ [attr.volume]="volume"
146
+ (player-statechange)="onStateChange($event)"
147
+ (player-error)="onError($event)"
148
+ >
149
+ </chart-preview-player>
150
+ `,
151
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
152
+ })
153
+ export class ChartPreviewComponent {
154
+ @ViewChild("player") player: ElementRef<ChartPreviewPlayer>;
155
+ volume = 50;
156
+
157
+ async loadChart(chartUrl: string, instrument: string, difficulty: string) {
158
+ await this.player.nativeElement.loadFromUrl({
159
+ url: chartUrl,
160
+ instrument,
161
+ difficulty,
162
+ });
163
+ }
164
+
165
+ onStateChange(event: CustomEvent) {
166
+ console.log("State:", event.detail.state);
167
+ }
168
+
169
+ onError(event: CustomEvent) {
170
+ console.error("Error:", event.detail.error);
171
+ }
172
+ }
173
+ ```
174
+
175
+ ### React
176
+
177
+ ```tsx
178
+ import { useRef, useEffect } from "react";
179
+ import type { ChartPreviewPlayer } from "chart-preview";
180
+ import "chart-preview";
181
+
182
+ function ChartPreview({ chartUrl, instrument, difficulty }) {
183
+ const playerRef = useRef<ChartPreviewPlayer>(null);
184
+
185
+ useEffect(() => {
186
+ const player = playerRef.current;
187
+ if (!player || !chartUrl) return;
188
+
189
+ player.loadFromUrl({ url: chartUrl, instrument, difficulty });
190
+
191
+ const handleError = (e: CustomEvent) => console.error(e.detail.error);
192
+ player.addEventListener("player-error", handleError);
193
+
194
+ return () => {
195
+ player.removeEventListener("player-error", handleError);
196
+ player.dispose();
197
+ };
198
+ }, [chartUrl, instrument, difficulty]);
199
+
200
+ return <chart-preview-player ref={playerRef} volume="50" />;
201
+ }
202
+ ```
203
+
204
+ ### Vue
205
+
206
+ ```vue
207
+ <template>
208
+ <chart-preview-player
209
+ ref="player"
210
+ :volume="volume"
211
+ @player-statechange="onStateChange"
212
+ @player-error="onError"
213
+ />
214
+ </template>
215
+
216
+ <script setup>
217
+ import { ref, onMounted, onUnmounted } from "vue";
218
+ import "chart-preview";
219
+
220
+ const player = ref(null);
221
+ const volume = ref(50);
222
+
223
+ async function loadChart(url, instrument, difficulty) {
224
+ await player.value.loadFromUrl({ url, instrument, difficulty });
225
+ }
226
+
227
+ function onStateChange(event) {
228
+ console.log("State:", event.detail.state);
229
+ }
230
+
231
+ function onError(event) {
232
+ console.error("Error:", event.detail.error);
233
+ }
234
+
235
+ onUnmounted(() => {
236
+ player.value?.dispose();
237
+ });
238
+ </script>
239
+ ```
240
+
241
+ ## Web Component API
242
+
243
+ ### `<chart-preview-player>`
244
+
245
+ A complete chart preview player with built-in controls.
246
+
247
+ #### Attributes
248
+
249
+ | Attribute | Type | Default | Description |
250
+ | --------- | -------- | ------- | ---------------------- |
251
+ | `volume` | `string` | `"50"` | Initial volume (0-100) |
252
+
253
+ #### Properties
254
+
255
+ | Property | Type | Description |
256
+ | --------------- | ------------- | ------------------------------- |
257
+ | `state` | `PlayerState` | Current player state |
258
+ | `isPlaying` | `boolean` | Whether currently playing |
259
+ | `volume` | `number` | Current volume (0-100) |
260
+ | `currentTimeMs` | `number` | Current playback position in ms |
261
+ | `durationMs` | `number` | Total duration in ms |
262
+ | `isFullscreen` | `boolean` | Whether in fullscreen mode |
263
+
264
+ #### Methods
265
+
266
+ | Method | Description |
267
+ | ---------------------------- | --------------------------------- |
268
+ | `loadFromUrl(config)` | Load from a URL to a .sng file |
269
+ | `loadFromSngFile(config)` | Load from raw .sng file data |
270
+ | `loadFromChartFiles(config)` | Load from individual files |
271
+ | `loadChart(config)` | Load from pre-processed data |
272
+ | `togglePlayPause()` | Toggle play/pause |
273
+ | `play()` | Start playback |
274
+ | `pause()` | Pause playback |
275
+ | `seek(percent)` | Seek to position (0-1) |
276
+ | `seekRelative(deltaMs)` | Seek relative to current position |
277
+ | `setVolume(volume)` | Set volume (0-100) |
278
+ | `toggleMute()` | Toggle mute |
279
+ | `toggleFullscreen()` | Toggle fullscreen mode |
280
+ | `dispose()` | Clean up resources |
281
+
282
+ #### Events
283
+
284
+ | Event | Detail | Description |
285
+ | -------------------- | --------------------------------- | ----------------- |
286
+ | `player-statechange` | `{ state, previousState }` | State changed |
287
+ | `player-progress` | `{ percent, currentMs, totalMs }` | Playback progress |
288
+ | `player-end` | - | Playback ended |
289
+ | `player-error` | `{ error }` | Error occurred |
290
+
291
+ #### Player States
292
+
293
+ ```typescript
294
+ type PlayerState =
295
+ | "idle" // No chart loaded
296
+ | "loading" // Loading chart/audio
297
+ | "ready" // Ready to play
298
+ | "playing" // Currently playing
299
+ | "paused" // Paused
300
+ | "seeking" // Seeking
301
+ | "ended" // Playback ended
302
+ | "error"; // Error occurred
303
+ ```
304
+
305
+ #### Keyboard Shortcuts
306
+
307
+ | Key | Action |
308
+ | -------- | ----------------- |
309
+ | `Space` | Play/Pause |
310
+ | `←` | Seek backward 5s |
311
+ | `→` | Seek forward 5s |
312
+ | `↑` | Volume up 10% |
313
+ | `↓` | Volume down 10% |
314
+ | `M` | Toggle mute |
315
+ | `F` | Toggle fullscreen |
316
+ | `Escape` | Exit fullscreen |
317
+
318
+ ## Multiple Instances
319
+
320
+ The library supports multiple simultaneous players on the same page:
321
+
322
+ ```html
323
+ <chart-preview-player id="player1"></chart-preview-player>
324
+ <chart-preview-player id="player2"></chart-preview-player>
325
+ <chart-preview-player id="player3"></chart-preview-player>
326
+
327
+ <script type="module">
328
+ const players = document.querySelectorAll("chart-preview-player");
329
+
330
+ // Each player can load a different chart
331
+ await players[0].loadFromUrl({
332
+ url: "chart1.sng",
333
+ instrument: "guitar",
334
+ difficulty: "expert",
335
+ });
336
+ await players[1].loadFromUrl({
337
+ url: "chart2.sng",
338
+ instrument: "drums",
339
+ difficulty: "hard",
340
+ });
341
+ await players[2].loadFromUrl({
342
+ url: "chart3.sng",
343
+ instrument: "bass",
344
+ difficulty: "medium",
345
+ });
346
+
347
+ // All can play simultaneously
348
+ players.forEach((p) => p.play());
349
+ </script>
350
+ ```
351
+
352
+ The library uses a shared `AudioContext` internally to support many players without hitting browser limits.
353
+
354
+ ## Configuration Types
355
+
356
+ ### LoadFromUrlConfig
357
+
358
+ ```typescript
359
+ interface LoadFromUrlConfig {
360
+ /** URL to the .sng file */
361
+ url: string;
362
+ /** The instrument to display */
363
+ instrument: Instrument;
364
+ /** The difficulty level to display */
365
+ difficulty: Difficulty;
366
+ /** Initial seek position (0-1). Defaults to 0 */
367
+ initialSeekPercent?: number;
368
+ }
369
+ ```
370
+
371
+ ### LoadFromSngFileConfig
372
+
373
+ ```typescript
374
+ interface LoadFromSngFileConfig {
375
+ /** Raw .sng file data */
376
+ sngFile: Uint8Array;
377
+ /** The instrument to display */
378
+ instrument: Instrument;
379
+ /** The difficulty level to display */
380
+ difficulty: Difficulty;
381
+ /** Initial seek position (0-1). Defaults to 0 */
382
+ initialSeekPercent?: number;
383
+ }
384
+ ```
385
+
386
+ ### LoadFromChartFilesConfig
387
+
388
+ ```typescript
389
+ interface LoadFromChartFilesConfig {
390
+ /** Array of files with their names and data */
391
+ files: { fileName: string; data: Uint8Array }[];
392
+ /** The instrument to display */
393
+ instrument: Instrument;
394
+ /** The difficulty level to display */
395
+ difficulty: Difficulty;
396
+ /** Initial seek position (0-1). Defaults to 0 */
397
+ initialSeekPercent?: number;
398
+ }
399
+ ```
400
+
401
+ ### ChartPreviewPlayerConfig (Advanced)
402
+
403
+ ```typescript
404
+ interface ChartPreviewPlayerConfig {
405
+ parsedChart: ParsedChart;
406
+ textures: Awaited<ReturnType<typeof ChartPreview.loadTextures>>;
407
+ audioFiles: Uint8Array[];
408
+ instrument: Instrument;
409
+ difficulty: Difficulty;
410
+ startDelayMs: number;
411
+ audioLengthMs: number;
412
+ initialSeekPercent?: number;
413
+ }
414
+ ```
415
+
416
+ ## Supported Instruments & Difficulties
417
+
418
+ ### Instruments
419
+
420
+ | Value | Description |
421
+ | ----------------- | -------------------- |
422
+ | `'guitar'` | Lead Guitar (5-fret) |
423
+ | `'guitarcoop'` | Co-op Guitar |
424
+ | `'rhythm'` | Rhythm Guitar |
425
+ | `'bass'` | Bass Guitar |
426
+ | `'drums'` | Drums |
427
+ | `'keys'` | Keys |
428
+ | `'guitarghl'` | GHL Guitar (6-fret) |
429
+ | `'guitarcoopghl'` | GHL Co-op Guitar |
430
+ | `'rhythmghl'` | GHL Rhythm Guitar |
431
+ | `'bassghl'` | GHL Bass |
432
+
433
+ ### Difficulties
434
+
435
+ | Value | Description |
436
+ | ---------- | ----------- |
437
+ | `'expert'` | Expert |
438
+ | `'hard'` | Hard |
439
+ | `'medium'` | Medium |
440
+ | `'easy'` | Easy |
441
+
442
+ ## Low-Level API
443
+
444
+ For advanced use cases, you can use the `ChartPreview` class directly:
445
+
446
+ ```typescript
447
+ import { ChartPreview, getInstrumentType } from "chart-preview";
448
+
449
+ // Load textures (cache for reuse)
450
+ const textures = await ChartPreview.loadTextures(getInstrumentType("guitar"));
451
+
452
+ // Create preview
453
+ const preview = await ChartPreview.create({
454
+ parsedChart,
455
+ textures,
456
+ audioFiles,
457
+ instrument: "guitar",
458
+ difficulty: "expert",
459
+ startDelayMs: 0,
460
+ audioLengthMs: 180000,
461
+ container: document.getElementById("container"),
462
+ });
463
+
464
+ // Control playback
465
+ await preview.togglePaused();
466
+ await preview.seek(0.5);
467
+ preview.volume = 0.8;
468
+
469
+ // Listen to events
470
+ preview.on("progress", (percent) => console.log(`${percent * 100}%`));
471
+ preview.on("end", () => console.log("Ended"));
472
+
473
+ // Clean up
474
+ preview.dispose();
475
+ ```
476
+
477
+ ## Helper Utilities
478
+
479
+ The library exports helper utilities for advanced use cases:
480
+
481
+ ```typescript
482
+ import {
483
+ extractSngFile,
484
+ fetchSngFile,
485
+ prepareChartData,
486
+ findChartFile,
487
+ findAudioFiles,
488
+ } from "chart-preview";
489
+
490
+ // Fetch and extract a .sng file
491
+ const sngData = await fetchSngFile("https://example.com/chart.sng");
492
+ const files = await extractSngFile(sngData);
493
+
494
+ // Find specific files
495
+ const chartFile = findChartFile(files); // Returns .chart or .mid file
496
+ const audioFiles = findAudioFiles(files); // Returns audio file data
497
+
498
+ // Prepare all data for playback
499
+ const preparedData = await prepareChartData(files, "guitar", "expert");
500
+ ```
501
+
502
+ ## Development
503
+
504
+ ```bash
505
+ # Install dependencies
506
+ npm install
507
+
508
+ # Start dev server
509
+ npm run dev
510
+
511
+ # Build for production
512
+ npm run build
513
+
514
+ # Type check
515
+ npm run lint
516
+ ```
517
+
518
+ ## Browser Compatibility
519
+
520
+ - Chrome 80+
521
+ - Firefox 75+
522
+ - Safari 14+
523
+ - Edge 80+
524
+
525
+ Requires support for:
526
+
527
+ - Web Components (Custom Elements v1)
528
+ - Web Audio API
529
+ - WebGL
530
+
531
+ ## Dependencies
532
+
533
+ - `three` - 3D rendering
534
+ - `scan-chart` - Chart parsing
535
+ - `parse-sng` - .sng file extraction
536
+ - `eventemitter3` - Event handling
537
+ - `lodash` - Utility functions
538
+
539
+ ## License
540
+
541
+ MIT