chart-preview 1.0.3 → 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 +12 -0
- package/README.md +34 -4
- package/dist/index.d.ts +82 -4
- package/dist/index.js +266 -21
- package/dist/index.mjs +265 -21
- package/package.json +1 -1
- package/src/ChartPreview.ts +324 -8
- package/src/ChartPreviewPlayer.ts +65 -12
- package/src/SngLoader.ts +27 -3
- package/src/index.ts +2 -0
package/src/ChartPreview.ts
CHANGED
|
@@ -25,6 +25,267 @@ const HIGHWAY_DURATION_MS = 1500;
|
|
|
25
25
|
const SCALE = 0.105;
|
|
26
26
|
const NOTE_SPAN_WIDTH = 0.95;
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Check if the ImageDecoder API is available for animated WebP support.
|
|
30
|
+
* This API is available in Chromium-based browsers (Chrome, Edge, Opera).
|
|
31
|
+
*/
|
|
32
|
+
function isImageDecoderSupported(): boolean {
|
|
33
|
+
return typeof ImageDecoder !== "undefined";
|
|
34
|
+
}
|
|
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
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Manages an animated WebP texture using the ImageDecoder API.
|
|
46
|
+
* Pre-decodes all frames during initialization for optimal performance.
|
|
47
|
+
* Falls back to a static texture if ImageDecoder is not supported.
|
|
48
|
+
*/
|
|
49
|
+
class AnimatedTexture {
|
|
50
|
+
private canvas: HTMLCanvasElement;
|
|
51
|
+
private ctx: CanvasRenderingContext2D;
|
|
52
|
+
public texture: THREE.CanvasTexture;
|
|
53
|
+
private frameIndex = 0;
|
|
54
|
+
private frameCount = 0;
|
|
55
|
+
private lastFrameTime = 0;
|
|
56
|
+
private frameDurations: number[] = [];
|
|
57
|
+
/** Pre-decoded frames stored as ImageBitmap for fast synchronous access */
|
|
58
|
+
private frameCache: ImageBitmap[] = [];
|
|
59
|
+
private isAnimated = false;
|
|
60
|
+
private disposed = false;
|
|
61
|
+
|
|
62
|
+
private constructor(width: number, height: number) {
|
|
63
|
+
this.canvas = document.createElement("canvas");
|
|
64
|
+
this.canvas.width = width;
|
|
65
|
+
this.canvas.height = height;
|
|
66
|
+
this.ctx = this.canvas.getContext("2d")!;
|
|
67
|
+
this.texture = new THREE.CanvasTexture(this.canvas);
|
|
68
|
+
this.texture.colorSpace = THREE.SRGBColorSpace;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates an AnimatedTexture from a URL.
|
|
73
|
+
* Uses ImageDecoder for animation if available, otherwise loads as static texture.
|
|
74
|
+
* Pre-decodes all frames during initialization for optimal render performance.
|
|
75
|
+
*/
|
|
76
|
+
static async create(url: string): Promise<AnimatedTexture | THREE.Texture> {
|
|
77
|
+
if (!isImageDecoderSupported()) {
|
|
78
|
+
// Fall back to static texture loading
|
|
79
|
+
return loadStaticTexture(url);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(url);
|
|
84
|
+
if (!response.ok || !response.body) {
|
|
85
|
+
throw new Error(`Failed to fetch ${url}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if the image type is supported for decoding
|
|
89
|
+
const contentType = response.headers.get("content-type") || "image/webp";
|
|
90
|
+
const isSupported = await ImageDecoder.isTypeSupported(contentType);
|
|
91
|
+
if (!isSupported) {
|
|
92
|
+
// Fall back to static texture
|
|
93
|
+
return loadStaticTexture(url);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const decoder = new ImageDecoder({
|
|
97
|
+
data: response.body,
|
|
98
|
+
type: contentType,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await decoder.completed;
|
|
102
|
+
|
|
103
|
+
const track = decoder.tracks.selectedTrack;
|
|
104
|
+
if (!track) {
|
|
105
|
+
throw new Error("No track found in image");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const frameCount = track.frameCount;
|
|
109
|
+
|
|
110
|
+
// If only one frame, just return a static texture
|
|
111
|
+
if (frameCount <= 1) {
|
|
112
|
+
const result = await decoder.decode({ frameIndex: 0 });
|
|
113
|
+
const frame = result.image;
|
|
114
|
+
const animTexture = new AnimatedTexture(
|
|
115
|
+
frame.displayWidth,
|
|
116
|
+
frame.displayHeight
|
|
117
|
+
);
|
|
118
|
+
animTexture.ctx.drawImage(frame, 0, 0);
|
|
119
|
+
animTexture.texture.needsUpdate = true;
|
|
120
|
+
frame.close();
|
|
121
|
+
decoder.close();
|
|
122
|
+
return animTexture.texture;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Animated image - pre-decode ALL frames for optimal performance
|
|
126
|
+
const firstResult = await decoder.decode({ frameIndex: 0 });
|
|
127
|
+
const firstFrame = firstResult.image;
|
|
128
|
+
const animTexture = new AnimatedTexture(
|
|
129
|
+
firstFrame.displayWidth,
|
|
130
|
+
firstFrame.displayHeight
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Pre-decode all frames into ImageBitmap cache
|
|
134
|
+
animTexture.frameDurations = [];
|
|
135
|
+
animTexture.frameCache = [];
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < frameCount; i++) {
|
|
138
|
+
try {
|
|
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
|
+
|
|
146
|
+
// Duration is in microseconds, convert to milliseconds
|
|
147
|
+
const durationMs = (videoFrame.duration ?? 100000) / 1000;
|
|
148
|
+
animTexture.frameDurations.push(durationMs);
|
|
149
|
+
videoFrame.close();
|
|
150
|
+
} catch {
|
|
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
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
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;
|
|
169
|
+
animTexture.lastFrameTime = performance.now();
|
|
170
|
+
|
|
171
|
+
// Close the decoder - we no longer need it since all frames are cached
|
|
172
|
+
firstFrame.close();
|
|
173
|
+
decoder.close();
|
|
174
|
+
|
|
175
|
+
return animTexture;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
// Fall back to static texture on any error
|
|
178
|
+
console.warn(
|
|
179
|
+
"Failed to load animated texture, falling back to static:",
|
|
180
|
+
error
|
|
181
|
+
);
|
|
182
|
+
return loadStaticTexture(url);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Updates the texture to the current animation frame based on elapsed time.
|
|
188
|
+
* This method is SYNCHRONOUS for optimal render performance - all frames
|
|
189
|
+
* are pre-decoded during initialization.
|
|
190
|
+
*/
|
|
191
|
+
tick(): void {
|
|
192
|
+
if (!this.isAnimated || this.disposed || this.frameCache.length === 0) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const now = performance.now();
|
|
197
|
+
const elapsed = now - this.lastFrameTime;
|
|
198
|
+
const currentFrameDuration = this.frameDurations[this.frameIndex] || 100;
|
|
199
|
+
|
|
200
|
+
if (elapsed >= currentFrameDuration) {
|
|
201
|
+
this.frameIndex = (this.frameIndex + 1) % this.frameCount;
|
|
202
|
+
this.lastFrameTime = now;
|
|
203
|
+
|
|
204
|
+
// Synchronous frame update from pre-decoded cache
|
|
205
|
+
const frame = this.frameCache[this.frameIndex];
|
|
206
|
+
if (frame) {
|
|
207
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
208
|
+
this.ctx.drawImage(frame, 0, 0);
|
|
209
|
+
this.texture.needsUpdate = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Disposes of resources used by this animated texture.
|
|
216
|
+
*/
|
|
217
|
+
dispose(): void {
|
|
218
|
+
this.disposed = true;
|
|
219
|
+
// Close all cached ImageBitmaps
|
|
220
|
+
for (const bitmap of this.frameCache) {
|
|
221
|
+
bitmap.close();
|
|
222
|
+
}
|
|
223
|
+
this.frameCache = [];
|
|
224
|
+
this.texture.dispose();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
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
|
+
|
|
262
|
+
/** Collection of animated textures that need to be ticked each frame */
|
|
263
|
+
class AnimatedTextureManager {
|
|
264
|
+
private animatedTextures: AnimatedTexture[] = [];
|
|
265
|
+
|
|
266
|
+
register(texture: AnimatedTexture | THREE.Texture): void {
|
|
267
|
+
if (texture instanceof AnimatedTexture) {
|
|
268
|
+
this.animatedTextures.push(texture);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
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
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
dispose(): void {
|
|
282
|
+
for (const texture of this.animatedTextures) {
|
|
283
|
+
texture.dispose();
|
|
284
|
+
}
|
|
285
|
+
this.animatedTextures = [];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
28
289
|
/**
|
|
29
290
|
* Custom note types for 6-fret barre notes.
|
|
30
291
|
* These are the only sprites without a dedicated NoteType from scan-chart.
|
|
@@ -89,6 +350,7 @@ export class ChartPreview {
|
|
|
89
350
|
private renderer: ChartRenderer;
|
|
90
351
|
private audioManager: AudioManager | SilentAudioManager;
|
|
91
352
|
private notesManager: NotesManager;
|
|
353
|
+
private animatedTextureManager: AnimatedTextureManager;
|
|
92
354
|
private progressIntervalMs: number;
|
|
93
355
|
private lastProgressEmitTime = 0;
|
|
94
356
|
|
|
@@ -162,6 +424,8 @@ export class ChartPreview {
|
|
|
162
424
|
const chartPreview = new ChartPreview();
|
|
163
425
|
chartPreview.instrumentType = getInstrumentType(config.instrument);
|
|
164
426
|
chartPreview.highwayTexture = config.textures.highwayTexture;
|
|
427
|
+
chartPreview.animatedTextureManager =
|
|
428
|
+
config.textures.animatedTextureManager;
|
|
165
429
|
chartPreview.progressIntervalMs =
|
|
166
430
|
config.progressIntervalMs ?? DEFAULT_PROGRESS_INTERVAL_MS;
|
|
167
431
|
chartPreview.camera = new ChartCamera(config.container);
|
|
@@ -257,6 +521,7 @@ export class ChartPreview {
|
|
|
257
521
|
this.renderer.dispose();
|
|
258
522
|
this.renderer.forceContextLoss();
|
|
259
523
|
this.audioManager.closeAudio();
|
|
524
|
+
this.animatedTextureManager.dispose();
|
|
260
525
|
}
|
|
261
526
|
|
|
262
527
|
private addHighwayToScene(highwayTexture: THREE.Texture) {
|
|
@@ -307,6 +572,9 @@ export class ChartPreview {
|
|
|
307
572
|
this.audioManager.chartCurrentTimeMs
|
|
308
573
|
);
|
|
309
574
|
|
|
575
|
+
// Update animated textures (for animated WebP notes)
|
|
576
|
+
this.animatedTextureManager.tick();
|
|
577
|
+
|
|
310
578
|
// Shift highway position
|
|
311
579
|
const scrollPosition =
|
|
312
580
|
-0.9 *
|
|
@@ -858,6 +1126,17 @@ class NotesManager {
|
|
|
858
1126
|
i++
|
|
859
1127
|
) {
|
|
860
1128
|
const note = this.noteEvents[i];
|
|
1129
|
+
|
|
1130
|
+
// Get the material for this note type and flags
|
|
1131
|
+
const material = this.noteMaterials.get(note.type)?.get(note.flags);
|
|
1132
|
+
if (!material) {
|
|
1133
|
+
// Skip notes with missing textures (shouldn't happen, but prevents crashes)
|
|
1134
|
+
console.warn(
|
|
1135
|
+
`Missing texture for note type ${note.type} with flags ${note.flags}`
|
|
1136
|
+
);
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
861
1140
|
const noteGroup = new THREE.Group();
|
|
862
1141
|
const scale =
|
|
863
1142
|
note.type === noteTypes.kick
|
|
@@ -866,9 +1145,7 @@ class NotesManager {
|
|
|
866
1145
|
this.instrumentType === instrumentTypes.sixFret
|
|
867
1146
|
? 0.04
|
|
868
1147
|
: SCALE;
|
|
869
|
-
const sprite = new THREE.Sprite(
|
|
870
|
-
this.noteMaterials.get(note.type)!.get(note.flags)!
|
|
871
|
-
);
|
|
1148
|
+
const sprite = new THREE.Sprite(material);
|
|
872
1149
|
noteGroup.add(sprite);
|
|
873
1150
|
sprite.center = new THREE.Vector2(
|
|
874
1151
|
note.type === noteTypes.kick ? 0.62 : 0.5,
|
|
@@ -973,16 +1250,56 @@ class EventSequence<
|
|
|
973
1250
|
}
|
|
974
1251
|
}
|
|
975
1252
|
|
|
1253
|
+
export interface LoadTexturesOptions {
|
|
1254
|
+
/**
|
|
1255
|
+
* Whether to enable animated textures.
|
|
1256
|
+
* When true (default), uses ImageDecoder API for animated WebP if available.
|
|
1257
|
+
* When false, always loads static textures for better performance.
|
|
1258
|
+
* Use `areAnimationsSupported()` to check if animations are supported on the current browser.
|
|
1259
|
+
*/
|
|
1260
|
+
animationsEnabled?: boolean;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
976
1263
|
/**
|
|
977
1264
|
* Loads all required textures for the chart preview.
|
|
978
1265
|
* @param instrumentType The type of instrument to load textures for
|
|
979
|
-
* @
|
|
1266
|
+
* @param options Optional configuration for texture loading
|
|
1267
|
+
* @returns An object containing highway, strikeline, note textures, and animated texture manager
|
|
980
1268
|
*/
|
|
981
|
-
async function loadTextures(
|
|
1269
|
+
async function loadTextures(
|
|
1270
|
+
instrumentType: InstrumentType,
|
|
1271
|
+
options: LoadTexturesOptions = {}
|
|
1272
|
+
) {
|
|
1273
|
+
const { animationsEnabled = true } = options;
|
|
982
1274
|
const textureLoader = new THREE.TextureLoader();
|
|
1275
|
+
const animatedTextureManager = new AnimatedTextureManager();
|
|
1276
|
+
|
|
983
1277
|
const load = (path: string) =>
|
|
984
1278
|
textureLoader.loadAsync("https://static.enchor.us/" + path);
|
|
985
1279
|
|
|
1280
|
+
/**
|
|
1281
|
+
* Loads a texture with animation support.
|
|
1282
|
+
* Uses ImageDecoder API for animated WebP when available, falls back to static.
|
|
1283
|
+
* If animationsEnabled is false, always loads static textures.
|
|
1284
|
+
*/
|
|
1285
|
+
const loadAnimated = async (path: string): Promise<THREE.Texture> => {
|
|
1286
|
+
const url = `https://static.enchor.us/preview-${path}.webp`;
|
|
1287
|
+
|
|
1288
|
+
// If animations are disabled, load static texture directly
|
|
1289
|
+
if (!animationsEnabled) {
|
|
1290
|
+
return loadStaticTexture(url);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const result = await AnimatedTexture.create(url);
|
|
1294
|
+
|
|
1295
|
+
if (result instanceof AnimatedTexture) {
|
|
1296
|
+
animatedTextureManager.register(result);
|
|
1297
|
+
return result.texture;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
return result;
|
|
1301
|
+
};
|
|
1302
|
+
|
|
986
1303
|
const [highwayTexture, strikelineTexture, noteTextures] = await Promise.all([
|
|
987
1304
|
(async () => {
|
|
988
1305
|
const texture = await load("preview-highway.png");
|
|
@@ -1014,9 +1331,7 @@ async function loadTextures(instrumentType: InstrumentType) {
|
|
|
1014
1331
|
flags: number,
|
|
1015
1332
|
path: string
|
|
1016
1333
|
) => {
|
|
1017
|
-
const texture =
|
|
1018
|
-
`https://static.enchor.us/preview-${path}.webp`
|
|
1019
|
-
);
|
|
1334
|
+
const texture = loadAnimated(path);
|
|
1020
1335
|
texturePromises.push({ type, flags, texture });
|
|
1021
1336
|
return texture;
|
|
1022
1337
|
};
|
|
@@ -1199,6 +1514,7 @@ async function loadTextures(instrumentType: InstrumentType) {
|
|
|
1199
1514
|
highwayTexture,
|
|
1200
1515
|
strikelineTexture,
|
|
1201
1516
|
noteTextures,
|
|
1517
|
+
animatedTextureManager,
|
|
1202
1518
|
};
|
|
1203
1519
|
}
|
|
1204
1520
|
|
|
@@ -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,20 @@ 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";
|
|
536
552
|
};
|
|
537
553
|
|
|
538
554
|
private onSeekStart() {
|
|
@@ -900,7 +916,8 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
900
916
|
files,
|
|
901
917
|
config.instrument,
|
|
902
918
|
config.difficulty,
|
|
903
|
-
config.initialSeekPercent
|
|
919
|
+
config.initialSeekPercent,
|
|
920
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
904
921
|
);
|
|
905
922
|
|
|
906
923
|
// Load the chart
|
|
@@ -948,7 +965,8 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
948
965
|
files,
|
|
949
966
|
config.instrument,
|
|
950
967
|
config.difficulty,
|
|
951
|
-
config.initialSeekPercent
|
|
968
|
+
config.initialSeekPercent,
|
|
969
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
952
970
|
);
|
|
953
971
|
|
|
954
972
|
// Load the chart
|
|
@@ -992,7 +1010,8 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
992
1010
|
config.files,
|
|
993
1011
|
config.instrument,
|
|
994
1012
|
config.difficulty,
|
|
995
|
-
config.initialSeekPercent
|
|
1013
|
+
config.initialSeekPercent,
|
|
1014
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
996
1015
|
);
|
|
997
1016
|
|
|
998
1017
|
// Load the chart
|
|
@@ -1105,12 +1124,46 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
1105
1124
|
* Toggle fullscreen mode
|
|
1106
1125
|
*/
|
|
1107
1126
|
async toggleFullscreen(): Promise<void> {
|
|
1108
|
-
|
|
1127
|
+
// Check for fullscreen support (standard and webkit)
|
|
1128
|
+
const doc = document as unknown as {
|
|
1129
|
+
fullscreenEnabled?: boolean;
|
|
1130
|
+
webkitFullscreenEnabled?: boolean;
|
|
1131
|
+
fullscreenElement?: Element;
|
|
1132
|
+
webkitFullscreenElement?: Element;
|
|
1133
|
+
exitFullscreen?: () => Promise<void>;
|
|
1134
|
+
webkitExitFullscreen?: () => void;
|
|
1135
|
+
};
|
|
1136
|
+
const container = this.container as unknown as {
|
|
1137
|
+
requestFullscreen?: () => Promise<void>;
|
|
1138
|
+
webkitRequestFullscreen?: () => void;
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
const isFullscreenEnabled =
|
|
1142
|
+
doc.fullscreenEnabled || doc.webkitFullscreenEnabled;
|
|
1143
|
+
if (!isFullscreenEnabled) return;
|
|
1144
|
+
|
|
1145
|
+
const fullscreenElement =
|
|
1146
|
+
doc.fullscreenElement || doc.webkitFullscreenElement;
|
|
1109
1147
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1148
|
+
try {
|
|
1149
|
+
if (fullscreenElement) {
|
|
1150
|
+
// Exit fullscreen
|
|
1151
|
+
if (doc.exitFullscreen) {
|
|
1152
|
+
await doc.exitFullscreen();
|
|
1153
|
+
} else if (doc.webkitExitFullscreen) {
|
|
1154
|
+
doc.webkitExitFullscreen();
|
|
1155
|
+
}
|
|
1156
|
+
} else {
|
|
1157
|
+
// Enter fullscreen
|
|
1158
|
+
if (container.requestFullscreen) {
|
|
1159
|
+
await container.requestFullscreen();
|
|
1160
|
+
} else if (container.webkitRequestFullscreen) {
|
|
1161
|
+
container.webkitRequestFullscreen();
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
// Fullscreen request can fail due to user gesture requirements or other reasons
|
|
1166
|
+
console.warn("Fullscreen toggle failed:", error);
|
|
1114
1167
|
}
|
|
1115
1168
|
}
|
|
1116
1169
|
|
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
|