@remotion/timeline-utils 4.0.529 → 4.0.531

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.
@@ -1,8 +1,9 @@
1
1
  export type WaveformVolume = number | readonly number[];
2
- export declare const drawBars: ({ canvas, color, peaks, volume, width, }: {
2
+ export declare const drawBars: ({ canvas, color, peaks, volume, width, horizontalOffset, }: {
3
3
  readonly canvas: HTMLCanvasElement | OffscreenCanvas;
4
4
  readonly peaks: Float32Array<ArrayBufferLike>;
5
5
  readonly color: string;
6
6
  readonly volume: WaveformVolume;
7
7
  readonly width: number;
8
+ readonly horizontalOffset: number;
8
9
  }) => void;
@@ -4,6 +4,7 @@ exports.drawBars = void 0;
4
4
  const parse_color_1 = require("./parse-color");
5
5
  const CLIPPING_COLOR = '#FF7F50';
6
6
  const WAVEFORM_HEIGHT_SCALE = 0.8;
7
+ const MIN_VISIBLE_DECIBELS = -37;
7
8
  const getVolumeAtBar = ({ barIndex, numBars, volume, }) => {
8
9
  var _a;
9
10
  if (typeof volume === 'number') {
@@ -18,7 +19,7 @@ const getVolumeAtBar = ({ barIndex, numBars, volume, }) => {
18
19
  const volumeIndex = Math.round((barIndex / (numBars - 1)) * (volume.length - 1));
19
20
  return (_a = volume[volumeIndex]) !== null && _a !== void 0 ? _a : 1;
20
21
  };
21
- const drawBars = ({ canvas, color, peaks, volume, width, }) => {
22
+ const drawBars = ({ canvas, color, peaks, volume, width, horizontalOffset, }) => {
22
23
  const ctx = canvas.getContext('2d');
23
24
  if (!ctx) {
24
25
  throw new Error('Failed to get canvas context');
@@ -29,7 +30,7 @@ const drawBars = ({ canvas, color, peaks, volume, width, }) => {
29
30
  // `createImageData(0, h)` / `(w, 0)` throws a DOMException, which
30
31
  // surfaces in Studio's console for compositions with many audio
31
32
  // sequences — some segments are 0 px wide at certain zoom levels.
32
- if (w === 0 || height === 0) {
33
+ if (w === 0 || height === 0 || width <= 0) {
33
34
  return;
34
35
  }
35
36
  ctx.clearRect(0, 0, w, height);
@@ -39,15 +40,17 @@ const drawBars = ({ canvas, color, peaks, volume, width, }) => {
39
40
  const { data } = imageData;
40
41
  const numBars = width;
41
42
  const fullScaleHalfBar = (height * WAVEFORM_HEIGHT_SCALE) / 2;
42
- for (let barIndex = 0; barIndex < numBars; barIndex++) {
43
- const x = barIndex;
44
- if (x >= w)
45
- break;
43
+ for (let x = 0; x < w; x++) {
44
+ // The canvas may start before its fractionally positioned container so its
45
+ // bitmap stays device-pixel aligned. Sample from the exact container origin.
46
+ const barIndex = Math.max(0, Math.min(numBars - Number.EPSILON * Math.max(1, numBars), x - horizontalOffset));
46
47
  const peakIndex = Math.floor((barIndex / numBars) * peaks.length);
47
48
  const peak = peaks[peakIndex] || 0;
48
49
  const barVolume = getVolumeAtBar({ barIndex, numBars, volume });
49
50
  const scaledPeak = peak * barVolume;
50
- const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * fullScaleHalfBar));
51
+ const decibelPeak = scaledPeak <= 0 ? MIN_VISIBLE_DECIBELS : 20 * Math.log10(scaledPeak);
52
+ const visualPeak = Math.max(0, (decibelPeak - MIN_VISIBLE_DECIBELS) / -MIN_VISIBLE_DECIBELS);
53
+ const halfBar = Math.max(0, Math.min(height / 2, visualPeak * fullScaleHalfBar));
51
54
  if (halfBar === 0)
52
55
  continue;
53
56
  const mid = height / 2;
@@ -0,0 +1,14 @@
1
+ export declare const getDevicePixelAlignedCanvasLayout: ({ cssWidth, cssHeight, containerLeft, devicePixelRatio, }: {
2
+ readonly cssWidth: number;
3
+ readonly cssHeight: number;
4
+ readonly containerLeft: number;
5
+ readonly devicePixelRatio: number;
6
+ }) => {
7
+ height: number;
8
+ horizontalOffset: number;
9
+ width: number;
10
+ };
11
+ export declare const snapCanvasPositionToDevicePixel: ({ position, horizontalOffset, }: {
12
+ readonly position: number;
13
+ readonly horizontalOffset: number;
14
+ }) => number;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.snapCanvasPositionToDevicePixel = exports.getDevicePixelAlignedCanvasLayout = void 0;
4
+ const getDevicePixelAlignedCanvasLayout = ({ cssWidth, cssHeight, containerLeft, devicePixelRatio, }) => {
5
+ const containerLeftInPixels = containerLeft * devicePixelRatio;
6
+ const horizontalOffset = cssWidth === 0
7
+ ? 0
8
+ : containerLeftInPixels - Math.floor(containerLeftInPixels);
9
+ return {
10
+ height: Math.ceil(cssHeight * devicePixelRatio),
11
+ horizontalOffset,
12
+ width: Math.ceil(cssWidth * devicePixelRatio + horizontalOffset),
13
+ };
14
+ };
15
+ exports.getDevicePixelAlignedCanvasLayout = getDevicePixelAlignedCanvasLayout;
16
+ const snapCanvasPositionToDevicePixel = ({ position, horizontalOffset, }) => {
17
+ return Math.floor(position + horizontalOffset) - horizontalOffset;
18
+ };
19
+ exports.snapCanvasPositionToDevicePixel = snapCanvasPositionToDevicePixel;
@@ -18,6 +18,7 @@ var parseColor = (color) => {
18
18
  // src/audio-waveform/draw-peaks.ts
19
19
  var CLIPPING_COLOR = "#FF7F50";
20
20
  var WAVEFORM_HEIGHT_SCALE = 0.8;
21
+ var MIN_VISIBLE_DECIBELS = -37;
21
22
  var getVolumeAtBar = ({
22
23
  barIndex,
23
24
  numBars,
@@ -40,7 +41,8 @@ var drawBars = ({
40
41
  color,
41
42
  peaks,
42
43
  volume,
43
- width
44
+ width,
45
+ horizontalOffset
44
46
  }) => {
45
47
  const ctx = canvas.getContext("2d");
46
48
  if (!ctx) {
@@ -48,7 +50,7 @@ var drawBars = ({
48
50
  }
49
51
  const { height } = canvas;
50
52
  const w = canvas.width;
51
- if (w === 0 || height === 0) {
53
+ if (w === 0 || height === 0 || width <= 0) {
52
54
  return;
53
55
  }
54
56
  ctx.clearRect(0, 0, w, height);
@@ -58,15 +60,15 @@ var drawBars = ({
58
60
  const { data } = imageData;
59
61
  const numBars = width;
60
62
  const fullScaleHalfBar = height * WAVEFORM_HEIGHT_SCALE / 2;
61
- for (let barIndex = 0;barIndex < numBars; barIndex++) {
62
- const x = barIndex;
63
- if (x >= w)
64
- break;
63
+ for (let x = 0;x < w; x++) {
64
+ const barIndex = Math.max(0, Math.min(numBars - Number.EPSILON * Math.max(1, numBars), x - horizontalOffset));
65
65
  const peakIndex = Math.floor(barIndex / numBars * peaks.length);
66
66
  const peak = peaks[peakIndex] || 0;
67
67
  const barVolume = getVolumeAtBar({ barIndex, numBars, volume });
68
68
  const scaledPeak = peak * barVolume;
69
- const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * fullScaleHalfBar));
69
+ const decibelPeak = scaledPeak <= 0 ? MIN_VISIBLE_DECIBELS : 20 * Math.log10(scaledPeak);
70
+ const visualPeak = Math.max(0, (decibelPeak - MIN_VISIBLE_DECIBELS) / -MIN_VISIBLE_DECIBELS);
71
+ const halfBar = Math.max(0, Math.min(height / 2, visualPeak * fullScaleHalfBar));
70
72
  if (halfBar === 0)
71
73
  continue;
72
74
  const mid = height / 2;
@@ -618,6 +620,27 @@ var clampTimestampsToDuration = ({
618
620
  const lastTimestamp = Math.max(0, durationInSeconds - LAST_FRAME_EPSILON_IN_SECONDS);
619
621
  return timestamps.map((timestamp) => Math.min(timestamp, lastTimestamp));
620
622
  };
623
+ // src/device-pixel-alignment.ts
624
+ var getDevicePixelAlignedCanvasLayout = ({
625
+ cssWidth,
626
+ cssHeight,
627
+ containerLeft,
628
+ devicePixelRatio
629
+ }) => {
630
+ const containerLeftInPixels = containerLeft * devicePixelRatio;
631
+ const horizontalOffset = cssWidth === 0 ? 0 : containerLeftInPixels - Math.floor(containerLeftInPixels);
632
+ return {
633
+ height: Math.ceil(cssHeight * devicePixelRatio),
634
+ horizontalOffset,
635
+ width: Math.ceil(cssWidth * devicePixelRatio + horizontalOffset)
636
+ };
637
+ };
638
+ var snapCanvasPositionToDevicePixel = ({
639
+ position,
640
+ horizontalOffset
641
+ }) => {
642
+ return Math.floor(position + horizontalOffset) - horizontalOffset;
643
+ };
621
644
  // src/extract-frames.ts
622
645
  import {
623
646
  ALL_FORMATS as ALL_FORMATS2,
@@ -855,7 +878,8 @@ var drawSlot = ({
855
878
  segmentDuration,
856
879
  fromSeconds,
857
880
  devicePixelRatio,
858
- frameHeight
881
+ frameHeight,
882
+ horizontalOffset
859
883
  }) => {
860
884
  const durationOfOneFrame = getDurationOfOneFrame({
861
885
  visualizationWidth,
@@ -866,8 +890,17 @@ var drawSlot = ({
866
890
  const relativeTimestamp = timestamp - fromSeconds * WEBCODECS_TIMESCALE;
867
891
  const frameIndex = relativeTimestamp / durationOfOneFrame;
868
892
  const thumbnailWidth = frame.displayWidth / devicePixelRatio;
869
- const left = frameIndex * thumbnailWidth;
870
- ctx.drawImage(frame, left, 0, thumbnailWidth, frame.displayHeight / devicePixelRatio);
893
+ const idealLeft = frameIndex * thumbnailWidth;
894
+ const idealRight = idealLeft + thumbnailWidth;
895
+ const left = snapCanvasPositionToDevicePixel({
896
+ horizontalOffset,
897
+ position: idealLeft
898
+ });
899
+ const right = snapCanvasPositionToDevicePixel({
900
+ horizontalOffset,
901
+ position: idealRight
902
+ });
903
+ ctx.drawImage(frame, left, 0, right - left, frame.displayHeight / devicePixelRatio);
871
904
  filledSlots.set(timestamp, frame.timestamp);
872
905
  };
873
906
  var fillWithCachedFrames = ({
@@ -878,7 +911,8 @@ var fillWithCachedFrames = ({
878
911
  segmentDuration,
879
912
  fromSeconds,
880
913
  devicePixelRatio,
881
- frameHeight
914
+ frameHeight,
915
+ horizontalOffset
882
916
  }) => {
883
917
  const prefix = getFrameDatabaseKeyPrefix(src);
884
918
  const keys = Array.from(frameDatabase.keys()).filter((k) => k.startsWith(prefix));
@@ -914,7 +948,8 @@ var fillWithCachedFrames = ({
914
948
  segmentDuration,
915
949
  fromSeconds,
916
950
  devicePixelRatio,
917
- frameHeight
951
+ frameHeight,
952
+ horizontalOffset
918
953
  });
919
954
  }
920
955
  };
@@ -926,7 +961,8 @@ var fillFrameWhereItFits = ({
926
961
  segmentDuration,
927
962
  fromSeconds,
928
963
  devicePixelRatio,
929
- frameHeight
964
+ frameHeight,
965
+ horizontalOffset
930
966
  }) => {
931
967
  const slots = Array.from(filledSlots.keys());
932
968
  for (let i = 0;i < slots.length; i++) {
@@ -947,7 +983,8 @@ var fillFrameWhereItFits = ({
947
983
  segmentDuration,
948
984
  fromSeconds,
949
985
  devicePixelRatio,
950
- frameHeight
986
+ frameHeight,
987
+ horizontalOffset
951
988
  });
952
989
  }
953
990
  };
@@ -1009,9 +1046,11 @@ var renderFrameStripToCanvas = async ({
1009
1046
  width,
1010
1047
  frameHeight,
1011
1048
  devicePixelRatio,
1049
+ horizontalOffset,
1012
1050
  signal
1013
1051
  }) => {
1014
- const naturalWidth = Math.max(1, Math.ceil(width));
1052
+ const drawingWidth = Math.max(1, width);
1053
+ const naturalWidth = Math.max(1, Math.ceil(width + horizontalOffset));
1015
1054
  const naturalHeight = Math.max(1, Math.ceil(frameHeight));
1016
1055
  const segmentDuration = toSeconds - fromSeconds;
1017
1056
  canvas.width = naturalWidth;
@@ -1021,12 +1060,13 @@ var renderFrameStripToCanvas = async ({
1021
1060
  }
1022
1061
  const ctx = getContext(canvas);
1023
1062
  ctx.clearRect(0, 0, naturalWidth, naturalHeight);
1063
+ ctx.setTransform(1, 0, 0, 1, horizontalOffset, 0);
1024
1064
  const filledSlots = new Map;
1025
1065
  let aspectRatio = getAspectRatioFromCache(src);
1026
1066
  if (aspectRatio !== null) {
1027
1067
  ensureSlots({
1028
1068
  filledSlots,
1029
- naturalWidth,
1069
+ naturalWidth: drawingWidth,
1030
1070
  fromSeconds,
1031
1071
  toSeconds,
1032
1072
  aspectRatio,
@@ -1034,13 +1074,14 @@ var renderFrameStripToCanvas = async ({
1034
1074
  });
1035
1075
  fillWithCachedFrames({
1036
1076
  ctx,
1037
- naturalWidth,
1077
+ naturalWidth: drawingWidth,
1038
1078
  filledSlots,
1039
1079
  src,
1040
1080
  segmentDuration,
1041
1081
  fromSeconds,
1042
1082
  devicePixelRatio,
1043
- frameHeight
1083
+ frameHeight,
1084
+ horizontalOffset
1044
1085
  });
1045
1086
  const hasUnfilledSlots = Array.from(filledSlots.values()).some((value) => value === undefined);
1046
1087
  if (!hasUnfilledSlots) {
@@ -1055,7 +1096,7 @@ var renderFrameStripToCanvas = async ({
1055
1096
  aspectRatioCache.set(src, aspectRatio);
1056
1097
  ensureSlots({
1057
1098
  filledSlots,
1058
- naturalWidth,
1099
+ naturalWidth: drawingWidth,
1059
1100
  fromSeconds,
1060
1101
  toSeconds,
1061
1102
  aspectRatio,
@@ -1079,7 +1120,7 @@ var renderFrameStripToCanvas = async ({
1079
1120
  }
1080
1121
  ensureSlots({
1081
1122
  filledSlots,
1082
- naturalWidth,
1123
+ naturalWidth: drawingWidth,
1083
1124
  fromSeconds,
1084
1125
  toSeconds,
1085
1126
  aspectRatio,
@@ -1088,12 +1129,13 @@ var renderFrameStripToCanvas = async ({
1088
1129
  fillFrameWhereItFits({
1089
1130
  ctx,
1090
1131
  filledSlots,
1091
- visualizationWidth: naturalWidth,
1132
+ visualizationWidth: drawingWidth,
1092
1133
  frame: transformed,
1093
1134
  segmentDuration,
1094
1135
  fromSeconds,
1095
1136
  devicePixelRatio,
1096
- frameHeight
1137
+ frameHeight,
1138
+ horizontalOffset
1097
1139
  });
1098
1140
  } catch (e) {
1099
1141
  if (frame) {
@@ -1110,13 +1152,14 @@ var renderFrameStripToCanvas = async ({
1110
1152
  }
1111
1153
  fillWithCachedFrames({
1112
1154
  ctx,
1113
- naturalWidth,
1155
+ naturalWidth: drawingWidth,
1114
1156
  filledSlots,
1115
1157
  src,
1116
1158
  segmentDuration,
1117
1159
  fromSeconds,
1118
1160
  devicePixelRatio,
1119
- frameHeight
1161
+ frameHeight,
1162
+ horizontalOffset
1120
1163
  });
1121
1164
  };
1122
1165
  // src/format-average-audio-volume.ts
@@ -1128,6 +1171,7 @@ var formatAverageAudioVolume = (volume) => {
1128
1171
  };
1129
1172
  export {
1130
1173
  subscribeToWaveformPeaks,
1174
+ snapCanvasPositionToDevicePixel,
1131
1175
  sliceWaveformPeaks,
1132
1176
  sliceVisibleWaveformPeaks,
1133
1177
  shouldTileLoopDisplay,
@@ -1141,6 +1185,7 @@ export {
1141
1185
  getLoopDisplaySegments,
1142
1186
  getFrameDatabaseKeyPrefix,
1143
1187
  getDurationOfOneFrame,
1188
+ getDevicePixelAlignedCanvasLayout,
1144
1189
  getAspectRatioFromCache,
1145
1190
  frameDatabase,
1146
1191
  formatAverageAudioVolume,
@@ -0,0 +1,10 @@
1
+ type ImageWithNaturalDimensions = CanvasImageSource & {
2
+ readonly naturalWidth: number;
3
+ readonly naturalHeight: number;
4
+ };
5
+ export declare const drawRepeatingImageThumbnail: ({ canvas, image, offsetInPixels, }: {
6
+ readonly canvas: HTMLCanvasElement | OffscreenCanvas;
7
+ readonly image: ImageWithNaturalDimensions;
8
+ readonly offsetInPixels?: number | undefined;
9
+ }) => void;
10
+ export {};
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.drawRepeatingImageThumbnail = void 0;
4
+ const get_scaled_image_thumbnail_dimensions_1 = require("./get-scaled-image-thumbnail-dimensions");
5
+ const createOffscreenCanvas = (width, height) => {
6
+ if (typeof document !== 'undefined') {
7
+ const canvas = document.createElement('canvas');
8
+ canvas.width = width;
9
+ canvas.height = height;
10
+ return canvas;
11
+ }
12
+ if (typeof OffscreenCanvas !== 'undefined') {
13
+ return new OffscreenCanvas(width, height);
14
+ }
15
+ throw new Error('No canvas implementation available');
16
+ };
17
+ const drawRepeatingImageThumbnail = ({ canvas, image, offsetInPixels = 0, }) => {
18
+ const ctx = canvas.getContext('2d');
19
+ if (!ctx) {
20
+ throw new Error('Failed to get canvas context');
21
+ }
22
+ const { width, height } = canvas;
23
+ if (width === 0 || height === 0) {
24
+ return;
25
+ }
26
+ const { scaledWidth, scaledHeight } = (0, get_scaled_image_thumbnail_dimensions_1.getScaledImageThumbnailDimensions)({
27
+ naturalWidth: image.naturalWidth,
28
+ naturalHeight: image.naturalHeight,
29
+ canvasHeight: height,
30
+ });
31
+ if (scaledWidth === 0 || scaledHeight === 0) {
32
+ return;
33
+ }
34
+ const offscreen = createOffscreenCanvas(scaledWidth, scaledHeight);
35
+ const offCtx = offscreen.getContext('2d');
36
+ if (!offCtx) {
37
+ throw new Error('Failed to get offscreen canvas context');
38
+ }
39
+ offCtx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
40
+ const pattern = ctx.createPattern(offscreen, 'repeat-x');
41
+ if (!pattern) {
42
+ return;
43
+ }
44
+ pattern.setTransform(new DOMMatrix().translate(-offsetInPixels, 0));
45
+ ctx.fillStyle = pattern;
46
+ ctx.fillRect(0, 0, width, height);
47
+ };
48
+ exports.drawRepeatingImageThumbnail = drawRepeatingImageThumbnail;
@@ -0,0 +1,8 @@
1
+ export declare const getScaledImageThumbnailDimensions: ({ naturalWidth, naturalHeight, canvasHeight, }: {
2
+ readonly naturalWidth: number;
3
+ readonly naturalHeight: number;
4
+ readonly canvasHeight: number;
5
+ }) => {
6
+ scaledWidth: number;
7
+ scaledHeight: number;
8
+ };
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getScaledImageThumbnailDimensions = void 0;
4
+ const getScaledImageThumbnailDimensions = ({ naturalWidth, naturalHeight, canvasHeight, }) => {
5
+ if (naturalWidth === 0 || naturalHeight === 0 || canvasHeight === 0) {
6
+ return {
7
+ scaledWidth: 0,
8
+ scaledHeight: 0,
9
+ };
10
+ }
11
+ const scale = canvasHeight / naturalHeight;
12
+ return {
13
+ scaledWidth: naturalWidth * scale,
14
+ scaledHeight: canvasHeight,
15
+ };
16
+ };
17
+ exports.getScaledImageThumbnailDimensions = getScaledImageThumbnailDimensions;
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export { subscribeToWaveformPeaks } from './audio-waveform/subscribe-to-waveform
8
8
  export { sliceWaveformPeaks } from './audio-waveform/slice-waveform-peaks';
9
9
  export { createWaveformPeakProcessor, emitWaveformProgress, } from './audio-waveform/waveform-peak-processor';
10
10
  export { clampTimestampsToDuration } from './clamp-timestamps-to-duration';
11
+ export { getDevicePixelAlignedCanvasLayout, snapCanvasPositionToDevicePixel, } from './device-pixel-alignment';
11
12
  export { extractFrames } from './extract-frames';
12
13
  export type { ExtractFramesProps, ExtractFramesTimestampsInSecondsFn, } from './extract-frames';
13
14
  export { addFrameToCache, aspectRatioCache, frameDatabase, getAspectRatioFromCache, getFrameDatabaseKeyPrefix, getTimestampFromFrameDatabaseKey, makeFrameDatabaseKey, } from './frame-database';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatAverageAudioVolume = exports.resizeVideoFrame = exports.renderFrameStripToCanvas = exports.getDurationOfOneFrame = exports.fillWithCachedFrames = exports.fillFrameWhereItFits = exports.ensureSlots = exports.drawSlot = exports.calculateTimestampSlots = exports.WEBCODECS_TIMESCALE = exports.shouldTileLoopDisplay = exports.getLoopDisplayWidth = exports.getLoopDisplaySegments = exports.makeFrameDatabaseKey = exports.getTimestampFromFrameDatabaseKey = exports.getFrameDatabaseKeyPrefix = exports.getAspectRatioFromCache = exports.frameDatabase = exports.aspectRatioCache = exports.addFrameToCache = exports.extractFrames = exports.clampTimestampsToDuration = exports.emitWaveformProgress = exports.createWaveformPeakProcessor = exports.sliceWaveformPeaks = exports.subscribeToWaveformPeaks = exports.sliceVisibleWaveformPeaks = exports.loadWaveformPeaks = exports.getVisibleWaveformVolume = exports.drawBars = exports.TARGET_SAMPLE_RATE = void 0;
3
+ exports.formatAverageAudioVolume = exports.resizeVideoFrame = exports.renderFrameStripToCanvas = exports.getDurationOfOneFrame = exports.fillWithCachedFrames = exports.fillFrameWhereItFits = exports.ensureSlots = exports.drawSlot = exports.calculateTimestampSlots = exports.WEBCODECS_TIMESCALE = exports.shouldTileLoopDisplay = exports.getLoopDisplayWidth = exports.getLoopDisplaySegments = exports.makeFrameDatabaseKey = exports.getTimestampFromFrameDatabaseKey = exports.getFrameDatabaseKeyPrefix = exports.getAspectRatioFromCache = exports.frameDatabase = exports.aspectRatioCache = exports.addFrameToCache = exports.extractFrames = exports.snapCanvasPositionToDevicePixel = exports.getDevicePixelAlignedCanvasLayout = exports.clampTimestampsToDuration = exports.emitWaveformProgress = exports.createWaveformPeakProcessor = exports.sliceWaveformPeaks = exports.subscribeToWaveformPeaks = exports.sliceVisibleWaveformPeaks = exports.loadWaveformPeaks = exports.getVisibleWaveformVolume = exports.drawBars = exports.TARGET_SAMPLE_RATE = void 0;
4
4
  const constants_1 = require("./audio-waveform/constants");
5
5
  Object.defineProperty(exports, "TARGET_SAMPLE_RATE", { enumerable: true, get: function () { return constants_1.TARGET_SAMPLE_RATE; } });
6
6
  const draw_peaks_1 = require("./audio-waveform/draw-peaks");
@@ -20,6 +20,9 @@ Object.defineProperty(exports, "createWaveformPeakProcessor", { enumerable: true
20
20
  Object.defineProperty(exports, "emitWaveformProgress", { enumerable: true, get: function () { return waveform_peak_processor_1.emitWaveformProgress; } });
21
21
  const clamp_timestamps_to_duration_1 = require("./clamp-timestamps-to-duration");
22
22
  Object.defineProperty(exports, "clampTimestampsToDuration", { enumerable: true, get: function () { return clamp_timestamps_to_duration_1.clampTimestampsToDuration; } });
23
+ const device_pixel_alignment_1 = require("./device-pixel-alignment");
24
+ Object.defineProperty(exports, "getDevicePixelAlignedCanvasLayout", { enumerable: true, get: function () { return device_pixel_alignment_1.getDevicePixelAlignedCanvasLayout; } });
25
+ Object.defineProperty(exports, "snapCanvasPositionToDevicePixel", { enumerable: true, get: function () { return device_pixel_alignment_1.snapCanvasPositionToDevicePixel; } });
23
26
  const extract_frames_1 = require("./extract-frames");
24
27
  Object.defineProperty(exports, "extractFrames", { enumerable: true, get: function () { return extract_frames_1.extractFrames; } });
25
28
  const frame_database_1 = require("./frame-database");
@@ -7,7 +7,8 @@ export type RenderFrameStripToCanvasOptions = {
7
7
  readonly width: number;
8
8
  readonly frameHeight: number;
9
9
  readonly devicePixelRatio: number;
10
+ readonly horizontalOffset: number;
10
11
  readonly signal?: AbortSignal;
11
12
  };
12
- export declare const renderFrameStripToCanvas: ({ canvas, src, fromSeconds, toSeconds, width, frameHeight, devicePixelRatio, signal, }: RenderFrameStripToCanvasOptions) => Promise<void>;
13
+ export declare const renderFrameStripToCanvas: ({ canvas, src, fromSeconds, toSeconds, width, frameHeight, devicePixelRatio, horizontalOffset, signal, }: RenderFrameStripToCanvasOptions) => Promise<void>;
13
14
  export {};
@@ -12,8 +12,9 @@ const getContext = (canvas) => {
12
12
  }
13
13
  return ctx;
14
14
  };
15
- const renderFrameStripToCanvas = async ({ canvas, src, fromSeconds, toSeconds, width, frameHeight, devicePixelRatio, signal, }) => {
16
- const naturalWidth = Math.max(1, Math.ceil(width));
15
+ const renderFrameStripToCanvas = async ({ canvas, src, fromSeconds, toSeconds, width, frameHeight, devicePixelRatio, horizontalOffset, signal, }) => {
16
+ const drawingWidth = Math.max(1, width);
17
+ const naturalWidth = Math.max(1, Math.ceil(width + horizontalOffset));
17
18
  const naturalHeight = Math.max(1, Math.ceil(frameHeight));
18
19
  const segmentDuration = toSeconds - fromSeconds;
19
20
  canvas.width = naturalWidth;
@@ -23,12 +24,13 @@ const renderFrameStripToCanvas = async ({ canvas, src, fromSeconds, toSeconds, w
23
24
  }
24
25
  const ctx = getContext(canvas);
25
26
  ctx.clearRect(0, 0, naturalWidth, naturalHeight);
27
+ ctx.setTransform(1, 0, 0, 1, horizontalOffset, 0);
26
28
  const filledSlots = new Map();
27
29
  let aspectRatio = (0, frame_database_1.getAspectRatioFromCache)(src);
28
30
  if (aspectRatio !== null) {
29
31
  (0, render_frame_strip_1.ensureSlots)({
30
32
  filledSlots,
31
- naturalWidth,
33
+ naturalWidth: drawingWidth,
32
34
  fromSeconds,
33
35
  toSeconds,
34
36
  aspectRatio,
@@ -36,13 +38,14 @@ const renderFrameStripToCanvas = async ({ canvas, src, fromSeconds, toSeconds, w
36
38
  });
37
39
  (0, render_frame_strip_1.fillWithCachedFrames)({
38
40
  ctx,
39
- naturalWidth,
41
+ naturalWidth: drawingWidth,
40
42
  filledSlots,
41
43
  src,
42
44
  segmentDuration,
43
45
  fromSeconds,
44
46
  devicePixelRatio,
45
47
  frameHeight,
48
+ horizontalOffset,
46
49
  });
47
50
  const hasUnfilledSlots = Array.from(filledSlots.values()).some((value) => value === undefined);
48
51
  if (!hasUnfilledSlots) {
@@ -57,7 +60,7 @@ const renderFrameStripToCanvas = async ({ canvas, src, fromSeconds, toSeconds, w
57
60
  frame_database_1.aspectRatioCache.set(src, aspectRatio);
58
61
  (0, render_frame_strip_1.ensureSlots)({
59
62
  filledSlots,
60
- naturalWidth,
63
+ naturalWidth: drawingWidth,
61
64
  fromSeconds,
62
65
  toSeconds,
63
66
  aspectRatio,
@@ -81,7 +84,7 @@ const renderFrameStripToCanvas = async ({ canvas, src, fromSeconds, toSeconds, w
81
84
  }
82
85
  (0, render_frame_strip_1.ensureSlots)({
83
86
  filledSlots,
84
- naturalWidth,
87
+ naturalWidth: drawingWidth,
85
88
  fromSeconds,
86
89
  toSeconds,
87
90
  aspectRatio,
@@ -90,12 +93,13 @@ const renderFrameStripToCanvas = async ({ canvas, src, fromSeconds, toSeconds, w
90
93
  (0, render_frame_strip_1.fillFrameWhereItFits)({
91
94
  ctx,
92
95
  filledSlots,
93
- visualizationWidth: naturalWidth,
96
+ visualizationWidth: drawingWidth,
94
97
  frame: transformed,
95
98
  segmentDuration,
96
99
  fromSeconds,
97
100
  devicePixelRatio,
98
101
  frameHeight,
102
+ horizontalOffset,
99
103
  });
100
104
  }
101
105
  catch (e) {
@@ -114,13 +118,14 @@ const renderFrameStripToCanvas = async ({ canvas, src, fromSeconds, toSeconds, w
114
118
  }
115
119
  (0, render_frame_strip_1.fillWithCachedFrames)({
116
120
  ctx,
117
- naturalWidth,
121
+ naturalWidth: drawingWidth,
118
122
  filledSlots,
119
123
  src,
120
124
  segmentDuration,
121
125
  fromSeconds,
122
126
  devicePixelRatio,
123
127
  frameHeight,
128
+ horizontalOffset,
124
129
  });
125
130
  };
126
131
  exports.renderFrameStripToCanvas = renderFrameStripToCanvas;
@@ -20,7 +20,7 @@ export declare const ensureSlots: ({ filledSlots, naturalWidth, fromSeconds, toS
20
20
  aspectRatio: number;
21
21
  frameHeight: number;
22
22
  }) => void;
23
- export declare const drawSlot: ({ frame, ctx, filledSlots, visualizationWidth, timestamp, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }: {
23
+ export declare const drawSlot: ({ frame, ctx, filledSlots, visualizationWidth, timestamp, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, horizontalOffset, }: {
24
24
  frame: VideoFrame;
25
25
  ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
26
26
  filledSlots: Map<number, number | undefined>;
@@ -30,8 +30,9 @@ export declare const drawSlot: ({ frame, ctx, filledSlots, visualizationWidth, t
30
30
  fromSeconds: number;
31
31
  devicePixelRatio: number;
32
32
  frameHeight: number;
33
+ horizontalOffset: number;
33
34
  }) => void;
34
- export declare const fillWithCachedFrames: ({ ctx, naturalWidth, filledSlots, src, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }: {
35
+ export declare const fillWithCachedFrames: ({ ctx, naturalWidth, filledSlots, src, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, horizontalOffset, }: {
35
36
  ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
36
37
  naturalWidth: number;
37
38
  filledSlots: Map<number, number | undefined>;
@@ -40,8 +41,9 @@ export declare const fillWithCachedFrames: ({ ctx, naturalWidth, filledSlots, sr
40
41
  fromSeconds: number;
41
42
  devicePixelRatio: number;
42
43
  frameHeight: number;
44
+ horizontalOffset: number;
43
45
  }) => void;
44
- export declare const fillFrameWhereItFits: ({ frame, filledSlots, ctx, visualizationWidth, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }: {
46
+ export declare const fillFrameWhereItFits: ({ frame, filledSlots, ctx, visualizationWidth, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, horizontalOffset, }: {
45
47
  frame: VideoFrame;
46
48
  filledSlots: Map<number, number | undefined>;
47
49
  ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
@@ -50,4 +52,5 @@ export declare const fillFrameWhereItFits: ({ frame, filledSlots, ctx, visualiza
50
52
  fromSeconds: number;
51
53
  devicePixelRatio: number;
52
54
  frameHeight: number;
55
+ horizontalOffset: number;
53
56
  }) => void;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fillFrameWhereItFits = exports.fillWithCachedFrames = exports.drawSlot = exports.ensureSlots = exports.calculateTimestampSlots = exports.getDurationOfOneFrame = exports.WEBCODECS_TIMESCALE = void 0;
4
+ const device_pixel_alignment_1 = require("./device-pixel-alignment");
4
5
  const frame_database_1 = require("./frame-database");
5
6
  exports.WEBCODECS_TIMESCALE = 1000000;
6
7
  const MAX_TIME_DEVIATION = exports.WEBCODECS_TIMESCALE * 0.05;
@@ -50,7 +51,7 @@ const ensureSlots = ({ filledSlots, naturalWidth, fromSeconds, toSeconds, aspect
50
51
  }
51
52
  };
52
53
  exports.ensureSlots = ensureSlots;
53
- const drawSlot = ({ frame, ctx, filledSlots, visualizationWidth, timestamp, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }) => {
54
+ const drawSlot = ({ frame, ctx, filledSlots, visualizationWidth, timestamp, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, horizontalOffset, }) => {
54
55
  const durationOfOneFrame = (0, exports.getDurationOfOneFrame)({
55
56
  visualizationWidth,
56
57
  aspectRatio: frame.displayWidth / frame.displayHeight,
@@ -60,12 +61,23 @@ const drawSlot = ({ frame, ctx, filledSlots, visualizationWidth, timestamp, segm
60
61
  const relativeTimestamp = timestamp - fromSeconds * exports.WEBCODECS_TIMESCALE;
61
62
  const frameIndex = relativeTimestamp / durationOfOneFrame;
62
63
  const thumbnailWidth = frame.displayWidth / devicePixelRatio;
63
- const left = frameIndex * thumbnailWidth;
64
- ctx.drawImage(frame, left, 0, thumbnailWidth, frame.displayHeight / devicePixelRatio);
64
+ const idealLeft = frameIndex * thumbnailWidth;
65
+ const idealRight = idealLeft + thumbnailWidth;
66
+ // When the canvas is aligned to the device-pixel grid, use the same absolute
67
+ // grid for both edges so adjacent thumbnails neither shimmer nor leave seams.
68
+ const left = (0, device_pixel_alignment_1.snapCanvasPositionToDevicePixel)({
69
+ horizontalOffset,
70
+ position: idealLeft,
71
+ });
72
+ const right = (0, device_pixel_alignment_1.snapCanvasPositionToDevicePixel)({
73
+ horizontalOffset,
74
+ position: idealRight,
75
+ });
76
+ ctx.drawImage(frame, left, 0, right - left, frame.displayHeight / devicePixelRatio);
65
77
  filledSlots.set(timestamp, frame.timestamp);
66
78
  };
67
79
  exports.drawSlot = drawSlot;
68
- const fillWithCachedFrames = ({ ctx, naturalWidth, filledSlots, src, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }) => {
80
+ const fillWithCachedFrames = ({ ctx, naturalWidth, filledSlots, src, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, horizontalOffset, }) => {
69
81
  const prefix = (0, frame_database_1.getFrameDatabaseKeyPrefix)(src);
70
82
  const keys = Array.from(frame_database_1.frameDatabase.keys()).filter((k) => k.startsWith(prefix));
71
83
  const targets = Array.from(filledSlots.keys());
@@ -103,11 +115,12 @@ const fillWithCachedFrames = ({ ctx, naturalWidth, filledSlots, src, segmentDura
103
115
  fromSeconds,
104
116
  devicePixelRatio,
105
117
  frameHeight,
118
+ horizontalOffset,
106
119
  });
107
120
  }
108
121
  };
109
122
  exports.fillWithCachedFrames = fillWithCachedFrames;
110
- const fillFrameWhereItFits = ({ frame, filledSlots, ctx, visualizationWidth, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }) => {
123
+ const fillFrameWhereItFits = ({ frame, filledSlots, ctx, visualizationWidth, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, horizontalOffset, }) => {
111
124
  const slots = Array.from(filledSlots.keys());
112
125
  for (let i = 0; i < slots.length; i++) {
113
126
  const slot = slots[i];
@@ -129,6 +142,7 @@ const fillFrameWhereItFits = ({ frame, filledSlots, ctx, visualizationWidth, seg
129
142
  fromSeconds,
130
143
  devicePixelRatio,
131
144
  frameHeight,
145
+ horizontalOffset,
132
146
  });
133
147
  }
134
148
  };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "url": "https://github.com/remotion-dev/remotion/tree/main/packages/timeline-utils"
4
4
  },
5
5
  "name": "@remotion/timeline-utils",
6
- "version": "4.0.529",
6
+ "version": "4.0.531",
7
7
  "description": "Internal utilities for rendering Remotion timelines",
8
8
  "main": "dist/index.js",
9
9
  "scripts": {
@@ -22,11 +22,11 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "mediabunny": "1.56.1",
25
- "remotion": "4.0.529"
25
+ "remotion": "4.0.531"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@mediabunny/server": "1.56.1",
29
- "@remotion/eslint-config-internal": "4.0.529",
29
+ "@remotion/eslint-config-internal": "4.0.531",
30
30
  "eslint": "9.19.0",
31
31
  "@typescript/native-preview": "7.0.0-dev.20260217.1"
32
32
  },