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.
@@ -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
- * @returns An object containing highway, strikeline, and note textures
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(instrumentType: InstrumentType) {
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 = textureLoader.loadAsync(
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
- this._isFullscreen = document.fullscreenElement === this.container;
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
- : "Toggle fullscreen";
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
- if (!document.fullscreenEnabled) return;
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
- if (this._isFullscreen) {
1111
- await document.exitFullscreen();
1112
- } else {
1113
- await this.container.requestFullscreen();
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 { ChartPreview, type ParsedChart } from "./ChartPreview";
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(instrumentType);
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
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  // Core classes
2
2
  export {
3
3
  ChartPreview,
4
+ areAnimationsSupported,
4
5
  type ChartPreviewConfig,
5
6
  type ChartPreviewEvents,
7
+ type LoadTexturesOptions,
6
8
  type ParsedChart,
7
9
  } from "./ChartPreview";
8
10