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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # chart-preview
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 95d0918: Add support for animated note textures and improve fullscreen handling
8
+
3
9
  ## 1.1.0
4
10
 
5
11
  ### 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
  /**
@@ -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
- 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) {
@@ -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(instrumentType);
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
- this._isFullscreen = document.fullscreenElement === this.container;
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" : "Toggle 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
- if (!document.fullscreenEnabled)
2380
+ const doc = document;
2381
+ const container = this.container;
2382
+ const isFullscreenEnabled = doc.fullscreenEnabled || doc.webkitFullscreenEnabled;
2383
+ if (!isFullscreenEnabled)
2318
2384
  return;
2319
- if (this._isFullscreen) {
2320
- await document.exitFullscreen();
2321
- } else {
2322
- await this.container.requestFullscreen();
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,