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 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 { ChartPreview, getInstrumentType } from "chart-preview";
459
+ import {
460
+ ChartPreview,
461
+ getInstrumentType,
462
+ areAnimationsSupported,
463
+ } from "chart-preview";
448
464
 
449
465
  // Load textures (cache for reuse)
450
- const textures = await ChartPreview.loadTextures(getInstrumentType("guitar"));
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
- * Call this each frame in the render loop.
37
+ * This method is SYNCHRONOUS for optimal render performance - all frames
38
+ * are pre-decoded during initialization.
30
39
  */
31
- tick(): Promise<void>;
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
- tick(): Promise<void>;
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
- const textureLoader = new THREE.TextureLoader();
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
- const textureLoader = new THREE.TextureLoader();
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 durationMs = (frameResult.image.duration ?? 1e5) / 1e3;
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
- frameResult.image.close();
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
- firstFrame.close();
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
- const textureLoader = new THREE.TextureLoader();
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
- * Call this each frame in the render loop.
189
+ * This method is SYNCHRONOUS for optimal render performance - all frames
190
+ * are pre-decoded during initialization.
176
191
  */
177
- async tick() {
178
- if (!this.isAnimated || !this.decoder || this.disposed) {
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
- try {
188
- const result = await this.decoder.decode({
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(result.image, 0, 0);
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
- if (this.decoder) {
205
- this.decoder.close();
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
- async tick() {
221
- await Promise.all(this.animatedTextures.map((t) => t.tick()));
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(instrumentType);
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
- this._isFullscreen = document.fullscreenElement === this.container;
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" : "Toggle 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
- if (!document.fullscreenEnabled)
2406
+ const doc = document;
2407
+ const container = this.container;
2408
+ const isFullscreenEnabled = doc.fullscreenEnabled || doc.webkitFullscreenEnabled;
2409
+ if (!isFullscreenEnabled)
2318
2410
  return;
2319
- if (this._isFullscreen) {
2320
- await document.exitFullscreen();
2321
- } else {
2322
- await this.container.requestFullscreen();
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,