chart-preview 1.1.0 → 1.3.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.
- package/CHANGELOG.md +12 -0
- package/README.md +34 -4
- package/dist/index.d.ts +50 -7
- package/dist/index.js +153 -47
- package/dist/index.mjs +152 -47
- package/package.json +1 -1
- package/src/ChartPreview.ts +156 -38
- package/src/ChartPreviewPlayer.ts +77 -12
- package/src/SngLoader.ts +27 -3
- package/src/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# chart-preview
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fe294b3: Add manual resize functionality to ChartPreview and trigger resize in ChartPreviewPlayer on fullscreen change
|
|
8
|
+
|
|
9
|
+
## 1.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 95d0918: Add support for animated note textures and improve fullscreen handling
|
|
14
|
+
|
|
3
15
|
## 1.1.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ A 3D chart preview player for rhythm games like Clone Hero. Renders chart files
|
|
|
13
13
|
- **Web Component** that can be dropped into any project
|
|
14
14
|
- **Multiple instance support** - run several players simultaneously
|
|
15
15
|
- **Simple URL-based loading** - just provide a URL to a .sng file
|
|
16
|
+
- **Animated note textures** - supports animated WebP textures
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -102,6 +103,7 @@ import {
|
|
|
102
103
|
ChartPreview,
|
|
103
104
|
ChartPreviewPlayer,
|
|
104
105
|
getInstrumentType,
|
|
106
|
+
areAnimationsSupported,
|
|
105
107
|
} from "chart-preview";
|
|
106
108
|
import { parseChartFile } from "scan-chart";
|
|
107
109
|
|
|
@@ -109,7 +111,9 @@ import { parseChartFile } from "scan-chart";
|
|
|
109
111
|
const parsedChart = parseChartFile(chartData, "chart", modifiers);
|
|
110
112
|
|
|
111
113
|
// 2. Load textures (cache and reuse for same instrument type)
|
|
112
|
-
const textures = await ChartPreview.loadTextures(getInstrumentType("guitar")
|
|
114
|
+
const textures = await ChartPreview.loadTextures(getInstrumentType("guitar"), {
|
|
115
|
+
animationsEnabled: areAnimationsSupported(),
|
|
116
|
+
});
|
|
113
117
|
|
|
114
118
|
// 3. Load the chart
|
|
115
119
|
await player.loadChart({
|
|
@@ -365,6 +369,10 @@ interface LoadFromUrlConfig {
|
|
|
365
369
|
difficulty: Difficulty;
|
|
366
370
|
/** Initial seek position (0-1). Defaults to 0 */
|
|
367
371
|
initialSeekPercent?: number;
|
|
372
|
+
/** AbortSignal to cancel the fetch operation */
|
|
373
|
+
signal?: AbortSignal;
|
|
374
|
+
/** Whether to enable animated textures. Defaults to true */
|
|
375
|
+
animationsEnabled?: boolean;
|
|
368
376
|
}
|
|
369
377
|
```
|
|
370
378
|
|
|
@@ -380,6 +388,8 @@ interface LoadFromSngFileConfig {
|
|
|
380
388
|
difficulty: Difficulty;
|
|
381
389
|
/** Initial seek position (0-1). Defaults to 0 */
|
|
382
390
|
initialSeekPercent?: number;
|
|
391
|
+
/** Whether to enable animated textures. Defaults to true */
|
|
392
|
+
animationsEnabled?: boolean;
|
|
383
393
|
}
|
|
384
394
|
```
|
|
385
395
|
|
|
@@ -395,6 +405,8 @@ interface LoadFromChartFilesConfig {
|
|
|
395
405
|
difficulty: Difficulty;
|
|
396
406
|
/** Initial seek position (0-1). Defaults to 0 */
|
|
397
407
|
initialSeekPercent?: number;
|
|
408
|
+
/** Whether to enable animated textures. Defaults to true */
|
|
409
|
+
animationsEnabled?: boolean;
|
|
398
410
|
}
|
|
399
411
|
```
|
|
400
412
|
|
|
@@ -444,10 +456,17 @@ interface ChartPreviewPlayerConfig {
|
|
|
444
456
|
For advanced use cases, you can use the `ChartPreview` class directly:
|
|
445
457
|
|
|
446
458
|
```typescript
|
|
447
|
-
import {
|
|
459
|
+
import {
|
|
460
|
+
ChartPreview,
|
|
461
|
+
getInstrumentType,
|
|
462
|
+
areAnimationsSupported,
|
|
463
|
+
} from "chart-preview";
|
|
448
464
|
|
|
449
465
|
// Load textures (cache for reuse)
|
|
450
|
-
|
|
466
|
+
// Optionally disable animations for better performance
|
|
467
|
+
const textures = await ChartPreview.loadTextures(getInstrumentType("guitar"), {
|
|
468
|
+
animationsEnabled: areAnimationsSupported(), // or set to false to always use static textures
|
|
469
|
+
});
|
|
451
470
|
|
|
452
471
|
// Create preview
|
|
453
472
|
const preview = await ChartPreview.create({
|
|
@@ -485,8 +504,15 @@ import {
|
|
|
485
504
|
prepareChartData,
|
|
486
505
|
findChartFile,
|
|
487
506
|
findAudioFiles,
|
|
507
|
+
isVideoFile,
|
|
508
|
+
areAnimationsSupported,
|
|
488
509
|
} from "chart-preview";
|
|
489
510
|
|
|
511
|
+
// Check if animated textures are supported (ImageDecoder API)
|
|
512
|
+
if (areAnimationsSupported()) {
|
|
513
|
+
console.log("Animated note textures will be used");
|
|
514
|
+
}
|
|
515
|
+
|
|
490
516
|
// Fetch and extract a .sng file
|
|
491
517
|
const sngData = await fetchSngFile("https://example.com/chart.sng");
|
|
492
518
|
const files = await extractSngFile(sngData);
|
|
@@ -495,6 +521,9 @@ const files = await extractSngFile(sngData);
|
|
|
495
521
|
const chartFile = findChartFile(files); // Returns .chart or .mid file
|
|
496
522
|
const audioFiles = findAudioFiles(files); // Returns audio file data
|
|
497
523
|
|
|
524
|
+
// Check if a file is a video (to exclude from processing)
|
|
525
|
+
const nonVideoFiles = files.filter((f) => !isVideoFile(f.fileName));
|
|
526
|
+
|
|
498
527
|
// Prepare all data for playback
|
|
499
528
|
const preparedData = await prepareChartData(files, "guitar", "expert");
|
|
500
529
|
```
|
|
@@ -528,13 +557,14 @@ Requires support for:
|
|
|
528
557
|
- Web Audio API
|
|
529
558
|
- WebGL
|
|
530
559
|
|
|
560
|
+
**Animated Textures:** Requires the ImageDecoder API (Chromium-based browsers only: Chrome, Edge, Opera). Use `areAnimationsSupported()` to check. Other browsers fall back to static textures.
|
|
561
|
+
|
|
531
562
|
## Dependencies
|
|
532
563
|
|
|
533
564
|
- `three` - 3D rendering
|
|
534
565
|
- `scan-chart` - Chart parsing
|
|
535
566
|
- `parse-sng` - .sng file extraction
|
|
536
567
|
- `eventemitter3` - Event handling
|
|
537
|
-
- `lodash` - Utility functions
|
|
538
568
|
|
|
539
569
|
## License
|
|
540
570
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,32 +3,41 @@ export { Difficulty, Instrument, InstrumentType, getInstrumentType, instrumentTy
|
|
|
3
3
|
import * as THREE from 'three';
|
|
4
4
|
|
|
5
5
|
type ParsedChart = ReturnType<typeof parseChartFile>;
|
|
6
|
+
/**
|
|
7
|
+
* Check if animations are supported on this browser.
|
|
8
|
+
* Use this to conditionally enable/disable animation features.
|
|
9
|
+
*/
|
|
10
|
+
declare function areAnimationsSupported(): boolean;
|
|
6
11
|
/**
|
|
7
12
|
* Manages an animated WebP texture using the ImageDecoder API.
|
|
13
|
+
* Pre-decodes all frames during initialization for optimal performance.
|
|
8
14
|
* Falls back to a static texture if ImageDecoder is not supported.
|
|
9
15
|
*/
|
|
10
16
|
declare class AnimatedTexture {
|
|
11
17
|
private canvas;
|
|
12
18
|
private ctx;
|
|
13
19
|
texture: THREE.CanvasTexture;
|
|
14
|
-
private decoder;
|
|
15
20
|
private frameIndex;
|
|
16
21
|
private frameCount;
|
|
17
22
|
private lastFrameTime;
|
|
18
23
|
private frameDurations;
|
|
24
|
+
/** Pre-decoded frames stored as ImageBitmap for fast synchronous access */
|
|
25
|
+
private frameCache;
|
|
19
26
|
private isAnimated;
|
|
20
27
|
private disposed;
|
|
21
28
|
private constructor();
|
|
22
29
|
/**
|
|
23
30
|
* Creates an AnimatedTexture from a URL.
|
|
24
31
|
* Uses ImageDecoder for animation if available, otherwise loads as static texture.
|
|
32
|
+
* Pre-decodes all frames during initialization for optimal render performance.
|
|
25
33
|
*/
|
|
26
34
|
static create(url: string): Promise<AnimatedTexture | THREE.Texture>;
|
|
27
35
|
/**
|
|
28
36
|
* Updates the texture to the current animation frame based on elapsed time.
|
|
29
|
-
*
|
|
37
|
+
* This method is SYNCHRONOUS for optimal render performance - all frames
|
|
38
|
+
* are pre-decoded during initialization.
|
|
30
39
|
*/
|
|
31
|
-
tick():
|
|
40
|
+
tick(): void;
|
|
32
41
|
/**
|
|
33
42
|
* Disposes of resources used by this animated texture.
|
|
34
43
|
*/
|
|
@@ -38,7 +47,10 @@ declare class AnimatedTexture {
|
|
|
38
47
|
declare class AnimatedTextureManager {
|
|
39
48
|
private animatedTextures;
|
|
40
49
|
register(texture: AnimatedTexture | THREE.Texture): void;
|
|
41
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Updates all animated textures. This is now SYNCHRONOUS for optimal performance.
|
|
52
|
+
*/
|
|
53
|
+
tick(): void;
|
|
42
54
|
dispose(): void;
|
|
43
55
|
}
|
|
44
56
|
/**
|
|
@@ -168,6 +180,11 @@ declare class ChartPreview {
|
|
|
168
180
|
get chartCurrentTimeMs(): number;
|
|
169
181
|
/** Total duration in milliseconds */
|
|
170
182
|
get chartEndTimeMs(): number;
|
|
183
|
+
/**
|
|
184
|
+
* Manually triggers a resize of the camera and renderer.
|
|
185
|
+
* Call this when the container size changes (e.g., entering/exiting fullscreen).
|
|
186
|
+
*/
|
|
187
|
+
resize(): void;
|
|
171
188
|
/**
|
|
172
189
|
* Cleans up all resources. Call this when discarding the preview.
|
|
173
190
|
* The preview should not be used after calling dispose().
|
|
@@ -177,12 +194,22 @@ declare class ChartPreview {
|
|
|
177
194
|
private addStrikelineToScene;
|
|
178
195
|
private animateFrame;
|
|
179
196
|
}
|
|
197
|
+
interface LoadTexturesOptions {
|
|
198
|
+
/**
|
|
199
|
+
* Whether to enable animated textures.
|
|
200
|
+
* When true (default), uses ImageDecoder API for animated WebP if available.
|
|
201
|
+
* When false, always loads static textures for better performance.
|
|
202
|
+
* Use `areAnimationsSupported()` to check if animations are supported on the current browser.
|
|
203
|
+
*/
|
|
204
|
+
animationsEnabled?: boolean;
|
|
205
|
+
}
|
|
180
206
|
/**
|
|
181
207
|
* Loads all required textures for the chart preview.
|
|
182
208
|
* @param instrumentType The type of instrument to load textures for
|
|
209
|
+
* @param options Optional configuration for texture loading
|
|
183
210
|
* @returns An object containing highway, strikeline, note textures, and animated texture manager
|
|
184
211
|
*/
|
|
185
|
-
declare function loadTextures(instrumentType: InstrumentType): Promise<{
|
|
212
|
+
declare function loadTextures(instrumentType: InstrumentType, options?: LoadTexturesOptions): Promise<{
|
|
186
213
|
highwayTexture: THREE.Texture;
|
|
187
214
|
strikelineTexture: THREE.Texture;
|
|
188
215
|
noteTextures: Map<ExtendedNoteType, Map<number, THREE.Texture>>;
|
|
@@ -203,6 +230,11 @@ interface LoadFromUrlConfig {
|
|
|
203
230
|
initialSeekPercent?: number;
|
|
204
231
|
/** AbortSignal to cancel the fetch operation */
|
|
205
232
|
signal?: AbortSignal;
|
|
233
|
+
/**
|
|
234
|
+
* Whether to enable animated textures. Defaults to true.
|
|
235
|
+
* Set to false for better performance or when animations aren't supported.
|
|
236
|
+
*/
|
|
237
|
+
animationsEnabled?: boolean;
|
|
206
238
|
}
|
|
207
239
|
/**
|
|
208
240
|
* Configuration for loading a chart from raw .sng file data.
|
|
@@ -216,6 +248,11 @@ interface LoadFromSngFileConfig {
|
|
|
216
248
|
difficulty: Difficulty;
|
|
217
249
|
/** Initial seek position as a percentage (0-1). Defaults to 0. */
|
|
218
250
|
initialSeekPercent?: number;
|
|
251
|
+
/**
|
|
252
|
+
* Whether to enable animated textures. Defaults to true.
|
|
253
|
+
* Set to false for better performance or when animations aren't supported.
|
|
254
|
+
*/
|
|
255
|
+
animationsEnabled?: boolean;
|
|
219
256
|
}
|
|
220
257
|
/**
|
|
221
258
|
* Configuration for loading a chart from individual chart and audio files.
|
|
@@ -232,6 +269,11 @@ interface LoadFromChartFilesConfig {
|
|
|
232
269
|
difficulty: Difficulty;
|
|
233
270
|
/** Initial seek position as a percentage (0-1). Defaults to 0. */
|
|
234
271
|
initialSeekPercent?: number;
|
|
272
|
+
/**
|
|
273
|
+
* Whether to enable animated textures. Defaults to true.
|
|
274
|
+
* Set to false for better performance or when animations aren't supported.
|
|
275
|
+
*/
|
|
276
|
+
animationsEnabled?: boolean;
|
|
235
277
|
}
|
|
236
278
|
/**
|
|
237
279
|
* Result of preparing chart data for playback.
|
|
@@ -292,12 +334,13 @@ declare function isVideoFile(fileName: string): boolean;
|
|
|
292
334
|
* @param instrument - The instrument to display
|
|
293
335
|
* @param difficulty - The difficulty level to display
|
|
294
336
|
* @param initialSeekPercent - Initial seek position (0-1)
|
|
337
|
+
* @param textureOptions - Options for texture loading (e.g., animations enabled)
|
|
295
338
|
* @returns Prepared chart data ready for the player
|
|
296
339
|
*/
|
|
297
340
|
declare function prepareChartData(files: {
|
|
298
341
|
fileName: string;
|
|
299
342
|
data: Uint8Array;
|
|
300
|
-
}[], instrument: Instrument, difficulty: Difficulty, initialSeekPercent?: number): Promise<PreparedChartData>;
|
|
343
|
+
}[], instrument: Instrument, difficulty: Difficulty, initialSeekPercent?: number, textureOptions?: LoadTexturesOptions): Promise<PreparedChartData>;
|
|
301
344
|
/**
|
|
302
345
|
* Fetches a .sng file from a URL.
|
|
303
346
|
*
|
|
@@ -533,4 +576,4 @@ declare global {
|
|
|
533
576
|
}
|
|
534
577
|
}
|
|
535
578
|
|
|
536
|
-
export { ChartPreview, ChartPreviewConfig, ChartPreviewEvents, ChartPreviewPlayer, ChartPreviewPlayerConfig, LoadFromChartFilesConfig, LoadFromSngFileConfig, LoadFromUrlConfig, ParsedChart, PlayerState, PreparedChartData, extractSngFile, fetchSngFile, findAudioFiles, findChartFile, isVideoFile, prepareChartData };
|
|
579
|
+
export { ChartPreview, ChartPreviewConfig, ChartPreviewEvents, ChartPreviewPlayer, ChartPreviewPlayerConfig, LoadFromChartFilesConfig, LoadFromSngFileConfig, LoadFromUrlConfig, LoadTexturesOptions, ParsedChart, PlayerState, PreparedChartData, areAnimationsSupported, extractSngFile, fetchSngFile, findAudioFiles, findChartFile, isVideoFile, prepareChartData };
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ var src_exports = {};
|
|
|
31
31
|
__export(src_exports, {
|
|
32
32
|
ChartPreview: () => ChartPreview,
|
|
33
33
|
ChartPreviewPlayer: () => ChartPreviewPlayer,
|
|
34
|
+
areAnimationsSupported: () => areAnimationsSupported,
|
|
34
35
|
extractSngFile: () => extractSngFile,
|
|
35
36
|
fetchSngFile: () => fetchSngFile,
|
|
36
37
|
findAudioFiles: () => findAudioFiles,
|
|
@@ -77,13 +78,17 @@ var NOTE_SPAN_WIDTH = 0.95;
|
|
|
77
78
|
function isImageDecoderSupported() {
|
|
78
79
|
return typeof ImageDecoder !== "undefined";
|
|
79
80
|
}
|
|
81
|
+
function areAnimationsSupported() {
|
|
82
|
+
return isImageDecoderSupported();
|
|
83
|
+
}
|
|
80
84
|
var AnimatedTexture = class {
|
|
81
85
|
constructor(width, height) {
|
|
82
|
-
this.decoder = null;
|
|
83
86
|
this.frameIndex = 0;
|
|
84
87
|
this.frameCount = 0;
|
|
85
88
|
this.lastFrameTime = 0;
|
|
86
89
|
this.frameDurations = [];
|
|
90
|
+
/** Pre-decoded frames stored as ImageBitmap for fast synchronous access */
|
|
91
|
+
this.frameCache = [];
|
|
87
92
|
this.isAnimated = false;
|
|
88
93
|
this.disposed = false;
|
|
89
94
|
this.canvas = document.createElement("canvas");
|
|
@@ -96,11 +101,11 @@ var AnimatedTexture = class {
|
|
|
96
101
|
/**
|
|
97
102
|
* Creates an AnimatedTexture from a URL.
|
|
98
103
|
* Uses ImageDecoder for animation if available, otherwise loads as static texture.
|
|
104
|
+
* Pre-decodes all frames during initialization for optimal render performance.
|
|
99
105
|
*/
|
|
100
106
|
static async create(url) {
|
|
101
107
|
if (!isImageDecoderSupported()) {
|
|
102
|
-
|
|
103
|
-
return textureLoader.loadAsync(url);
|
|
108
|
+
return loadStaticTexture(url);
|
|
104
109
|
}
|
|
105
110
|
try {
|
|
106
111
|
const response = await fetch(url);
|
|
@@ -110,8 +115,7 @@ var AnimatedTexture = class {
|
|
|
110
115
|
const contentType = response.headers.get("content-type") || "image/webp";
|
|
111
116
|
const isSupported = await ImageDecoder.isTypeSupported(contentType);
|
|
112
117
|
if (!isSupported) {
|
|
113
|
-
|
|
114
|
-
return textureLoader.loadAsync(url);
|
|
118
|
+
return loadStaticTexture(url);
|
|
115
119
|
}
|
|
116
120
|
const decoder = new ImageDecoder({
|
|
117
121
|
data: response.body,
|
|
@@ -142,40 +146,51 @@ var AnimatedTexture = class {
|
|
|
142
146
|
firstFrame.displayWidth,
|
|
143
147
|
firstFrame.displayHeight
|
|
144
148
|
);
|
|
145
|
-
animTexture.ctx.drawImage(firstFrame, 0, 0);
|
|
146
|
-
animTexture.texture.needsUpdate = true;
|
|
147
|
-
animTexture.decoder = decoder;
|
|
148
|
-
animTexture.frameCount = frameCount;
|
|
149
|
-
animTexture.isAnimated = true;
|
|
150
149
|
animTexture.frameDurations = [];
|
|
150
|
+
animTexture.frameCache = [];
|
|
151
151
|
for (let i = 0; i < frameCount; i++) {
|
|
152
152
|
try {
|
|
153
153
|
const frameResult = await decoder.decode({ frameIndex: i });
|
|
154
|
-
const
|
|
154
|
+
const videoFrame = frameResult.image;
|
|
155
|
+
const bitmap = await createImageBitmap(videoFrame);
|
|
156
|
+
animTexture.frameCache.push(bitmap);
|
|
157
|
+
const durationMs = (videoFrame.duration ?? 1e5) / 1e3;
|
|
155
158
|
animTexture.frameDurations.push(durationMs);
|
|
156
|
-
|
|
159
|
+
videoFrame.close();
|
|
157
160
|
} catch {
|
|
158
161
|
animTexture.frameDurations.push(100);
|
|
162
|
+
if (animTexture.frameCache.length > 0) {
|
|
163
|
+
animTexture.frameCache.push(
|
|
164
|
+
animTexture.frameCache[animTexture.frameCache.length - 1]
|
|
165
|
+
);
|
|
166
|
+
}
|
|
159
167
|
}
|
|
160
168
|
}
|
|
161
|
-
|
|
169
|
+
if (animTexture.frameCache.length > 0) {
|
|
170
|
+
animTexture.ctx.drawImage(animTexture.frameCache[0], 0, 0);
|
|
171
|
+
animTexture.texture.needsUpdate = true;
|
|
172
|
+
}
|
|
173
|
+
animTexture.frameCount = animTexture.frameCache.length;
|
|
174
|
+
animTexture.isAnimated = animTexture.frameCount > 1;
|
|
162
175
|
animTexture.lastFrameTime = performance.now();
|
|
176
|
+
firstFrame.close();
|
|
177
|
+
decoder.close();
|
|
163
178
|
return animTexture;
|
|
164
179
|
} catch (error) {
|
|
165
180
|
console.warn(
|
|
166
181
|
"Failed to load animated texture, falling back to static:",
|
|
167
182
|
error
|
|
168
183
|
);
|
|
169
|
-
|
|
170
|
-
return textureLoader.loadAsync(url);
|
|
184
|
+
return loadStaticTexture(url);
|
|
171
185
|
}
|
|
172
186
|
}
|
|
173
187
|
/**
|
|
174
188
|
* Updates the texture to the current animation frame based on elapsed time.
|
|
175
|
-
*
|
|
189
|
+
* This method is SYNCHRONOUS for optimal render performance - all frames
|
|
190
|
+
* are pre-decoded during initialization.
|
|
176
191
|
*/
|
|
177
|
-
|
|
178
|
-
if (!this.isAnimated ||
|
|
192
|
+
tick() {
|
|
193
|
+
if (!this.isAnimated || this.disposed || this.frameCache.length === 0) {
|
|
179
194
|
return;
|
|
180
195
|
}
|
|
181
196
|
const now = performance.now();
|
|
@@ -184,15 +199,11 @@ var AnimatedTexture = class {
|
|
|
184
199
|
if (elapsed >= currentFrameDuration) {
|
|
185
200
|
this.frameIndex = (this.frameIndex + 1) % this.frameCount;
|
|
186
201
|
this.lastFrameTime = now;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
frameIndex: this.frameIndex
|
|
190
|
-
});
|
|
202
|
+
const frame = this.frameCache[this.frameIndex];
|
|
203
|
+
if (frame) {
|
|
191
204
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
192
|
-
this.ctx.drawImage(
|
|
205
|
+
this.ctx.drawImage(frame, 0, 0);
|
|
193
206
|
this.texture.needsUpdate = true;
|
|
194
|
-
result.image.close();
|
|
195
|
-
} catch {
|
|
196
207
|
}
|
|
197
208
|
}
|
|
198
209
|
}
|
|
@@ -201,13 +212,35 @@ var AnimatedTexture = class {
|
|
|
201
212
|
*/
|
|
202
213
|
dispose() {
|
|
203
214
|
this.disposed = true;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
this.decoder = null;
|
|
215
|
+
for (const bitmap of this.frameCache) {
|
|
216
|
+
bitmap.close();
|
|
207
217
|
}
|
|
218
|
+
this.frameCache = [];
|
|
208
219
|
this.texture.dispose();
|
|
209
220
|
}
|
|
210
221
|
};
|
|
222
|
+
async function loadStaticTexture(url) {
|
|
223
|
+
const textureLoader = new THREE.TextureLoader();
|
|
224
|
+
try {
|
|
225
|
+
const texture = await textureLoader.loadAsync(url);
|
|
226
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
227
|
+
return texture;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.warn(`Failed to load static texture from ${url}:`, error);
|
|
230
|
+
return createPlaceholderTexture();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function createPlaceholderTexture() {
|
|
234
|
+
const canvas = document.createElement("canvas");
|
|
235
|
+
canvas.width = 32;
|
|
236
|
+
canvas.height = 32;
|
|
237
|
+
const ctx = canvas.getContext("2d");
|
|
238
|
+
ctx.fillStyle = "#FF00FF";
|
|
239
|
+
ctx.fillRect(0, 0, 32, 32);
|
|
240
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
241
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
242
|
+
return texture;
|
|
243
|
+
}
|
|
211
244
|
var AnimatedTextureManager = class {
|
|
212
245
|
constructor() {
|
|
213
246
|
this.animatedTextures = [];
|
|
@@ -217,8 +250,13 @@ var AnimatedTextureManager = class {
|
|
|
217
250
|
this.animatedTextures.push(texture);
|
|
218
251
|
}
|
|
219
252
|
}
|
|
220
|
-
|
|
221
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Updates all animated textures. This is now SYNCHRONOUS for optimal performance.
|
|
255
|
+
*/
|
|
256
|
+
tick() {
|
|
257
|
+
for (const texture of this.animatedTextures) {
|
|
258
|
+
texture.tick();
|
|
259
|
+
}
|
|
222
260
|
}
|
|
223
261
|
dispose() {
|
|
224
262
|
for (const texture of this.animatedTextures) {
|
|
@@ -371,6 +409,17 @@ var _ChartPreview = class {
|
|
|
371
409
|
get chartEndTimeMs() {
|
|
372
410
|
return this.audioManager.chartEndTimeMs;
|
|
373
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Manually triggers a resize of the camera and renderer.
|
|
414
|
+
* Call this when the container size changes (e.g., entering/exiting fullscreen).
|
|
415
|
+
*/
|
|
416
|
+
resize() {
|
|
417
|
+
this.camera.triggerResize();
|
|
418
|
+
this.renderer.triggerResize();
|
|
419
|
+
if (this.paused) {
|
|
420
|
+
this.animateFrame(false);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
374
423
|
/**
|
|
375
424
|
* Cleans up all resources. Call this when discarding the preview.
|
|
376
425
|
* The preview should not be used after calling dispose().
|
|
@@ -458,6 +507,10 @@ var ChartCamera = class extends THREE.PerspectiveCamera {
|
|
|
458
507
|
this.aspect = width / height;
|
|
459
508
|
this.updateProjectionMatrix();
|
|
460
509
|
}
|
|
510
|
+
/** Manually trigger a resize update */
|
|
511
|
+
triggerResize() {
|
|
512
|
+
this.onResize();
|
|
513
|
+
}
|
|
461
514
|
dispose() {
|
|
462
515
|
window.removeEventListener("resize", this.resizeListener);
|
|
463
516
|
this.clear();
|
|
@@ -480,6 +533,10 @@ var ChartRenderer = class extends THREE.WebGLRenderer {
|
|
|
480
533
|
const height = this.divContainer.offsetHeight ?? window.innerHeight;
|
|
481
534
|
this.setSize(width, height);
|
|
482
535
|
}
|
|
536
|
+
/** Manually trigger a resize update */
|
|
537
|
+
triggerResize() {
|
|
538
|
+
this.onResize();
|
|
539
|
+
}
|
|
483
540
|
dispose() {
|
|
484
541
|
window.removeEventListener("resize", this.resizeListener);
|
|
485
542
|
super.dispose();
|
|
@@ -821,11 +878,16 @@ var NotesManager = class {
|
|
|
821
878
|
}
|
|
822
879
|
for (let i = maxNoteEventIndex + 1; this.noteEvents[i] && this.noteEvents[i].msTime < renderEndTimeMs; i++) {
|
|
823
880
|
const note = this.noteEvents[i];
|
|
881
|
+
const material = this.noteMaterials.get(note.type)?.get(note.flags);
|
|
882
|
+
if (!material) {
|
|
883
|
+
console.warn(
|
|
884
|
+
`Missing texture for note type ${note.type} with flags ${note.flags}`
|
|
885
|
+
);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
824
888
|
const noteGroup = new THREE.Group();
|
|
825
889
|
const scale = note.type === import_scan_chart.noteTypes.kick ? 0.045 : note.type === import_scan_chart.noteTypes.open && this.instrumentType === import_scan_chart.instrumentTypes.sixFret ? 0.04 : SCALE;
|
|
826
|
-
const sprite = new THREE.Sprite(
|
|
827
|
-
this.noteMaterials.get(note.type).get(note.flags)
|
|
828
|
-
);
|
|
890
|
+
const sprite = new THREE.Sprite(material);
|
|
829
891
|
noteGroup.add(sprite);
|
|
830
892
|
sprite.center = new THREE.Vector2(
|
|
831
893
|
note.type === import_scan_chart.noteTypes.kick ? 0.62 : 0.5,
|
|
@@ -902,12 +964,16 @@ var EventSequence = class {
|
|
|
902
964
|
return earliestActiveEventIndex === null ? this.lastPrecedingEventIndex + 1 : earliestActiveEventIndex;
|
|
903
965
|
}
|
|
904
966
|
};
|
|
905
|
-
async function loadTextures(instrumentType) {
|
|
967
|
+
async function loadTextures(instrumentType, options = {}) {
|
|
968
|
+
const { animationsEnabled = true } = options;
|
|
906
969
|
const textureLoader = new THREE.TextureLoader();
|
|
907
970
|
const animatedTextureManager = new AnimatedTextureManager();
|
|
908
971
|
const load = (path) => textureLoader.loadAsync("https://static.enchor.us/" + path);
|
|
909
972
|
const loadAnimated = async (path) => {
|
|
910
973
|
const url = `https://static.enchor.us/preview-${path}.webp`;
|
|
974
|
+
if (!animationsEnabled) {
|
|
975
|
+
return loadStaticTexture(url);
|
|
976
|
+
}
|
|
911
977
|
const result = await AnimatedTexture.create(url);
|
|
912
978
|
if (result instanceof AnimatedTexture) {
|
|
913
979
|
animatedTextureManager.register(result);
|
|
@@ -1349,7 +1415,7 @@ function isVideoFile(fileName) {
|
|
|
1349
1415
|
const ext = fileName.toLowerCase().slice(fileName.lastIndexOf("."));
|
|
1350
1416
|
return videoExtensions.includes(ext);
|
|
1351
1417
|
}
|
|
1352
|
-
async function prepareChartData(files, instrument, difficulty, initialSeekPercent = 0) {
|
|
1418
|
+
async function prepareChartData(files, instrument, difficulty, initialSeekPercent = 0, textureOptions = {}) {
|
|
1353
1419
|
const filteredFiles = files.filter((f) => !isVideoFile(f.fileName));
|
|
1354
1420
|
const scannedChart = (0, import_scan_chart2.scanChartFolder)(filteredFiles);
|
|
1355
1421
|
if (!scannedChart.playable || !scannedChart.notesData) {
|
|
@@ -1384,7 +1450,10 @@ async function prepareChartData(files, instrument, difficulty, initialSeekPercen
|
|
|
1384
1450
|
}
|
|
1385
1451
|
const audioFiles = findAudioFiles(filteredFiles);
|
|
1386
1452
|
const instrumentType = (0, import_scan_chart2.getInstrumentType)(instrument);
|
|
1387
|
-
const textures = await ChartPreview.loadTextures(
|
|
1453
|
+
const textures = await ChartPreview.loadTextures(
|
|
1454
|
+
instrumentType,
|
|
1455
|
+
textureOptions
|
|
1456
|
+
);
|
|
1388
1457
|
const lastNote = parsedChart.trackData.flatMap((t) => t.noteEventGroups.flat()).reduce((max, note) => Math.max(max, note.msTime + note.msLength), 0);
|
|
1389
1458
|
const audioLengthMs = scannedChart.song_length || lastNote + 5e3;
|
|
1390
1459
|
const startDelayMs = (scannedChart.delay || 0) + (scannedChart.chart_offset || 0) * 1e3;
|
|
@@ -1705,10 +1774,18 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1705
1774
|
}
|
|
1706
1775
|
};
|
|
1707
1776
|
this.handleFullscreenChange = () => {
|
|
1708
|
-
|
|
1777
|
+
const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement;
|
|
1778
|
+
this._isFullscreen = fullscreenElement !== null && fullscreenElement !== void 0;
|
|
1709
1779
|
this.container.classList.toggle("fullscreen", this._isFullscreen);
|
|
1710
1780
|
this.fullscreenBtn.innerHTML = this._isFullscreen ? ICONS.fullscreenExit : ICONS.fullscreen;
|
|
1711
|
-
this.fullscreenBtn.title = this._isFullscreen ? "Exit fullscreen" : "
|
|
1781
|
+
this.fullscreenBtn.title = this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen";
|
|
1782
|
+
if (this.chartPreview) {
|
|
1783
|
+
requestAnimationFrame(() => {
|
|
1784
|
+
requestAnimationFrame(() => {
|
|
1785
|
+
this.chartPreview?.resize();
|
|
1786
|
+
});
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1712
1789
|
};
|
|
1713
1790
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
1714
1791
|
this.render();
|
|
@@ -1733,6 +1810,10 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1733
1810
|
connectedCallback() {
|
|
1734
1811
|
this.setAttribute("tabindex", "0");
|
|
1735
1812
|
document.addEventListener("fullscreenchange", this.handleFullscreenChange);
|
|
1813
|
+
document.addEventListener(
|
|
1814
|
+
"webkitfullscreenchange",
|
|
1815
|
+
this.handleFullscreenChange
|
|
1816
|
+
);
|
|
1736
1817
|
}
|
|
1737
1818
|
disconnectedCallback() {
|
|
1738
1819
|
this.dispose();
|
|
@@ -1740,6 +1821,10 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1740
1821
|
"fullscreenchange",
|
|
1741
1822
|
this.handleFullscreenChange
|
|
1742
1823
|
);
|
|
1824
|
+
document.removeEventListener(
|
|
1825
|
+
"webkitfullscreenchange",
|
|
1826
|
+
this.handleFullscreenChange
|
|
1827
|
+
);
|
|
1743
1828
|
this.removeEventListener("keydown", this.handleKeydown);
|
|
1744
1829
|
}
|
|
1745
1830
|
render() {
|
|
@@ -1820,7 +1905,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1820
1905
|
this.onSeekEnd();
|
|
1821
1906
|
}
|
|
1822
1907
|
});
|
|
1823
|
-
this.fullscreenBtn.addEventListener("click", () => {
|
|
1908
|
+
this.fullscreenBtn.addEventListener("click", (e) => {
|
|
1909
|
+
e.stopPropagation();
|
|
1824
1910
|
this.toggleFullscreen();
|
|
1825
1911
|
});
|
|
1826
1912
|
this.addEventListener("keydown", this.handleKeydown);
|
|
@@ -2133,7 +2219,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
2133
2219
|
files,
|
|
2134
2220
|
config.instrument,
|
|
2135
2221
|
config.difficulty,
|
|
2136
|
-
config.initialSeekPercent
|
|
2222
|
+
config.initialSeekPercent,
|
|
2223
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
2137
2224
|
);
|
|
2138
2225
|
await this.loadChart({
|
|
2139
2226
|
parsedChart: preparedData.parsedChart,
|
|
@@ -2175,7 +2262,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
2175
2262
|
files,
|
|
2176
2263
|
config.instrument,
|
|
2177
2264
|
config.difficulty,
|
|
2178
|
-
config.initialSeekPercent
|
|
2265
|
+
config.initialSeekPercent,
|
|
2266
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
2179
2267
|
);
|
|
2180
2268
|
await this.loadChart({
|
|
2181
2269
|
parsedChart: preparedData.parsedChart,
|
|
@@ -2214,7 +2302,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
2214
2302
|
config.files,
|
|
2215
2303
|
config.instrument,
|
|
2216
2304
|
config.difficulty,
|
|
2217
|
-
config.initialSeekPercent
|
|
2305
|
+
config.initialSeekPercent,
|
|
2306
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
2218
2307
|
);
|
|
2219
2308
|
await this.loadChart({
|
|
2220
2309
|
parsedChart: preparedData.parsedChart,
|
|
@@ -2314,12 +2403,28 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
2314
2403
|
* Toggle fullscreen mode
|
|
2315
2404
|
*/
|
|
2316
2405
|
async toggleFullscreen() {
|
|
2317
|
-
|
|
2406
|
+
const doc = document;
|
|
2407
|
+
const container = this.container;
|
|
2408
|
+
const isFullscreenEnabled = doc.fullscreenEnabled || doc.webkitFullscreenEnabled;
|
|
2409
|
+
if (!isFullscreenEnabled)
|
|
2318
2410
|
return;
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2411
|
+
const fullscreenElement = doc.fullscreenElement || doc.webkitFullscreenElement;
|
|
2412
|
+
try {
|
|
2413
|
+
if (fullscreenElement) {
|
|
2414
|
+
if (doc.exitFullscreen) {
|
|
2415
|
+
await doc.exitFullscreen();
|
|
2416
|
+
} else if (doc.webkitExitFullscreen) {
|
|
2417
|
+
doc.webkitExitFullscreen();
|
|
2418
|
+
}
|
|
2419
|
+
} else {
|
|
2420
|
+
if (container.requestFullscreen) {
|
|
2421
|
+
await container.requestFullscreen();
|
|
2422
|
+
} else if (container.webkitRequestFullscreen) {
|
|
2423
|
+
container.webkitRequestFullscreen();
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
} catch (error) {
|
|
2427
|
+
console.warn("Fullscreen toggle failed:", error);
|
|
2323
2428
|
}
|
|
2324
2429
|
}
|
|
2325
2430
|
/**
|
|
@@ -2352,6 +2457,7 @@ var import_scan_chart3 = require("scan-chart");
|
|
|
2352
2457
|
0 && (module.exports = {
|
|
2353
2458
|
ChartPreview,
|
|
2354
2459
|
ChartPreviewPlayer,
|
|
2460
|
+
areAnimationsSupported,
|
|
2355
2461
|
extractSngFile,
|
|
2356
2462
|
fetchSngFile,
|
|
2357
2463
|
findAudioFiles,
|