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/dist/index.mjs CHANGED
@@ -38,13 +38,17 @@ var NOTE_SPAN_WIDTH = 0.95;
38
38
  function isImageDecoderSupported() {
39
39
  return typeof ImageDecoder !== "undefined";
40
40
  }
41
+ function areAnimationsSupported() {
42
+ return isImageDecoderSupported();
43
+ }
41
44
  var AnimatedTexture = class {
42
45
  constructor(width, height) {
43
- this.decoder = null;
44
46
  this.frameIndex = 0;
45
47
  this.frameCount = 0;
46
48
  this.lastFrameTime = 0;
47
49
  this.frameDurations = [];
50
+ /** Pre-decoded frames stored as ImageBitmap for fast synchronous access */
51
+ this.frameCache = [];
48
52
  this.isAnimated = false;
49
53
  this.disposed = false;
50
54
  this.canvas = document.createElement("canvas");
@@ -57,11 +61,11 @@ var AnimatedTexture = class {
57
61
  /**
58
62
  * Creates an AnimatedTexture from a URL.
59
63
  * Uses ImageDecoder for animation if available, otherwise loads as static texture.
64
+ * Pre-decodes all frames during initialization for optimal render performance.
60
65
  */
61
66
  static async create(url) {
62
67
  if (!isImageDecoderSupported()) {
63
- const textureLoader = new THREE.TextureLoader();
64
- return textureLoader.loadAsync(url);
68
+ return loadStaticTexture(url);
65
69
  }
66
70
  try {
67
71
  const response = await fetch(url);
@@ -71,8 +75,7 @@ var AnimatedTexture = class {
71
75
  const contentType = response.headers.get("content-type") || "image/webp";
72
76
  const isSupported = await ImageDecoder.isTypeSupported(contentType);
73
77
  if (!isSupported) {
74
- const textureLoader = new THREE.TextureLoader();
75
- return textureLoader.loadAsync(url);
78
+ return loadStaticTexture(url);
76
79
  }
77
80
  const decoder = new ImageDecoder({
78
81
  data: response.body,
@@ -103,40 +106,51 @@ var AnimatedTexture = class {
103
106
  firstFrame.displayWidth,
104
107
  firstFrame.displayHeight
105
108
  );
106
- animTexture.ctx.drawImage(firstFrame, 0, 0);
107
- animTexture.texture.needsUpdate = true;
108
- animTexture.decoder = decoder;
109
- animTexture.frameCount = frameCount;
110
- animTexture.isAnimated = true;
111
109
  animTexture.frameDurations = [];
110
+ animTexture.frameCache = [];
112
111
  for (let i = 0; i < frameCount; i++) {
113
112
  try {
114
113
  const frameResult = await decoder.decode({ frameIndex: i });
115
- const durationMs = (frameResult.image.duration ?? 1e5) / 1e3;
114
+ const videoFrame = frameResult.image;
115
+ const bitmap = await createImageBitmap(videoFrame);
116
+ animTexture.frameCache.push(bitmap);
117
+ const durationMs = (videoFrame.duration ?? 1e5) / 1e3;
116
118
  animTexture.frameDurations.push(durationMs);
117
- frameResult.image.close();
119
+ videoFrame.close();
118
120
  } catch {
119
121
  animTexture.frameDurations.push(100);
122
+ if (animTexture.frameCache.length > 0) {
123
+ animTexture.frameCache.push(
124
+ animTexture.frameCache[animTexture.frameCache.length - 1]
125
+ );
126
+ }
120
127
  }
121
128
  }
122
- firstFrame.close();
129
+ if (animTexture.frameCache.length > 0) {
130
+ animTexture.ctx.drawImage(animTexture.frameCache[0], 0, 0);
131
+ animTexture.texture.needsUpdate = true;
132
+ }
133
+ animTexture.frameCount = animTexture.frameCache.length;
134
+ animTexture.isAnimated = animTexture.frameCount > 1;
123
135
  animTexture.lastFrameTime = performance.now();
136
+ firstFrame.close();
137
+ decoder.close();
124
138
  return animTexture;
125
139
  } catch (error) {
126
140
  console.warn(
127
141
  "Failed to load animated texture, falling back to static:",
128
142
  error
129
143
  );
130
- const textureLoader = new THREE.TextureLoader();
131
- return textureLoader.loadAsync(url);
144
+ return loadStaticTexture(url);
132
145
  }
133
146
  }
134
147
  /**
135
148
  * Updates the texture to the current animation frame based on elapsed time.
136
- * Call this each frame in the render loop.
149
+ * This method is SYNCHRONOUS for optimal render performance - all frames
150
+ * are pre-decoded during initialization.
137
151
  */
138
- async tick() {
139
- if (!this.isAnimated || !this.decoder || this.disposed) {
152
+ tick() {
153
+ if (!this.isAnimated || this.disposed || this.frameCache.length === 0) {
140
154
  return;
141
155
  }
142
156
  const now = performance.now();
@@ -145,15 +159,11 @@ var AnimatedTexture = class {
145
159
  if (elapsed >= currentFrameDuration) {
146
160
  this.frameIndex = (this.frameIndex + 1) % this.frameCount;
147
161
  this.lastFrameTime = now;
148
- try {
149
- const result = await this.decoder.decode({
150
- frameIndex: this.frameIndex
151
- });
162
+ const frame = this.frameCache[this.frameIndex];
163
+ if (frame) {
152
164
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
153
- this.ctx.drawImage(result.image, 0, 0);
165
+ this.ctx.drawImage(frame, 0, 0);
154
166
  this.texture.needsUpdate = true;
155
- result.image.close();
156
- } catch {
157
167
  }
158
168
  }
159
169
  }
@@ -162,13 +172,35 @@ var AnimatedTexture = class {
162
172
  */
163
173
  dispose() {
164
174
  this.disposed = true;
165
- if (this.decoder) {
166
- this.decoder.close();
167
- this.decoder = null;
175
+ for (const bitmap of this.frameCache) {
176
+ bitmap.close();
168
177
  }
178
+ this.frameCache = [];
169
179
  this.texture.dispose();
170
180
  }
171
181
  };
182
+ async function loadStaticTexture(url) {
183
+ const textureLoader = new THREE.TextureLoader();
184
+ try {
185
+ const texture = await textureLoader.loadAsync(url);
186
+ texture.colorSpace = THREE.SRGBColorSpace;
187
+ return texture;
188
+ } catch (error) {
189
+ console.warn(`Failed to load static texture from ${url}:`, error);
190
+ return createPlaceholderTexture();
191
+ }
192
+ }
193
+ function createPlaceholderTexture() {
194
+ const canvas = document.createElement("canvas");
195
+ canvas.width = 32;
196
+ canvas.height = 32;
197
+ const ctx = canvas.getContext("2d");
198
+ ctx.fillStyle = "#FF00FF";
199
+ ctx.fillRect(0, 0, 32, 32);
200
+ const texture = new THREE.CanvasTexture(canvas);
201
+ texture.colorSpace = THREE.SRGBColorSpace;
202
+ return texture;
203
+ }
172
204
  var AnimatedTextureManager = class {
173
205
  constructor() {
174
206
  this.animatedTextures = [];
@@ -178,8 +210,13 @@ var AnimatedTextureManager = class {
178
210
  this.animatedTextures.push(texture);
179
211
  }
180
212
  }
181
- async tick() {
182
- await Promise.all(this.animatedTextures.map((t) => t.tick()));
213
+ /**
214
+ * Updates all animated textures. This is now SYNCHRONOUS for optimal performance.
215
+ */
216
+ tick() {
217
+ for (const texture of this.animatedTextures) {
218
+ texture.tick();
219
+ }
183
220
  }
184
221
  dispose() {
185
222
  for (const texture of this.animatedTextures) {
@@ -332,6 +369,17 @@ var _ChartPreview = class {
332
369
  get chartEndTimeMs() {
333
370
  return this.audioManager.chartEndTimeMs;
334
371
  }
372
+ /**
373
+ * Manually triggers a resize of the camera and renderer.
374
+ * Call this when the container size changes (e.g., entering/exiting fullscreen).
375
+ */
376
+ resize() {
377
+ this.camera.triggerResize();
378
+ this.renderer.triggerResize();
379
+ if (this.paused) {
380
+ this.animateFrame(false);
381
+ }
382
+ }
335
383
  /**
336
384
  * Cleans up all resources. Call this when discarding the preview.
337
385
  * The preview should not be used after calling dispose().
@@ -419,6 +467,10 @@ var ChartCamera = class extends THREE.PerspectiveCamera {
419
467
  this.aspect = width / height;
420
468
  this.updateProjectionMatrix();
421
469
  }
470
+ /** Manually trigger a resize update */
471
+ triggerResize() {
472
+ this.onResize();
473
+ }
422
474
  dispose() {
423
475
  window.removeEventListener("resize", this.resizeListener);
424
476
  this.clear();
@@ -441,6 +493,10 @@ var ChartRenderer = class extends THREE.WebGLRenderer {
441
493
  const height = this.divContainer.offsetHeight ?? window.innerHeight;
442
494
  this.setSize(width, height);
443
495
  }
496
+ /** Manually trigger a resize update */
497
+ triggerResize() {
498
+ this.onResize();
499
+ }
444
500
  dispose() {
445
501
  window.removeEventListener("resize", this.resizeListener);
446
502
  super.dispose();
@@ -782,11 +838,16 @@ var NotesManager = class {
782
838
  }
783
839
  for (let i = maxNoteEventIndex + 1; this.noteEvents[i] && this.noteEvents[i].msTime < renderEndTimeMs; i++) {
784
840
  const note = this.noteEvents[i];
841
+ const material = this.noteMaterials.get(note.type)?.get(note.flags);
842
+ if (!material) {
843
+ console.warn(
844
+ `Missing texture for note type ${note.type} with flags ${note.flags}`
845
+ );
846
+ continue;
847
+ }
785
848
  const noteGroup = new THREE.Group();
786
849
  const scale = note.type === noteTypes.kick ? 0.045 : note.type === noteTypes.open && this.instrumentType === instrumentTypes.sixFret ? 0.04 : SCALE;
787
- const sprite = new THREE.Sprite(
788
- this.noteMaterials.get(note.type).get(note.flags)
789
- );
850
+ const sprite = new THREE.Sprite(material);
790
851
  noteGroup.add(sprite);
791
852
  sprite.center = new THREE.Vector2(
792
853
  note.type === noteTypes.kick ? 0.62 : 0.5,
@@ -863,12 +924,16 @@ var EventSequence = class {
863
924
  return earliestActiveEventIndex === null ? this.lastPrecedingEventIndex + 1 : earliestActiveEventIndex;
864
925
  }
865
926
  };
866
- async function loadTextures(instrumentType) {
927
+ async function loadTextures(instrumentType, options = {}) {
928
+ const { animationsEnabled = true } = options;
867
929
  const textureLoader = new THREE.TextureLoader();
868
930
  const animatedTextureManager = new AnimatedTextureManager();
869
931
  const load = (path) => textureLoader.loadAsync("https://static.enchor.us/" + path);
870
932
  const loadAnimated = async (path) => {
871
933
  const url = `https://static.enchor.us/preview-${path}.webp`;
934
+ if (!animationsEnabled) {
935
+ return loadStaticTexture(url);
936
+ }
872
937
  const result = await AnimatedTexture.create(url);
873
938
  if (result instanceof AnimatedTexture) {
874
939
  animatedTextureManager.register(result);
@@ -1314,7 +1379,7 @@ function isVideoFile(fileName) {
1314
1379
  const ext = fileName.toLowerCase().slice(fileName.lastIndexOf("."));
1315
1380
  return videoExtensions.includes(ext);
1316
1381
  }
1317
- async function prepareChartData(files, instrument, difficulty, initialSeekPercent = 0) {
1382
+ async function prepareChartData(files, instrument, difficulty, initialSeekPercent = 0, textureOptions = {}) {
1318
1383
  const filteredFiles = files.filter((f) => !isVideoFile(f.fileName));
1319
1384
  const scannedChart = scanChartFolder(filteredFiles);
1320
1385
  if (!scannedChart.playable || !scannedChart.notesData) {
@@ -1349,7 +1414,10 @@ async function prepareChartData(files, instrument, difficulty, initialSeekPercen
1349
1414
  }
1350
1415
  const audioFiles = findAudioFiles(filteredFiles);
1351
1416
  const instrumentType = getInstrumentType2(instrument);
1352
- const textures = await ChartPreview.loadTextures(instrumentType);
1417
+ const textures = await ChartPreview.loadTextures(
1418
+ instrumentType,
1419
+ textureOptions
1420
+ );
1353
1421
  const lastNote = parsedChart.trackData.flatMap((t) => t.noteEventGroups.flat()).reduce((max, note) => Math.max(max, note.msTime + note.msLength), 0);
1354
1422
  const audioLengthMs = scannedChart.song_length || lastNote + 5e3;
1355
1423
  const startDelayMs = (scannedChart.delay || 0) + (scannedChart.chart_offset || 0) * 1e3;
@@ -1670,10 +1738,18 @@ var ChartPreviewPlayer = class extends HTMLElement {
1670
1738
  }
1671
1739
  };
1672
1740
  this.handleFullscreenChange = () => {
1673
- this._isFullscreen = document.fullscreenElement === this.container;
1741
+ const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement;
1742
+ this._isFullscreen = fullscreenElement !== null && fullscreenElement !== void 0;
1674
1743
  this.container.classList.toggle("fullscreen", this._isFullscreen);
1675
1744
  this.fullscreenBtn.innerHTML = this._isFullscreen ? ICONS.fullscreenExit : ICONS.fullscreen;
1676
- this.fullscreenBtn.title = this._isFullscreen ? "Exit fullscreen" : "Toggle fullscreen";
1745
+ this.fullscreenBtn.title = this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen";
1746
+ if (this.chartPreview) {
1747
+ requestAnimationFrame(() => {
1748
+ requestAnimationFrame(() => {
1749
+ this.chartPreview?.resize();
1750
+ });
1751
+ });
1752
+ }
1677
1753
  };
1678
1754
  this.shadow = this.attachShadow({ mode: "open" });
1679
1755
  this.render();
@@ -1698,6 +1774,10 @@ var ChartPreviewPlayer = class extends HTMLElement {
1698
1774
  connectedCallback() {
1699
1775
  this.setAttribute("tabindex", "0");
1700
1776
  document.addEventListener("fullscreenchange", this.handleFullscreenChange);
1777
+ document.addEventListener(
1778
+ "webkitfullscreenchange",
1779
+ this.handleFullscreenChange
1780
+ );
1701
1781
  }
1702
1782
  disconnectedCallback() {
1703
1783
  this.dispose();
@@ -1705,6 +1785,10 @@ var ChartPreviewPlayer = class extends HTMLElement {
1705
1785
  "fullscreenchange",
1706
1786
  this.handleFullscreenChange
1707
1787
  );
1788
+ document.removeEventListener(
1789
+ "webkitfullscreenchange",
1790
+ this.handleFullscreenChange
1791
+ );
1708
1792
  this.removeEventListener("keydown", this.handleKeydown);
1709
1793
  }
1710
1794
  render() {
@@ -1785,7 +1869,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
1785
1869
  this.onSeekEnd();
1786
1870
  }
1787
1871
  });
1788
- this.fullscreenBtn.addEventListener("click", () => {
1872
+ this.fullscreenBtn.addEventListener("click", (e) => {
1873
+ e.stopPropagation();
1789
1874
  this.toggleFullscreen();
1790
1875
  });
1791
1876
  this.addEventListener("keydown", this.handleKeydown);
@@ -2098,7 +2183,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
2098
2183
  files,
2099
2184
  config.instrument,
2100
2185
  config.difficulty,
2101
- config.initialSeekPercent
2186
+ config.initialSeekPercent,
2187
+ { animationsEnabled: config.animationsEnabled }
2102
2188
  );
2103
2189
  await this.loadChart({
2104
2190
  parsedChart: preparedData.parsedChart,
@@ -2140,7 +2226,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
2140
2226
  files,
2141
2227
  config.instrument,
2142
2228
  config.difficulty,
2143
- config.initialSeekPercent
2229
+ config.initialSeekPercent,
2230
+ { animationsEnabled: config.animationsEnabled }
2144
2231
  );
2145
2232
  await this.loadChart({
2146
2233
  parsedChart: preparedData.parsedChart,
@@ -2179,7 +2266,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
2179
2266
  config.files,
2180
2267
  config.instrument,
2181
2268
  config.difficulty,
2182
- config.initialSeekPercent
2269
+ config.initialSeekPercent,
2270
+ { animationsEnabled: config.animationsEnabled }
2183
2271
  );
2184
2272
  await this.loadChart({
2185
2273
  parsedChart: preparedData.parsedChart,
@@ -2279,12 +2367,28 @@ var ChartPreviewPlayer = class extends HTMLElement {
2279
2367
  * Toggle fullscreen mode
2280
2368
  */
2281
2369
  async toggleFullscreen() {
2282
- if (!document.fullscreenEnabled)
2370
+ const doc = document;
2371
+ const container = this.container;
2372
+ const isFullscreenEnabled = doc.fullscreenEnabled || doc.webkitFullscreenEnabled;
2373
+ if (!isFullscreenEnabled)
2283
2374
  return;
2284
- if (this._isFullscreen) {
2285
- await document.exitFullscreen();
2286
- } else {
2287
- await this.container.requestFullscreen();
2375
+ const fullscreenElement = doc.fullscreenElement || doc.webkitFullscreenElement;
2376
+ try {
2377
+ if (fullscreenElement) {
2378
+ if (doc.exitFullscreen) {
2379
+ await doc.exitFullscreen();
2380
+ } else if (doc.webkitExitFullscreen) {
2381
+ doc.webkitExitFullscreen();
2382
+ }
2383
+ } else {
2384
+ if (container.requestFullscreen) {
2385
+ await container.requestFullscreen();
2386
+ } else if (container.webkitRequestFullscreen) {
2387
+ container.webkitRequestFullscreen();
2388
+ }
2389
+ }
2390
+ } catch (error) {
2391
+ console.warn("Fullscreen toggle failed:", error);
2288
2392
  }
2289
2393
  }
2290
2394
  /**
@@ -2319,6 +2423,7 @@ import {
2319
2423
  export {
2320
2424
  ChartPreview,
2321
2425
  ChartPreviewPlayer,
2426
+ areAnimationsSupported,
2322
2427
  extractSngFile,
2323
2428
  fetchSngFile,
2324
2429
  findAudioFiles,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chart-preview",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "author": "Geo",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",