chart-preview 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +34 -4
- package/dist/index.d.ts +82 -4
- package/dist/index.js +266 -21
- package/dist/index.mjs +265 -21
- package/package.json +1 -1
- package/src/ChartPreview.ts +324 -8
- package/src/ChartPreviewPlayer.ts +65 -12
- package/src/SngLoader.ts +27 -3
- package/src/index.ts +2 -0
package/dist/index.mjs
CHANGED
|
@@ -35,6 +35,196 @@ function isAudioContextSupported() {
|
|
|
35
35
|
var HIGHWAY_DURATION_MS = 1500;
|
|
36
36
|
var SCALE = 0.105;
|
|
37
37
|
var NOTE_SPAN_WIDTH = 0.95;
|
|
38
|
+
function isImageDecoderSupported() {
|
|
39
|
+
return typeof ImageDecoder !== "undefined";
|
|
40
|
+
}
|
|
41
|
+
function areAnimationsSupported() {
|
|
42
|
+
return isImageDecoderSupported();
|
|
43
|
+
}
|
|
44
|
+
var AnimatedTexture = class {
|
|
45
|
+
constructor(width, height) {
|
|
46
|
+
this.frameIndex = 0;
|
|
47
|
+
this.frameCount = 0;
|
|
48
|
+
this.lastFrameTime = 0;
|
|
49
|
+
this.frameDurations = [];
|
|
50
|
+
/** Pre-decoded frames stored as ImageBitmap for fast synchronous access */
|
|
51
|
+
this.frameCache = [];
|
|
52
|
+
this.isAnimated = false;
|
|
53
|
+
this.disposed = false;
|
|
54
|
+
this.canvas = document.createElement("canvas");
|
|
55
|
+
this.canvas.width = width;
|
|
56
|
+
this.canvas.height = height;
|
|
57
|
+
this.ctx = this.canvas.getContext("2d");
|
|
58
|
+
this.texture = new THREE.CanvasTexture(this.canvas);
|
|
59
|
+
this.texture.colorSpace = THREE.SRGBColorSpace;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates an AnimatedTexture from a URL.
|
|
63
|
+
* Uses ImageDecoder for animation if available, otherwise loads as static texture.
|
|
64
|
+
* Pre-decodes all frames during initialization for optimal render performance.
|
|
65
|
+
*/
|
|
66
|
+
static async create(url) {
|
|
67
|
+
if (!isImageDecoderSupported()) {
|
|
68
|
+
return loadStaticTexture(url);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url);
|
|
72
|
+
if (!response.ok || !response.body) {
|
|
73
|
+
throw new Error(`Failed to fetch ${url}`);
|
|
74
|
+
}
|
|
75
|
+
const contentType = response.headers.get("content-type") || "image/webp";
|
|
76
|
+
const isSupported = await ImageDecoder.isTypeSupported(contentType);
|
|
77
|
+
if (!isSupported) {
|
|
78
|
+
return loadStaticTexture(url);
|
|
79
|
+
}
|
|
80
|
+
const decoder = new ImageDecoder({
|
|
81
|
+
data: response.body,
|
|
82
|
+
type: contentType
|
|
83
|
+
});
|
|
84
|
+
await decoder.completed;
|
|
85
|
+
const track = decoder.tracks.selectedTrack;
|
|
86
|
+
if (!track) {
|
|
87
|
+
throw new Error("No track found in image");
|
|
88
|
+
}
|
|
89
|
+
const frameCount = track.frameCount;
|
|
90
|
+
if (frameCount <= 1) {
|
|
91
|
+
const result = await decoder.decode({ frameIndex: 0 });
|
|
92
|
+
const frame = result.image;
|
|
93
|
+
const animTexture2 = new AnimatedTexture(
|
|
94
|
+
frame.displayWidth,
|
|
95
|
+
frame.displayHeight
|
|
96
|
+
);
|
|
97
|
+
animTexture2.ctx.drawImage(frame, 0, 0);
|
|
98
|
+
animTexture2.texture.needsUpdate = true;
|
|
99
|
+
frame.close();
|
|
100
|
+
decoder.close();
|
|
101
|
+
return animTexture2.texture;
|
|
102
|
+
}
|
|
103
|
+
const firstResult = await decoder.decode({ frameIndex: 0 });
|
|
104
|
+
const firstFrame = firstResult.image;
|
|
105
|
+
const animTexture = new AnimatedTexture(
|
|
106
|
+
firstFrame.displayWidth,
|
|
107
|
+
firstFrame.displayHeight
|
|
108
|
+
);
|
|
109
|
+
animTexture.frameDurations = [];
|
|
110
|
+
animTexture.frameCache = [];
|
|
111
|
+
for (let i = 0; i < frameCount; i++) {
|
|
112
|
+
try {
|
|
113
|
+
const frameResult = await decoder.decode({ frameIndex: i });
|
|
114
|
+
const videoFrame = frameResult.image;
|
|
115
|
+
const bitmap = await createImageBitmap(videoFrame);
|
|
116
|
+
animTexture.frameCache.push(bitmap);
|
|
117
|
+
const durationMs = (videoFrame.duration ?? 1e5) / 1e3;
|
|
118
|
+
animTexture.frameDurations.push(durationMs);
|
|
119
|
+
videoFrame.close();
|
|
120
|
+
} catch {
|
|
121
|
+
animTexture.frameDurations.push(100);
|
|
122
|
+
if (animTexture.frameCache.length > 0) {
|
|
123
|
+
animTexture.frameCache.push(
|
|
124
|
+
animTexture.frameCache[animTexture.frameCache.length - 1]
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
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;
|
|
135
|
+
animTexture.lastFrameTime = performance.now();
|
|
136
|
+
firstFrame.close();
|
|
137
|
+
decoder.close();
|
|
138
|
+
return animTexture;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.warn(
|
|
141
|
+
"Failed to load animated texture, falling back to static:",
|
|
142
|
+
error
|
|
143
|
+
);
|
|
144
|
+
return loadStaticTexture(url);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Updates the texture to the current animation frame based on elapsed time.
|
|
149
|
+
* This method is SYNCHRONOUS for optimal render performance - all frames
|
|
150
|
+
* are pre-decoded during initialization.
|
|
151
|
+
*/
|
|
152
|
+
tick() {
|
|
153
|
+
if (!this.isAnimated || this.disposed || this.frameCache.length === 0) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const now = performance.now();
|
|
157
|
+
const elapsed = now - this.lastFrameTime;
|
|
158
|
+
const currentFrameDuration = this.frameDurations[this.frameIndex] || 100;
|
|
159
|
+
if (elapsed >= currentFrameDuration) {
|
|
160
|
+
this.frameIndex = (this.frameIndex + 1) % this.frameCount;
|
|
161
|
+
this.lastFrameTime = now;
|
|
162
|
+
const frame = this.frameCache[this.frameIndex];
|
|
163
|
+
if (frame) {
|
|
164
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
165
|
+
this.ctx.drawImage(frame, 0, 0);
|
|
166
|
+
this.texture.needsUpdate = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Disposes of resources used by this animated texture.
|
|
172
|
+
*/
|
|
173
|
+
dispose() {
|
|
174
|
+
this.disposed = true;
|
|
175
|
+
for (const bitmap of this.frameCache) {
|
|
176
|
+
bitmap.close();
|
|
177
|
+
}
|
|
178
|
+
this.frameCache = [];
|
|
179
|
+
this.texture.dispose();
|
|
180
|
+
}
|
|
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
|
+
}
|
|
204
|
+
var AnimatedTextureManager = class {
|
|
205
|
+
constructor() {
|
|
206
|
+
this.animatedTextures = [];
|
|
207
|
+
}
|
|
208
|
+
register(texture) {
|
|
209
|
+
if (texture instanceof AnimatedTexture) {
|
|
210
|
+
this.animatedTextures.push(texture);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
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
|
+
}
|
|
220
|
+
}
|
|
221
|
+
dispose() {
|
|
222
|
+
for (const texture of this.animatedTextures) {
|
|
223
|
+
texture.dispose();
|
|
224
|
+
}
|
|
225
|
+
this.animatedTextures = [];
|
|
226
|
+
}
|
|
227
|
+
};
|
|
38
228
|
var BARRE1_TYPE = 99991;
|
|
39
229
|
var BARRE2_TYPE = 99992;
|
|
40
230
|
var BARRE3_TYPE = 99993;
|
|
@@ -106,6 +296,7 @@ var _ChartPreview = class {
|
|
|
106
296
|
const chartPreview = new _ChartPreview();
|
|
107
297
|
chartPreview.instrumentType = getInstrumentType(config.instrument);
|
|
108
298
|
chartPreview.highwayTexture = config.textures.highwayTexture;
|
|
299
|
+
chartPreview.animatedTextureManager = config.textures.animatedTextureManager;
|
|
109
300
|
chartPreview.progressIntervalMs = config.progressIntervalMs ?? DEFAULT_PROGRESS_INTERVAL_MS;
|
|
110
301
|
chartPreview.camera = new ChartCamera(config.container);
|
|
111
302
|
chartPreview.renderer = new ChartRenderer(config.container);
|
|
@@ -190,6 +381,7 @@ var _ChartPreview = class {
|
|
|
190
381
|
this.renderer.dispose();
|
|
191
382
|
this.renderer.forceContextLoss();
|
|
192
383
|
this.audioManager.closeAudio();
|
|
384
|
+
this.animatedTextureManager.dispose();
|
|
193
385
|
}
|
|
194
386
|
addHighwayToScene(highwayTexture) {
|
|
195
387
|
const mat = new THREE.MeshBasicMaterial({ map: highwayTexture });
|
|
@@ -225,6 +417,7 @@ var _ChartPreview = class {
|
|
|
225
417
|
this.notesManager.updateDisplayedNotes(
|
|
226
418
|
this.audioManager.chartCurrentTimeMs
|
|
227
419
|
);
|
|
420
|
+
this.animatedTextureManager.tick();
|
|
228
421
|
const scrollPosition = -0.9 * (this.audioManager.chartCurrentTimeMs / 1e3) * (HIGHWAY_DURATION_MS / 1e3);
|
|
229
422
|
this.highwayTexture.offset.y = -1 * scrollPosition;
|
|
230
423
|
this.renderer.render(this.scene, this.camera);
|
|
@@ -626,11 +819,16 @@ var NotesManager = class {
|
|
|
626
819
|
}
|
|
627
820
|
for (let i = maxNoteEventIndex + 1; this.noteEvents[i] && this.noteEvents[i].msTime < renderEndTimeMs; i++) {
|
|
628
821
|
const note = this.noteEvents[i];
|
|
822
|
+
const material = this.noteMaterials.get(note.type)?.get(note.flags);
|
|
823
|
+
if (!material) {
|
|
824
|
+
console.warn(
|
|
825
|
+
`Missing texture for note type ${note.type} with flags ${note.flags}`
|
|
826
|
+
);
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
629
829
|
const noteGroup = new THREE.Group();
|
|
630
830
|
const scale = note.type === noteTypes.kick ? 0.045 : note.type === noteTypes.open && this.instrumentType === instrumentTypes.sixFret ? 0.04 : SCALE;
|
|
631
|
-
const sprite = new THREE.Sprite(
|
|
632
|
-
this.noteMaterials.get(note.type).get(note.flags)
|
|
633
|
-
);
|
|
831
|
+
const sprite = new THREE.Sprite(material);
|
|
634
832
|
noteGroup.add(sprite);
|
|
635
833
|
sprite.center = new THREE.Vector2(
|
|
636
834
|
note.type === noteTypes.kick ? 0.62 : 0.5,
|
|
@@ -707,9 +905,23 @@ var EventSequence = class {
|
|
|
707
905
|
return earliestActiveEventIndex === null ? this.lastPrecedingEventIndex + 1 : earliestActiveEventIndex;
|
|
708
906
|
}
|
|
709
907
|
};
|
|
710
|
-
async function loadTextures(instrumentType) {
|
|
908
|
+
async function loadTextures(instrumentType, options = {}) {
|
|
909
|
+
const { animationsEnabled = true } = options;
|
|
711
910
|
const textureLoader = new THREE.TextureLoader();
|
|
911
|
+
const animatedTextureManager = new AnimatedTextureManager();
|
|
712
912
|
const load = (path) => textureLoader.loadAsync("https://static.enchor.us/" + path);
|
|
913
|
+
const loadAnimated = async (path) => {
|
|
914
|
+
const url = `https://static.enchor.us/preview-${path}.webp`;
|
|
915
|
+
if (!animationsEnabled) {
|
|
916
|
+
return loadStaticTexture(url);
|
|
917
|
+
}
|
|
918
|
+
const result = await AnimatedTexture.create(url);
|
|
919
|
+
if (result instanceof AnimatedTexture) {
|
|
920
|
+
animatedTextureManager.register(result);
|
|
921
|
+
return result.texture;
|
|
922
|
+
}
|
|
923
|
+
return result;
|
|
924
|
+
};
|
|
713
925
|
const [highwayTexture, strikelineTexture, noteTextures] = await Promise.all([
|
|
714
926
|
(async () => {
|
|
715
927
|
const texture = await load("preview-highway.png");
|
|
@@ -731,9 +943,7 @@ async function loadTextures(instrumentType) {
|
|
|
731
943
|
(async () => {
|
|
732
944
|
const texturePromises = [];
|
|
733
945
|
const addTexture = (type, flags, path) => {
|
|
734
|
-
const texture =
|
|
735
|
-
`https://static.enchor.us/preview-${path}.webp`
|
|
736
|
-
);
|
|
946
|
+
const texture = loadAnimated(path);
|
|
737
947
|
texturePromises.push({ type, flags, texture });
|
|
738
948
|
return texture;
|
|
739
949
|
};
|
|
@@ -898,7 +1108,8 @@ async function loadTextures(instrumentType) {
|
|
|
898
1108
|
return {
|
|
899
1109
|
highwayTexture,
|
|
900
1110
|
strikelineTexture,
|
|
901
|
-
noteTextures
|
|
1111
|
+
noteTextures,
|
|
1112
|
+
animatedTextureManager
|
|
902
1113
|
};
|
|
903
1114
|
}
|
|
904
1115
|
function adjustParsedChart(parsedChart, instrument, difficulty) {
|
|
@@ -1149,7 +1360,7 @@ function isVideoFile(fileName) {
|
|
|
1149
1360
|
const ext = fileName.toLowerCase().slice(fileName.lastIndexOf("."));
|
|
1150
1361
|
return videoExtensions.includes(ext);
|
|
1151
1362
|
}
|
|
1152
|
-
async function prepareChartData(files, instrument, difficulty, initialSeekPercent = 0) {
|
|
1363
|
+
async function prepareChartData(files, instrument, difficulty, initialSeekPercent = 0, textureOptions = {}) {
|
|
1153
1364
|
const filteredFiles = files.filter((f) => !isVideoFile(f.fileName));
|
|
1154
1365
|
const scannedChart = scanChartFolder(filteredFiles);
|
|
1155
1366
|
if (!scannedChart.playable || !scannedChart.notesData) {
|
|
@@ -1184,7 +1395,10 @@ async function prepareChartData(files, instrument, difficulty, initialSeekPercen
|
|
|
1184
1395
|
}
|
|
1185
1396
|
const audioFiles = findAudioFiles(filteredFiles);
|
|
1186
1397
|
const instrumentType = getInstrumentType2(instrument);
|
|
1187
|
-
const textures = await ChartPreview.loadTextures(
|
|
1398
|
+
const textures = await ChartPreview.loadTextures(
|
|
1399
|
+
instrumentType,
|
|
1400
|
+
textureOptions
|
|
1401
|
+
);
|
|
1188
1402
|
const lastNote = parsedChart.trackData.flatMap((t) => t.noteEventGroups.flat()).reduce((max, note) => Math.max(max, note.msTime + note.msLength), 0);
|
|
1189
1403
|
const audioLengthMs = scannedChart.song_length || lastNote + 5e3;
|
|
1190
1404
|
const startDelayMs = (scannedChart.delay || 0) + (scannedChart.chart_offset || 0) * 1e3;
|
|
@@ -1505,10 +1719,11 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1505
1719
|
}
|
|
1506
1720
|
};
|
|
1507
1721
|
this.handleFullscreenChange = () => {
|
|
1508
|
-
|
|
1722
|
+
const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement;
|
|
1723
|
+
this._isFullscreen = fullscreenElement !== null && fullscreenElement !== void 0;
|
|
1509
1724
|
this.container.classList.toggle("fullscreen", this._isFullscreen);
|
|
1510
1725
|
this.fullscreenBtn.innerHTML = this._isFullscreen ? ICONS.fullscreenExit : ICONS.fullscreen;
|
|
1511
|
-
this.fullscreenBtn.title = this._isFullscreen ? "Exit fullscreen" : "
|
|
1726
|
+
this.fullscreenBtn.title = this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen";
|
|
1512
1727
|
};
|
|
1513
1728
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
1514
1729
|
this.render();
|
|
@@ -1533,6 +1748,10 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1533
1748
|
connectedCallback() {
|
|
1534
1749
|
this.setAttribute("tabindex", "0");
|
|
1535
1750
|
document.addEventListener("fullscreenchange", this.handleFullscreenChange);
|
|
1751
|
+
document.addEventListener(
|
|
1752
|
+
"webkitfullscreenchange",
|
|
1753
|
+
this.handleFullscreenChange
|
|
1754
|
+
);
|
|
1536
1755
|
}
|
|
1537
1756
|
disconnectedCallback() {
|
|
1538
1757
|
this.dispose();
|
|
@@ -1540,6 +1759,10 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1540
1759
|
"fullscreenchange",
|
|
1541
1760
|
this.handleFullscreenChange
|
|
1542
1761
|
);
|
|
1762
|
+
document.removeEventListener(
|
|
1763
|
+
"webkitfullscreenchange",
|
|
1764
|
+
this.handleFullscreenChange
|
|
1765
|
+
);
|
|
1543
1766
|
this.removeEventListener("keydown", this.handleKeydown);
|
|
1544
1767
|
}
|
|
1545
1768
|
render() {
|
|
@@ -1620,7 +1843,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1620
1843
|
this.onSeekEnd();
|
|
1621
1844
|
}
|
|
1622
1845
|
});
|
|
1623
|
-
this.fullscreenBtn.addEventListener("click", () => {
|
|
1846
|
+
this.fullscreenBtn.addEventListener("click", (e) => {
|
|
1847
|
+
e.stopPropagation();
|
|
1624
1848
|
this.toggleFullscreen();
|
|
1625
1849
|
});
|
|
1626
1850
|
this.addEventListener("keydown", this.handleKeydown);
|
|
@@ -1933,7 +2157,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1933
2157
|
files,
|
|
1934
2158
|
config.instrument,
|
|
1935
2159
|
config.difficulty,
|
|
1936
|
-
config.initialSeekPercent
|
|
2160
|
+
config.initialSeekPercent,
|
|
2161
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
1937
2162
|
);
|
|
1938
2163
|
await this.loadChart({
|
|
1939
2164
|
parsedChart: preparedData.parsedChart,
|
|
@@ -1975,7 +2200,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1975
2200
|
files,
|
|
1976
2201
|
config.instrument,
|
|
1977
2202
|
config.difficulty,
|
|
1978
|
-
config.initialSeekPercent
|
|
2203
|
+
config.initialSeekPercent,
|
|
2204
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
1979
2205
|
);
|
|
1980
2206
|
await this.loadChart({
|
|
1981
2207
|
parsedChart: preparedData.parsedChart,
|
|
@@ -2014,7 +2240,8 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
2014
2240
|
config.files,
|
|
2015
2241
|
config.instrument,
|
|
2016
2242
|
config.difficulty,
|
|
2017
|
-
config.initialSeekPercent
|
|
2243
|
+
config.initialSeekPercent,
|
|
2244
|
+
{ animationsEnabled: config.animationsEnabled }
|
|
2018
2245
|
);
|
|
2019
2246
|
await this.loadChart({
|
|
2020
2247
|
parsedChart: preparedData.parsedChart,
|
|
@@ -2114,12 +2341,28 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
2114
2341
|
* Toggle fullscreen mode
|
|
2115
2342
|
*/
|
|
2116
2343
|
async toggleFullscreen() {
|
|
2117
|
-
|
|
2344
|
+
const doc = document;
|
|
2345
|
+
const container = this.container;
|
|
2346
|
+
const isFullscreenEnabled = doc.fullscreenEnabled || doc.webkitFullscreenEnabled;
|
|
2347
|
+
if (!isFullscreenEnabled)
|
|
2118
2348
|
return;
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2349
|
+
const fullscreenElement = doc.fullscreenElement || doc.webkitFullscreenElement;
|
|
2350
|
+
try {
|
|
2351
|
+
if (fullscreenElement) {
|
|
2352
|
+
if (doc.exitFullscreen) {
|
|
2353
|
+
await doc.exitFullscreen();
|
|
2354
|
+
} else if (doc.webkitExitFullscreen) {
|
|
2355
|
+
doc.webkitExitFullscreen();
|
|
2356
|
+
}
|
|
2357
|
+
} else {
|
|
2358
|
+
if (container.requestFullscreen) {
|
|
2359
|
+
await container.requestFullscreen();
|
|
2360
|
+
} else if (container.webkitRequestFullscreen) {
|
|
2361
|
+
container.webkitRequestFullscreen();
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
} catch (error) {
|
|
2365
|
+
console.warn("Fullscreen toggle failed:", error);
|
|
2123
2366
|
}
|
|
2124
2367
|
}
|
|
2125
2368
|
/**
|
|
@@ -2154,6 +2397,7 @@ import {
|
|
|
2154
2397
|
export {
|
|
2155
2398
|
ChartPreview,
|
|
2156
2399
|
ChartPreviewPlayer,
|
|
2400
|
+
areAnimationsSupported,
|
|
2157
2401
|
extractSngFile,
|
|
2158
2402
|
fetchSngFile,
|
|
2159
2403
|
findAudioFiles,
|