@remotion/studio 4.0.444 → 4.0.445

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.
@@ -21,13 +21,14 @@ const use_is_ruler_visible_1 = require("./EditorRuler/use-is-ruler-visible");
21
21
  const layout_1 = require("./layout");
22
22
  const Preview_1 = require("./Preview");
23
23
  const ResetZoomButton_1 = require("./ResetZoomButton");
24
- const container = {
24
+ const getContainerStyle = (editorZoomGestures) => ({
25
25
  flex: 1,
26
26
  display: 'flex',
27
27
  overflow: 'hidden',
28
28
  position: 'relative',
29
29
  backgroundColor: colors_1.BACKGROUND,
30
- };
30
+ ...(editorZoomGestures ? { touchAction: 'none' } : {}),
31
+ });
31
32
  const resetZoom = {
32
33
  position: 'absolute',
33
34
  top: layout_1.SPACING_UNIT * 2,
@@ -37,6 +38,14 @@ const ZOOM_PX_FACTOR = 0.003;
37
38
  const Canvas = ({ canvasContent, size }) => {
38
39
  const { setSize, size: previewSize } = (0, react_1.useContext)(remotion_1.Internals.PreviewSizeContext);
39
40
  const { editorZoomGestures } = (0, react_1.useContext)(editor_zoom_gestures_1.EditorZoomGesturesContext);
41
+ const previewSnapshotRef = (0, react_1.useRef)({
42
+ previewSize,
43
+ canvasSize: size,
44
+ contentDimensions: null,
45
+ });
46
+ const pinchBaseZoomRef = (0, react_1.useRef)(null);
47
+ const suppressWheelFromWebKitPinchRef = (0, react_1.useRef)(false);
48
+ const touchPinchRef = (0, react_1.useRef)(null);
40
49
  const keybindings = (0, use_keybinding_1.useKeybinding)();
41
50
  const config = remotion_1.Internals.useUnsafeVideoConfig();
42
51
  const areRulersVisible = (0, use_is_ruler_visible_1.useIsRulerVisible)();
@@ -56,7 +65,13 @@ const Canvas = ({ canvasContent, size }) => {
56
65
  return null;
57
66
  }, [assetResolution, config, canvasContent]);
58
67
  const isFit = previewSize.size === 'auto';
68
+ previewSnapshotRef.current = {
69
+ previewSize,
70
+ canvasSize: size,
71
+ contentDimensions,
72
+ };
59
73
  const onWheel = (0, react_1.useCallback)((e) => {
74
+ const ev = e;
60
75
  if (!editorZoomGestures) {
61
76
  return;
62
77
  }
@@ -66,11 +81,15 @@ const Canvas = ({ canvasContent, size }) => {
66
81
  if (!contentDimensions || contentDimensions === 'none') {
67
82
  return;
68
83
  }
69
- const wantsToZoom = e.ctrlKey || e.metaKey;
84
+ const wantsToZoom = ev.ctrlKey || ev.metaKey;
70
85
  if (!wantsToZoom && isFit) {
71
86
  return;
72
87
  }
73
- e.preventDefault();
88
+ if (suppressWheelFromWebKitPinchRef.current && wantsToZoom) {
89
+ ev.preventDefault();
90
+ return;
91
+ }
92
+ ev.preventDefault();
74
93
  setSize((prevSize) => {
75
94
  const scale = remotion_1.Internals.calculateScale({
76
95
  canvasSize: size,
@@ -78,41 +97,20 @@ const Canvas = ({ canvasContent, size }) => {
78
97
  compositionWidth: contentDimensions.width,
79
98
  previewSize: prevSize.size,
80
99
  });
81
- // Zoom in/out
82
100
  if (wantsToZoom) {
83
101
  const oldSize = prevSize.size === 'auto' ? scale : prevSize.size;
84
102
  const smoothened = (0, smooth_zoom_1.smoothenZoom)(oldSize);
85
- const added = smoothened + e.deltaY * ZOOM_PX_FACTOR;
103
+ const added = smoothened + ev.deltaY * ZOOM_PX_FACTOR;
86
104
  const unsmoothened = (0, smooth_zoom_1.unsmoothenZoom)(added);
87
- const { centerX, centerY } = (0, get_effective_translation_1.getCenterPointWhileScrolling)({
88
- size,
89
- clientX: e.clientX,
90
- clientY: e.clientY,
91
- compositionWidth: contentDimensions.width,
92
- compositionHeight: contentDimensions.height,
93
- scale,
94
- translation: prevSize.translation,
105
+ return (0, get_effective_translation_1.applyZoomAroundFocalPoint)({
106
+ canvasSize: size,
107
+ contentDimensions,
108
+ previewSizeBefore: prevSize,
109
+ oldNumericSize: oldSize,
110
+ newNumericSize: unsmoothened,
111
+ clientX: ev.clientX,
112
+ clientY: ev.clientY,
95
113
  });
96
- const zoomDifference = unsmoothened - oldSize;
97
- const uvCoordinatesX = centerX / contentDimensions.width;
98
- const uvCoordinatesY = centerY / contentDimensions.height;
99
- const correctionLeft = -uvCoordinatesX * (zoomDifference * contentDimensions.width) +
100
- (1 - uvCoordinatesX) * zoomDifference * contentDimensions.width;
101
- const correctionTop = -uvCoordinatesY * (zoomDifference * contentDimensions.height) +
102
- (1 - uvCoordinatesY) * zoomDifference * contentDimensions.height;
103
- return {
104
- translation: (0, get_effective_translation_1.getEffectiveTranslation)({
105
- translation: {
106
- x: prevSize.translation.x - correctionLeft / 2,
107
- y: prevSize.translation.y - correctionTop / 2,
108
- },
109
- canvasSize: size,
110
- compositionHeight: contentDimensions.height,
111
- compositionWidth: contentDimensions.width,
112
- scale,
113
- }),
114
- size: unsmoothened,
115
- };
116
114
  }
117
115
  const effectiveTranslation = (0, get_effective_translation_1.getEffectiveTranslation)({
118
116
  translation: prevSize.translation,
@@ -121,13 +119,12 @@ const Canvas = ({ canvasContent, size }) => {
121
119
  compositionWidth: contentDimensions.width,
122
120
  scale,
123
121
  });
124
- // Pan
125
122
  return {
126
123
  ...prevSize,
127
124
  translation: (0, get_effective_translation_1.getEffectiveTranslation)({
128
125
  translation: {
129
- x: effectiveTranslation.x + e.deltaX,
130
- y: effectiveTranslation.y + e.deltaY,
126
+ x: effectiveTranslation.x + ev.deltaX,
127
+ y: effectiveTranslation.y + ev.deltaY,
131
128
  },
132
129
  canvasSize: size,
133
130
  compositionHeight: contentDimensions.height,
@@ -143,12 +140,170 @@ const Canvas = ({ canvasContent, size }) => {
143
140
  return;
144
141
  }
145
142
  current.addEventListener('wheel', onWheel, { passive: false });
146
- return () =>
147
- // @ts-expect-error
148
- current.removeEventListener('wheel', onWheel, {
143
+ return () => {
144
+ current.removeEventListener('wheel', onWheel);
145
+ };
146
+ }, [onWheel]);
147
+ const supportsWebKitPinch = typeof window !== 'undefined' && 'GestureEvent' in window;
148
+ (0, react_1.useEffect)(() => {
149
+ const { current } = canvas_ref_1.canvasRef;
150
+ if (!current || !editorZoomGestures || !supportsWebKitPinch) {
151
+ return;
152
+ }
153
+ const endWebKitPinch = () => {
154
+ pinchBaseZoomRef.current = null;
155
+ suppressWheelFromWebKitPinchRef.current = false;
156
+ };
157
+ const onGestureStart = (event) => {
158
+ const e = event;
159
+ const snap = previewSnapshotRef.current;
160
+ const canvasSz = snap.canvasSize;
161
+ const cdim = snap.contentDimensions;
162
+ if (!canvasSz || !cdim || cdim === 'none') {
163
+ return;
164
+ }
165
+ e.preventDefault();
166
+ suppressWheelFromWebKitPinchRef.current = true;
167
+ const fitted = remotion_1.Internals.calculateScale({
168
+ canvasSize: canvasSz,
169
+ compositionHeight: cdim.height,
170
+ compositionWidth: cdim.width,
171
+ previewSize: snap.previewSize.size,
172
+ });
173
+ pinchBaseZoomRef.current =
174
+ snap.previewSize.size === 'auto' ? fitted : snap.previewSize.size;
175
+ };
176
+ const onGestureChange = (event) => {
177
+ const e = event;
178
+ const base = pinchBaseZoomRef.current;
179
+ const snap = previewSnapshotRef.current;
180
+ const canvasSz = snap.canvasSize;
181
+ const cdim = snap.contentDimensions;
182
+ if (base === null || !canvasSz || !cdim || cdim === 'none') {
183
+ return;
184
+ }
185
+ const dimensions = cdim;
186
+ e.preventDefault();
187
+ setSize((prevSize) => {
188
+ const scale = remotion_1.Internals.calculateScale({
189
+ canvasSize: canvasSz,
190
+ compositionHeight: dimensions.height,
191
+ compositionWidth: dimensions.width,
192
+ previewSize: prevSize.size,
193
+ });
194
+ const oldNumeric = prevSize.size === 'auto' ? scale : prevSize.size;
195
+ return (0, get_effective_translation_1.applyZoomAroundFocalPoint)({
196
+ canvasSize: canvasSz,
197
+ contentDimensions: dimensions,
198
+ previewSizeBefore: prevSize,
199
+ oldNumericSize: oldNumeric,
200
+ newNumericSize: base * e.scale,
201
+ clientX: e.clientX,
202
+ clientY: e.clientY,
203
+ });
204
+ });
205
+ };
206
+ const onGestureEnd = () => {
207
+ endWebKitPinch();
208
+ };
209
+ current.addEventListener('gesturestart', onGestureStart, {
149
210
  passive: false,
150
211
  });
151
- }, [onWheel]);
212
+ current.addEventListener('gesturechange', onGestureChange, {
213
+ passive: false,
214
+ });
215
+ current.addEventListener('gestureend', onGestureEnd);
216
+ current.addEventListener('gesturecancel', onGestureEnd);
217
+ return () => {
218
+ current.removeEventListener('gesturestart', onGestureStart);
219
+ current.removeEventListener('gesturechange', onGestureChange);
220
+ current.removeEventListener('gestureend', onGestureEnd);
221
+ current.removeEventListener('gesturecancel', onGestureEnd);
222
+ };
223
+ }, [editorZoomGestures, setSize, supportsWebKitPinch]);
224
+ (0, react_1.useEffect)(() => {
225
+ const { current } = canvas_ref_1.canvasRef;
226
+ if (!current || !editorZoomGestures) {
227
+ return;
228
+ }
229
+ const onTouchStart = (event) => {
230
+ if (event.touches.length !== 2) {
231
+ touchPinchRef.current = null;
232
+ return;
233
+ }
234
+ const snap = previewSnapshotRef.current;
235
+ if (!snap.canvasSize ||
236
+ !snap.contentDimensions ||
237
+ snap.contentDimensions === 'none') {
238
+ return;
239
+ }
240
+ const [t0, t1] = [event.touches[0], event.touches[1]];
241
+ const initialDistance = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
242
+ if (initialDistance < 1e-6) {
243
+ return;
244
+ }
245
+ const fitted = remotion_1.Internals.calculateScale({
246
+ canvasSize: snap.canvasSize,
247
+ compositionHeight: snap.contentDimensions.height,
248
+ compositionWidth: snap.contentDimensions.width,
249
+ previewSize: snap.previewSize.size,
250
+ });
251
+ const initialZoom = snap.previewSize.size === 'auto' ? fitted : snap.previewSize.size;
252
+ touchPinchRef.current = { initialDistance, initialZoom };
253
+ };
254
+ const onTouchMove = (event) => {
255
+ const pinch = touchPinchRef.current;
256
+ const snap = previewSnapshotRef.current;
257
+ if (pinch === null ||
258
+ event.touches.length !== 2 ||
259
+ !snap.canvasSize ||
260
+ !snap.contentDimensions ||
261
+ snap.contentDimensions === 'none') {
262
+ return;
263
+ }
264
+ event.preventDefault();
265
+ const [t0, t1] = [event.touches[0], event.touches[1]];
266
+ const dist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
267
+ const ratio = dist / pinch.initialDistance;
268
+ const clientX = (t0.clientX + t1.clientX) / 2;
269
+ const clientY = (t0.clientY + t1.clientY) / 2;
270
+ setSize((prevSize) => {
271
+ const canvasSz = snap.canvasSize;
272
+ const cdim = snap.contentDimensions;
273
+ const scale = remotion_1.Internals.calculateScale({
274
+ canvasSize: canvasSz,
275
+ compositionHeight: cdim.height,
276
+ compositionWidth: cdim.width,
277
+ previewSize: prevSize.size,
278
+ });
279
+ const oldNumeric = prevSize.size === 'auto' ? scale : prevSize.size;
280
+ return (0, get_effective_translation_1.applyZoomAroundFocalPoint)({
281
+ canvasSize: canvasSz,
282
+ contentDimensions: cdim,
283
+ previewSizeBefore: prevSize,
284
+ oldNumericSize: oldNumeric,
285
+ newNumericSize: pinch.initialZoom * ratio,
286
+ clientX,
287
+ clientY,
288
+ });
289
+ });
290
+ };
291
+ const onTouchEnd = (event) => {
292
+ if (event.touches.length < 2) {
293
+ touchPinchRef.current = null;
294
+ }
295
+ };
296
+ current.addEventListener('touchstart', onTouchStart, { passive: true });
297
+ current.addEventListener('touchmove', onTouchMove, { passive: false });
298
+ current.addEventListener('touchend', onTouchEnd);
299
+ current.addEventListener('touchcancel', onTouchEnd);
300
+ return () => {
301
+ current.removeEventListener('touchstart', onTouchStart);
302
+ current.removeEventListener('touchmove', onTouchMove);
303
+ current.removeEventListener('touchend', onTouchEnd);
304
+ current.removeEventListener('touchcancel', onTouchEnd);
305
+ };
306
+ }, [editorZoomGestures, setSize]);
152
307
  const onReset = (0, react_1.useCallback)(() => {
153
308
  setSize(() => {
154
309
  return {
@@ -263,6 +418,6 @@ const Canvas = ({ canvasContent, size }) => {
263
418
  fetchMetadata();
264
419
  }, [fetchMetadata]);
265
420
  return (jsx_runtime_1.jsxs(jsx_runtime_1.Fragment, { children: [
266
- jsx_runtime_1.jsxs("div", { ref: canvas_ref_1.canvasRef, style: container, children: [size ? (jsx_runtime_1.jsx(Preview_1.VideoPreview, { canvasContent: canvasContent, contentDimensions: contentDimensions, canvasSize: size, assetMetadata: assetResolution })) : null, isFit ? null : (jsx_runtime_1.jsx("div", { style: resetZoom, className: "css-reset", children: jsx_runtime_1.jsx(ResetZoomButton_1.ResetZoomButton, { onClick: onReset }) })), editorShowGuides && canvasContent.type === 'composition' && (jsx_runtime_1.jsx(EditorGuides_1.default, { canvasSize: size, contentDimensions: contentDimensions, assetMetadata: assetResolution }))] }), areRulersVisible && (jsx_runtime_1.jsx(EditorRuler_1.EditorRulers, { contentDimensions: contentDimensions, canvasSize: size, assetMetadata: assetResolution, containerRef: canvas_ref_1.canvasRef }))] }));
421
+ jsx_runtime_1.jsxs("div", { ref: canvas_ref_1.canvasRef, style: getContainerStyle(editorZoomGestures), children: [size ? (jsx_runtime_1.jsx(Preview_1.VideoPreview, { canvasContent: canvasContent, contentDimensions: contentDimensions, canvasSize: size, assetMetadata: assetResolution })) : null, isFit ? null : (jsx_runtime_1.jsx("div", { style: resetZoom, className: "css-reset", children: jsx_runtime_1.jsx(ResetZoomButton_1.ResetZoomButton, { onClick: onReset }) })), editorShowGuides && canvasContent.type === 'composition' && (jsx_runtime_1.jsx(EditorGuides_1.default, { canvasSize: size, contentDimensions: contentDimensions, assetMetadata: assetResolution }))] }), areRulersVisible && (jsx_runtime_1.jsx(EditorRuler_1.EditorRulers, { contentDimensions: contentDimensions, canvasSize: size, assetMetadata: assetResolution, containerRef: canvas_ref_1.canvasRef }))] }));
267
422
  };
268
423
  exports.Canvas = Canvas;
@@ -52,6 +52,7 @@ const timeline_refs_1 = require("./timeline-refs");
52
52
  const TimelineDragHandler_1 = require("./TimelineDragHandler");
53
53
  const TimelineInOutPointer_1 = require("./TimelineInOutPointer");
54
54
  const TimelineList_1 = require("./TimelineList");
55
+ const TimelinePinchZoom_1 = require("./TimelinePinchZoom");
55
56
  const TimelinePlayCursorSyncer_1 = require("./TimelinePlayCursorSyncer");
56
57
  const TimelineScrollable_1 = require("./TimelineScrollable");
57
58
  const TimelineSlider_1 = require("./TimelineSlider");
@@ -116,10 +117,12 @@ const TimelineInner = () => {
116
117
  overflowX: 'hidden',
117
118
  };
118
119
  }, [hasBeenCut, shown, expandedTracks, visualModeEnabled]);
119
- return (jsx_runtime_1.jsx("div", { ref: timeline_refs_1.timelineVerticalScroll, style: container, className: 'css-reset ' + is_menu_item_1.VERTICAL_SCROLLBAR_CLASSNAME, children: jsx_runtime_1.jsx(TimelineWidthProvider_1.TimelineWidthProvider, { children: jsx_runtime_1.jsx("div", { style: inner, children: jsx_runtime_1.jsxs(SplitterContainer_1.SplitterContainer, { orientation: "vertical", defaultFlex: 0.2, id: "names-to-timeline", maxFlex: 0.5, minFlex: 0.15, children: [
120
- jsx_runtime_1.jsx(SplitterElement_1.SplitterElement, { type: "flexer", sticky: jsx_runtime_1.jsx(TimelineTimeIndicators_1.TimelineTimePlaceholders, {}), children: jsx_runtime_1.jsx(TimelineList_1.TimelineList, { timeline: shown }) }), jsx_runtime_1.jsx(SplitterHandle_1.SplitterHandle, { onCollapse: noop, allowToCollapse: "none" }), jsx_runtime_1.jsx(SplitterElement_1.SplitterElement, { type: "anti-flexer", sticky: null, children: jsx_runtime_1.jsxs(TimelineScrollable_1.TimelineScrollable, { children: [
121
- jsx_runtime_1.jsx(TimelineTracks_1.TimelineTracks, { timeline: shown, hasBeenCut: hasBeenCut }), jsx_runtime_1.jsx(TimelineInOutPointer_1.TimelineInOutPointer, {}), jsx_runtime_1.jsx(TimelinePlayCursorSyncer_1.TimelinePlayCursorSyncer, {}), jsx_runtime_1.jsx(TimelineDragHandler_1.TimelineDragHandler, {}), jsx_runtime_1.jsx(TimelineTimeIndicators_1.TimelineTimeIndicators, {}), jsx_runtime_1.jsx(TimelineSlider_1.TimelineSlider, {})
122
- ] }) })
123
- ] }) }) }) }));
120
+ return (jsx_runtime_1.jsx("div", { ref: timeline_refs_1.timelineVerticalScroll, style: container, className: 'css-reset ' + is_menu_item_1.VERTICAL_SCROLLBAR_CLASSNAME, children: jsx_runtime_1.jsxs(TimelineWidthProvider_1.TimelineWidthProvider, { children: [
121
+ jsx_runtime_1.jsx(TimelinePinchZoom_1.TimelinePinchZoom, {}), jsx_runtime_1.jsx("div", { style: inner, children: jsx_runtime_1.jsxs(SplitterContainer_1.SplitterContainer, { orientation: "vertical", defaultFlex: 0.2, id: "names-to-timeline", maxFlex: 0.5, minFlex: 0.15, children: [
122
+ jsx_runtime_1.jsx(SplitterElement_1.SplitterElement, { type: "flexer", sticky: jsx_runtime_1.jsx(TimelineTimeIndicators_1.TimelineTimePlaceholders, {}), children: jsx_runtime_1.jsx(TimelineList_1.TimelineList, { timeline: shown }) }), jsx_runtime_1.jsx(SplitterHandle_1.SplitterHandle, { onCollapse: noop, allowToCollapse: "none" }), jsx_runtime_1.jsx(SplitterElement_1.SplitterElement, { type: "anti-flexer", sticky: null, children: jsx_runtime_1.jsxs(TimelineScrollable_1.TimelineScrollable, { children: [
123
+ jsx_runtime_1.jsx(TimelineTracks_1.TimelineTracks, { timeline: shown, hasBeenCut: hasBeenCut }), jsx_runtime_1.jsx(TimelineInOutPointer_1.TimelineInOutPointer, {}), jsx_runtime_1.jsx(TimelinePlayCursorSyncer_1.TimelinePlayCursorSyncer, {}), jsx_runtime_1.jsx(TimelineDragHandler_1.TimelineDragHandler, {}), jsx_runtime_1.jsx(TimelineTimeIndicators_1.TimelineTimeIndicators, {}), jsx_runtime_1.jsx(TimelineSlider_1.TimelineSlider, {})
124
+ ] }) })
125
+ ] }) })
126
+ ] }) }));
124
127
  };
125
128
  exports.Timeline = react_1.default.memo(TimelineInner);
@@ -0,0 +1,2 @@
1
+ import { type FC } from 'react';
2
+ export declare const TimelinePinchZoom: FC;
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TimelinePinchZoom = void 0;
4
+ const react_1 = require("react");
5
+ const remotion_1 = require("remotion");
6
+ const is_current_selected_still_1 = require("../../helpers/is-current-selected-still");
7
+ const editor_zoom_gestures_1 = require("../../state/editor-zoom-gestures");
8
+ const timeline_zoom_1 = require("../../state/timeline-zoom");
9
+ const timeline_refs_1 = require("./timeline-refs");
10
+ const timeline_scroll_logic_1 = require("./timeline-scroll-logic");
11
+ /**
12
+ * Maps wheel deltaY to zoom delta. Must be large enough that typical ctrl+wheel
13
+ * pinch steps change `TimelineZoomCtx` zoom by at least one 0.1 step after
14
+ * `Math.round(z * 10) / 10` in `timeline-zoom.tsx` (0.005 was too small).
15
+ */
16
+ const ZOOM_WHEEL_DELTA = 0.06;
17
+ const TimelinePinchZoom = () => {
18
+ const isVideoComposition = (0, is_current_selected_still_1.useIsVideoComposition)();
19
+ const videoConfig = remotion_1.Internals.useUnsafeVideoConfig();
20
+ const { canvasContent } = (0, react_1.useContext)(remotion_1.Internals.CompositionManager);
21
+ const { zoom, setZoom } = (0, react_1.useContext)(timeline_zoom_1.TimelineZoomCtx);
22
+ const { editorZoomGestures } = (0, react_1.useContext)(editor_zoom_gestures_1.EditorZoomGesturesContext);
23
+ const zoomRef = (0, react_1.useRef)(zoom);
24
+ zoomRef.current = zoom;
25
+ const pinchBaseZoomRef = (0, react_1.useRef)(null);
26
+ const suppressWheelFromWebKitPinchRef = (0, react_1.useRef)(false);
27
+ const touchPinchRef = (0, react_1.useRef)(null);
28
+ const onWheel = (0, react_1.useCallback)((e) => {
29
+ if (!editorZoomGestures || !isVideoComposition) {
30
+ return;
31
+ }
32
+ const { ctrlKey, metaKey, clientX, deltaY, deltaMode } = e;
33
+ const wantsToZoom = ctrlKey || metaKey;
34
+ if (!wantsToZoom) {
35
+ return;
36
+ }
37
+ if (suppressWheelFromWebKitPinchRef.current && wantsToZoom) {
38
+ e.preventDefault();
39
+ return;
40
+ }
41
+ if (!videoConfig || videoConfig.durationInFrames < 2) {
42
+ return;
43
+ }
44
+ if (!canvasContent || canvasContent.type !== 'composition') {
45
+ return;
46
+ }
47
+ const scrollEl = timeline_refs_1.scrollableRef.current;
48
+ if (!scrollEl) {
49
+ return;
50
+ }
51
+ e.preventDefault();
52
+ const anchorContentX = (0, timeline_scroll_logic_1.viewportClientXToScrollContentX)({
53
+ clientX,
54
+ scrollEl,
55
+ });
56
+ let scaledDeltaY = deltaY;
57
+ if (deltaMode === WheelEvent.DOM_DELTA_LINE) {
58
+ scaledDeltaY *= 16;
59
+ }
60
+ else if (deltaMode === WheelEvent.DOM_DELTA_PAGE) {
61
+ scaledDeltaY *= scrollEl.clientHeight;
62
+ }
63
+ setZoom(canvasContent.compositionId, (z) => z - scaledDeltaY * ZOOM_WHEEL_DELTA, { anchorFrame: null, anchorContentX });
64
+ }, [
65
+ editorZoomGestures,
66
+ isVideoComposition,
67
+ videoConfig,
68
+ canvasContent,
69
+ setZoom,
70
+ ]);
71
+ const supportsWebKitPinch = typeof window !== 'undefined' && 'GestureEvent' in window;
72
+ (0, react_1.useEffect)(() => {
73
+ const el = timeline_refs_1.timelineVerticalScroll.current;
74
+ if (!el) {
75
+ return;
76
+ }
77
+ el.addEventListener('wheel', onWheel, { passive: false });
78
+ return () => {
79
+ el.removeEventListener('wheel', onWheel);
80
+ };
81
+ }, [onWheel]);
82
+ (0, react_1.useEffect)(() => {
83
+ const el = timeline_refs_1.timelineVerticalScroll.current;
84
+ if (!el || !editorZoomGestures || !supportsWebKitPinch) {
85
+ return;
86
+ }
87
+ const endWebKitPinch = () => {
88
+ pinchBaseZoomRef.current = null;
89
+ suppressWheelFromWebKitPinchRef.current = false;
90
+ };
91
+ const onGestureStart = (event) => {
92
+ var _a;
93
+ const e = event;
94
+ if (!isVideoComposition) {
95
+ return;
96
+ }
97
+ if (!videoConfig || videoConfig.durationInFrames < 2) {
98
+ return;
99
+ }
100
+ if (!canvasContent || canvasContent.type !== 'composition') {
101
+ return;
102
+ }
103
+ const scrollEl = timeline_refs_1.scrollableRef.current;
104
+ if (!scrollEl) {
105
+ return;
106
+ }
107
+ e.preventDefault();
108
+ suppressWheelFromWebKitPinchRef.current = true;
109
+ pinchBaseZoomRef.current = (_a = zoomRef.current[canvasContent.compositionId]) !== null && _a !== void 0 ? _a : timeline_zoom_1.TIMELINE_MIN_ZOOM;
110
+ };
111
+ const onGestureChange = (event) => {
112
+ const e = event;
113
+ const base = pinchBaseZoomRef.current;
114
+ if (base === null ||
115
+ !isVideoComposition ||
116
+ !videoConfig ||
117
+ videoConfig.durationInFrames < 2) {
118
+ return;
119
+ }
120
+ if (!canvasContent || canvasContent.type !== 'composition') {
121
+ return;
122
+ }
123
+ const scrollEl = timeline_refs_1.scrollableRef.current;
124
+ if (!scrollEl) {
125
+ return;
126
+ }
127
+ e.preventDefault();
128
+ const anchorContentX = (0, timeline_scroll_logic_1.viewportClientXToScrollContentX)({
129
+ clientX: e.clientX,
130
+ scrollEl,
131
+ });
132
+ setZoom(canvasContent.compositionId, () => base * e.scale, {
133
+ anchorFrame: null,
134
+ anchorContentX,
135
+ });
136
+ };
137
+ const onGestureEnd = () => {
138
+ endWebKitPinch();
139
+ };
140
+ el.addEventListener('gesturestart', onGestureStart, { passive: false });
141
+ el.addEventListener('gesturechange', onGestureChange, { passive: false });
142
+ el.addEventListener('gestureend', onGestureEnd);
143
+ el.addEventListener('gesturecancel', onGestureEnd);
144
+ return () => {
145
+ el.removeEventListener('gesturestart', onGestureStart);
146
+ el.removeEventListener('gesturechange', onGestureChange);
147
+ el.removeEventListener('gestureend', onGestureEnd);
148
+ el.removeEventListener('gesturecancel', onGestureEnd);
149
+ };
150
+ }, [
151
+ editorZoomGestures,
152
+ supportsWebKitPinch,
153
+ isVideoComposition,
154
+ videoConfig,
155
+ canvasContent,
156
+ setZoom,
157
+ ]);
158
+ (0, react_1.useEffect)(() => {
159
+ const el = timeline_refs_1.timelineVerticalScroll.current;
160
+ if (!el || !editorZoomGestures) {
161
+ return;
162
+ }
163
+ const onTouchStart = (event) => {
164
+ var _a;
165
+ if (event.touches.length !== 2) {
166
+ touchPinchRef.current = null;
167
+ return;
168
+ }
169
+ if (!isVideoComposition ||
170
+ !videoConfig ||
171
+ videoConfig.durationInFrames < 2) {
172
+ return;
173
+ }
174
+ if (!canvasContent || canvasContent.type !== 'composition') {
175
+ return;
176
+ }
177
+ const [t0, t1] = [event.touches[0], event.touches[1]];
178
+ const initialDistance = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
179
+ if (initialDistance < 1e-6) {
180
+ return;
181
+ }
182
+ touchPinchRef.current = {
183
+ initialDistance,
184
+ initialZoom: (_a = zoomRef.current[canvasContent.compositionId]) !== null && _a !== void 0 ? _a : timeline_zoom_1.TIMELINE_MIN_ZOOM,
185
+ };
186
+ };
187
+ const onTouchMove = (event) => {
188
+ const pinch = touchPinchRef.current;
189
+ if (pinch === null ||
190
+ event.touches.length !== 2 ||
191
+ !videoConfig ||
192
+ videoConfig.durationInFrames < 2) {
193
+ return;
194
+ }
195
+ if (!canvasContent || canvasContent.type !== 'composition') {
196
+ return;
197
+ }
198
+ const scrollEl = timeline_refs_1.scrollableRef.current;
199
+ if (!scrollEl) {
200
+ return;
201
+ }
202
+ event.preventDefault();
203
+ const [t0, t1] = [event.touches[0], event.touches[1]];
204
+ const dist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
205
+ const ratio = dist / pinch.initialDistance;
206
+ const clientX = (t0.clientX + t1.clientX) / 2;
207
+ const anchorContentX = (0, timeline_scroll_logic_1.viewportClientXToScrollContentX)({
208
+ clientX,
209
+ scrollEl,
210
+ });
211
+ setZoom(canvasContent.compositionId, () => pinch.initialZoom * ratio, {
212
+ anchorFrame: null,
213
+ anchorContentX,
214
+ });
215
+ };
216
+ const onTouchEnd = (event) => {
217
+ if (event.touches.length < 2) {
218
+ touchPinchRef.current = null;
219
+ }
220
+ };
221
+ el.addEventListener('touchstart', onTouchStart, { passive: true });
222
+ el.addEventListener('touchmove', onTouchMove, { passive: false });
223
+ el.addEventListener('touchend', onTouchEnd);
224
+ el.addEventListener('touchcancel', onTouchEnd);
225
+ return () => {
226
+ el.removeEventListener('touchstart', onTouchStart);
227
+ el.removeEventListener('touchmove', onTouchMove);
228
+ el.removeEventListener('touchend', onTouchEnd);
229
+ el.removeEventListener('touchcancel', onTouchEnd);
230
+ };
231
+ }, [
232
+ editorZoomGestures,
233
+ isVideoComposition,
234
+ videoConfig,
235
+ canvasContent,
236
+ setZoom,
237
+ ]);
238
+ return null;
239
+ };
240
+ exports.TimelinePinchZoom = TimelinePinchZoom;
@@ -5,6 +5,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const remotion_1 = require("remotion");
7
7
  const get_left_of_timeline_slider_1 = require("../../helpers/get-left-of-timeline-slider");
8
+ const timeline_zoom_1 = require("../../state/timeline-zoom");
8
9
  const imperative_state_1 = require("./imperative-state");
9
10
  const timeline_refs_1 = require("./timeline-refs");
10
11
  const TimelineSliderHandle_1 = require("./TimelineSliderHandle");
@@ -32,20 +33,33 @@ const TimelineSlider = () => {
32
33
  };
33
34
  exports.TimelineSlider = TimelineSlider;
34
35
  const Inner = () => {
36
+ var _a;
35
37
  const videoConfig = (0, remotion_1.useVideoConfig)();
36
38
  const timelinePosition = remotion_1.Internals.Timeline.useTimelinePosition();
37
39
  const ref = (0, react_1.useRef)(null);
38
40
  const timelineWidth = (0, react_1.useContext)(TimelineWidthProvider_1.TimelineWidthContext);
41
+ const { zoom: zoomMap } = (0, react_1.useContext)(timeline_zoom_1.TimelineZoomCtx);
42
+ const { canvasContent } = (0, react_1.useContext)(remotion_1.Internals.CompositionManager);
39
43
  if (timelineWidth === null) {
40
44
  throw new Error('Unexpectedly did not have timeline width');
41
45
  }
42
- const style = (0, react_1.useMemo)(() => {
43
- const left = (0, get_left_of_timeline_slider_1.getXPositionOfItemInTimelineImperatively)(timelinePosition, videoConfig.durationInFrames, timelineWidth);
44
- return {
45
- ...container,
46
- transform: `translateX(${left}px)`,
47
- };
48
- }, [timelinePosition, videoConfig.durationInFrames, timelineWidth]);
46
+ const zoomLevel = (canvasContent === null || canvasContent === void 0 ? void 0 : canvasContent.type) === 'composition'
47
+ ? ((_a = zoomMap[canvasContent.compositionId]) !== null && _a !== void 0 ? _a : timeline_zoom_1.TIMELINE_MIN_ZOOM)
48
+ : timeline_zoom_1.TIMELINE_MIN_ZOOM;
49
+ (0, react_1.useLayoutEffect)(() => {
50
+ var _a;
51
+ const el = ref.current;
52
+ const measuredWidth = (_a = timeline_refs_1.sliderAreaRef.current) === null || _a === void 0 ? void 0 : _a.clientWidth;
53
+ if (!el || measuredWidth === undefined || measuredWidth === 0) {
54
+ return;
55
+ }
56
+ el.style.transform = `translateX(${(0, get_left_of_timeline_slider_1.getXPositionOfItemInTimelineImperatively)(timelinePosition, videoConfig.durationInFrames, measuredWidth)}px)`;
57
+ }, [
58
+ timelinePosition,
59
+ videoConfig.durationInFrames,
60
+ timelineWidth,
61
+ zoomLevel,
62
+ ]);
49
63
  (0, react_1.useImperativeHandle)(exports.redrawTimelineSliderFast, () => {
50
64
  return {
51
65
  draw: (frame, width) => {
@@ -76,7 +90,7 @@ const Inner = () => {
76
90
  current.removeEventListener('scroll', onScroll);
77
91
  };
78
92
  }, []);
79
- return (jsx_runtime_1.jsxs("div", { ref: ref, style: style, children: [
93
+ return (jsx_runtime_1.jsxs("div", { ref: ref, style: container, children: [
80
94
  jsx_runtime_1.jsx("div", { style: line }), jsx_runtime_1.jsx(TimelineSliderHandle_1.TimelineSliderHandle, {})
81
95
  ] }));
82
96
  };
@@ -33,19 +33,22 @@ const TimelineZoomControls = () => {
33
33
  if (canvasContent === null || canvasContent.type !== 'composition') {
34
34
  return;
35
35
  }
36
- setZoom(canvasContent.compositionId, (z) => Math.max(timeline_zoom_1.TIMELINE_MIN_ZOOM, z - 0.2));
36
+ setZoom(canvasContent.compositionId, (z) => Math.max(timeline_zoom_1.TIMELINE_MIN_ZOOM, z - 0.2), { anchorFrame: null, anchorContentX: null });
37
37
  }, [canvasContent, setZoom]);
38
38
  const onPlusClicked = (0, react_1.useCallback)(() => {
39
39
  if (canvasContent === null || canvasContent.type !== 'composition') {
40
40
  return;
41
41
  }
42
- setZoom(canvasContent.compositionId, (z) => Math.min(timeline_zoom_1.TIMELINE_MAX_ZOOM, z + 0.2));
42
+ setZoom(canvasContent.compositionId, (z) => Math.min(timeline_zoom_1.TIMELINE_MAX_ZOOM, z + 0.2), { anchorFrame: null, anchorContentX: null });
43
43
  }, [canvasContent, setZoom]);
44
44
  const onChange = (0, react_1.useCallback)((e) => {
45
45
  if (canvasContent === null || canvasContent.type !== 'composition') {
46
46
  return;
47
47
  }
48
- setZoom(canvasContent.compositionId, () => Number(e.target.value));
48
+ setZoom(canvasContent.compositionId, () => Number(e.target.value), {
49
+ anchorFrame: null,
50
+ anchorContentX: null,
51
+ });
49
52
  }, [canvasContent, setZoom]);
50
53
  const isStill = (0, is_current_selected_still_1.useIsStill)();
51
54
  if (isStill ||