@waveform-playlist/ui-components 13.0.1 → 13.1.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.
package/dist/index.d.mts CHANGED
@@ -142,6 +142,14 @@ interface ChannelProps {
142
142
  barWidth?: number;
143
143
  /** Spacing in pixels between waveform bars. Default: 0 */
144
144
  barGap?: number;
145
+ /**
146
+ * Draw bars with pill-shaped rounded caps (radius barWidth/2). Most visible
147
+ * with barWidth >= 3 and a non-zero barGap. In 'inverted' draw mode the
148
+ * canvas paints the complement around the rounded bars, so gap columns and
149
+ * regions without peak data are covered by waveOutlineColor (square inverted
150
+ * mode leaves them transparent). Default: false
151
+ */
152
+ roundedBars?: boolean;
145
153
  /** If true, background is transparent (for use with external progress overlay) */
146
154
  transparentBackground?: boolean;
147
155
  /**
@@ -354,15 +362,16 @@ interface PlayheadProps {
354
362
  getAudioContextTime?: () => number;
355
363
  /**
356
364
  * Returns raw playback time from the engine (auto-wraps at loop boundaries).
357
- * This is the scheduling position — for playhead display use `visualTimeRef`
358
- * which already has `outputLatency` and `lookAhead` subtracted.
365
+ * This is the scheduling position — for playhead display use `visualTimeRef`,
366
+ * which is latency-compensated while playing.
359
367
  */
360
368
  getPlaybackTime?: () => number;
361
369
  /**
362
- * Ref to the visually-aligned playback time (raw time minus `outputLatency`
363
- * and `engine.lookAhead`), kept current by the provider's animation loop
364
- * during playback and by pause/stop/seek paths when stopped. Use this for
365
- * playhead positioning so it lines up with audible output and matches the
370
+ * Ref to the visually-aligned playback time: engine-compensated audible time
371
+ * (raw minus `outputLatency` and `engine.lookAhead`) while playing, the raw
372
+ * commanded position when resting (seek/pause/stop). Kept current by the
373
+ * provider's animation loop during playback and by pause/stop/seek paths
374
+ * when stopped. Use this for playhead positioning so it matches the
366
375
  * progress fill in `ChannelWithProgress`.
367
376
  */
368
377
  visualTimeRef?: react__default.RefObject<number>;
@@ -781,6 +790,8 @@ type PlaylistInfo = {
781
790
  barWidth: number;
782
791
  /** Spacing in pixels between waveform bars. Default: 0 */
783
792
  barGap: number;
793
+ /** Draw bars with pill-shaped rounded caps (radius barWidth/2). Default: false */
794
+ roundedBars?: boolean;
784
795
  /** Width in pixels of progress bars. Default: barWidth + barGap (fills gaps). Set to barWidth for no gap fill. */
785
796
  progressBarWidth?: number;
786
797
  };
package/dist/index.d.ts CHANGED
@@ -142,6 +142,14 @@ interface ChannelProps {
142
142
  barWidth?: number;
143
143
  /** Spacing in pixels between waveform bars. Default: 0 */
144
144
  barGap?: number;
145
+ /**
146
+ * Draw bars with pill-shaped rounded caps (radius barWidth/2). Most visible
147
+ * with barWidth >= 3 and a non-zero barGap. In 'inverted' draw mode the
148
+ * canvas paints the complement around the rounded bars, so gap columns and
149
+ * regions without peak data are covered by waveOutlineColor (square inverted
150
+ * mode leaves them transparent). Default: false
151
+ */
152
+ roundedBars?: boolean;
145
153
  /** If true, background is transparent (for use with external progress overlay) */
146
154
  transparentBackground?: boolean;
147
155
  /**
@@ -354,15 +362,16 @@ interface PlayheadProps {
354
362
  getAudioContextTime?: () => number;
355
363
  /**
356
364
  * Returns raw playback time from the engine (auto-wraps at loop boundaries).
357
- * This is the scheduling position — for playhead display use `visualTimeRef`
358
- * which already has `outputLatency` and `lookAhead` subtracted.
365
+ * This is the scheduling position — for playhead display use `visualTimeRef`,
366
+ * which is latency-compensated while playing.
359
367
  */
360
368
  getPlaybackTime?: () => number;
361
369
  /**
362
- * Ref to the visually-aligned playback time (raw time minus `outputLatency`
363
- * and `engine.lookAhead`), kept current by the provider's animation loop
364
- * during playback and by pause/stop/seek paths when stopped. Use this for
365
- * playhead positioning so it lines up with audible output and matches the
370
+ * Ref to the visually-aligned playback time: engine-compensated audible time
371
+ * (raw minus `outputLatency` and `engine.lookAhead`) while playing, the raw
372
+ * commanded position when resting (seek/pause/stop). Kept current by the
373
+ * provider's animation loop during playback and by pause/stop/seek paths
374
+ * when stopped. Use this for playhead positioning so it matches the
366
375
  * progress fill in `ChannelWithProgress`.
367
376
  */
368
377
  visualTimeRef?: react__default.RefObject<number>;
@@ -781,6 +790,8 @@ type PlaylistInfo = {
781
790
  barWidth: number;
782
791
  /** Spacing in pixels between waveform bars. Default: 0 */
783
792
  barGap: number;
793
+ /** Draw bars with pill-shaped rounded caps (radius barWidth/2). Default: false */
794
+ roundedBars?: boolean;
784
795
  /** Width in pixels of progress bars. Default: barWidth + barGap (fills gaps). Set to barWidth for no gap fill. */
785
796
  progressBarWidth?: number;
786
797
  };
package/dist/index.js CHANGED
@@ -833,6 +833,13 @@ function calculateBarRects(x, barWidth, halfHeight, minPeak, maxPeak, drawMode)
833
833
  { x, y: halfHeight + min, width: barWidth, height: halfHeight - min }
834
834
  ];
835
835
  }
836
+ function addBarToPath(ctx, rect, radius) {
837
+ if (typeof ctx.roundRect === "function") {
838
+ ctx.roundRect(rect.x, rect.y, rect.width, rect.height, radius);
839
+ } else {
840
+ ctx.rect(rect.x, rect.y, rect.width, rect.height);
841
+ }
842
+ }
836
843
  function calculateFirstBarPosition(canvasStartGlobal, barWidth, step) {
837
844
  return Math.floor((canvasStartGlobal - barWidth + step) / step) * step;
838
845
  }
@@ -893,6 +900,7 @@ var Channel = (props) => {
893
900
  waveFillColor = "grey",
894
901
  barWidth = 1,
895
902
  barGap = 0,
903
+ roundedBars = false,
896
904
  transparentBackground = false,
897
905
  drawMode = "inverted"
898
906
  } = props;
@@ -905,7 +913,7 @@ var Channel = (props) => {
905
913
  dataVersionRef.current += 1;
906
914
  prevDataRef.current = data;
907
915
  }
908
- const drawVersion = `${dataVersionRef.current}-${bits}-${waveHeight}-${devicePixelRatio}-${length}-${barWidth}-${barGap}-${drawMode}-${waveformColorToCss(waveOutlineColor)}-${waveformColorToCss(waveFillColor)}`;
916
+ const drawVersion = `${dataVersionRef.current}-${bits}-${waveHeight}-${devicePixelRatio}-${length}-${barWidth}-${barGap}-${drawMode}-${roundedBars}-${waveformColorToCss(waveOutlineColor)}-${waveformColorToCss(waveFillColor)}`;
909
917
  (0, import_react4.useLayoutEffect)(() => {
910
918
  const step = barWidth + barGap;
911
919
  for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
@@ -929,18 +937,36 @@ var Channel = (props) => {
929
937
  const canvasStartGlobal = globalPixelOffset;
930
938
  const canvasEndGlobal = globalPixelOffset + canvasWidth;
931
939
  const firstBarGlobal = calculateFirstBarPosition(canvasStartGlobal, barWidth, step);
940
+ const rectMode = roundedBars ? "normal" : drawMode;
941
+ const barRadius = barWidth / 2;
942
+ const complementFill = roundedBars && drawMode === "inverted";
943
+ if (roundedBars) {
944
+ ctx.beginPath();
945
+ }
946
+ if (complementFill) {
947
+ ctx.rect(0, 0, canvasWidth, waveHeight);
948
+ }
932
949
  for (let barGlobal = Math.max(0, firstBarGlobal); barGlobal < canvasEndGlobal; barGlobal += step) {
933
950
  const x = barGlobal - canvasStartGlobal;
934
951
  if (x + barWidth <= 0) continue;
935
952
  const peakEnd = Math.min(barGlobal + step, length);
936
953
  const peak = aggregatePeaks(data, bits, barGlobal, peakEnd);
937
954
  if (peak) {
938
- const rects = calculateBarRects(x, barWidth, h2, peak.min, peak.max, drawMode);
955
+ const rects = calculateBarRects(x, barWidth, h2, peak.min, peak.max, rectMode);
939
956
  for (const rect of rects) {
940
- ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
957
+ if (roundedBars) {
958
+ addBarToPath(ctx, rect, barRadius);
959
+ } else {
960
+ ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
961
+ }
941
962
  }
942
963
  }
943
964
  }
965
+ if (complementFill) {
966
+ ctx.fill("evenodd");
967
+ } else if (roundedBars) {
968
+ ctx.fill();
969
+ }
944
970
  canvas.dataset.drawVersion = drawVersion;
945
971
  }
946
972
  }
@@ -955,6 +981,7 @@ var Channel = (props) => {
955
981
  length,
956
982
  barWidth,
957
983
  barGap,
984
+ roundedBars,
958
985
  drawVersion,
959
986
  drawMode,
960
987
  visibleChunkIndices,
@@ -2374,7 +2401,8 @@ var PlaylistInfoContext = (0, import_react16.createContext)({
2374
2401
  },
2375
2402
  duration: 3e4,
2376
2403
  barWidth: 1,
2377
- barGap: 0
2404
+ barGap: 0,
2405
+ roundedBars: false
2378
2406
  });
2379
2407
  var usePlaylistInfo = () => (0, import_react16.useContext)(PlaylistInfoContext);
2380
2408
 
@@ -2485,12 +2513,12 @@ var SpectrogramChannel = ({
2485
2513
  const register = onCanvasRegisterRef.current;
2486
2514
  const remaining = [];
2487
2515
  for (const id of registeredIdsRef.current) {
2488
- const match = id.match(/chunk(\d+)$/);
2489
- if (!match) {
2516
+ const parsed = (0, import_core5.parseSpectrogramCanvasId)(id);
2517
+ if (!parsed) {
2490
2518
  remaining.push(id);
2491
2519
  continue;
2492
2520
  }
2493
- const chunkIdx = parseInt(match[1], 10);
2521
+ const chunkIdx = parsed.chunkIndex;
2494
2522
  const canvas = canvasMapRef.current.get(chunkIdx);
2495
2523
  if (canvas && canvas.isConnected) {
2496
2524
  remaining.push(id);
@@ -2505,7 +2533,7 @@ var SpectrogramChannel = ({
2505
2533
  registeredIdsRef.current = remaining;
2506
2534
  for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
2507
2535
  if (transferredCanvasesRef.current.has(canvas)) continue;
2508
- const canvasId = `${clipId}-ch${channelIndex}-chunk${canvasIdx}`;
2536
+ const canvasId = (0, import_core5.buildSpectrogramCanvasId)({ clipId, channelIndex, chunkIndex: canvasIdx });
2509
2537
  let offscreen;
2510
2538
  try {
2511
2539
  offscreen = canvas.transferControlToOffscreen();
@@ -2584,6 +2612,7 @@ var SmartChannel = ({
2584
2612
  waveHeight,
2585
2613
  barWidth,
2586
2614
  barGap,
2615
+ roundedBars,
2587
2616
  samplesPerPixel: contextSpp,
2588
2617
  sampleRate: contextSampleRate
2589
2618
  } = usePlaylistInfo();
@@ -2645,6 +2674,7 @@ var SmartChannel = ({
2645
2674
  devicePixelRatio,
2646
2675
  barWidth,
2647
2676
  barGap,
2677
+ roundedBars,
2648
2678
  transparentBackground,
2649
2679
  drawMode
2650
2680
  }
@@ -2683,6 +2713,7 @@ var SmartChannel = ({
2683
2713
  devicePixelRatio,
2684
2714
  barWidth,
2685
2715
  barGap,
2716
+ roundedBars,
2686
2717
  transparentBackground,
2687
2718
  drawMode
2688
2719
  }