@remotion/studio
Version:
APIs for interacting with the Remotion Studio
424 lines (423 loc) • 18.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Canvas = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const remotion_1 = require("remotion");
const colors_1 = require("../helpers/colors");
const get_asset_metadata_1 = require("../helpers/get-asset-metadata");
const get_effective_translation_1 = require("../helpers/get-effective-translation");
const smooth_zoom_1 = require("../helpers/smooth-zoom");
const use_keybinding_1 = require("../helpers/use-keybinding");
const canvas_ref_1 = require("../state/canvas-ref");
const editor_guides_1 = require("../state/editor-guides");
const editor_zoom_gestures_1 = require("../state/editor-zoom-gestures");
const EditorGuides_1 = __importDefault(require("./EditorGuides"));
const EditorRuler_1 = require("./EditorRuler");
const use_is_ruler_visible_1 = require("./EditorRuler/use-is-ruler-visible");
const layout_1 = require("./layout");
const Preview_1 = require("./Preview");
const ResetZoomButton_1 = require("./ResetZoomButton");
const getContainerStyle = (editorZoomGestures) => ({
flex: 1,
display: 'flex',
overflow: 'hidden',
position: 'relative',
backgroundColor: colors_1.BACKGROUND,
...(editorZoomGestures ? { touchAction: 'none' } : {}),
});
const resetZoom = {
position: 'absolute',
top: layout_1.SPACING_UNIT * 2,
right: layout_1.SPACING_UNIT * 2,
};
const ZOOM_PX_FACTOR = 0.003;
const Canvas = ({ canvasContent, size }) => {
const { setSize, size: previewSize } = (0, react_1.useContext)(remotion_1.Internals.PreviewSizeContext);
const { editorZoomGestures } = (0, react_1.useContext)(editor_zoom_gestures_1.EditorZoomGesturesContext);
const previewSnapshotRef = (0, react_1.useRef)({
previewSize,
canvasSize: size,
contentDimensions: null,
});
const pinchBaseZoomRef = (0, react_1.useRef)(null);
const suppressWheelFromWebKitPinchRef = (0, react_1.useRef)(false);
const touchPinchRef = (0, react_1.useRef)(null);
const keybindings = (0, use_keybinding_1.useKeybinding)();
const config = remotion_1.Internals.useUnsafeVideoConfig();
const areRulersVisible = (0, use_is_ruler_visible_1.useIsRulerVisible)();
const { editorShowGuides } = (0, react_1.useContext)(editor_guides_1.EditorShowGuidesContext);
const [assetResolution, setAssetResolution] = (0, react_1.useState)(null);
const contentDimensions = (0, react_1.useMemo)(() => {
if ((canvasContent.type === 'asset' ||
canvasContent.type === 'output' ||
canvasContent.type === 'output-blob') &&
assetResolution &&
assetResolution.type === 'found') {
return assetResolution.dimensions;
}
if (config) {
return { width: config.width, height: config.height };
}
return null;
}, [assetResolution, config, canvasContent]);
const isFit = previewSize.size === 'auto';
previewSnapshotRef.current = {
previewSize,
canvasSize: size,
contentDimensions,
};
const onWheel = (0, react_1.useCallback)((e) => {
const ev = e;
if (!editorZoomGestures) {
return;
}
if (!size) {
return;
}
if (!contentDimensions || contentDimensions === 'none') {
return;
}
const wantsToZoom = ev.ctrlKey || ev.metaKey;
if (!wantsToZoom && isFit) {
return;
}
if (suppressWheelFromWebKitPinchRef.current && wantsToZoom) {
ev.preventDefault();
return;
}
ev.preventDefault();
setSize((prevSize) => {
const scale = remotion_1.Internals.calculateScale({
canvasSize: size,
compositionHeight: contentDimensions.height,
compositionWidth: contentDimensions.width,
previewSize: prevSize.size,
});
if (wantsToZoom) {
const oldSize = prevSize.size === 'auto' ? scale : prevSize.size;
const smoothened = (0, smooth_zoom_1.smoothenZoom)(oldSize);
const added = smoothened + ev.deltaY * ZOOM_PX_FACTOR;
const unsmoothened = (0, smooth_zoom_1.unsmoothenZoom)(added);
return (0, get_effective_translation_1.applyZoomAroundFocalPoint)({
canvasSize: size,
contentDimensions,
previewSizeBefore: prevSize,
oldNumericSize: oldSize,
newNumericSize: unsmoothened,
clientX: ev.clientX,
clientY: ev.clientY,
});
}
const effectiveTranslation = (0, get_effective_translation_1.getEffectiveTranslation)({
translation: prevSize.translation,
canvasSize: size,
compositionHeight: contentDimensions.height,
compositionWidth: contentDimensions.width,
scale,
});
return {
...prevSize,
translation: (0, get_effective_translation_1.getEffectiveTranslation)({
translation: {
x: effectiveTranslation.x + ev.deltaX,
y: effectiveTranslation.y + ev.deltaY,
},
canvasSize: size,
compositionHeight: contentDimensions.height,
compositionWidth: contentDimensions.width,
scale,
}),
};
});
}, [editorZoomGestures, contentDimensions, isFit, setSize, size]);
(0, react_1.useEffect)(() => {
const { current } = canvas_ref_1.canvasRef;
if (!current) {
return;
}
current.addEventListener('wheel', onWheel, { passive: false });
return () => {
current.removeEventListener('wheel', onWheel);
};
}, [onWheel]);
const supportsWebKitPinch = typeof window !== 'undefined' && 'GestureEvent' in window;
(0, react_1.useEffect)(() => {
const { current } = canvas_ref_1.canvasRef;
if (!current || !editorZoomGestures || !supportsWebKitPinch) {
return;
}
const endWebKitPinch = () => {
pinchBaseZoomRef.current = null;
suppressWheelFromWebKitPinchRef.current = false;
};
const onGestureStart = (event) => {
const e = event;
const snap = previewSnapshotRef.current;
const canvasSz = snap.canvasSize;
const cdim = snap.contentDimensions;
if (!canvasSz || !cdim || cdim === 'none') {
return;
}
e.preventDefault();
suppressWheelFromWebKitPinchRef.current = true;
const fitted = remotion_1.Internals.calculateScale({
canvasSize: canvasSz,
compositionHeight: cdim.height,
compositionWidth: cdim.width,
previewSize: snap.previewSize.size,
});
pinchBaseZoomRef.current =
snap.previewSize.size === 'auto' ? fitted : snap.previewSize.size;
};
const onGestureChange = (event) => {
const e = event;
const base = pinchBaseZoomRef.current;
const snap = previewSnapshotRef.current;
const canvasSz = snap.canvasSize;
const cdim = snap.contentDimensions;
if (base === null || !canvasSz || !cdim || cdim === 'none') {
return;
}
const dimensions = cdim;
e.preventDefault();
setSize((prevSize) => {
const scale = remotion_1.Internals.calculateScale({
canvasSize: canvasSz,
compositionHeight: dimensions.height,
compositionWidth: dimensions.width,
previewSize: prevSize.size,
});
const oldNumeric = prevSize.size === 'auto' ? scale : prevSize.size;
return (0, get_effective_translation_1.applyZoomAroundFocalPoint)({
canvasSize: canvasSz,
contentDimensions: dimensions,
previewSizeBefore: prevSize,
oldNumericSize: oldNumeric,
newNumericSize: base * e.scale,
clientX: e.clientX,
clientY: e.clientY,
});
});
};
const onGestureEnd = () => {
endWebKitPinch();
};
current.addEventListener('gesturestart', onGestureStart, {
passive: false,
});
current.addEventListener('gesturechange', onGestureChange, {
passive: false,
});
current.addEventListener('gestureend', onGestureEnd);
current.addEventListener('gesturecancel', onGestureEnd);
return () => {
current.removeEventListener('gesturestart', onGestureStart);
current.removeEventListener('gesturechange', onGestureChange);
current.removeEventListener('gestureend', onGestureEnd);
current.removeEventListener('gesturecancel', onGestureEnd);
};
}, [editorZoomGestures, setSize, supportsWebKitPinch]);
(0, react_1.useEffect)(() => {
const { current } = canvas_ref_1.canvasRef;
if (!current || !editorZoomGestures) {
return;
}
const onTouchStart = (event) => {
if (event.touches.length !== 2) {
touchPinchRef.current = null;
return;
}
const snap = previewSnapshotRef.current;
if (!snap.canvasSize ||
!snap.contentDimensions ||
snap.contentDimensions === 'none') {
return;
}
const [t0, t1] = [event.touches[0], event.touches[1]];
const initialDistance = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
if (initialDistance < 1e-6) {
return;
}
const fitted = remotion_1.Internals.calculateScale({
canvasSize: snap.canvasSize,
compositionHeight: snap.contentDimensions.height,
compositionWidth: snap.contentDimensions.width,
previewSize: snap.previewSize.size,
});
const initialZoom = snap.previewSize.size === 'auto' ? fitted : snap.previewSize.size;
touchPinchRef.current = { initialDistance, initialZoom };
};
const onTouchMove = (event) => {
const pinch = touchPinchRef.current;
const snap = previewSnapshotRef.current;
if (pinch === null ||
event.touches.length !== 2 ||
!snap.canvasSize ||
!snap.contentDimensions ||
snap.contentDimensions === 'none') {
return;
}
event.preventDefault();
const [t0, t1] = [event.touches[0], event.touches[1]];
const dist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
const ratio = dist / pinch.initialDistance;
const clientX = (t0.clientX + t1.clientX) / 2;
const clientY = (t0.clientY + t1.clientY) / 2;
setSize((prevSize) => {
const canvasSz = snap.canvasSize;
const cdim = snap.contentDimensions;
const scale = remotion_1.Internals.calculateScale({
canvasSize: canvasSz,
compositionHeight: cdim.height,
compositionWidth: cdim.width,
previewSize: prevSize.size,
});
const oldNumeric = prevSize.size === 'auto' ? scale : prevSize.size;
return (0, get_effective_translation_1.applyZoomAroundFocalPoint)({
canvasSize: canvasSz,
contentDimensions: cdim,
previewSizeBefore: prevSize,
oldNumericSize: oldNumeric,
newNumericSize: pinch.initialZoom * ratio,
clientX,
clientY,
});
});
};
const onTouchEnd = (event) => {
if (event.touches.length < 2) {
touchPinchRef.current = null;
}
};
current.addEventListener('touchstart', onTouchStart, { passive: true });
current.addEventListener('touchmove', onTouchMove, { passive: false });
current.addEventListener('touchend', onTouchEnd);
current.addEventListener('touchcancel', onTouchEnd);
return () => {
current.removeEventListener('touchstart', onTouchStart);
current.removeEventListener('touchmove', onTouchMove);
current.removeEventListener('touchend', onTouchEnd);
current.removeEventListener('touchcancel', onTouchEnd);
};
}, [editorZoomGestures, setSize]);
const onReset = (0, react_1.useCallback)(() => {
setSize(() => {
return {
translation: {
x: 0,
y: 0,
},
size: 'auto',
};
});
}, [setSize]);
const onZoomIn = (0, react_1.useCallback)(() => {
if (!contentDimensions || contentDimensions === 'none') {
return;
}
if (!size) {
return;
}
setSize((prevSize) => {
const scale = remotion_1.Internals.calculateScale({
canvasSize: size,
compositionHeight: contentDimensions.height,
compositionWidth: contentDimensions.width,
previewSize: prevSize.size,
});
return {
translation: {
x: 0,
y: 0,
},
size: Math.min(smooth_zoom_1.MAX_ZOOM, scale * 2),
};
});
}, [contentDimensions, setSize, size]);
const onZoomOut = (0, react_1.useCallback)(() => {
if (!contentDimensions || contentDimensions === 'none') {
return;
}
if (!size) {
return;
}
setSize((prevSize) => {
const scale = remotion_1.Internals.calculateScale({
canvasSize: size,
compositionHeight: contentDimensions.height,
compositionWidth: contentDimensions.width,
previewSize: prevSize.size,
});
return {
translation: {
x: 0,
y: 0,
},
size: Math.max(smooth_zoom_1.MIN_ZOOM, scale / 2),
};
});
}, [contentDimensions, setSize, size]);
(0, react_1.useEffect)(() => {
const resetBinding = keybindings.registerKeybinding({
event: 'keydown',
key: '0',
commandCtrlKey: false,
callback: onReset,
preventDefault: true,
triggerIfInputFieldFocused: false,
keepRegisteredWhenNotHighestContext: false,
});
const zoomIn = keybindings.registerKeybinding({
event: 'keydown',
key: '+',
commandCtrlKey: false,
callback: onZoomIn,
preventDefault: true,
triggerIfInputFieldFocused: false,
keepRegisteredWhenNotHighestContext: false,
});
const zoomOut = keybindings.registerKeybinding({
event: 'keydown',
key: '-',
commandCtrlKey: false,
callback: onZoomOut,
preventDefault: true,
triggerIfInputFieldFocused: false,
keepRegisteredWhenNotHighestContext: false,
});
return () => {
resetBinding.unregister();
zoomIn.unregister();
zoomOut.unregister();
};
}, [keybindings, onReset, onZoomIn, onZoomOut]);
const fetchMetadata = (0, react_1.useCallback)(async () => {
setAssetResolution(null);
if (canvasContent.type === 'composition') {
return;
}
const metadata = await (0, get_asset_metadata_1.getAssetMetadata)(canvasContent, canvasContent.type === 'asset');
setAssetResolution(metadata);
}, [canvasContent]);
(0, react_1.useEffect)(() => {
if (canvasContent.type !== 'asset') {
return;
}
const file = (0, remotion_1.watchStaticFile)(canvasContent.asset, () => {
fetchMetadata();
});
return () => {
file.cancel();
};
}, [canvasContent, fetchMetadata]);
(0, react_1.useEffect)(() => {
fetchMetadata();
}, [fetchMetadata]);
return (jsx_runtime_1.jsxs(jsx_runtime_1.Fragment, { children: [
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 }))] }));
};
exports.Canvas = Canvas;