@remotion/timeline-utils 4.0.529 → 4.0.530

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;
@@ -18,7 +18,7 @@ const getVolumeAtBar = ({ barIndex, numBars, volume, }) => {
18
18
  const volumeIndex = Math.round((barIndex / (numBars - 1)) * (volume.length - 1));
19
19
  return (_a = volume[volumeIndex]) !== null && _a !== void 0 ? _a : 1;
20
20
  };
21
- const drawBars = ({ canvas, color, peaks, volume, width, }) => {
21
+ const drawBars = ({ canvas, color, peaks, volume, width, horizontalOffset, }) => {
22
22
  const ctx = canvas.getContext('2d');
23
23
  if (!ctx) {
24
24
  throw new Error('Failed to get canvas context');
@@ -29,7 +29,7 @@ const drawBars = ({ canvas, color, peaks, volume, width, }) => {
29
29
  // `createImageData(0, h)` / `(w, 0)` throws a DOMException, which
30
30
  // surfaces in Studio's console for compositions with many audio
31
31
  // sequences — some segments are 0 px wide at certain zoom levels.
32
- if (w === 0 || height === 0) {
32
+ if (w === 0 || height === 0 || width <= 0) {
33
33
  return;
34
34
  }
35
35
  ctx.clearRect(0, 0, w, height);
@@ -39,10 +39,10 @@ const drawBars = ({ canvas, color, peaks, volume, width, }) => {
39
39
  const { data } = imageData;
40
40
  const numBars = width;
41
41
  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;
42
+ for (let x = 0; x < w; x++) {
43
+ // The canvas may start before its fractionally positioned container so its
44
+ // bitmap stays device-pixel aligned. Sample from the exact container origin.
45
+ const barIndex = Math.max(0, Math.min(numBars - Number.EPSILON * Math.max(1, numBars), x - horizontalOffset));
46
46
  const peakIndex = Math.floor((barIndex / numBars) * peaks.length);
47
47
  const peak = peaks[peakIndex] || 0;
48
48
  const barVolume = getVolumeAtBar({ barIndex, numBars, volume });
@@ -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;
@@ -40,7 +40,8 @@ var drawBars = ({
40
40
  color,
41
41
  peaks,
42
42
  volume,
43
- width
43
+ width,
44
+ horizontalOffset
44
45
  }) => {
45
46
  const ctx = canvas.getContext("2d");
46
47
  if (!ctx) {
@@ -48,7 +49,7 @@ var drawBars = ({
48
49
  }
49
50
  const { height } = canvas;
50
51
  const w = canvas.width;
51
- if (w === 0 || height === 0) {
52
+ if (w === 0 || height === 0 || width <= 0) {
52
53
  return;
53
54
  }
54
55
  ctx.clearRect(0, 0, w, height);
@@ -58,10 +59,8 @@ var drawBars = ({
58
59
  const { data } = imageData;
59
60
  const numBars = width;
60
61
  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;
62
+ for (let x = 0;x < w; x++) {
63
+ const barIndex = Math.max(0, Math.min(numBars - Number.EPSILON * Math.max(1, numBars), x - horizontalOffset));
65
64
  const peakIndex = Math.floor(barIndex / numBars * peaks.length);
66
65
  const peak = peaks[peakIndex] || 0;
67
66
  const barVolume = getVolumeAtBar({ barIndex, numBars, volume });
@@ -618,6 +617,27 @@ var clampTimestampsToDuration = ({
618
617
  const lastTimestamp = Math.max(0, durationInSeconds - LAST_FRAME_EPSILON_IN_SECONDS);
619
618
  return timestamps.map((timestamp) => Math.min(timestamp, lastTimestamp));
620
619
  };
620
+ // src/device-pixel-alignment.ts
621
+ var getDevicePixelAlignedCanvasLayout = ({
622
+ cssWidth,
623
+ cssHeight,
624
+ containerLeft,
625
+ devicePixelRatio
626
+ }) => {
627
+ const containerLeftInPixels = containerLeft * devicePixelRatio;
628
+ const horizontalOffset = cssWidth === 0 ? 0 : containerLeftInPixels - Math.floor(containerLeftInPixels);
629
+ return {
630
+ height: Math.ceil(cssHeight * devicePixelRatio),
631
+ horizontalOffset,
632
+ width: Math.ceil(cssWidth * devicePixelRatio + horizontalOffset)
633
+ };
634
+ };
635
+ var snapCanvasPositionToDevicePixel = ({
636
+ position,
637
+ horizontalOffset
638
+ }) => {
639
+ return Math.floor(position + horizontalOffset) - horizontalOffset;
640
+ };
621
641
  // src/extract-frames.ts
622
642
  import {
623
643
  ALL_FORMATS as ALL_FORMATS2,
@@ -855,7 +875,8 @@ var drawSlot = ({
855
875
  segmentDuration,
856
876
  fromSeconds,
857
877
  devicePixelRatio,
858
- frameHeight
878
+ frameHeight,
879
+ horizontalOffset
859
880
  }) => {
860
881
  const durationOfOneFrame = getDurationOfOneFrame({
861
882
  visualizationWidth,
@@ -866,8 +887,17 @@ var drawSlot = ({
866
887
  const relativeTimestamp = timestamp - fromSeconds * WEBCODECS_TIMESCALE;
867
888
  const frameIndex = relativeTimestamp / durationOfOneFrame;
868
889
  const thumbnailWidth = frame.displayWidth / devicePixelRatio;
869
- const left = frameIndex * thumbnailWidth;
870
- ctx.drawImage(frame, left, 0, thumbnailWidth, frame.displayHeight / devicePixelRatio);
890
+ const idealLeft = frameIndex * thumbnailWidth;
891
+ const idealRight = idealLeft + thumbnailWidth;
892
+ const left = snapCanvasPositionToDevicePixel({
893
+ horizontalOffset,
894
+ position: idealLeft
895
+ });
896
+ const right = snapCanvasPositionToDevicePixel({
897
+ horizontalOffset,
898
+ position: idealRight
899
+ });
900
+ ctx.drawImage(frame, left, 0, right - left, frame.displayHeight / devicePixelRatio);
871
901
  filledSlots.set(timestamp, frame.timestamp);
872
902
  };
873
903
  var fillWithCachedFrames = ({
@@ -878,7 +908,8 @@ var fillWithCachedFrames = ({
878
908
  segmentDuration,
879
909
  fromSeconds,
880
910
  devicePixelRatio,
881
- frameHeight
911
+ frameHeight,
912
+ horizontalOffset
882
913
  }) => {
883
914
  const prefix = getFrameDatabaseKeyPrefix(src);
884
915
  const keys = Array.from(frameDatabase.keys()).filter((k) => k.startsWith(prefix));
@@ -914,7 +945,8 @@ var fillWithCachedFrames = ({
914
945
  segmentDuration,
915
946
  fromSeconds,
916
947
  devicePixelRatio,
917
- frameHeight
948
+ frameHeight,
949
+ horizontalOffset
918
950
  });
919
951
  }
920
952
  };
@@ -926,7 +958,8 @@ var fillFrameWhereItFits = ({
926
958
  segmentDuration,
927
959
  fromSeconds,
928
960
  devicePixelRatio,
929
- frameHeight
961
+ frameHeight,
962
+ horizontalOffset
930
963
  }) => {
931
964
  const slots = Array.from(filledSlots.keys());
932
965
  for (let i = 0;i < slots.length; i++) {
@@ -947,7 +980,8 @@ var fillFrameWhereItFits = ({
947
980
  segmentDuration,
948
981
  fromSeconds,
949
982
  devicePixelRatio,
950
- frameHeight
983
+ frameHeight,
984
+ horizontalOffset
951
985
  });
952
986
  }
953
987
  };
@@ -1009,9 +1043,11 @@ var renderFrameStripToCanvas = async ({
1009
1043
  width,
1010
1044
  frameHeight,
1011
1045
  devicePixelRatio,
1046
+ horizontalOffset,
1012
1047
  signal
1013
1048
  }) => {
1014
- const naturalWidth = Math.max(1, Math.ceil(width));
1049
+ const drawingWidth = Math.max(1, width);
1050
+ const naturalWidth = Math.max(1, Math.ceil(width + horizontalOffset));
1015
1051
  const naturalHeight = Math.max(1, Math.ceil(frameHeight));
1016
1052
  const segmentDuration = toSeconds - fromSeconds;
1017
1053
  canvas.width = naturalWidth;
@@ -1021,12 +1057,13 @@ var renderFrameStripToCanvas = async ({
1021
1057
  }
1022
1058
  const ctx = getContext(canvas);
1023
1059
  ctx.clearRect(0, 0, naturalWidth, naturalHeight);
1060
+ ctx.setTransform(1, 0, 0, 1, horizontalOffset, 0);
1024
1061
  const filledSlots = new Map;
1025
1062
  let aspectRatio = getAspectRatioFromCache(src);
1026
1063
  if (aspectRatio !== null) {
1027
1064
  ensureSlots({
1028
1065
  filledSlots,
1029
- naturalWidth,
1066
+ naturalWidth: drawingWidth,
1030
1067
  fromSeconds,
1031
1068
  toSeconds,
1032
1069
  aspectRatio,
@@ -1034,13 +1071,14 @@ var renderFrameStripToCanvas = async ({
1034
1071
  });
1035
1072
  fillWithCachedFrames({
1036
1073
  ctx,
1037
- naturalWidth,
1074
+ naturalWidth: drawingWidth,
1038
1075
  filledSlots,
1039
1076
  src,
1040
1077
  segmentDuration,
1041
1078
  fromSeconds,
1042
1079
  devicePixelRatio,
1043
- frameHeight
1080
+ frameHeight,
1081
+ horizontalOffset
1044
1082
  });
1045
1083
  const hasUnfilledSlots = Array.from(filledSlots.values()).some((value) => value === undefined);
1046
1084
  if (!hasUnfilledSlots) {
@@ -1055,7 +1093,7 @@ var renderFrameStripToCanvas = async ({
1055
1093
  aspectRatioCache.set(src, aspectRatio);
1056
1094
  ensureSlots({
1057
1095
  filledSlots,
1058
- naturalWidth,
1096
+ naturalWidth: drawingWidth,
1059
1097
  fromSeconds,
1060
1098
  toSeconds,
1061
1099
  aspectRatio,
@@ -1079,7 +1117,7 @@ var renderFrameStripToCanvas = async ({
1079
1117
  }
1080
1118
  ensureSlots({
1081
1119
  filledSlots,
1082
- naturalWidth,
1120
+ naturalWidth: drawingWidth,
1083
1121
  fromSeconds,
1084
1122
  toSeconds,
1085
1123
  aspectRatio,
@@ -1088,12 +1126,13 @@ var renderFrameStripToCanvas = async ({
1088
1126
  fillFrameWhereItFits({
1089
1127
  ctx,
1090
1128
  filledSlots,
1091
- visualizationWidth: naturalWidth,
1129
+ visualizationWidth: drawingWidth,
1092
1130
  frame: transformed,
1093
1131
  segmentDuration,
1094
1132
  fromSeconds,
1095
1133
  devicePixelRatio,
1096
- frameHeight
1134
+ frameHeight,
1135
+ horizontalOffset
1097
1136
  });
1098
1137
  } catch (e) {
1099
1138
  if (frame) {
@@ -1110,13 +1149,14 @@ var renderFrameStripToCanvas = async ({
1110
1149
  }
1111
1150
  fillWithCachedFrames({
1112
1151
  ctx,
1113
- naturalWidth,
1152
+ naturalWidth: drawingWidth,
1114
1153
  filledSlots,
1115
1154
  src,
1116
1155
  segmentDuration,
1117
1156
  fromSeconds,
1118
1157
  devicePixelRatio,
1119
- frameHeight
1158
+ frameHeight,
1159
+ horizontalOffset
1120
1160
  });
1121
1161
  };
1122
1162
  // src/format-average-audio-volume.ts
@@ -1128,6 +1168,7 @@ var formatAverageAudioVolume = (volume) => {
1128
1168
  };
1129
1169
  export {
1130
1170
  subscribeToWaveformPeaks,
1171
+ snapCanvasPositionToDevicePixel,
1131
1172
  sliceWaveformPeaks,
1132
1173
  sliceVisibleWaveformPeaks,
1133
1174
  shouldTileLoopDisplay,
@@ -1141,6 +1182,7 @@ export {
1141
1182
  getLoopDisplaySegments,
1142
1183
  getFrameDatabaseKeyPrefix,
1143
1184
  getDurationOfOneFrame,
1185
+ getDevicePixelAlignedCanvasLayout,
1144
1186
  getAspectRatioFromCache,
1145
1187
  frameDatabase,
1146
1188
  formatAverageAudioVolume,
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.530",
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.530"
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.530",
30
30
  "eslint": "9.19.0",
31
31
  "@typescript/native-preview": "7.0.0-dev.20260217.1"
32
32
  },