UNPKG

@remotion/studio

Version:

APIs for interacting with the Remotion Studio

424 lines (423 loc) 18.1 kB
"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;