chart-preview 1.1.0 → 1.2.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 +6 -0
- package/README.md +34 -4
- package/dist/index.d.ts +45 -7
- package/dist/index.js +127 -47
- package/dist/index.mjs +126 -47
- package/package.json +1 -1
- package/src/ChartPreview.ts +133 -38
- package/src/ChartPreviewPlayer.ts +65 -12
- package/src/SngLoader.ts +27 -3
- package/src/index.ts +2 -0
package/CHANGELOG.md
CHANGED
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
|
/**
|
|
@@ -177,12 +189,22 @@ declare class ChartPreview {
|
|
|
177
189
|
private addStrikelineToScene;
|
|
178
190
|
private animateFrame;
|
|
179
191
|
}
|
|
192
|
+
interface LoadTexturesOptions {
|
|
193
|
+
/**
|
|
194
|
+
* Whether to enable animated textures.
|
|
195
|
+
* When true (default), uses ImageDecoder API for animated WebP if available.
|
|
196
|
+
* When false, always loads static textures for better performance.
|
|
197
|
+
* Use `areAnimationsSupported()` to check if animations are supported on the current browser.
|
|
198
|
+
*/
|
|
199
|
+
animationsEnabled?: boolean;
|
|
200
|
+
}
|
|
180
201
|
/**
|
|
181
202
|
* Loads all required textures for the chart preview.
|
|
182
203
|
* @param instrumentType The type of instrument to load textures for
|
|
204
|
+
* @param options Optional configuration for texture loading
|
|
183
205
|
* @returns An object containing highway, strikeline, note textures, and animated texture manager
|
|
184
206
|
*/
|
|
185
|
-
declare function loadTextures(instrumentType: InstrumentType): Promise<{
|
|
207
|
+
declare function loadTextures(instrumentType: InstrumentType, options?: LoadTexturesOptions): Promise<{
|
|
186
208
|
highwayTexture: THREE.Texture;
|
|
187
209
|
strikelineTexture: THREE.Texture;
|
|
188
210
|
noteTextures: Map<ExtendedNoteType, Map<number, THREE.Texture>>;
|
|
@@ -203,6 +225,11 @@ interface LoadFromUrlConfig {
|
|
|
203
225
|
initialSeekPercent?: number;
|
|
204
226
|
/** AbortSignal to cancel the fetch operation */
|
|
205
227
|
signal?: AbortSignal;
|
|
228
|
+
/**
|
|
229
|
+
* Whether to enable animated textures. Defaults to true.
|
|
230
|
+
* Set to false for better performance or when animations aren't supported.
|
|
231
|
+
*/
|
|
232
|
+
animationsEnabled?: boolean;
|
|
206
233
|
}
|
|
207
234
|
/**
|
|
208
235
|
* Configuration for loading a chart from raw .sng file data.
|
|
@@ -216,6 +243,11 @@ interface LoadFromSngFileConfig {
|
|
|
216
243
|
difficulty: Difficulty;
|
|
217
244
|
/** Initial seek position as a percentage (0-1). Defaults to 0. */
|
|
218
245
|
initialSeekPercent?: number;
|
|
246
|
+
/**
|
|
247
|
+
* Whether to enable animated textures. Defaults to true.
|
|
248
|
+
* Set to false for better performance or when animations aren't supported.
|
|
249
|
+
*/
|
|
250
|
+
animationsEnabled?: boolean;
|
|
219
251
|
}
|
|
220
252
|
/**
|
|
221
253
|
* Configuration for loading a chart from individual chart and audio files.
|
|
@@ -232,6 +264,11 @@ interface LoadFromChartFilesConfig {
|
|
|
232
264
|
difficulty: Difficulty;
|
|
233
265
|
/** Initial seek position as a percentage (0-1). Defaults to 0. */
|
|
234
266
|
initialSeekPercent?: number;
|
|
267
|
+
/**
|
|
268
|
+
* Whether to enable animated textures. Defaults to true.
|
|
269
|
+
* Set to false for better performance or when animations aren't supported.
|
|
270
|
+
*/
|
|
271
|
+
animationsEnabled?: boolean;
|
|
235
272
|
}
|
|
236
273
|
/**
|
|
237
274
|
* Result of preparing chart data for playback.
|
|
@@ -292,12 +329,13 @@ declare function isVideoFile(fileName: string): boolean;
|
|
|
292
329
|
* @param instrument - The instrument to display
|
|
293
330
|
* @param difficulty - The difficulty level to display
|
|
294
331
|
* @param initialSeekPercent - Initial seek position (0-1)
|
|
332
|
+
* @param textureOptions - Options for texture loading (e.g., animations enabled)
|
|
295
333
|
* @returns Prepared chart data ready for the player
|
|
296
334
|
*/
|
|
297
335
|
declare function prepareChartData(files: {
|
|
298
336
|
fileName: string;
|
|
299
337
|
data: Uint8Array;
|
|
300
|
-
}[], instrument: Instrument, difficulty: Difficulty, initialSeekPercent?: number): Promise<PreparedChartData>;
|
|
338
|
+
}[], instrument: Instrument, difficulty: Difficulty, initialSeekPercent?: number, textureOptions?: LoadTexturesOptions): Promise<PreparedChartData>;
|
|
301
339
|
/**
|
|
302
340
|
* Fetches a .sng file from a URL.
|
|
303
341
|
*
|
|
@@ -533,4 +571,4 @@ declare global {
|
|
|
533
571
|
}
|
|
534
572
|
}
|
|
535
573
|
|
|
536
|
-
export { ChartPreview, ChartPreviewConfig, ChartPreviewEvents, ChartPreviewPlayer, ChartPreviewPlayerConfig, LoadFromChartFilesConfig, LoadFromSngFileConfig, LoadFromUrlConfig, ParsedChart, PlayerState, PreparedChartData, extractSngFile, fetchSngFile, findAudioFiles, findChartFile, isVideoFile, prepareChartData };
|
|
574
|
+
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) {
|
|
@@ -821,11 +859,16 @@ var NotesManager = class {
|
|
|
821
859
|
}
|
|
822
860
|
for (let i = maxNoteEventIndex + 1; this.noteEvents[i] && this.noteEvents[i].msTime < renderEndTimeMs; i++) {
|
|
823
861
|
const note = this.noteEvents[i];
|
|
862
|
+
const material = this.noteMaterials.get(note.type)?.get(note.flags);
|
|
863
|
+
if (!material) {
|
|
864
|
+
console.warn(
|
|
865
|
+
`Missing texture for note type ${note.type} with flags ${note.flags}`
|
|
866
|
+
);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
824
869
|
const noteGroup = new THREE.Group();
|
|
825
870
|
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
|
-
);
|
|
871
|
+
const sprite = new THREE.Sprite(material);
|
|
829
872
|
noteGroup.add(sprite);
|
|
830
873
|
sprite.center = new THREE.Vector2(
|
|
831
874
|
note.type === import_scan_chart.noteTypes.kick ? 0.62 : 0.5,
|
|
@@ -902,12 +945,16 @@ var EventSequence = class {
|
|
|
902
945
|
return earliestActiveEventIndex === null ? this.lastPrecedingEventIndex + 1 : earliestActiveEventIndex;
|
|
903
946
|
}
|
|
904
947
|
};
|
|
905
|
-
async function loadTextures(instrumentType) {
|
|
948
|
+
async function loadTextures(instrumentType, options = {}) {
|
|
949
|
+
const { animationsEnabled = true } = options;
|
|
906
950
|
const textureLoader = new THREE.TextureLoader();
|
|
907
951
|
const animatedTextureManager = new AnimatedTextureManager();
|
|
908
952
|
const load = (path) => textureLoader.loadAsync("https://static.enchor.us/" + path);
|
|
909
953
|
const loadAnimated = async (path) => {
|
|
910
954
|
const url = `https://static.enchor.us/preview-${path}.webp`;
|
|
955
|
+
if (!animationsEnabled) {
|
|
956
|
+
return loadStaticTexture(url);
|
|
957
|
+
}
|
|
911
958
|
const result = await AnimatedTexture.create(url);
|
|
912
959
|
if (result instanceof AnimatedTexture) {
|
|
913
960
|
animatedTextureManager.register(result);
|
|
@@ -1349,7 +1396,7 @@ function isVideoFile(fileName) {
|
|
|
1349
1396
|
const ext = fileName.toLowerCase().slice(fileName.lastIndexOf("."));
|
|
1350
1397
|
return videoExtensions.includes(ext);
|
|
1351
1398
|
}
|
|
1352
|
-
async function prepareChartData(files, instrument, difficulty, initialSeekPercent = 0) {
|
|
1399
|
+
async function prepareChartData(files, instrument, difficulty, initialSeekPercent = 0, textureOptions = {}) {
|
|
1353
1400
|
const filteredFiles = files.filter((f) => !isVideoFile(f.fileName));
|
|
1354
1401
|
const scannedChart = (0, import_scan_chart2.scanChartFolder)(filteredFiles);
|
|
1355
1402
|
if (!scannedChart.playable || !scannedChart.notesData) {
|
|
@@ -1384,7 +1431,10 @@ async function prepareChartData(files, instrument, difficulty, initialSeekPercen
|
|
|
1384
1431
|
}
|
|
1385
1432
|
const audioFiles = findAudioFiles(filteredFiles);
|
|
1386
1433
|
const instrumentType = (0, import_scan_chart2.getInstrumentType)(instrument);
|
|
1387
|
-
const textures = await ChartPreview.loadTextures(
|
|
1434
|
+
const textures = await ChartPreview.loadTextures(
|
|
1435
|
+
instrumentType,
|
|
1436
|
+
textureOptions
|
|
1437
|
+
);
|
|
1388
1438
|
const lastNote = parsedChart.trackData.flatMap((t) => t.noteEventGroups.flat()).reduce((max, note) => Math.max(max, note.msTime + note.msLength), 0);
|
|
1389
1439
|
const audioLengthMs = scannedChart.song_length || lastNote + 5e3;
|
|
1390
1440
|
const startDelayMs = (scannedChart.delay || 0) + (scannedChart.chart_offset || 0) * 1e3;
|
|
@@ -1705,10 +1755,11 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1705
1755
|
}
|
|
1706
1756
|
};
|
|
1707
1757
|
this.handleFullscreenChange = () => {
|
|
1708
|
-
|
|
1758
|
+
const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement;
|
|
1759
|
+
this._isFullscreen = fullscreenElement !== null && fullscreenElement !== void 0;
|
|
1709
1760
|
this.container.classList.toggle("fullscreen", this._isFullscreen);
|
|
1710
1761
|
this.fullscreenBtn.innerHTML = this._isFullscreen ? ICONS.fullscreenExit : ICONS.fullscreen;
|
|
1711
|
-
this.fullscreenBtn.title = this._isFullscreen ? "Exit fullscreen" : "
|
|
1762
|
+
this.fullscreenBtn.title = this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen";
|
|
1712
1763
|
};
|
|
1713
1764
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
1714
1765
|
this.render();
|
|
@@ -1733,6 +1784,10 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1733
1784
|
connectedCallback() {
|
|
1734
1785
|
this.setAttribute("tabindex", "0");
|
|
1735
1786
|
document.addEventListener("fullscreenchange", this.handleFullscreenChange);
|
|
1787
|
+
document.addEventListener(
|
|
1788
|
+
"webkitfullscreenchange",
|
|
1789
|
+
this.handleFullscreenChange
|
|
1790
|
+
);
|
|
1736
1791
|
}
|
|
1737
1792
|
disconnectedCallback() {
|
|
1738
1793
|
this.dispose();
|
|
@@ -1740,6 +1795,10 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1740
1795
|
"fullscreenchange",
|
|
1741
1796
|
this.handleFullscreenChange
|
|
1742
1797
|
);
|
|
1798
|
+
document.removeEventListener(
|
|
1799
|
+
"webkitfullscreenchange",
|
|
1800
|
+
this.handleFullscreenChange
|
|
1801
|
+
);
|
|
1743
1802
|
this.removeEventListener("keydown", this.handleKeydown);
|
|
1744
1803
|
}
|
|
1745
1804
|
render() {
|
|
@@ -1820,7 +1879,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1820
1879
|
this.onSeekEnd();
|
|
1821
1880
|
}
|
|
1822
1881
|
});
|
|
1823
|
-
this.fullscreenBtn.addEventListener("click", () => {
|
|
1882
|
+
this.fullscreenBtn.addEventListener("click", (e) => {
|
|
1883
|
+
e.stopPropagation();
|
|
1824
1884
|
this.toggleFullscreen();
|
|
1825
1885
|
});
|
|
1826
1886
|
this.addEventListener("keydown", this.handleKeydown);
|
|
@@ -2133,7 +2193,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
2133
2193
|
files,
|
|
2134
2194
|
config.instrument,
|
|
2135
2195
|
config.difficulty,
|
|
2136
|
-
config.initialSeekPercent
|
|
2196
|
+
config.initialSeekPercent,
|
|
2197
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
2137
2198
|
);
|
|
2138
2199
|
await this.loadChart({
|
|
2139
2200
|
parsedChart: preparedData.parsedChart,
|
|
@@ -2175,7 +2236,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
2175
2236
|
files,
|
|
2176
2237
|
config.instrument,
|
|
2177
2238
|
config.difficulty,
|
|
2178
|
-
config.initialSeekPercent
|
|
2239
|
+
config.initialSeekPercent,
|
|
2240
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
2179
2241
|
);
|
|
2180
2242
|
await this.loadChart({
|
|
2181
2243
|
parsedChart: preparedData.parsedChart,
|
|
@@ -2214,7 +2276,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
2214
2276
|
config.files,
|
|
2215
2277
|
config.instrument,
|
|
2216
2278
|
config.difficulty,
|
|
2217
|
-
config.initialSeekPercent
|
|
2279
|
+
config.initialSeekPercent,
|
|
2280
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
2218
2281
|
);
|
|
2219
2282
|
await this.loadChart({
|
|
2220
2283
|
parsedChart: preparedData.parsedChart,
|
|
@@ -2314,12 +2377,28 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
2314
2377
|
* Toggle fullscreen mode
|
|
2315
2378
|
*/
|
|
2316
2379
|
async toggleFullscreen() {
|
|
2317
|
-
|
|
2380
|
+
const doc = document;
|
|
2381
|
+
const container = this.container;
|
|
2382
|
+
const isFullscreenEnabled = doc.fullscreenEnabled || doc.webkitFullscreenEnabled;
|
|
2383
|
+
if (!isFullscreenEnabled)
|
|
2318
2384
|
return;
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2385
|
+
const fullscreenElement = doc.fullscreenElement || doc.webkitFullscreenElement;
|
|
2386
|
+
try {
|
|
2387
|
+
if (fullscreenElement) {
|
|
2388
|
+
if (doc.exitFullscreen) {
|
|
2389
|
+
await doc.exitFullscreen();
|
|
2390
|
+
} else if (doc.webkitExitFullscreen) {
|
|
2391
|
+
doc.webkitExitFullscreen();
|
|
2392
|
+
}
|
|
2393
|
+
} else {
|
|
2394
|
+
if (container.requestFullscreen) {
|
|
2395
|
+
await container.requestFullscreen();
|
|
2396
|
+
} else if (container.webkitRequestFullscreen) {
|
|
2397
|
+
container.webkitRequestFullscreen();
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
console.warn("Fullscreen toggle failed:", error);
|
|
2323
2402
|
}
|
|
2324
2403
|
}
|
|
2325
2404
|
/**
|
|
@@ -2352,6 +2431,7 @@ var import_scan_chart3 = require("scan-chart");
|
|
|
2352
2431
|
0 && (module.exports = {
|
|
2353
2432
|
ChartPreview,
|
|
2354
2433
|
ChartPreviewPlayer,
|
|
2434
|
+
areAnimationsSupported,
|
|
2355
2435
|
extractSngFile,
|
|
2356
2436
|
fetchSngFile,
|
|
2357
2437
|
findAudioFiles,
|