chart-preview 1.2.0 → 1.3.1

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.
@@ -22,9 +22,14 @@ jobs:
22
22
  - name: Setup Node.js
23
23
  uses: actions/setup-node@v4
24
24
  with:
25
- node-version: 20
25
+ node-version: 24
26
26
  cache: "npm"
27
- registry-url: "https://registry.npmjs.org"
27
+
28
+ # npm trusted publishing needs npm >= 11.5.1; a cached Node 24.x can bundle an older npm.
29
+ - name: Install npm for trusted publishing
30
+ run: |
31
+ npm install -g npm@^11.5.1
32
+ npm --version
28
33
 
29
34
  - name: Install Dependencies
30
35
  run: npm ci
@@ -35,4 +40,3 @@ jobs:
35
40
  publish: npm run release
36
41
  env:
37
42
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # chart-preview
2
2
 
3
+ ## 1.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2175c5f: Draw five-fret open tap notes instead of skipping them. They use the open note texture with the tap marking of the other tap notes.
8
+
9
+ ## 1.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - fe294b3: Add manual resize functionality to ChartPreview and trigger resize in ChartPreviewPlayer on fullscreen change
14
+
3
15
  ## 1.2.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -180,6 +180,11 @@ declare class ChartPreview {
180
180
  get chartCurrentTimeMs(): number;
181
181
  /** Total duration in milliseconds */
182
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;
183
188
  /**
184
189
  * Cleans up all resources. Call this when discarding the preview.
185
190
  * The preview should not be used after calling dispose().
package/dist/index.js CHANGED
@@ -241,6 +241,44 @@ function createPlaceholderTexture() {
241
241
  texture.colorSpace = THREE.SRGBColorSpace;
242
242
  return texture;
243
243
  }
244
+ function createTapTexture(strumTexture) {
245
+ const rimPx = 8;
246
+ const rimFadePx = 6;
247
+ const image = strumTexture.image;
248
+ const canvas = document.createElement("canvas");
249
+ canvas.width = image.width;
250
+ canvas.height = image.height;
251
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
252
+ ctx.drawImage(image, 0, 0);
253
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
254
+ const pixels = imageData.data;
255
+ const clamp01 = (value) => Math.min(Math.max(value, 0), 1);
256
+ const colorWeight = (i) => {
257
+ const max = Math.max(pixels[i], pixels[i + 1], pixels[i + 2]);
258
+ const min = Math.min(pixels[i], pixels[i + 1], pixels[i + 2]);
259
+ const saturation = max === 0 ? 0 : (max - min) / max;
260
+ return pixels[i + 3] === 0 ? 0 : clamp01((saturation - 0.2) / 0.3);
261
+ };
262
+ for (let y = 0; y < canvas.height; y++) {
263
+ let runStart = 0;
264
+ for (let x = 0; x <= canvas.width; x++) {
265
+ if (x < canvas.width && colorWeight((y * canvas.width + x) * 4) > 0) {
266
+ continue;
267
+ }
268
+ for (let runX = runStart; runX < x; runX++) {
269
+ const distanceFromEnd = Math.min(runX - runStart, x - 1 - runX) + 1;
270
+ const rim = clamp01((rimPx + rimFadePx - distanceFromEnd) / rimFadePx);
271
+ const i = (y * canvas.width + runX) * 4;
272
+ pixels[i + 3] *= 1 - colorWeight(i) * (1 - rim);
273
+ }
274
+ runStart = x + 1;
275
+ }
276
+ }
277
+ ctx.putImageData(imageData, 0, 0);
278
+ const texture = new THREE.CanvasTexture(canvas);
279
+ texture.colorSpace = strumTexture.colorSpace;
280
+ return texture;
281
+ }
244
282
  var AnimatedTextureManager = class {
245
283
  constructor() {
246
284
  this.animatedTextures = [];
@@ -409,6 +447,17 @@ var _ChartPreview = class {
409
447
  get chartEndTimeMs() {
410
448
  return this.audioManager.chartEndTimeMs;
411
449
  }
450
+ /**
451
+ * Manually triggers a resize of the camera and renderer.
452
+ * Call this when the container size changes (e.g., entering/exiting fullscreen).
453
+ */
454
+ resize() {
455
+ this.camera.triggerResize();
456
+ this.renderer.triggerResize();
457
+ if (this.paused) {
458
+ this.animateFrame(false);
459
+ }
460
+ }
412
461
  /**
413
462
  * Cleans up all resources. Call this when discarding the preview.
414
463
  * The preview should not be used after calling dispose().
@@ -496,6 +545,10 @@ var ChartCamera = class extends THREE.PerspectiveCamera {
496
545
  this.aspect = width / height;
497
546
  this.updateProjectionMatrix();
498
547
  }
548
+ /** Manually trigger a resize update */
549
+ triggerResize() {
550
+ this.onResize();
551
+ }
499
552
  dispose() {
500
553
  window.removeEventListener("resize", this.resizeListener);
501
554
  this.clear();
@@ -518,6 +571,10 @@ var ChartRenderer = class extends THREE.WebGLRenderer {
518
571
  const height = this.divContainer.offsetHeight ?? window.innerHeight;
519
572
  this.setSize(width, height);
520
573
  }
574
+ /** Manually trigger a resize update */
575
+ triggerResize() {
576
+ this.onResize();
577
+ }
521
578
  dispose() {
522
579
  window.removeEventListener("resize", this.resizeListener);
523
580
  super.dispose();
@@ -1102,26 +1159,33 @@ async function loadTextures(instrumentType, options = {}) {
1102
1159
  [import_scan_chart.noteTypes.blue, "blue"],
1103
1160
  [import_scan_chart.noteTypes.orange, "orange"]
1104
1161
  ]);
1105
- const modifiers = /* @__PURE__ */ new Map([
1106
- [import_scan_chart.noteFlags.strum, "-strum"],
1107
- [import_scan_chart.noteFlags.hopo, "-hopo"],
1108
- [import_scan_chart.noteFlags.tap, "-tap"]
1109
- ]);
1110
1162
  const spFlags = /* @__PURE__ */ new Map([
1111
1163
  [import_scan_chart.noteFlags.none, ""],
1112
1164
  [SP_FLAG, "-sp"]
1113
1165
  ]);
1114
1166
  for (const [laneKey, laneName] of lanes) {
1115
- for (let [modifierKey, modifierName] of modifiers) {
1116
- for (const [spFlagKey, spFlagName] of spFlags) {
1117
- if (laneKey === import_scan_chart.noteTypes.open && modifierKey === import_scan_chart.noteFlags.tap) {
1118
- modifierName = "-hopo";
1119
- modifierKey = import_scan_chart.noteFlags.hopo;
1120
- }
1167
+ for (const [spFlagKey, spFlagName] of spFlags) {
1168
+ const strumTexture = addTexture(
1169
+ laneKey,
1170
+ import_scan_chart.noteFlags.strum | spFlagKey,
1171
+ `5fret-${laneName}-strum${spFlagName}`
1172
+ );
1173
+ addTexture(
1174
+ laneKey,
1175
+ import_scan_chart.noteFlags.hopo | spFlagKey,
1176
+ `5fret-${laneName}-hopo${spFlagName}`
1177
+ );
1178
+ if (laneKey === import_scan_chart.noteTypes.open) {
1179
+ reuseTexture(
1180
+ laneKey,
1181
+ import_scan_chart.noteFlags.tap | spFlagKey,
1182
+ strumTexture.then(createTapTexture)
1183
+ );
1184
+ } else {
1121
1185
  addTexture(
1122
1186
  laneKey,
1123
- modifierKey | spFlagKey,
1124
- `5fret-${laneName}${modifierName}${spFlagName}`
1187
+ import_scan_chart.noteFlags.tap | spFlagKey,
1188
+ `5fret-${laneName}-tap${spFlagName}`
1125
1189
  );
1126
1190
  }
1127
1191
  }
@@ -1760,6 +1824,13 @@ var ChartPreviewPlayer = class extends HTMLElement {
1760
1824
  this.container.classList.toggle("fullscreen", this._isFullscreen);
1761
1825
  this.fullscreenBtn.innerHTML = this._isFullscreen ? ICONS.fullscreenExit : ICONS.fullscreen;
1762
1826
  this.fullscreenBtn.title = this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen";
1827
+ if (this.chartPreview) {
1828
+ requestAnimationFrame(() => {
1829
+ requestAnimationFrame(() => {
1830
+ this.chartPreview?.resize();
1831
+ });
1832
+ });
1833
+ }
1763
1834
  };
1764
1835
  this.shadow = this.attachShadow({ mode: "open" });
1765
1836
  this.render();
package/dist/index.mjs CHANGED
@@ -201,6 +201,44 @@ function createPlaceholderTexture() {
201
201
  texture.colorSpace = THREE.SRGBColorSpace;
202
202
  return texture;
203
203
  }
204
+ function createTapTexture(strumTexture) {
205
+ const rimPx = 8;
206
+ const rimFadePx = 6;
207
+ const image = strumTexture.image;
208
+ const canvas = document.createElement("canvas");
209
+ canvas.width = image.width;
210
+ canvas.height = image.height;
211
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
212
+ ctx.drawImage(image, 0, 0);
213
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
214
+ const pixels = imageData.data;
215
+ const clamp01 = (value) => Math.min(Math.max(value, 0), 1);
216
+ const colorWeight = (i) => {
217
+ const max = Math.max(pixels[i], pixels[i + 1], pixels[i + 2]);
218
+ const min = Math.min(pixels[i], pixels[i + 1], pixels[i + 2]);
219
+ const saturation = max === 0 ? 0 : (max - min) / max;
220
+ return pixels[i + 3] === 0 ? 0 : clamp01((saturation - 0.2) / 0.3);
221
+ };
222
+ for (let y = 0; y < canvas.height; y++) {
223
+ let runStart = 0;
224
+ for (let x = 0; x <= canvas.width; x++) {
225
+ if (x < canvas.width && colorWeight((y * canvas.width + x) * 4) > 0) {
226
+ continue;
227
+ }
228
+ for (let runX = runStart; runX < x; runX++) {
229
+ const distanceFromEnd = Math.min(runX - runStart, x - 1 - runX) + 1;
230
+ const rim = clamp01((rimPx + rimFadePx - distanceFromEnd) / rimFadePx);
231
+ const i = (y * canvas.width + runX) * 4;
232
+ pixels[i + 3] *= 1 - colorWeight(i) * (1 - rim);
233
+ }
234
+ runStart = x + 1;
235
+ }
236
+ }
237
+ ctx.putImageData(imageData, 0, 0);
238
+ const texture = new THREE.CanvasTexture(canvas);
239
+ texture.colorSpace = strumTexture.colorSpace;
240
+ return texture;
241
+ }
204
242
  var AnimatedTextureManager = class {
205
243
  constructor() {
206
244
  this.animatedTextures = [];
@@ -369,6 +407,17 @@ var _ChartPreview = class {
369
407
  get chartEndTimeMs() {
370
408
  return this.audioManager.chartEndTimeMs;
371
409
  }
410
+ /**
411
+ * Manually triggers a resize of the camera and renderer.
412
+ * Call this when the container size changes (e.g., entering/exiting fullscreen).
413
+ */
414
+ resize() {
415
+ this.camera.triggerResize();
416
+ this.renderer.triggerResize();
417
+ if (this.paused) {
418
+ this.animateFrame(false);
419
+ }
420
+ }
372
421
  /**
373
422
  * Cleans up all resources. Call this when discarding the preview.
374
423
  * The preview should not be used after calling dispose().
@@ -456,6 +505,10 @@ var ChartCamera = class extends THREE.PerspectiveCamera {
456
505
  this.aspect = width / height;
457
506
  this.updateProjectionMatrix();
458
507
  }
508
+ /** Manually trigger a resize update */
509
+ triggerResize() {
510
+ this.onResize();
511
+ }
459
512
  dispose() {
460
513
  window.removeEventListener("resize", this.resizeListener);
461
514
  this.clear();
@@ -478,6 +531,10 @@ var ChartRenderer = class extends THREE.WebGLRenderer {
478
531
  const height = this.divContainer.offsetHeight ?? window.innerHeight;
479
532
  this.setSize(width, height);
480
533
  }
534
+ /** Manually trigger a resize update */
535
+ triggerResize() {
536
+ this.onResize();
537
+ }
481
538
  dispose() {
482
539
  window.removeEventListener("resize", this.resizeListener);
483
540
  super.dispose();
@@ -1062,26 +1119,33 @@ async function loadTextures(instrumentType, options = {}) {
1062
1119
  [noteTypes.blue, "blue"],
1063
1120
  [noteTypes.orange, "orange"]
1064
1121
  ]);
1065
- const modifiers = /* @__PURE__ */ new Map([
1066
- [noteFlags.strum, "-strum"],
1067
- [noteFlags.hopo, "-hopo"],
1068
- [noteFlags.tap, "-tap"]
1069
- ]);
1070
1122
  const spFlags = /* @__PURE__ */ new Map([
1071
1123
  [noteFlags.none, ""],
1072
1124
  [SP_FLAG, "-sp"]
1073
1125
  ]);
1074
1126
  for (const [laneKey, laneName] of lanes) {
1075
- for (let [modifierKey, modifierName] of modifiers) {
1076
- for (const [spFlagKey, spFlagName] of spFlags) {
1077
- if (laneKey === noteTypes.open && modifierKey === noteFlags.tap) {
1078
- modifierName = "-hopo";
1079
- modifierKey = noteFlags.hopo;
1080
- }
1127
+ for (const [spFlagKey, spFlagName] of spFlags) {
1128
+ const strumTexture = addTexture(
1129
+ laneKey,
1130
+ noteFlags.strum | spFlagKey,
1131
+ `5fret-${laneName}-strum${spFlagName}`
1132
+ );
1133
+ addTexture(
1134
+ laneKey,
1135
+ noteFlags.hopo | spFlagKey,
1136
+ `5fret-${laneName}-hopo${spFlagName}`
1137
+ );
1138
+ if (laneKey === noteTypes.open) {
1139
+ reuseTexture(
1140
+ laneKey,
1141
+ noteFlags.tap | spFlagKey,
1142
+ strumTexture.then(createTapTexture)
1143
+ );
1144
+ } else {
1081
1145
  addTexture(
1082
1146
  laneKey,
1083
- modifierKey | spFlagKey,
1084
- `5fret-${laneName}${modifierName}${spFlagName}`
1147
+ noteFlags.tap | spFlagKey,
1148
+ `5fret-${laneName}-tap${spFlagName}`
1085
1149
  );
1086
1150
  }
1087
1151
  }
@@ -1724,6 +1788,13 @@ var ChartPreviewPlayer = class extends HTMLElement {
1724
1788
  this.container.classList.toggle("fullscreen", this._isFullscreen);
1725
1789
  this.fullscreenBtn.innerHTML = this._isFullscreen ? ICONS.fullscreenExit : ICONS.fullscreen;
1726
1790
  this.fullscreenBtn.title = this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen";
1791
+ if (this.chartPreview) {
1792
+ requestAnimationFrame(() => {
1793
+ requestAnimationFrame(() => {
1794
+ this.chartPreview?.resize();
1795
+ });
1796
+ });
1797
+ }
1727
1798
  };
1728
1799
  this.shadow = this.attachShadow({ mode: "open" });
1729
1800
  this.render();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chart-preview",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "author": "Geo",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "git+ssh://git@github.com/Geomitron/chart-preview.git"
29
+ "url": "git+https://github.com/Geomitron/chart-preview.git"
30
30
  },
31
31
  "bugs": {
32
32
  "url": "https://github.com/Geomitron/chart-preview/issues"
@@ -259,6 +259,56 @@ function createPlaceholderTexture(): THREE.Texture {
259
259
  return texture;
260
260
  }
261
261
 
262
+ /**
263
+ * Marks `strumTexture` the way the five-fret tap textures mark tap notes:
264
+ * each row of colored pixels is hollowed out, except for a rim at both ends of
265
+ * the row. With these values, the result closely matches each five-fret tap
266
+ * texture when this is applied to the matching strum texture.
267
+ */
268
+ function createTapTexture(strumTexture: THREE.Texture): THREE.Texture {
269
+ const rimPx = 8;
270
+ const rimFadePx = 6;
271
+ const image = strumTexture.image as HTMLCanvasElement | HTMLImageElement;
272
+ const canvas = document.createElement("canvas");
273
+ canvas.width = image.width;
274
+ canvas.height = image.height;
275
+ const ctx = canvas.getContext("2d", { willReadFrequently: true })!;
276
+ ctx.drawImage(image, 0, 0);
277
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
278
+ const pixels = imageData.data;
279
+ const clamp01 = (value: number) => Math.min(Math.max(value, 0), 1);
280
+
281
+ /** 0 for grey or transparent pixels, rising to 1 for strongly colored pixels. */
282
+ const colorWeight = (i: number) => {
283
+ const max = Math.max(pixels[i], pixels[i + 1], pixels[i + 2]);
284
+ const min = Math.min(pixels[i], pixels[i + 1], pixels[i + 2]);
285
+ const saturation = max === 0 ? 0 : (max - min) / max;
286
+ return pixels[i + 3] === 0 ? 0 : clamp01((saturation - 0.2) / 0.3);
287
+ };
288
+
289
+ for (let y = 0; y < canvas.height; y++) {
290
+ let runStart = 0;
291
+ for (let x = 0; x <= canvas.width; x++) {
292
+ if (x < canvas.width && colorWeight((y * canvas.width + x) * 4) > 0) {
293
+ continue;
294
+ }
295
+ // Pixels runStart to x - 1 are a run of colored pixels
296
+ for (let runX = runStart; runX < x; runX++) {
297
+ const distanceFromEnd = Math.min(runX - runStart, x - 1 - runX) + 1;
298
+ const rim = clamp01((rimPx + rimFadePx - distanceFromEnd) / rimFadePx);
299
+ const i = (y * canvas.width + runX) * 4;
300
+ pixels[i + 3] *= 1 - colorWeight(i) * (1 - rim);
301
+ }
302
+ runStart = x + 1;
303
+ }
304
+ }
305
+
306
+ ctx.putImageData(imageData, 0, 0);
307
+ const texture = new THREE.CanvasTexture(canvas);
308
+ texture.colorSpace = strumTexture.colorSpace;
309
+ return texture;
310
+ }
311
+
262
312
  /** Collection of animated textures that need to be ticked each frame */
263
313
  class AnimatedTextureManager {
264
314
  private animatedTextures: AnimatedTexture[] = [];
@@ -509,6 +559,19 @@ export class ChartPreview {
509
559
  return this.audioManager.chartEndTimeMs;
510
560
  }
511
561
 
562
+ /**
563
+ * Manually triggers a resize of the camera and renderer.
564
+ * Call this when the container size changes (e.g., entering/exiting fullscreen).
565
+ */
566
+ resize() {
567
+ this.camera.triggerResize();
568
+ this.renderer.triggerResize();
569
+ // Re-render a frame to immediately show the updated size
570
+ if (this.paused) {
571
+ this.animateFrame(false);
572
+ }
573
+ }
574
+
512
575
  /**
513
576
  * Cleans up all resources. Call this when discarding the preview.
514
577
  * The preview should not be used after calling dispose().
@@ -618,6 +681,11 @@ class ChartCamera extends THREE.PerspectiveCamera {
618
681
  this.updateProjectionMatrix();
619
682
  }
620
683
 
684
+ /** Manually trigger a resize update */
685
+ triggerResize() {
686
+ this.onResize();
687
+ }
688
+
621
689
  dispose() {
622
690
  window.removeEventListener("resize", this.resizeListener);
623
691
  this.clear();
@@ -642,6 +710,11 @@ class ChartRenderer extends THREE.WebGLRenderer {
642
710
  this.setSize(width, height);
643
711
  }
644
712
 
713
+ /** Manually trigger a resize update */
714
+ triggerResize() {
715
+ this.onResize();
716
+ }
717
+
645
718
  override dispose() {
646
719
  window.removeEventListener("resize", this.resizeListener);
647
720
  super.dispose();
@@ -1459,27 +1532,35 @@ async function loadTextures(
1459
1532
  [noteTypes.blue, "blue"],
1460
1533
  [noteTypes.orange, "orange"],
1461
1534
  ]);
1462
- const modifiers = new Map([
1463
- [noteFlags.strum, "-strum"],
1464
- [noteFlags.hopo, "-hopo"],
1465
- [noteFlags.tap, "-tap"],
1466
- ]);
1467
1535
  const spFlags = new Map([
1468
1536
  [noteFlags.none, ""],
1469
1537
  [SP_FLAG, "-sp"],
1470
1538
  ]);
1471
1539
 
1472
1540
  for (const [laneKey, laneName] of lanes) {
1473
- for (let [modifierKey, modifierName] of modifiers) {
1474
- for (const [spFlagKey, spFlagName] of spFlags) {
1475
- if (laneKey === noteTypes.open && modifierKey === noteFlags.tap) {
1476
- modifierName = "-hopo";
1477
- modifierKey = noteFlags.hopo;
1478
- }
1541
+ for (const [spFlagKey, spFlagName] of spFlags) {
1542
+ const strumTexture = addTexture(
1543
+ laneKey,
1544
+ noteFlags.strum | spFlagKey,
1545
+ `5fret-${laneName}-strum${spFlagName}`
1546
+ );
1547
+ addTexture(
1548
+ laneKey,
1549
+ noteFlags.hopo | spFlagKey,
1550
+ `5fret-${laneName}-hopo${spFlagName}`
1551
+ );
1552
+ if (laneKey === noteTypes.open) {
1553
+ // There is no open tap texture
1554
+ reuseTexture(
1555
+ laneKey,
1556
+ noteFlags.tap | spFlagKey,
1557
+ strumTexture.then(createTapTexture)
1558
+ );
1559
+ } else {
1479
1560
  addTexture(
1480
1561
  laneKey,
1481
- modifierKey | spFlagKey,
1482
- `5fret-${laneName}${modifierName}${spFlagName}`
1562
+ noteFlags.tap | spFlagKey,
1563
+ `5fret-${laneName}-tap${spFlagName}`
1483
1564
  );
1484
1565
  }
1485
1566
  }
@@ -549,6 +549,18 @@ export class ChartPreviewPlayer extends HTMLElement {
549
549
  this.fullscreenBtn.title = this._isFullscreen
550
550
  ? "Exit fullscreen"
551
551
  : "Enter fullscreen";
552
+
553
+ // Trigger resize on the chart preview after a short delay
554
+ // to ensure the container dimensions have updated
555
+ if (this.chartPreview) {
556
+ // Use requestAnimationFrame to ensure DOM has updated
557
+ requestAnimationFrame(() => {
558
+ // Double RAF to ensure layout has been recalculated
559
+ requestAnimationFrame(() => {
560
+ this.chartPreview?.resize();
561
+ });
562
+ });
563
+ }
552
564
  };
553
565
 
554
566
  private onSeekStart() {