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/src/ChartPreview.ts
CHANGED
|
@@ -33,19 +33,29 @@ function isImageDecoderSupported(): boolean {
|
|
|
33
33
|
return typeof ImageDecoder !== "undefined";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Check if animations are supported on this browser.
|
|
38
|
+
* Use this to conditionally enable/disable animation features.
|
|
39
|
+
*/
|
|
40
|
+
export function areAnimationsSupported(): boolean {
|
|
41
|
+
return isImageDecoderSupported();
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
/**
|
|
37
45
|
* Manages an animated WebP texture using the ImageDecoder API.
|
|
46
|
+
* Pre-decodes all frames during initialization for optimal performance.
|
|
38
47
|
* Falls back to a static texture if ImageDecoder is not supported.
|
|
39
48
|
*/
|
|
40
49
|
class AnimatedTexture {
|
|
41
50
|
private canvas: HTMLCanvasElement;
|
|
42
51
|
private ctx: CanvasRenderingContext2D;
|
|
43
52
|
public texture: THREE.CanvasTexture;
|
|
44
|
-
private decoder: ImageDecoder | null = null;
|
|
45
53
|
private frameIndex = 0;
|
|
46
54
|
private frameCount = 0;
|
|
47
55
|
private lastFrameTime = 0;
|
|
48
56
|
private frameDurations: number[] = [];
|
|
57
|
+
/** Pre-decoded frames stored as ImageBitmap for fast synchronous access */
|
|
58
|
+
private frameCache: ImageBitmap[] = [];
|
|
49
59
|
private isAnimated = false;
|
|
50
60
|
private disposed = false;
|
|
51
61
|
|
|
@@ -61,12 +71,12 @@ class AnimatedTexture {
|
|
|
61
71
|
/**
|
|
62
72
|
* Creates an AnimatedTexture from a URL.
|
|
63
73
|
* Uses ImageDecoder for animation if available, otherwise loads as static texture.
|
|
74
|
+
* Pre-decodes all frames during initialization for optimal render performance.
|
|
64
75
|
*/
|
|
65
76
|
static async create(url: string): Promise<AnimatedTexture | THREE.Texture> {
|
|
66
77
|
if (!isImageDecoderSupported()) {
|
|
67
78
|
// Fall back to static texture loading
|
|
68
|
-
|
|
69
|
-
return textureLoader.loadAsync(url);
|
|
79
|
+
return loadStaticTexture(url);
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
try {
|
|
@@ -80,8 +90,7 @@ class AnimatedTexture {
|
|
|
80
90
|
const isSupported = await ImageDecoder.isTypeSupported(contentType);
|
|
81
91
|
if (!isSupported) {
|
|
82
92
|
// Fall back to static texture
|
|
83
|
-
|
|
84
|
-
return textureLoader.loadAsync(url);
|
|
93
|
+
return loadStaticTexture(url);
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
const decoder = new ImageDecoder({
|
|
@@ -113,37 +122,56 @@ class AnimatedTexture {
|
|
|
113
122
|
return animTexture.texture;
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
// Animated image - decode
|
|
125
|
+
// Animated image - pre-decode ALL frames for optimal performance
|
|
117
126
|
const firstResult = await decoder.decode({ frameIndex: 0 });
|
|
118
127
|
const firstFrame = firstResult.image;
|
|
119
128
|
const animTexture = new AnimatedTexture(
|
|
120
129
|
firstFrame.displayWidth,
|
|
121
130
|
firstFrame.displayHeight
|
|
122
131
|
);
|
|
123
|
-
animTexture.ctx.drawImage(firstFrame, 0, 0);
|
|
124
|
-
animTexture.texture.needsUpdate = true;
|
|
125
132
|
|
|
126
|
-
|
|
127
|
-
animTexture.frameCount = frameCount;
|
|
128
|
-
animTexture.isAnimated = true;
|
|
129
|
-
|
|
130
|
-
// Pre-decode frame durations
|
|
133
|
+
// Pre-decode all frames into ImageBitmap cache
|
|
131
134
|
animTexture.frameDurations = [];
|
|
135
|
+
animTexture.frameCache = [];
|
|
136
|
+
|
|
132
137
|
for (let i = 0; i < frameCount; i++) {
|
|
133
138
|
try {
|
|
134
139
|
const frameResult = await decoder.decode({ frameIndex: i });
|
|
140
|
+
const videoFrame = frameResult.image;
|
|
141
|
+
|
|
142
|
+
// Create an ImageBitmap from the VideoFrame for fast synchronous access
|
|
143
|
+
const bitmap = await createImageBitmap(videoFrame);
|
|
144
|
+
animTexture.frameCache.push(bitmap);
|
|
145
|
+
|
|
135
146
|
// Duration is in microseconds, convert to milliseconds
|
|
136
|
-
const durationMs = (
|
|
147
|
+
const durationMs = (videoFrame.duration ?? 100000) / 1000;
|
|
137
148
|
animTexture.frameDurations.push(durationMs);
|
|
138
|
-
|
|
149
|
+
videoFrame.close();
|
|
139
150
|
} catch {
|
|
140
151
|
animTexture.frameDurations.push(100); // Default 100ms
|
|
152
|
+
// If frame decode fails, reuse the last successful frame or create empty bitmap
|
|
153
|
+
if (animTexture.frameCache.length > 0) {
|
|
154
|
+
animTexture.frameCache.push(
|
|
155
|
+
animTexture.frameCache[animTexture.frameCache.length - 1]
|
|
156
|
+
);
|
|
157
|
+
}
|
|
141
158
|
}
|
|
142
159
|
}
|
|
143
160
|
|
|
144
|
-
|
|
161
|
+
// Draw the first frame
|
|
162
|
+
if (animTexture.frameCache.length > 0) {
|
|
163
|
+
animTexture.ctx.drawImage(animTexture.frameCache[0], 0, 0);
|
|
164
|
+
animTexture.texture.needsUpdate = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
animTexture.frameCount = animTexture.frameCache.length;
|
|
168
|
+
animTexture.isAnimated = animTexture.frameCount > 1;
|
|
145
169
|
animTexture.lastFrameTime = performance.now();
|
|
146
170
|
|
|
171
|
+
// Close the decoder - we no longer need it since all frames are cached
|
|
172
|
+
firstFrame.close();
|
|
173
|
+
decoder.close();
|
|
174
|
+
|
|
147
175
|
return animTexture;
|
|
148
176
|
} catch (error) {
|
|
149
177
|
// Fall back to static texture on any error
|
|
@@ -151,17 +179,17 @@ class AnimatedTexture {
|
|
|
151
179
|
"Failed to load animated texture, falling back to static:",
|
|
152
180
|
error
|
|
153
181
|
);
|
|
154
|
-
|
|
155
|
-
return textureLoader.loadAsync(url);
|
|
182
|
+
return loadStaticTexture(url);
|
|
156
183
|
}
|
|
157
184
|
}
|
|
158
185
|
|
|
159
186
|
/**
|
|
160
187
|
* Updates the texture to the current animation frame based on elapsed time.
|
|
161
|
-
*
|
|
188
|
+
* This method is SYNCHRONOUS for optimal render performance - all frames
|
|
189
|
+
* are pre-decoded during initialization.
|
|
162
190
|
*/
|
|
163
|
-
|
|
164
|
-
if (!this.isAnimated ||
|
|
191
|
+
tick(): void {
|
|
192
|
+
if (!this.isAnimated || this.disposed || this.frameCache.length === 0) {
|
|
165
193
|
return;
|
|
166
194
|
}
|
|
167
195
|
|
|
@@ -173,16 +201,12 @@ class AnimatedTexture {
|
|
|
173
201
|
this.frameIndex = (this.frameIndex + 1) % this.frameCount;
|
|
174
202
|
this.lastFrameTime = now;
|
|
175
203
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
});
|
|
204
|
+
// Synchronous frame update from pre-decoded cache
|
|
205
|
+
const frame = this.frameCache[this.frameIndex];
|
|
206
|
+
if (frame) {
|
|
180
207
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
181
|
-
this.ctx.drawImage(
|
|
208
|
+
this.ctx.drawImage(frame, 0, 0);
|
|
182
209
|
this.texture.needsUpdate = true;
|
|
183
|
-
result.image.close();
|
|
184
|
-
} catch {
|
|
185
|
-
// Frame decode failed, skip this frame
|
|
186
210
|
}
|
|
187
211
|
}
|
|
188
212
|
}
|
|
@@ -192,14 +216,49 @@ class AnimatedTexture {
|
|
|
192
216
|
*/
|
|
193
217
|
dispose(): void {
|
|
194
218
|
this.disposed = true;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
219
|
+
// Close all cached ImageBitmaps
|
|
220
|
+
for (const bitmap of this.frameCache) {
|
|
221
|
+
bitmap.close();
|
|
198
222
|
}
|
|
223
|
+
this.frameCache = [];
|
|
199
224
|
this.texture.dispose();
|
|
200
225
|
}
|
|
201
226
|
}
|
|
202
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Loads a static texture from a URL with proper error handling.
|
|
230
|
+
* Creates a fallback placeholder texture if loading fails.
|
|
231
|
+
*/
|
|
232
|
+
async function loadStaticTexture(url: string): Promise<THREE.Texture> {
|
|
233
|
+
const textureLoader = new THREE.TextureLoader();
|
|
234
|
+
try {
|
|
235
|
+
const texture = await textureLoader.loadAsync(url);
|
|
236
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
237
|
+
return texture;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.warn(`Failed to load static texture from ${url}:`, error);
|
|
240
|
+
// Create a small placeholder texture so notes still render
|
|
241
|
+
return createPlaceholderTexture();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Creates a placeholder texture (magenta square) for missing textures.
|
|
247
|
+
* This ensures notes always render even if texture loading fails.
|
|
248
|
+
*/
|
|
249
|
+
function createPlaceholderTexture(): THREE.Texture {
|
|
250
|
+
const canvas = document.createElement("canvas");
|
|
251
|
+
canvas.width = 32;
|
|
252
|
+
canvas.height = 32;
|
|
253
|
+
const ctx = canvas.getContext("2d")!;
|
|
254
|
+
// Draw a magenta square to make missing textures obvious
|
|
255
|
+
ctx.fillStyle = "#FF00FF";
|
|
256
|
+
ctx.fillRect(0, 0, 32, 32);
|
|
257
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
258
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
259
|
+
return texture;
|
|
260
|
+
}
|
|
261
|
+
|
|
203
262
|
/** Collection of animated textures that need to be ticked each frame */
|
|
204
263
|
class AnimatedTextureManager {
|
|
205
264
|
private animatedTextures: AnimatedTexture[] = [];
|
|
@@ -210,8 +269,13 @@ class AnimatedTextureManager {
|
|
|
210
269
|
}
|
|
211
270
|
}
|
|
212
271
|
|
|
213
|
-
|
|
214
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Updates all animated textures. This is now SYNCHRONOUS for optimal performance.
|
|
274
|
+
*/
|
|
275
|
+
tick(): void {
|
|
276
|
+
for (const texture of this.animatedTextures) {
|
|
277
|
+
texture.tick();
|
|
278
|
+
}
|
|
215
279
|
}
|
|
216
280
|
|
|
217
281
|
dispose(): void {
|
|
@@ -445,6 +509,19 @@ export class ChartPreview {
|
|
|
445
509
|
return this.audioManager.chartEndTimeMs;
|
|
446
510
|
}
|
|
447
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Manually triggers a resize of the camera and renderer.
|
|
514
|
+
* Call this when the container size changes (e.g., entering/exiting fullscreen).
|
|
515
|
+
*/
|
|
516
|
+
resize() {
|
|
517
|
+
this.camera.triggerResize();
|
|
518
|
+
this.renderer.triggerResize();
|
|
519
|
+
// Re-render a frame to immediately show the updated size
|
|
520
|
+
if (this.paused) {
|
|
521
|
+
this.animateFrame(false);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
448
525
|
/**
|
|
449
526
|
* Cleans up all resources. Call this when discarding the preview.
|
|
450
527
|
* The preview should not be used after calling dispose().
|
|
@@ -554,6 +631,11 @@ class ChartCamera extends THREE.PerspectiveCamera {
|
|
|
554
631
|
this.updateProjectionMatrix();
|
|
555
632
|
}
|
|
556
633
|
|
|
634
|
+
/** Manually trigger a resize update */
|
|
635
|
+
triggerResize() {
|
|
636
|
+
this.onResize();
|
|
637
|
+
}
|
|
638
|
+
|
|
557
639
|
dispose() {
|
|
558
640
|
window.removeEventListener("resize", this.resizeListener);
|
|
559
641
|
this.clear();
|
|
@@ -578,6 +660,11 @@ class ChartRenderer extends THREE.WebGLRenderer {
|
|
|
578
660
|
this.setSize(width, height);
|
|
579
661
|
}
|
|
580
662
|
|
|
663
|
+
/** Manually trigger a resize update */
|
|
664
|
+
triggerResize() {
|
|
665
|
+
this.onResize();
|
|
666
|
+
}
|
|
667
|
+
|
|
581
668
|
override dispose() {
|
|
582
669
|
window.removeEventListener("resize", this.resizeListener);
|
|
583
670
|
super.dispose();
|
|
@@ -1062,6 +1149,17 @@ class NotesManager {
|
|
|
1062
1149
|
i++
|
|
1063
1150
|
) {
|
|
1064
1151
|
const note = this.noteEvents[i];
|
|
1152
|
+
|
|
1153
|
+
// Get the material for this note type and flags
|
|
1154
|
+
const material = this.noteMaterials.get(note.type)?.get(note.flags);
|
|
1155
|
+
if (!material) {
|
|
1156
|
+
// Skip notes with missing textures (shouldn't happen, but prevents crashes)
|
|
1157
|
+
console.warn(
|
|
1158
|
+
`Missing texture for note type ${note.type} with flags ${note.flags}`
|
|
1159
|
+
);
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1065
1163
|
const noteGroup = new THREE.Group();
|
|
1066
1164
|
const scale =
|
|
1067
1165
|
note.type === noteTypes.kick
|
|
@@ -1070,9 +1168,7 @@ class NotesManager {
|
|
|
1070
1168
|
this.instrumentType === instrumentTypes.sixFret
|
|
1071
1169
|
? 0.04
|
|
1072
1170
|
: SCALE;
|
|
1073
|
-
const sprite = new THREE.Sprite(
|
|
1074
|
-
this.noteMaterials.get(note.type)!.get(note.flags)!
|
|
1075
|
-
);
|
|
1171
|
+
const sprite = new THREE.Sprite(material);
|
|
1076
1172
|
noteGroup.add(sprite);
|
|
1077
1173
|
sprite.center = new THREE.Vector2(
|
|
1078
1174
|
note.type === noteTypes.kick ? 0.62 : 0.5,
|
|
@@ -1177,12 +1273,27 @@ class EventSequence<
|
|
|
1177
1273
|
}
|
|
1178
1274
|
}
|
|
1179
1275
|
|
|
1276
|
+
export interface LoadTexturesOptions {
|
|
1277
|
+
/**
|
|
1278
|
+
* Whether to enable animated textures.
|
|
1279
|
+
* When true (default), uses ImageDecoder API for animated WebP if available.
|
|
1280
|
+
* When false, always loads static textures for better performance.
|
|
1281
|
+
* Use `areAnimationsSupported()` to check if animations are supported on the current browser.
|
|
1282
|
+
*/
|
|
1283
|
+
animationsEnabled?: boolean;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1180
1286
|
/**
|
|
1181
1287
|
* Loads all required textures for the chart preview.
|
|
1182
1288
|
* @param instrumentType The type of instrument to load textures for
|
|
1289
|
+
* @param options Optional configuration for texture loading
|
|
1183
1290
|
* @returns An object containing highway, strikeline, note textures, and animated texture manager
|
|
1184
1291
|
*/
|
|
1185
|
-
async function loadTextures(
|
|
1292
|
+
async function loadTextures(
|
|
1293
|
+
instrumentType: InstrumentType,
|
|
1294
|
+
options: LoadTexturesOptions = {}
|
|
1295
|
+
) {
|
|
1296
|
+
const { animationsEnabled = true } = options;
|
|
1186
1297
|
const textureLoader = new THREE.TextureLoader();
|
|
1187
1298
|
const animatedTextureManager = new AnimatedTextureManager();
|
|
1188
1299
|
|
|
@@ -1192,9 +1303,16 @@ async function loadTextures(instrumentType: InstrumentType) {
|
|
|
1192
1303
|
/**
|
|
1193
1304
|
* Loads a texture with animation support.
|
|
1194
1305
|
* Uses ImageDecoder API for animated WebP when available, falls back to static.
|
|
1306
|
+
* If animationsEnabled is false, always loads static textures.
|
|
1195
1307
|
*/
|
|
1196
1308
|
const loadAnimated = async (path: string): Promise<THREE.Texture> => {
|
|
1197
1309
|
const url = `https://static.enchor.us/preview-${path}.webp`;
|
|
1310
|
+
|
|
1311
|
+
// If animations are disabled, load static texture directly
|
|
1312
|
+
if (!animationsEnabled) {
|
|
1313
|
+
return loadStaticTexture(url);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1198
1316
|
const result = await AnimatedTexture.create(url);
|
|
1199
1317
|
|
|
1200
1318
|
if (result instanceof AnimatedTexture) {
|
|
@@ -357,6 +357,11 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
357
357
|
connectedCallback() {
|
|
358
358
|
this.setAttribute("tabindex", "0");
|
|
359
359
|
document.addEventListener("fullscreenchange", this.handleFullscreenChange);
|
|
360
|
+
// Safari uses webkit prefix
|
|
361
|
+
document.addEventListener(
|
|
362
|
+
"webkitfullscreenchange",
|
|
363
|
+
this.handleFullscreenChange
|
|
364
|
+
);
|
|
360
365
|
}
|
|
361
366
|
|
|
362
367
|
disconnectedCallback() {
|
|
@@ -365,6 +370,10 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
365
370
|
"fullscreenchange",
|
|
366
371
|
this.handleFullscreenChange
|
|
367
372
|
);
|
|
373
|
+
document.removeEventListener(
|
|
374
|
+
"webkitfullscreenchange",
|
|
375
|
+
this.handleFullscreenChange
|
|
376
|
+
);
|
|
368
377
|
this.removeEventListener("keydown", this.handleKeydown);
|
|
369
378
|
}
|
|
370
379
|
|
|
@@ -467,8 +476,9 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
467
476
|
}
|
|
468
477
|
});
|
|
469
478
|
|
|
470
|
-
// Fullscreen button
|
|
471
|
-
this.fullscreenBtn.addEventListener("click", () => {
|
|
479
|
+
// Fullscreen button - stop propagation to prevent any parent handlers
|
|
480
|
+
this.fullscreenBtn.addEventListener("click", (e) => {
|
|
481
|
+
e.stopPropagation();
|
|
472
482
|
this.toggleFullscreen();
|
|
473
483
|
});
|
|
474
484
|
|
|
@@ -525,14 +535,32 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
525
535
|
};
|
|
526
536
|
|
|
527
537
|
private handleFullscreenChange = () => {
|
|
528
|
-
|
|
538
|
+
// Check if we're in fullscreen mode (support both standard and webkit prefix)
|
|
539
|
+
const fullscreenElement =
|
|
540
|
+
document.fullscreenElement ||
|
|
541
|
+
(document as unknown as { webkitFullscreenElement?: Element })
|
|
542
|
+
.webkitFullscreenElement;
|
|
543
|
+
this._isFullscreen =
|
|
544
|
+
fullscreenElement !== null && fullscreenElement !== undefined;
|
|
529
545
|
this.container.classList.toggle("fullscreen", this._isFullscreen);
|
|
530
546
|
this.fullscreenBtn.innerHTML = this._isFullscreen
|
|
531
547
|
? ICONS.fullscreenExit
|
|
532
548
|
: ICONS.fullscreen;
|
|
533
549
|
this.fullscreenBtn.title = this._isFullscreen
|
|
534
550
|
? "Exit fullscreen"
|
|
535
|
-
: "
|
|
551
|
+
: "Enter fullscreen";
|
|
552
|
+
|
|
553
|
+
// Trigger resize on the chart preview after a short delay
|
|
554
|
+
// to ensure the container dimensions have updated
|
|
555
|
+
if (this.chartPreview) {
|
|
556
|
+
// Use requestAnimationFrame to ensure DOM has updated
|
|
557
|
+
requestAnimationFrame(() => {
|
|
558
|
+
// Double RAF to ensure layout has been recalculated
|
|
559
|
+
requestAnimationFrame(() => {
|
|
560
|
+
this.chartPreview?.resize();
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
}
|
|
536
564
|
};
|
|
537
565
|
|
|
538
566
|
private onSeekStart() {
|
|
@@ -900,7 +928,8 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
900
928
|
files,
|
|
901
929
|
config.instrument,
|
|
902
930
|
config.difficulty,
|
|
903
|
-
config.initialSeekPercent
|
|
931
|
+
config.initialSeekPercent,
|
|
932
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
904
933
|
);
|
|
905
934
|
|
|
906
935
|
// Load the chart
|
|
@@ -948,7 +977,8 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
948
977
|
files,
|
|
949
978
|
config.instrument,
|
|
950
979
|
config.difficulty,
|
|
951
|
-
config.initialSeekPercent
|
|
980
|
+
config.initialSeekPercent,
|
|
981
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
952
982
|
);
|
|
953
983
|
|
|
954
984
|
// Load the chart
|
|
@@ -992,7 +1022,8 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
992
1022
|
config.files,
|
|
993
1023
|
config.instrument,
|
|
994
1024
|
config.difficulty,
|
|
995
|
-
config.initialSeekPercent
|
|
1025
|
+
config.initialSeekPercent,
|
|
1026
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
996
1027
|
);
|
|
997
1028
|
|
|
998
1029
|
// Load the chart
|
|
@@ -1105,12 +1136,46 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
1105
1136
|
* Toggle fullscreen mode
|
|
1106
1137
|
*/
|
|
1107
1138
|
async toggleFullscreen(): Promise<void> {
|
|
1108
|
-
|
|
1139
|
+
// Check for fullscreen support (standard and webkit)
|
|
1140
|
+
const doc = document as unknown as {
|
|
1141
|
+
fullscreenEnabled?: boolean;
|
|
1142
|
+
webkitFullscreenEnabled?: boolean;
|
|
1143
|
+
fullscreenElement?: Element;
|
|
1144
|
+
webkitFullscreenElement?: Element;
|
|
1145
|
+
exitFullscreen?: () => Promise<void>;
|
|
1146
|
+
webkitExitFullscreen?: () => void;
|
|
1147
|
+
};
|
|
1148
|
+
const container = this.container as unknown as {
|
|
1149
|
+
requestFullscreen?: () => Promise<void>;
|
|
1150
|
+
webkitRequestFullscreen?: () => void;
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
const isFullscreenEnabled =
|
|
1154
|
+
doc.fullscreenEnabled || doc.webkitFullscreenEnabled;
|
|
1155
|
+
if (!isFullscreenEnabled) return;
|
|
1156
|
+
|
|
1157
|
+
const fullscreenElement =
|
|
1158
|
+
doc.fullscreenElement || doc.webkitFullscreenElement;
|
|
1109
1159
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1160
|
+
try {
|
|
1161
|
+
if (fullscreenElement) {
|
|
1162
|
+
// Exit fullscreen
|
|
1163
|
+
if (doc.exitFullscreen) {
|
|
1164
|
+
await doc.exitFullscreen();
|
|
1165
|
+
} else if (doc.webkitExitFullscreen) {
|
|
1166
|
+
doc.webkitExitFullscreen();
|
|
1167
|
+
}
|
|
1168
|
+
} else {
|
|
1169
|
+
// Enter fullscreen
|
|
1170
|
+
if (container.requestFullscreen) {
|
|
1171
|
+
await container.requestFullscreen();
|
|
1172
|
+
} else if (container.webkitRequestFullscreen) {
|
|
1173
|
+
container.webkitRequestFullscreen();
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
// Fullscreen request can fail due to user gesture requirements or other reasons
|
|
1178
|
+
console.warn("Fullscreen toggle failed:", error);
|
|
1114
1179
|
}
|
|
1115
1180
|
}
|
|
1116
1181
|
|
package/src/SngLoader.ts
CHANGED
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
type Instrument,
|
|
8
8
|
type ScannedChart,
|
|
9
9
|
} from "scan-chart";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ChartPreview,
|
|
12
|
+
type LoadTexturesOptions,
|
|
13
|
+
type ParsedChart,
|
|
14
|
+
} from "./ChartPreview";
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Audio file extensions supported by the chart preview.
|
|
@@ -33,6 +37,11 @@ export interface LoadFromUrlConfig {
|
|
|
33
37
|
initialSeekPercent?: number;
|
|
34
38
|
/** AbortSignal to cancel the fetch operation */
|
|
35
39
|
signal?: AbortSignal;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to enable animated textures. Defaults to true.
|
|
42
|
+
* Set to false for better performance or when animations aren't supported.
|
|
43
|
+
*/
|
|
44
|
+
animationsEnabled?: boolean;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/**
|
|
@@ -47,6 +56,11 @@ export interface LoadFromSngFileConfig {
|
|
|
47
56
|
difficulty: Difficulty;
|
|
48
57
|
/** Initial seek position as a percentage (0-1). Defaults to 0. */
|
|
49
58
|
initialSeekPercent?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Whether to enable animated textures. Defaults to true.
|
|
61
|
+
* Set to false for better performance or when animations aren't supported.
|
|
62
|
+
*/
|
|
63
|
+
animationsEnabled?: boolean;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
/**
|
|
@@ -61,6 +75,11 @@ export interface LoadFromChartFilesConfig {
|
|
|
61
75
|
difficulty: Difficulty;
|
|
62
76
|
/** Initial seek position as a percentage (0-1). Defaults to 0. */
|
|
63
77
|
initialSeekPercent?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Whether to enable animated textures. Defaults to true.
|
|
80
|
+
* Set to false for better performance or when animations aren't supported.
|
|
81
|
+
*/
|
|
82
|
+
animationsEnabled?: boolean;
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
/**
|
|
@@ -218,13 +237,15 @@ export function isVideoFile(fileName: string): boolean {
|
|
|
218
237
|
* @param instrument - The instrument to display
|
|
219
238
|
* @param difficulty - The difficulty level to display
|
|
220
239
|
* @param initialSeekPercent - Initial seek position (0-1)
|
|
240
|
+
* @param textureOptions - Options for texture loading (e.g., animations enabled)
|
|
221
241
|
* @returns Prepared chart data ready for the player
|
|
222
242
|
*/
|
|
223
243
|
export async function prepareChartData(
|
|
224
244
|
files: { fileName: string; data: Uint8Array }[],
|
|
225
245
|
instrument: Instrument,
|
|
226
246
|
difficulty: Difficulty,
|
|
227
|
-
initialSeekPercent = 0
|
|
247
|
+
initialSeekPercent = 0,
|
|
248
|
+
textureOptions: LoadTexturesOptions = {}
|
|
228
249
|
): Promise<PreparedChartData> {
|
|
229
250
|
// Filter out video files
|
|
230
251
|
const filteredFiles = files.filter((f) => !isVideoFile(f.fileName));
|
|
@@ -276,7 +297,10 @@ export async function prepareChartData(
|
|
|
276
297
|
|
|
277
298
|
// Load textures for the instrument type
|
|
278
299
|
const instrumentType = getInstrumentType(instrument);
|
|
279
|
-
const textures = await ChartPreview.loadTextures(
|
|
300
|
+
const textures = await ChartPreview.loadTextures(
|
|
301
|
+
instrumentType,
|
|
302
|
+
textureOptions
|
|
303
|
+
);
|
|
280
304
|
|
|
281
305
|
// Calculate audio length from the last note or from metadata
|
|
282
306
|
const lastNote = parsedChart.trackData
|