From 08761c3e473898f3b447c758eef95f3800333508 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 9 Jun 2026 15:39:30 +0200 Subject: [PATCH 1/8] `@remotion/studio`: Release Studio interactivity (#8271) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../studio/src/components/PreviewToolbar.tsx | 9 +- .../src/components/SelectedOutlineOverlay.tsx | 3 +- .../Timeline/TimelineDragHandler.tsx | 13 +-- .../Timeline/TimelineEffectItem.tsx | 2 - .../Timeline/TimelineExpandedKeyframeRow.tsx | 9 +- .../components/Timeline/TimelineItemStack.tsx | 91 +------------------ .../Timeline/TimelineKeyframeControls.tsx | 5 +- .../components/Timeline/TimelineMediaInfo.tsx | 58 ++---------- .../components/Timeline/TimelineSelection.tsx | 26 +++--- .../components/Timeline/TimelineSequence.tsx | 6 +- .../Timeline/TimelineSequenceItem.tsx | 14 +-- .../Timeline/TimelineSequenceName.tsx | 34 +------ .../Timeline/TimelineTimeIndicators.tsx | 6 +- .../src/test/timeline-selection.test.ts | 25 +---- 14 files changed, 56 insertions(+), 245 deletions(-) diff --git a/packages/studio/src/components/PreviewToolbar.tsx b/packages/studio/src/components/PreviewToolbar.tsx index 9138e72da3f..8265e080e7f 100644 --- a/packages/studio/src/components/PreviewToolbar.tsx +++ b/packages/studio/src/components/PreviewToolbar.tsx @@ -28,7 +28,6 @@ import {PlaybackRateSelector} from './PlaybackRateSelector'; import {PlayPause} from './PlayPause'; import {RenderButton} from './RenderButton'; import {SizeSelector} from './SizeSelector'; -import {ENABLE_OUTLINES} from './Timeline/TimelineSelection'; import {TimelineZoomControls} from './Timeline/TimelineZoomControls'; import {TimelineInOutPointToggle} from './TimelineInOutToggle'; @@ -239,11 +238,9 @@ export const PreviewToolbar: React.FC<{ - {ENABLE_OUTLINES ? ( - - - - ) : null} + + + ) : null} diff --git a/packages/studio/src/components/SelectedOutlineOverlay.tsx b/packages/studio/src/components/SelectedOutlineOverlay.tsx index 17c296b096a..8ddbea84af3 100644 --- a/packages/studio/src/components/SelectedOutlineOverlay.tsx +++ b/packages/studio/src/components/SelectedOutlineOverlay.tsx @@ -70,7 +70,6 @@ import { } from './Timeline/timeline-translate-utils'; import {getLinkedScale} from './Timeline/TimelineScaleField'; import { - ENABLE_OUTLINES, getTimelineSequenceSelectionKey, useTimelineSelection, type TimelineSelection, @@ -2448,7 +2447,7 @@ export const SelectedOutlineOverlay: React.FC<{ }, []); const outlineTargets = useMemo((): SelectedOutlineTarget[] => { - if (!ENABLE_OUTLINES || !editorShowOutlines) { + if (!editorShowOutlines) { return []; } diff --git a/packages/studio/src/components/Timeline/TimelineDragHandler.tsx b/packages/studio/src/components/Timeline/TimelineDragHandler.tsx index fb3c4b1ce3b..fac28eb9beb 100644 --- a/packages/studio/src/components/Timeline/TimelineDragHandler.tsx +++ b/packages/studio/src/components/Timeline/TimelineDragHandler.tsx @@ -26,11 +26,7 @@ import { getScrollPositionForCursorOnRightEdge, scrollToTimelineXOffset, } from './timeline-scroll-logic'; -import { - TIMELINE_SCRUBBER_ATTR, - TIMELINE_TOP_DRAG, - useTimelineSelection, -} from './TimelineSelection'; +import {TIMELINE_SCRUBBER_ATTR} from './TimelineSelection'; import {redrawTimelineSliderFast} from './TimelineSlider'; import {TIMELINE_TIME_INDICATOR_HEIGHT} from './TimelineTimeIndicators'; @@ -64,7 +60,6 @@ export const TimelineDragHandler: React.FC = () => { const {zoom: zoomMap} = useContext(TimelineZoomCtx); const {canvasContent} = useContext(Internals.CompositionManager); - const {canSelect, canSelectEasing} = useTimelineSelection(); const containerStyle: React.CSSProperties = useMemo(() => { if (!canvasContent || canvasContent.type !== 'composition') { @@ -75,11 +70,9 @@ export const TimelineDragHandler: React.FC = () => { return { ...container, width: 100 * zoom + '%', - ...(TIMELINE_TOP_DRAG || canSelect || canSelectEasing - ? {height: TIMELINE_TIME_INDICATOR_HEIGHT} - : {}), + height: TIMELINE_TIME_INDICATOR_HEIGHT, }; - }, [canSelect, canSelectEasing, canvasContent, zoomMap]); + }, [canvasContent, zoomMap]); if (!canvasContent || canvasContent.type !== 'composition') { return null; diff --git a/packages/studio/src/components/Timeline/TimelineEffectItem.tsx b/packages/studio/src/components/Timeline/TimelineEffectItem.tsx index 5a620cf33db..eaa22d4da63 100644 --- a/packages/studio/src/components/Timeline/TimelineEffectItem.tsx +++ b/packages/studio/src/components/Timeline/TimelineEffectItem.tsx @@ -20,7 +20,6 @@ import {TimelineRowChrome} from './TimelineRowChrome'; import { getTimelineColor, getTimelineSelectedLabelStyle, - SELECTION_ENABLED, useTimelineRowSelection, } from './TimelineSelection'; @@ -162,7 +161,6 @@ export const TimelineEffectItem: React.FC<{ !validatedLocation.source; const canReorder = - SELECTION_ENABLED && previewConnected && effectStatus.type === 'can-update-effect' && Boolean(validatedLocation.source); diff --git a/packages/studio/src/components/Timeline/TimelineExpandedKeyframeRow.tsx b/packages/studio/src/components/Timeline/TimelineExpandedKeyframeRow.tsx index 89ebe29a720..38bec3eb2d3 100644 --- a/packages/studio/src/components/Timeline/TimelineExpandedKeyframeRow.tsx +++ b/packages/studio/src/components/Timeline/TimelineExpandedKeyframeRow.tsx @@ -6,8 +6,6 @@ import type {getTimelineKeyframes} from './get-timeline-keyframes'; import {TimelineKeyframeDiamond} from './TimelineKeyframeDiamond'; import {TimelineKeyframeEasingLine} from './TimelineKeyframeEasingLine'; import { - EASING_SELECTION_ENABLED, - ENABLE_OUTLINES, getTimelineSelectedTrackHighlightStyle, useTimelineRowSelection, } from './TimelineSelection'; @@ -30,10 +28,9 @@ const TimelineExpandedKeyframeRowUnmemoized: React.FC<{ }> = ({height, keyframes, canEditEasing, nodePathInfo, showSeparator}) => { const timelineWidth = useContext(TimelineWidthContext); const {selected: rowSelected} = useTimelineRowSelection(nodePathInfo); - const easingSegments = - (ENABLE_OUTLINES || EASING_SELECTION_ENABLED) && canEditEasing - ? getTimelineEasingSegments(keyframes) - : []; + const easingSegments = canEditEasing + ? getTimelineEasingSegments(keyframes) + : []; return ( <> diff --git a/packages/studio/src/components/Timeline/TimelineItemStack.tsx b/packages/studio/src/components/Timeline/TimelineItemStack.tsx index f08cad05f5b..2a5c5147559 100644 --- a/packages/studio/src/components/Timeline/TimelineItemStack.tsx +++ b/packages/studio/src/components/Timeline/TimelineItemStack.tsx @@ -1,81 +1,15 @@ -import type {GitSource} from '@remotion/studio-shared'; -import React, {useCallback, useContext, useMemo, useState} from 'react'; +import React, {useMemo} from 'react'; import type {OriginalPosition} from '../../error-overlay/react-overlay/utils/get-source-map'; -import {StudioServerConnectionCtx} from '../../helpers/client-id'; -import {LIGHT_TEXT, VERY_LIGHT_TEXT} from '../../helpers/colors'; -import {getGitRefUrl} from '../../helpers/get-git-menu-item'; -import {openOriginalPositionInEditor} from '../../helpers/open-in-editor'; -import {Spacing} from '../layout'; -import {showNotification} from '../Notifications/NotificationCenter'; -import {Spinner} from '../Spinner'; -import {SELECTION_ENABLED} from './TimelineSelection'; +import {VERY_LIGHT_TEXT} from '../../helpers/colors'; import {getOriginalSourceAttribution} from './TimelineStack/source-attribution'; export const TimelineItemStack: React.FC<{ readonly originalLocation: OriginalPosition | null; }> = ({originalLocation}) => { - const [hovered, setHovered] = useState(false); - const [opening, setOpening] = useState(false); - - const connectionStatus = useContext(StudioServerConnectionCtx) - .previewServerState.type; - - const openEditor = useCallback(async (location: OriginalPosition) => { - if (!window.remotion_editorName) { - return; - } - - setOpening(true); - try { - await openOriginalPositionInEditor(location); - } catch (err) { - showNotification((err as Error).message, 2000); - } finally { - setOpening(false); - } - }, []); - - const canOpenInEditor = Boolean( - window.remotion_editorName && - connectionStatus === 'connected' && - originalLocation, - ); - - const canOpenInGitHub = Boolean( - window.remotion_gitSource && originalLocation, - ); - const hoverable = !SELECTION_ENABLED && (canOpenInEditor || canOpenInGitHub); - - const onClick = useCallback(() => { - if (!originalLocation) { - return; - } - - if (canOpenInEditor) { - openEditor(originalLocation); - return; - } - - if (canOpenInGitHub) { - window.open( - getGitRefUrl(window.remotion_gitSource as GitSource, originalLocation), - '_blank', - ); - } - }, [canOpenInEditor, canOpenInGitHub, openEditor, originalLocation]); - - const onPointerEnter = useCallback(() => setHovered(true), []); - const onPointerLeave = useCallback(() => setHovered(false), []); - const style = useMemo((): React.CSSProperties => { return { fontSize: 12, - color: opening - ? VERY_LIGHT_TEXT - : hovered && hoverable - ? LIGHT_TEXT - : VERY_LIGHT_TEXT, - cursor: hoverable ? 'pointer' : undefined, + color: VERY_LIGHT_TEXT, display: 'flex', flexDirection: 'row', alignItems: 'center', @@ -86,28 +20,13 @@ export const TimelineItemStack: React.FC<{ userSelect: 'none', WebkitUserSelect: 'none', }; - }, [opening, hovered, hoverable]); + }, []); if (!originalLocation) { return null; } return ( - <> -
- {getOriginalSourceAttribution(originalLocation)} -
- {opening ? ( - <> - - - - ) : null} - +
{getOriginalSourceAttribution(originalLocation)}
); }; diff --git a/packages/studio/src/components/Timeline/TimelineKeyframeControls.tsx b/packages/studio/src/components/Timeline/TimelineKeyframeControls.tsx index 8eb9c818c43..c233ff9b531 100644 --- a/packages/studio/src/components/Timeline/TimelineKeyframeControls.tsx +++ b/packages/studio/src/components/Timeline/TimelineKeyframeControls.tsx @@ -43,9 +43,8 @@ import {useTimelineKeyframeTracks} from './TimelineKeyframeTracksContext'; import { getTimelineSelectionFromNodePathInfo, getTimelineSelectionKey, - SELECTION_ENABLED, - type TimelineSelection, useTimelineSelection, + type TimelineSelection, } from './TimelineSelection'; const controlsContainerStyle: React.CSSProperties = { @@ -425,7 +424,7 @@ export const shouldShowTimelineKeyframeControls = ({ return true; } - return SELECTION_ENABLED && isKeyframedStatus(propStatus); + return isKeyframedStatus(propStatus); }; export const TimelineKeyframeControls: React.FC<{ diff --git a/packages/studio/src/components/Timeline/TimelineMediaInfo.tsx b/packages/studio/src/components/Timeline/TimelineMediaInfo.tsx index f202f702f2e..437964b8706 100644 --- a/packages/studio/src/components/Timeline/TimelineMediaInfo.tsx +++ b/packages/studio/src/components/Timeline/TimelineMediaInfo.tsx @@ -1,12 +1,7 @@ -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useMemo} from 'react'; import {Internals} from 'remotion'; -import {LIGHT_TEXT, VERY_LIGHT_TEXT} from '../../helpers/colors'; -import {useSelectAsset} from '../use-select-asset'; -import { - getTimelineAssetLinkInfo, - openTimelineAssetLink, -} from './timeline-asset-link'; -import {SELECTION_ENABLED} from './TimelineSelection'; +import {VERY_LIGHT_TEXT} from '../../helpers/colors'; +import {getTimelineAssetLinkInfo} from './timeline-asset-link'; const lineStyle: React.CSSProperties = { whiteSpace: 'nowrap', @@ -19,34 +14,12 @@ const lineStyle: React.CSSProperties = { }; const useAssetLink = (src: string) => { - const selectAsset = useSelectAsset(); - const [hovered, setHovered] = useState(false); - const linkInfo = useMemo(() => getTimelineAssetLinkInfo(src), [src]); - const interactive = !SELECTION_ENABLED && linkInfo !== null; - - const onClick = useCallback( - (e: React.MouseEvent) => { - if (!linkInfo) { - return; - } - - e.preventDefault(); - e.stopPropagation(); - - openTimelineAssetLink(linkInfo, selectAsset); - }, - [linkInfo, selectAsset], - ); - - const onPointerEnter = useCallback(() => setHovered(true), []); - const onPointerLeave = useCallback(() => setHovered(false), []); const fileNameStyle: React.CSSProperties = useMemo( () => ({ ...lineStyle, - color: interactive && hovered ? LIGHT_TEXT : VERY_LIGHT_TEXT, - cursor: interactive ? 'pointer' : undefined, + color: VERY_LIGHT_TEXT, textDecoration: 'none', display: 'inline-block', overflow: 'hidden', @@ -55,15 +28,11 @@ const useAssetLink = (src: string) => { userSelect: 'none', WebkitUserSelect: 'none', }), - [interactive, hovered], + [], ); return { linkInfo, - interactive, - onClick, - onPointerEnter, - onPointerLeave, fileNameStyle, }; }; @@ -73,23 +42,10 @@ export const TimelineMediaInfo: React.FC<{ }> = ({src}) => { const fileName = useMemo(() => Internals.getAssetDisplayName(src), [src]); - const { - linkInfo, - interactive, - onClick, - onPointerEnter, - onPointerLeave, - fileNameStyle, - } = useAssetLink(src); + const {linkInfo, fileNameStyle} = useAssetLink(src); return ( -
+
{fileName}
); diff --git a/packages/studio/src/components/Timeline/TimelineSelection.tsx b/packages/studio/src/components/Timeline/TimelineSelection.tsx index f13fedb1273..0a645a7d909 100644 --- a/packages/studio/src/components/Timeline/TimelineSelection.tsx +++ b/packages/studio/src/components/Timeline/TimelineSelection.tsx @@ -18,6 +18,7 @@ import type { import {TIMELINE_PADDING} from '../../helpers/timeline-layout'; import {timelineNodePathInfoToKey} from '../../helpers/timeline-node-path-key'; import {useKeybinding} from '../../helpers/use-keybinding'; +import {useZIndex} from '../../state/z-index'; import {TimelineClipboardKeybindings} from './TimelineClipboardKeybindings'; import {TimelineDeleteKeybindings} from './TimelineDeleteKeybindings'; @@ -61,14 +62,8 @@ export const getTimelineSelectedTrackHighlightStyle = ( width: timelineWidth, }); -export const SELECTION_ENABLED = false; -export const TIMELINE_TOP_DRAG = false; -export const ENABLE_OUTLINES = false; -export const TIMELINE_BACKGROUND = ENABLE_OUTLINES ? '#0F1113' : '#111'; -export const TIMELINE_TICKS_BACKGROUND = ENABLE_OUTLINES - ? BACKGROUND - : TIMELINE_BACKGROUND; -export const EASING_SELECTION_ENABLED = false; +export const TIMELINE_BACKGROUND = '#0F1113'; +export const TIMELINE_TICKS_BACKGROUND = BACKGROUND; type TimelineSelectionBase = { readonly nodePathInfo: SequenceNodePathInfo; @@ -655,11 +650,9 @@ export const TimelineSelectionProvider: React.FC<{ }> = ({children}) => { const {previewServerState} = useContext(StudioServerConnectionCtx); const canSelect = - (SELECTION_ENABLED || ENABLE_OUTLINES) && previewServerState.type === 'connected' && !window.remotion_isReadOnlyStudio; const canSelectEasing = - EASING_SELECTION_ENABLED && previewServerState.type === 'connected' && !window.remotion_isReadOnlyStudio; const [selectedItems, setSelectedItems] = useState< @@ -902,12 +895,17 @@ export const useCurrentTimelineSelectionStateAsRef = () => { export const useTimelineMarqueeSelection = () => { const {canSelect, canSelectEasing, getMarqueeSelection, selectItems} = useTimelineSelection(); + const {isHighestContext} = useZIndex(); const [marqueeRect, setMarqueeRect] = useState( null, ); const onPointerDownCapture = useCallback( (event: React.PointerEvent) => { + if (!isHighestContext) { + return; + } + if (event.button !== 0 || (!canSelect && !canSelectEasing)) { return; } @@ -1011,7 +1009,13 @@ export const useTimelineMarqueeSelection = () => { window.addEventListener('pointerup', onPointerUp); window.addEventListener('pointercancel', onPointerCancel); }, - [canSelect, canSelectEasing, getMarqueeSelection, selectItems], + [ + canSelect, + canSelectEasing, + getMarqueeSelection, + isHighestContext, + selectItems, + ], ); return {marqueeRect, onPointerDownCapture}; diff --git a/packages/studio/src/components/Timeline/TimelineSequence.tsx b/packages/studio/src/components/Timeline/TimelineSequence.tsx index 655d1d14a1c..88a5005dbbe 100644 --- a/packages/studio/src/components/Timeline/TimelineSequence.tsx +++ b/packages/studio/src/components/Timeline/TimelineSequence.tsx @@ -16,9 +16,8 @@ import {AudioWaveform} from '../AudioWaveform'; import {LoopedTimelineIndicator} from './LoopedTimelineIndicators'; import {TimelineImageInfo} from './TimelineImageInfo'; import { - TIMELINE_MARQUEE_ITEM_ATTR, - TIMELINE_TOP_DRAG, shouldSelectTimelineRowOnPointerDown, + TIMELINE_MARQUEE_ITEM_ATTR, useTimelineMarqueeSelectableItem, useTimelineRowSelection, } from './TimelineSelection'; @@ -104,7 +103,7 @@ const TimelineSequenceCurrentFrame: React.FC<{ }); } - if (TIMELINE_TOP_DRAG && fromCanUpdate) { + if (fromCanUpdate) { onMoveDragPointerDown(e); } } @@ -309,7 +308,6 @@ const TimelineSequenceInner: React.FC<{ }, [marginLeft, s.type, width]); const showRightEdgeDragHandle = - TIMELINE_TOP_DRAG && (s.type === 'sequence' || s.type === 'image') && !s.loopDisplay && !s.isInsideSeries && diff --git a/packages/studio/src/components/Timeline/TimelineSequenceItem.tsx b/packages/studio/src/components/Timeline/TimelineSequenceItem.tsx index 00852463b9f..0df05bd94a3 100644 --- a/packages/studio/src/components/Timeline/TimelineSequenceItem.tsx +++ b/packages/studio/src/components/Timeline/TimelineSequenceItem.tsx @@ -17,8 +17,8 @@ import {ContextMenu} from '../ContextMenu'; import { addEffectFromDragData, getEffectDragData, - hasExplicitEffectDragType, hasEffectDragType, + hasExplicitEffectDragType, } from '../effect-drag-and-drop'; import { ExpandedTracksGetterContext, @@ -45,7 +45,6 @@ import {TimelineMediaInfo} from './TimelineMediaInfo'; import {TimelineRowChrome} from './TimelineRowChrome'; import { isTimelineSelectionModifierEvent, - SELECTION_ENABLED, useTimelineRowContainsSelection, useTimelineRowSelection, } from './TimelineSelection'; @@ -236,11 +235,10 @@ export const TimelineSequenceItem: React.FC<{ ); const parentId = sequence.parent ?? null; const canReorderSequence = - SELECTION_ENABLED && previewConnected && Boolean(nodePath && nodePathKey && validatedLocation?.source) && nodePathInfo?.numberOfSequencesWithThisNodePath === 1; - const canHandleSequenceDrag = SELECTION_ENABLED && previewConnected; + const canHandleSequenceDrag = previewConnected; const confirm = useConfirmationDialog(); const deleteDisabled = useMemo( @@ -671,7 +669,7 @@ export const TimelineSequenceItem: React.FC<{ const onShowInEditorDoubleClick = useCallback( (e: React.MouseEvent) => { - if (!SELECTION_ENABLED || !canOpenInEditor) { + if (!canOpenInEditor) { return; } @@ -913,11 +911,7 @@ export const TimelineSequenceItem: React.FC<{ onDragLeave={canDropEffect ? onEffectDragLeave : undefined} onDragOver={canDropEffect ? onEffectDragOver : undefined} onDrop={canDropEffect ? onEffectDrop : undefined} - onDoubleClick={ - SELECTION_ENABLED && canOpenInEditor - ? onShowInEditorDoubleClick - : undefined - } + onDoubleClick={canOpenInEditor ? onShowInEditorDoubleClick : undefined} >
= ({sequence, selected, containsSelection}) => { - const [hovered, setHovered] = useState(false); - const {documentationLink} = sequence; - const hoverable = !SELECTION_ENABLED && documentationLink !== null; - - const onClick = useCallback(() => { - if (documentationLink === null) { - return; - } - - window.open(documentationLink, '_blank', 'noopener,noreferrer'); - }, [documentationLink]); - - const onPointerEnter = useCallback(() => setHovered(true), []); - const onPointerLeave = useCallback(() => setHovered(false), []); - const style = useMemo((): React.CSSProperties => { - const hoverEffect = hovered && hoverable; return { alignItems: 'center', alignSelf: 'stretch', @@ -50,27 +33,18 @@ export const TimelineSequenceName: React.FC<{ color: getTimelineColor(selected, false), userSelect: 'none', WebkitUserSelect: 'none', - textDecoration: hoverEffect ? 'underline' : 'none', - cursor: hoverEffect ? 'pointer' : undefined, + textDecoration: 'none', boxShadow: containsSelection && !selected ? `inset 0 0 0 2px ${TIMELINE_SELECTED_LABEL_BACKGROUND}` : undefined, }; - }, [hovered, hoverable, selected, containsSelection]); + }, [selected, containsSelection]); const text = getTruncatedDisplayName(sequence.displayName) || ''; return ( -
+
{text}
); diff --git a/packages/studio/src/components/Timeline/TimelineTimeIndicators.tsx b/packages/studio/src/components/Timeline/TimelineTimeIndicators.tsx index 5970e525f28..781a0a7f4df 100644 --- a/packages/studio/src/components/Timeline/TimelineTimeIndicators.tsx +++ b/packages/studio/src/components/Timeline/TimelineTimeIndicators.tsx @@ -13,7 +13,7 @@ import {renderFrame} from '../../state/render-frame'; import {TimeValue} from '../TimeValue'; import {timelineVerticalScroll} from './timeline-refs'; import {getFrameIncrementFromWidth} from './timeline-scroll-logic'; -import {ENABLE_OUTLINES, TIMELINE_TICKS_BACKGROUND} from './TimelineSelection'; +import {TIMELINE_TICKS_BACKGROUND} from './TimelineSelection'; import {TimelineWidthContext} from './TimelineWidthProvider'; export const TIMELINE_TIME_INDICATOR_HEIGHT = 39; @@ -23,9 +23,7 @@ const container: React.CSSProperties = { position: 'absolute', backgroundColor: TIMELINE_TICKS_BACKGROUND, top: 0, - borderBottom: ENABLE_OUTLINES - ? `${TIMELINE_ITEM_BORDER_BOTTOM}px solid ${TIMELINE_TRACK_SEPARATOR}` - : undefined, + borderBottom: `${TIMELINE_ITEM_BORDER_BOTTOM}px solid ${TIMELINE_TRACK_SEPARATOR}`, }; const tick: React.CSSProperties = { diff --git a/packages/studio/src/test/timeline-selection.test.ts b/packages/studio/src/test/timeline-selection.test.ts index 8fccd9a0e5a..7236449224b 100644 --- a/packages/studio/src/test/timeline-selection.test.ts +++ b/packages/studio/src/test/timeline-selection.test.ts @@ -18,6 +18,7 @@ import { } from '../components/selected-outline-uv'; import { applySelectedOutlineDragAxisLock, + compensateTranslateForTransformOrigin, getOutlineSelectionInteraction, getSelectedEffectFieldsBySequenceKey, getSelectedOutlineDragChanges, @@ -31,7 +32,6 @@ import { getSelectedOutlineScaleEdgeInfo, getSelectedSequenceKeys, getSequencesWithSelectableOutlines, - compensateTranslateForTransformOrigin, type SelectedOutlineDragState, type SelectedOutlineRotationDragState, type SelectedOutlineScaleDragState, @@ -49,7 +49,6 @@ import { } from '../components/Timeline/TimelineClipboardKeybindings'; import {getSelectedKeyframeControlNodePathInfos} from '../components/Timeline/TimelineKeyframeControls'; import { - ENABLE_OUTLINES, getClampedTimelineMarqueePoint, getSelectableTimelineSequenceSelections, getTimelineMarqueeSelection, @@ -57,11 +56,9 @@ import { getTimelineSelectionFromNodePathInfo, getTimelineSequenceSelectionKey, isTimelineSelectionModifierEvent, - SELECTION_ENABLED, shouldSelectTimelineRowOnPointerDown, TIMELINE_BACKGROUND, TIMELINE_TICKS_BACKGROUND, - TIMELINE_TOP_DRAG, timelineMarqueeRectsIntersect, } from '../components/Timeline/TimelineSelection'; import { @@ -73,8 +70,8 @@ import { getTimelineSequenceFromDragValue, } from '../components/Timeline/TimelineSequenceRightEdgeDragHandle'; import { - parseTransformOrigin, parsedTransformOriginToUv, + parseTransformOrigin, serializeTransformOrigin, } from '../components/Timeline/transform-origin-utils'; import type {SequenceNodePathInfo} from '../helpers/get-timeline-sequence-sort-key'; @@ -220,10 +217,6 @@ const makeFromPropStatuses = ( return propStatuses; }; -test('Timeline selection should stay disabled until released publicly', () => { - expect(SELECTION_ENABLED).toBe(false); -}); - test('timeline marquee rectangle intersection detects overlapping targets', () => { expect( timelineMarqueeRectsIntersect( @@ -757,10 +750,6 @@ test('pasting an effect prop requires the same effect type and prop key', () => ).toEqual({type: 'effect-type-mismatch'} satisfies PasteEffectPropTarget); }); -test('Timeline top drag should not be enabled', () => { - expect(TIMELINE_TOP_DRAG).toBe(false); -}); - test('Timeline duration drag applies the same delta to selected sequences', () => { const schema = {} satisfies SequenceSchema; const firstNodePathInfo = makeNodePathInfo(['body', 0], []); @@ -1178,13 +1167,9 @@ test('Timeline from drag removes the prop at the default value', () => { }); }); -test('Timeline outlines should not be enabled', () => { - expect(ENABLE_OUTLINES).toBe(false); -}); - -test('Timeline colors should use the old palette when outlines are disabled', () => { - expect(TIMELINE_BACKGROUND).toBe('#111'); - expect(TIMELINE_TICKS_BACKGROUND).toBe('#111'); +test('Timeline colors use the outlines palette', () => { + expect(TIMELINE_BACKGROUND).toBe('#0F1113'); + expect(TIMELINE_TICKS_BACKGROUND).not.toBe(TIMELINE_BACKGROUND); }); test('Timeline outlines visibility is enabled by default and persisted', () => { From 95193a508016679ece222c8ff183657afa6a2525 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 9 Jun 2026 16:48:33 +0200 Subject: [PATCH 2/8] Internal: Experimental high-DPI screen recorder (#8275) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bun.lock | 24 + packages/canvas-capture/.npmignore | 10 + packages/canvas-capture/README.md | 7 + packages/canvas-capture/bundle.ts | 35 + packages/canvas-capture/eslint.config.mjs | 7 + packages/canvas-capture/package.json | 55 + packages/canvas-capture/src/index.tsx | 559 +++++ packages/canvas-capture/tsconfig.json | 16 + packages/cli/src/list-of-remotion-packages.ts | 1 + .../src/list-of-remotion-packages.ts | 1 + packages/example/package.json | 1 + .../remotion-studio-canvas-recording.json | 1885 +++++++++++++++++ packages/example/src/CanvasCapturePreview.tsx | 434 ++++ packages/example/src/Root.tsx | 15 + packages/example/tsconfig.json | 1 + packages/studio-shared/src/package-info.ts | 4 + packages/studio/bundle.ts | 1 + packages/studio/package.json | 1 + packages/studio/src/components/Editor.tsx | 10 +- .../src/components/StudioCanvasCapture.tsx | 55 + .../src/components/canvas-capture-enabled.ts | 1 + .../src/test/canvas-capture-enabled.test.ts | 6 + packages/studio/tsconfig.json | 3 + tsconfig.json | 3 + 24 files changed, 3134 insertions(+), 1 deletion(-) create mode 100644 packages/canvas-capture/.npmignore create mode 100644 packages/canvas-capture/README.md create mode 100644 packages/canvas-capture/bundle.ts create mode 100644 packages/canvas-capture/eslint.config.mjs create mode 100644 packages/canvas-capture/package.json create mode 100644 packages/canvas-capture/src/index.tsx create mode 100644 packages/canvas-capture/tsconfig.json create mode 100644 packages/example/public/remotion-studio-canvas-recording.json create mode 100644 packages/example/src/CanvasCapturePreview.tsx create mode 100644 packages/studio/src/components/StudioCanvasCapture.tsx create mode 100644 packages/studio/src/components/canvas-capture-enabled.ts create mode 100644 packages/studio/src/test/canvas-capture-enabled.test.ts diff --git a/bun.lock b/bun.lock index 3bf80147223..a8d05400418 100644 --- a/bun.lock +++ b/bun.lock @@ -151,6 +151,25 @@ "react-dom": ">=16.8.0", }, }, + "packages/canvas-capture": { + "name": "@remotion/canvas-capture", + "version": "4.0.474", + "dependencies": { + "mediabunny": "catalog:", + "remotion": "workspace:*", + }, + "devDependencies": { + "@remotion/eslint-config-internal": "workspace:*", + "@typescript/native-preview": "catalog:", + "eslint": "catalog:", + "react": "catalog:", + "react-dom": "catalog:", + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + }, + }, "packages/captions": { "name": "@remotion/captions", "version": "4.0.474", @@ -427,6 +446,7 @@ "@remotion/animation-utils": "workspace:*", "@remotion/babel-loader": "workspace:*", "@remotion/bundler": "workspace:*", + "@remotion/canvas-capture": "workspace:*", "@remotion/captions": "workspace:*", "@remotion/cli": "workspace:*", "@remotion/cloudrun": "workspace:*", @@ -675,6 +695,7 @@ "@remotion/animation-utils": "workspace:*", "@remotion/babel-loader": "workspace:*", "@remotion/bundler": "workspace:*", + "@remotion/canvas-capture": "workspace:*", "@remotion/captions": "workspace:*", "@remotion/cli": "workspace:*", "@remotion/cloudrun": "workspace:*", @@ -1551,6 +1572,7 @@ "version": "4.0.474", "dependencies": { "@jridgewell/trace-mapping": "0.3.31", + "@remotion/canvas-capture": "workspace:*", "@remotion/media-utils": "workspace:*", "@remotion/player": "workspace:*", "@remotion/renderer": "workspace:*", @@ -3900,6 +3922,8 @@ "@remotion/bundler": ["@remotion/bundler@workspace:packages/bundler"], + "@remotion/canvas-capture": ["@remotion/canvas-capture@workspace:packages/canvas-capture"], + "@remotion/captions": ["@remotion/captions@workspace:packages/captions"], "@remotion/cli": ["@remotion/cli@workspace:packages/cli"], diff --git a/packages/canvas-capture/.npmignore b/packages/canvas-capture/.npmignore new file mode 100644 index 00000000000..eb76a5ae811 --- /dev/null +++ b/packages/canvas-capture/.npmignore @@ -0,0 +1,10 @@ +dist/test/** +src +eslint.config.mjs +tsconfig.tsbuildinfo +*.tgz +.turbo +tsconfig.json +.prettierrc.js +bundle.ts +dist/cjs/test diff --git a/packages/canvas-capture/README.md b/packages/canvas-capture/README.md new file mode 100644 index 00000000000..abbe5e85ff8 --- /dev/null +++ b/packages/canvas-capture/README.md @@ -0,0 +1,7 @@ +# @remotion/canvas-capture + +Capture HTML-in-canvas content as a video + +## Usage + +This is an internal package and has no documentation. diff --git a/packages/canvas-capture/bundle.ts b/packages/canvas-capture/bundle.ts new file mode 100644 index 00000000000..ddcee82940e --- /dev/null +++ b/packages/canvas-capture/bundle.ts @@ -0,0 +1,35 @@ +import path from 'path'; +import {build} from 'bun'; + +if (process.env.NODE_ENV !== 'production') { + throw new Error('This script must be run using NODE_ENV=production'); +} + +console.time('Generated.'); +const output = await build({ + entrypoints: ['src/index.tsx'], + naming: '[name].mjs', + external: [ + 'mediabunny', + 'remotion', + 'remotion/no-react', + 'react', + 'react/jsx-runtime', + 'react/jsx-dev-runtime', + 'react-dom', + ], +}); + +if (!output.success) { + console.log(output.logs.join('\n')); + process.exit(1); +} + +for (const file of output.outputs) { + const str = await file.text(); + const out = path.join('dist', 'esm', file.path); + + await Bun.write(out, str); +} + +console.timeEnd('Generated.'); diff --git a/packages/canvas-capture/eslint.config.mjs b/packages/canvas-capture/eslint.config.mjs new file mode 100644 index 00000000000..ab29acaba08 --- /dev/null +++ b/packages/canvas-capture/eslint.config.mjs @@ -0,0 +1,7 @@ +import {remotionFlatConfig} from '@remotion/eslint-config-internal'; + +const config = remotionFlatConfig({react: true}); + +export default { + ...config, +}; diff --git a/packages/canvas-capture/package.json b/packages/canvas-capture/package.json new file mode 100644 index 00000000000..2f57a6ac32a --- /dev/null +++ b/packages/canvas-capture/package.json @@ -0,0 +1,55 @@ +{ + "repository": { + "url": "https://github.com/remotion-dev/remotion/tree/main/packages/canvas-capture" + }, + "name": "@remotion/canvas-capture", + "version": "4.0.474", + "description": "Capture HTML-in-canvas content as a video", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/esm/index.mjs", + "type": "module", + "scripts": { + "formatting": "oxfmt src --check", + "format": "oxfmt src", + "lint": "eslint src", + "watch": "tsgo -w", + "make": "tsgo && bun --env-file=../.env.bundle bundle.ts" + }, + "author": "Jonny Burger ", + "contributors": [], + "license": "Remotion License", + "bugs": { + "url": "https://github.com/remotion-dev/remotion/issues" + }, + "dependencies": { + "mediabunny": "catalog:", + "remotion": "workspace:*" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "module": "./dist/esm/index.mjs", + "import": "./dist/esm/index.mjs" + }, + "./package.json": "./package.json" + }, + "devDependencies": { + "react": "catalog:", + "react-dom": "catalog:", + "@remotion/eslint-config-internal": "workspace:*", + "eslint": "catalog:", + "@typescript/native-preview": "catalog:" + }, + "keywords": [ + "remotion", + "canvas-capture" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/canvas-capture/src/index.tsx b/packages/canvas-capture/src/index.tsx new file mode 100644 index 00000000000..94f16f71d28 --- /dev/null +++ b/packages/canvas-capture/src/index.tsx @@ -0,0 +1,559 @@ +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, +} from 'react'; + +type HtmlInCanvasElement = HTMLCanvasElement & { + layoutSubtree?: boolean; + requestPaint?: () => void; +}; + +type HtmlInCanvasRenderingContext2D = CanvasRenderingContext2D & { + drawElementImage?: ( + element: Element, + dx: number, + dy: number, + dWidth?: number, + dHeight?: number, + ) => DOMMatrix; + reset?: () => void; +}; + +type CanvasVideoSource = { + add: ( + timestamp: number, + duration: number, + encodeOptions?: VideoEncoderEncodeOptions, + ) => Promise; + close: () => void; +}; + +type RecordingOutput = { + start: () => Promise; + finalize: () => Promise; + cancel: () => Promise; +}; + +type RecordingTarget = { + readonly buffer: ArrayBuffer | null; +}; + +type MouseMovement = { + readonly timeInSeconds: number; + readonly clientX: number; + readonly clientY: number; + readonly pageX: number; + readonly pageY: number; + readonly canvasX: number | null; + readonly canvasY: number | null; + readonly cursor: string; +}; + +type CaptureMetadata = { + readonly density: number; + readonly contentRect: { + readonly left: number; + readonly top: number; + readonly width: number; + readonly height: number; + }; + readonly canvasSize: { + readonly width: number; + readonly height: number; + }; + readonly viewport: { + readonly width: number; + readonly height: number; + readonly scrollX: number; + readonly scrollY: number; + }; +}; + +type RecordingState = { + readonly output: RecordingOutput; + readonly target: RecordingTarget; + readonly source: CanvasVideoSource; + readonly startedAt: number; + readonly mouseMovements: MouseMovement[]; + lastTimestampInSeconds: number | null; + lastFramePromise: Promise; + frameCount: number; + captureMetadata: CaptureMetadata | null; + isFinalizing: boolean; +}; + +export type HtmlInCanvasCaptureHandle = { + readonly toggleRecording: () => Promise; + readonly startRecording: () => Promise; + readonly stopRecording: () => Promise; +}; + +type HtmlInCanvasCaptureProps = { + readonly children: React.ReactNode; + readonly density: number; + readonly filename: string; +}; + +type WithHtmlInCanvasCaptureProps = { + readonly density: number; + readonly filename: string; +}; + +const canvasStyle: React.CSSProperties = { + position: 'absolute', + inset: 0, + width: '100%', + height: '100%', + display: 'block', +}; + +const contentStyle: React.CSSProperties = { + position: 'absolute', + inset: 0, + width: '100%', + height: '100%', + transformOrigin: 'top left', +}; + +const fallbackFrameDurationInSeconds = 1 / 30; +const recordingVideoBitrate = 60_000_000; +const recordingKeyFrameIntervalInSeconds = 0.5; + +export const isHtmlInCanvasAvailable = () => { + if (typeof document === 'undefined') { + return false; + } + + const canvas = document.createElement('canvas') as HtmlInCanvasElement; + const context = canvas.getContext( + '2d', + ) as HtmlInCanvasRenderingContext2D | null; + + return ( + typeof canvas.requestPaint === 'function' && + typeof context?.drawElementImage === 'function' + ); +}; + +const resetCanvas = ( + context: HtmlInCanvasRenderingContext2D, + canvas: HTMLCanvasElement, +) => { + if (typeof context.reset === 'function') { + context.reset(); + return; + } + + context.setTransform(1, 0, 0, 1, 0, 0); + context.clearRect(0, 0, canvas.width, canvas.height); +}; + +const roundUpToEven = (value: number) => { + const rounded = Math.max(2, Math.ceil(value)); + return rounded % 2 === 0 ? rounded : rounded + 1; +}; + +const syncCanvasSize = ( + canvas: HTMLCanvasElement, + width: number, + height: number, + density: number, +) => { + const scaledWidth = roundUpToEven(width * density); + const scaledHeight = roundUpToEven(height * density); + + if (canvas.width !== scaledWidth) { + canvas.width = scaledWidth; + } + + if (canvas.height !== scaledHeight) { + canvas.height = scaledHeight; + } +}; + +const getCursorForElement = (element: Element | null): string => { + let current: Element | null = element; + + while (current) { + const {cursor} = window.getComputedStyle(current); + if (cursor !== 'auto') { + return cursor; + } + + current = current.parentElement; + } + + return 'auto'; +}; + +const downloadBlob = (blob: Blob, filename: string) => { + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = filename; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + URL.revokeObjectURL(url); +}; + +const getJsonFilename = (filename: string) => { + return filename.replace(/\.[^.]+$/, '') + '.json'; +}; + +const logCaptureError = (message: string, err: unknown) => { + // eslint-disable-next-line no-console + console.error(message, err instanceof Error ? err.message : String(err)); +}; + +const addPaintFrameToRecording = ( + recording: RecordingState, + source: CanvasVideoSource, +) => { + const elapsedInSeconds = Math.max( + 0, + (performance.now() - recording.startedAt) / 1000, + ); + const timestampInSeconds = + recording.lastTimestampInSeconds === null ? 0 : elapsedInSeconds; + const durationInSeconds = + recording.lastTimestampInSeconds === null + ? fallbackFrameDurationInSeconds + : Math.max( + fallbackFrameDurationInSeconds, + elapsedInSeconds - recording.lastTimestampInSeconds, + ); + const keyFrame = recording.lastTimestampInSeconds === null; + + recording.lastTimestampInSeconds = timestampInSeconds; + recording.lastFramePromise = recording.lastFramePromise.then(async () => { + await source.add(timestampInSeconds, durationInSeconds, {keyFrame}); + recording.frameCount++; + }); +}; + +const finalizeRecording = async ( + recording: RecordingState, + filename: string, +) => { + addPaintFrameToRecording(recording, recording.source); + await recording.lastFramePromise; + + if (recording.frameCount === 0) { + throw new Error('No frames were added to the Studio canvas recording.'); + } + + recording.source.close(); + await recording.output.finalize(); + + if (!recording.target.buffer) { + throw new Error('Mediabunny did not return an output buffer.'); + } + + downloadBlob( + new Blob([recording.target.buffer], {type: 'video/webm'}), + filename, + ); + downloadBlob( + new Blob( + [ + JSON.stringify( + { + startedAt: recording.startedAt, + endedAt: performance.now(), + captureMetadata: recording.captureMetadata, + mouseMovements: recording.mouseMovements, + }, + null, + 2, + ), + ], + {type: 'application/json'}, + ), + getJsonFilename(filename), + ); +}; + +export const HtmlInCanvasCapture = forwardRef< + HtmlInCanvasCaptureHandle, + HtmlInCanvasCaptureProps +>(({children, density, filename}, ref) => { + if (!Number.isFinite(density) || density <= 0) { + throw new Error('HTML-in-canvas capture density must be greater than 0.'); + } + + const isSupported = useMemo(() => isHtmlInCanvasAvailable(), []); + const canvasRef = useRef(null); + const contentRef = useRef(null); + const recordingRef = useRef(null); + const recordingActionRef = useRef>(Promise.resolve()); + + const requestPaint = useCallback(() => { + const canvas = canvasRef.current; + if (typeof canvas?.requestPaint !== 'function') { + return; + } + + canvas.requestPaint(); + }, []); + + const startRecording = useCallback(async () => { + const canvas = canvasRef.current; + if (!canvas || recordingRef.current) { + return; + } + + const {BufferTarget, CanvasSource, Output, WebMOutputFormat} = + await import('mediabunny'); + const target = new BufferTarget(); + const output = new Output({ + format: new WebMOutputFormat(), + target, + }); + const source = new CanvasSource(canvas, { + codec: 'vp9', + bitrate: recordingVideoBitrate, + latencyMode: 'realtime', + keyFrameInterval: recordingKeyFrameIntervalInSeconds, + }); + + output.addVideoTrack(source); + await output.start(); + + recordingRef.current = { + output, + target, + source, + startedAt: performance.now(), + mouseMovements: [], + lastTimestampInSeconds: null, + lastFramePromise: Promise.resolve(), + frameCount: 0, + captureMetadata: null, + isFinalizing: false, + }; + requestPaint(); + }, [requestPaint]); + + const stopRecording = useCallback(async () => { + const recording = recordingRef.current; + if (!recording || recording.isFinalizing) { + return; + } + + recording.isFinalizing = true; + + try { + await finalizeRecording(recording, filename); + } catch (err) { + logCaptureError('Could not finalize Studio canvas recording:', err); + await recording.output.cancel().catch((cancelErr) => { + logCaptureError('Could not cancel Studio canvas recording:', cancelErr); + }); + } finally { + recordingRef.current = null; + } + }, [filename]); + + const toggleRecording = useCallback(async () => { + recordingActionRef.current = recordingActionRef.current.then(async () => { + if (recordingRef.current) { + await stopRecording(); + return; + } + + await startRecording(); + }); + + await recordingActionRef.current; + }, [startRecording, stopRecording]); + + useImperativeHandle( + ref, + () => ({ + toggleRecording, + startRecording, + stopRecording, + }), + [startRecording, stopRecording, toggleRecording], + ); + + const drawCurrentPaint = useCallback(() => { + const canvas = canvasRef.current; + const content = contentRef.current; + if (!canvas || !content) { + return; + } + + const context = canvas.getContext( + '2d', + ) as HtmlInCanvasRenderingContext2D | null; + if (!context || typeof context.drawElementImage !== 'function') { + throw new Error('drawElementImage() is not available.'); + } + + const rect = content.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) { + return; + } + + syncCanvasSize(canvas, rect.width, rect.height, density); + resetCanvas(context, canvas); + context.scale(density, density); + context.drawElementImage(content, 0, 0, rect.width, rect.height); + + const recording = recordingRef.current; + if (recording && !recording.isFinalizing) { + recording.captureMetadata = { + density, + contentRect: { + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + }, + canvasSize: { + width: canvas.width, + height: canvas.height, + }, + viewport: { + width: window.innerWidth, + height: window.innerHeight, + scrollX: window.scrollX, + scrollY: window.scrollY, + }, + }; + addPaintFrameToRecording(recording, recording.source); + } + }, [density]); + + useEffect(() => { + const onMouseMove = (event: MouseEvent) => { + const recording = recordingRef.current; + if (!recording || recording.isFinalizing) { + return; + } + + const rect = contentRef.current?.getBoundingClientRect(); + + recording.mouseMovements.push({ + timeInSeconds: (performance.now() - recording.startedAt) / 1000, + clientX: event.clientX, + clientY: event.clientY, + pageX: event.pageX, + pageY: event.pageY, + canvasX: rect ? (event.clientX - rect.left) * density : null, + canvasY: rect ? (event.clientY - rect.top) * density : null, + cursor: getCursorForElement( + document.elementFromPoint(event.clientX, event.clientY), + ), + }); + }; + + window.addEventListener('mousemove', onMouseMove); + + return () => { + window.removeEventListener('mousemove', onMouseMove); + }; + }, [density]); + + useEffect(() => { + if (!isSupported) { + return; + } + + const canvas = canvasRef.current; + if (!canvas) { + return; + } + + canvas.setAttribute('layoutsubtree', ''); + canvas.layoutSubtree = true; + + const onPaint = () => { + try { + drawCurrentPaint(); + } catch (err) { + logCaptureError('Could not capture Studio canvas paint:', err); + } + }; + + canvas.addEventListener('paint', onPaint as EventListener); + const frame = requestAnimationFrame(requestPaint); + + return () => { + cancelAnimationFrame(frame); + canvas.removeEventListener('paint', onPaint as EventListener); + }; + }, [drawCurrentPaint, isSupported, requestPaint]); + + useEffect(() => { + if (!isSupported) { + return; + } + + const content = contentRef.current; + const canvas = canvasRef.current; + if (!content || !canvas) { + return; + } + + const resizeObserver = new ResizeObserver(([entry]) => { + const {width, height} = entry.contentRect; + syncCanvasSize(canvas, width, height, density); + requestPaint(); + }); + + resizeObserver.observe(content); + + return () => { + resizeObserver.disconnect(); + }; + }, [density, isSupported, requestPaint]); + + useEffect(() => { + return () => { + const recording = recordingRef.current; + if (!recording || recording.isFinalizing) { + return; + } + + recording.isFinalizing = true; + recording.output.cancel().catch((err) => { + logCaptureError('Could not cancel Studio canvas recording:', err); + }); + recordingRef.current = null; + }; + }, []); + + if (!isSupported) { + return children; + } + + return ( + +
+ {children} +
+
+ ); +}); + +export const withHtmlInCanvasCapture = ( + Component: React.ComponentType, +) => { + return forwardRef< + HtmlInCanvasCaptureHandle, + Props & WithHtmlInCanvasCaptureProps + >(({density, filename, ...props}, ref) => { + return ( + + + + ); + }); +}; diff --git a/packages/canvas-capture/tsconfig.json b/packages/canvas-capture/tsconfig.json new file mode 100644 index 00000000000..22b60bbe95b --- /dev/null +++ b/packages/canvas-capture/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.settings.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "jsx": "react-jsx", + "skipLibCheck": true, + "target": "ES2022", + "module": "es2020", + "moduleResolution": "bundler", + "declaration": true, + "emitDeclarationOnly": true + }, + "include": ["src"], + "references": [{"path": "../core"}] +} diff --git a/packages/cli/src/list-of-remotion-packages.ts b/packages/cli/src/list-of-remotion-packages.ts index 44f151356cd..a72aa959e46 100644 --- a/packages/cli/src/list-of-remotion-packages.ts +++ b/packages/cli/src/list-of-remotion-packages.ts @@ -8,6 +8,7 @@ export const listOfRemotionPackages = [ '@remotion/bugs', '@remotion/brand', '@remotion/bundler', + '@remotion/canvas-capture', '@remotion/cli', '@remotion/cloudrun', '@remotion/codex-plugin', diff --git a/packages/create-video/src/list-of-remotion-packages.ts b/packages/create-video/src/list-of-remotion-packages.ts index 44f151356cd..a72aa959e46 100644 --- a/packages/create-video/src/list-of-remotion-packages.ts +++ b/packages/create-video/src/list-of-remotion-packages.ts @@ -8,6 +8,7 @@ export const listOfRemotionPackages = [ '@remotion/bugs', '@remotion/brand', '@remotion/bundler', + '@remotion/canvas-capture', '@remotion/cli', '@remotion/cloudrun', '@remotion/codex-plugin', diff --git a/packages/example/package.json b/packages/example/package.json index 0aa4abe7a09..e3e90f7f0fd 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -40,6 +40,7 @@ "@remotion/animation-utils": "workspace:*", "@remotion/babel-loader": "workspace:*", "@remotion/bundler": "workspace:*", + "@remotion/canvas-capture": "workspace:*", "@remotion/effects": "workspace:*", "@remotion/captions": "workspace:*", "@remotion/cli": "workspace:*", diff --git a/packages/example/public/remotion-studio-canvas-recording.json b/packages/example/public/remotion-studio-canvas-recording.json new file mode 100644 index 00000000000..202127111b1 --- /dev/null +++ b/packages/example/public/remotion-studio-canvas-recording.json @@ -0,0 +1,1885 @@ +{ + "startedAt": 50139.40000009537, + "endedAt": 55572.90000009537, + "captureMetadata": { + "density": 2, + "contentRect": { + "left": 0, + "top": 0, + "width": 2131, + "height": 1328 + }, + "canvasSize": { + "width": 4262, + "height": 2656 + }, + "viewport": { + "width": 2131, + "height": 1328, + "scrollX": 0, + "scrollY": 0 + } + }, + "mouseMovements": [ + { + "timeInSeconds": 0.5171999998092651, + "clientX": 767, + "clientY": 557, + "pageX": 767, + "pageY": 557, + "canvasX": 1534, + "canvasY": 1114, + "cursor": "auto" + }, + { + "timeInSeconds": 0.5400999999046325, + "clientX": 772, + "clientY": 692, + "pageX": 772, + "pageY": 692, + "canvasX": 1544, + "canvasY": 1384, + "cursor": "auto" + }, + { + "timeInSeconds": 0.5575, + "clientX": 792, + "clientY": 844, + "pageX": 792, + "pageY": 844, + "canvasX": 1584, + "canvasY": 1688, + "cursor": "auto" + }, + { + "timeInSeconds": 0.5743999996185303, + "clientX": 823, + "clientY": 990, + "pageX": 823, + "pageY": 990, + "canvasX": 1646, + "canvasY": 1980, + "cursor": "auto" + }, + { + "timeInSeconds": 0.5903999996185303, + "clientX": 835, + "clientY": 1065, + "pageX": 835, + "pageY": 1065, + "canvasX": 1670, + "canvasY": 2130, + "cursor": "auto" + }, + { + "timeInSeconds": 0.6071999998092651, + "clientX": 836, + "clientY": 1094, + "pageX": 836, + "pageY": 1094, + "canvasX": 1672, + "canvasY": 2188, + "cursor": "auto" + }, + { + "timeInSeconds": 0.6565999999046326, + "clientX": 836, + "clientY": 1094, + "pageX": 836, + "pageY": 1094, + "canvasX": 1672, + "canvasY": 2188, + "cursor": "auto" + }, + { + "timeInSeconds": 0.6737999997138977, + "clientX": 831, + "clientY": 1089, + "pageX": 831, + "pageY": 1089, + "canvasX": 1662, + "canvasY": 2178, + "cursor": "auto" + }, + { + "timeInSeconds": 0.6905, + "clientX": 820, + "clientY": 1075, + "pageX": 820, + "pageY": 1075, + "canvasX": 1640, + "canvasY": 2150, + "cursor": "auto" + }, + { + "timeInSeconds": 0.7071999998092652, + "clientX": 793, + "clientY": 1040, + "pageX": 793, + "pageY": 1040, + "canvasX": 1586, + "canvasY": 2080, + "cursor": "auto" + }, + { + "timeInSeconds": 0.7237999997138977, + "clientX": 769, + "clientY": 1010, + "pageX": 769, + "pageY": 1010, + "canvasX": 1538, + "canvasY": 2020, + "cursor": "auto" + }, + { + "timeInSeconds": 0.741, + "clientX": 741, + "clientY": 964, + "pageX": 741, + "pageY": 964, + "canvasX": 1482, + "canvasY": 1928, + "cursor": "auto" + }, + { + "timeInSeconds": 0.7572999997138977, + "clientX": 735, + "clientY": 945, + "pageX": 735, + "pageY": 945, + "canvasX": 1470, + "canvasY": 1890, + "cursor": "auto" + }, + { + "timeInSeconds": 0.7738999996185303, + "clientX": 734, + "clientY": 945, + "pageX": 734, + "pageY": 945, + "canvasX": 1468, + "canvasY": 1890, + "cursor": "auto" + }, + { + "timeInSeconds": 0.8508999996185302, + "clientX": 735, + "clientY": 945, + "pageX": 735, + "pageY": 945, + "canvasX": 1470, + "canvasY": 1890, + "cursor": "auto" + }, + { + "timeInSeconds": 0.8655999999046325, + "clientX": 739, + "clientY": 944, + "pageX": 739, + "pageY": 944, + "canvasX": 1478, + "canvasY": 1888, + "cursor": "auto" + }, + { + "timeInSeconds": 0.8872999997138977, + "clientX": 744, + "clientY": 945, + "pageX": 744, + "pageY": 945, + "canvasX": 1488, + "canvasY": 1890, + "cursor": "auto" + }, + { + "timeInSeconds": 0.8988999996185303, + "clientX": 752, + "clientY": 946, + "pageX": 752, + "pageY": 946, + "canvasX": 1504, + "canvasY": 1892, + "cursor": "auto" + }, + { + "timeInSeconds": 0.9168999996185303, + "clientX": 762, + "clientY": 950, + "pageX": 762, + "pageY": 950, + "canvasX": 1524, + "canvasY": 1900, + "cursor": "auto" + }, + { + "timeInSeconds": 0.9326999998092651, + "clientX": 769, + "clientY": 953, + "pageX": 769, + "pageY": 953, + "canvasX": 1538, + "canvasY": 1906, + "cursor": "auto" + }, + { + "timeInSeconds": 0.9548999996185302, + "clientX": 774, + "clientY": 954, + "pageX": 774, + "pageY": 954, + "canvasX": 1548, + "canvasY": 1908, + "cursor": "auto" + }, + { + "timeInSeconds": 0.9658999996185302, + "clientX": 779, + "clientY": 954, + "pageX": 779, + "pageY": 954, + "canvasX": 1558, + "canvasY": 1908, + "cursor": "auto" + }, + { + "timeInSeconds": 0.9836999998092651, + "clientX": 782, + "clientY": 955, + "pageX": 782, + "pageY": 955, + "canvasX": 1564, + "canvasY": 1910, + "cursor": "auto" + }, + { + "timeInSeconds": 0.9990999999046326, + "clientX": 786, + "clientY": 958, + "pageX": 786, + "pageY": 958, + "canvasX": 1572, + "canvasY": 1916, + "cursor": "auto" + }, + { + "timeInSeconds": 1.0192999997138976, + "clientX": 789, + "clientY": 963, + "pageX": 789, + "pageY": 963, + "canvasX": 1578, + "canvasY": 1926, + "cursor": "auto" + }, + { + "timeInSeconds": 1.0335, + "clientX": 790, + "clientY": 970, + "pageX": 790, + "pageY": 970, + "canvasX": 1580, + "canvasY": 1940, + "cursor": "auto" + }, + { + "timeInSeconds": 1.051, + "clientX": 790, + "clientY": 980, + "pageX": 790, + "pageY": 980, + "canvasX": 1580, + "canvasY": 1960, + "cursor": "auto" + }, + { + "timeInSeconds": 1.0663999996185303, + "clientX": 790, + "clientY": 986, + "pageX": 790, + "pageY": 986, + "canvasX": 1580, + "canvasY": 1972, + "cursor": "auto" + }, + { + "timeInSeconds": 1.087, + "clientX": 790, + "clientY": 991, + "pageX": 790, + "pageY": 991, + "canvasX": 1580, + "canvasY": 1982, + "cursor": "auto" + }, + { + "timeInSeconds": 1.0987999997138977, + "clientX": 790, + "clientY": 994, + "pageX": 790, + "pageY": 994, + "canvasX": 1580, + "canvasY": 1988, + "cursor": "auto" + }, + { + "timeInSeconds": 1.1172999997138977, + "clientX": 790, + "clientY": 995, + "pageX": 790, + "pageY": 995, + "canvasX": 1580, + "canvasY": 1990, + "cursor": "auto" + }, + { + "timeInSeconds": 1.1320999999046326, + "clientX": 791, + "clientY": 995, + "pageX": 791, + "pageY": 995, + "canvasX": 1582, + "canvasY": 1990, + "cursor": "auto" + }, + { + "timeInSeconds": 1.153, + "clientX": 793, + "clientY": 995, + "pageX": 793, + "pageY": 995, + "canvasX": 1586, + "canvasY": 1990, + "cursor": "auto" + }, + { + "timeInSeconds": 1.1655999999046325, + "clientX": 794, + "clientY": 996, + "pageX": 794, + "pageY": 996, + "canvasX": 1588, + "canvasY": 1992, + "cursor": "auto" + }, + { + "timeInSeconds": 1.1850999999046326, + "clientX": 797, + "clientY": 996, + "pageX": 797, + "pageY": 996, + "canvasX": 1594, + "canvasY": 1992, + "cursor": "auto" + }, + { + "timeInSeconds": 1.1992999997138978, + "clientX": 802, + "clientY": 996, + "pageX": 802, + "pageY": 996, + "canvasX": 1604, + "canvasY": 1992, + "cursor": "auto" + }, + { + "timeInSeconds": 1.2192999997138978, + "clientX": 808, + "clientY": 996, + "pageX": 808, + "pageY": 996, + "canvasX": 1616, + "canvasY": 1992, + "cursor": "auto" + }, + { + "timeInSeconds": 1.2325, + "clientX": 813, + "clientY": 997, + "pageX": 813, + "pageY": 997, + "canvasX": 1626, + "canvasY": 1994, + "cursor": "auto" + }, + { + "timeInSeconds": 1.2501999998092652, + "clientX": 817, + "clientY": 998, + "pageX": 817, + "pageY": 998, + "canvasX": 1634, + "canvasY": 1996, + "cursor": "auto" + }, + { + "timeInSeconds": 1.2661999998092652, + "clientX": 818, + "clientY": 998, + "pageX": 818, + "pageY": 998, + "canvasX": 1636, + "canvasY": 1996, + "cursor": "auto" + }, + { + "timeInSeconds": 1.2860999999046325, + "clientX": 818, + "clientY": 998, + "pageX": 818, + "pageY": 998, + "canvasX": 1636, + "canvasY": 1996, + "cursor": "auto" + }, + { + "timeInSeconds": 1.3663999996185303, + "clientX": 818, + "clientY": 998, + "pageX": 818, + "pageY": 998, + "canvasX": 1636, + "canvasY": 1996, + "cursor": "auto" + }, + { + "timeInSeconds": 1.3835, + "clientX": 818, + "clientY": 998, + "pageX": 818, + "pageY": 998, + "canvasX": 1636, + "canvasY": 1996, + "cursor": "auto" + }, + { + "timeInSeconds": 1.424, + "clientX": 821, + "clientY": 998, + "pageX": 821, + "pageY": 998, + "canvasX": 1642, + "canvasY": 1996, + "cursor": "auto" + }, + { + "timeInSeconds": 1.4405999999046326, + "clientX": 833, + "clientY": 998, + "pageX": 833, + "pageY": 998, + "canvasX": 1666, + "canvasY": 1996, + "cursor": "auto" + }, + { + "timeInSeconds": 1.4582999997138977, + "clientX": 849, + "clientY": 998, + "pageX": 849, + "pageY": 998, + "canvasX": 1698, + "canvasY": 1996, + "cursor": "auto" + }, + { + "timeInSeconds": 1.4740999999046325, + "clientX": 885, + "clientY": 999, + "pageX": 885, + "pageY": 999, + "canvasX": 1770, + "canvasY": 1998, + "cursor": "auto" + }, + { + "timeInSeconds": 1.4910999999046326, + "clientX": 919, + "clientY": 1000, + "pageX": 919, + "pageY": 1000, + "canvasX": 1838, + "canvasY": 2000, + "cursor": "auto" + }, + { + "timeInSeconds": 1.5232999997138976, + "clientX": 977, + "clientY": 1002, + "pageX": 977, + "pageY": 1002, + "canvasX": 1954, + "canvasY": 2004, + "cursor": "auto" + }, + { + "timeInSeconds": 1.5410999999046326, + "clientX": 1003, + "clientY": 1003, + "pageX": 1003, + "pageY": 1003, + "canvasX": 2006, + "canvasY": 2006, + "cursor": "auto" + }, + { + "timeInSeconds": 1.557699999809265, + "clientX": 1020, + "clientY": 1003, + "pageX": 1020, + "pageY": 1003, + "canvasX": 2040, + "canvasY": 2006, + "cursor": "auto" + }, + { + "timeInSeconds": 1.5907999997138977, + "clientX": 1112, + "clientY": 1009, + "pageX": 1112, + "pageY": 1009, + "canvasX": 2224, + "canvasY": 2018, + "cursor": "auto" + }, + { + "timeInSeconds": 1.6075999999046326, + "clientX": 1149, + "clientY": 1012, + "pageX": 1149, + "pageY": 1012, + "canvasX": 2298, + "canvasY": 2024, + "cursor": "auto" + }, + { + "timeInSeconds": 1.6243999996185303, + "clientX": 1175, + "clientY": 1013, + "pageX": 1175, + "pageY": 1013, + "canvasX": 2350, + "canvasY": 2026, + "cursor": "auto" + }, + { + "timeInSeconds": 1.6568999996185303, + "clientX": 1238, + "clientY": 1015, + "pageX": 1238, + "pageY": 1015, + "canvasX": 2476, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 1.6741999998092651, + "clientX": 1273, + "clientY": 1014, + "pageX": 1273, + "pageY": 1014, + "canvasX": 2546, + "canvasY": 2028, + "cursor": "auto" + }, + { + "timeInSeconds": 1.691, + "clientX": 1301, + "clientY": 1015, + "pageX": 1301, + "pageY": 1015, + "canvasX": 2602, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 1.7076999998092652, + "clientX": 1325, + "clientY": 1015, + "pageX": 1325, + "pageY": 1015, + "canvasX": 2650, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 1.7242999997138977, + "clientX": 1343, + "clientY": 1015, + "pageX": 1343, + "pageY": 1015, + "canvasX": 2686, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 1.7408999996185304, + "clientX": 1372, + "clientY": 1015, + "pageX": 1372, + "pageY": 1015, + "canvasX": 2744, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 1.757699999809265, + "clientX": 1389, + "clientY": 1015, + "pageX": 1389, + "pageY": 1015, + "canvasX": 2778, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 1.7907999997138977, + "clientX": 1405, + "clientY": 1015, + "pageX": 1405, + "pageY": 1015, + "canvasX": 2810, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 1.8075999999046326, + "clientX": 1405, + "clientY": 1015, + "pageX": 1405, + "pageY": 1015, + "canvasX": 2810, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 1.8850999999046325, + "clientX": 1405, + "clientY": 1015, + "pageX": 1405, + "pageY": 1015, + "canvasX": 2810, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 1.898699999809265, + "clientX": 1404, + "clientY": 1015, + "pageX": 1404, + "pageY": 1015, + "canvasX": 2808, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 1.9188999996185303, + "clientX": 1385, + "clientY": 1017, + "pageX": 1385, + "pageY": 1017, + "canvasX": 2770, + "canvasY": 2034, + "cursor": "auto" + }, + { + "timeInSeconds": 1.9408999996185303, + "clientX": 1332, + "clientY": 1025, + "pageX": 1332, + "pageY": 1025, + "canvasX": 2664, + "canvasY": 2050, + "cursor": "auto" + }, + { + "timeInSeconds": 1.9575999999046325, + "clientX": 1280, + "clientY": 1030, + "pageX": 1280, + "pageY": 1030, + "canvasX": 2560, + "canvasY": 2060, + "cursor": "auto" + }, + { + "timeInSeconds": 1.9740999999046325, + "clientX": 1221, + "clientY": 1033, + "pageX": 1221, + "pageY": 1033, + "canvasX": 2442, + "canvasY": 2066, + "cursor": "auto" + }, + { + "timeInSeconds": 1.9910999999046326, + "clientX": 1155, + "clientY": 1035, + "pageX": 1155, + "pageY": 1035, + "canvasX": 2310, + "canvasY": 2070, + "cursor": "auto" + }, + { + "timeInSeconds": 2.0077999997138978, + "clientX": 1071, + "clientY": 1035, + "pageX": 1071, + "pageY": 1035, + "canvasX": 2142, + "canvasY": 2070, + "cursor": "auto" + }, + { + "timeInSeconds": 2.0243999996185305, + "clientX": 947, + "clientY": 1033, + "pageX": 947, + "pageY": 1033, + "canvasX": 1894, + "canvasY": 2066, + "cursor": "auto" + }, + { + "timeInSeconds": 2.04089999961853, + "clientX": 892, + "clientY": 1031, + "pageX": 892, + "pageY": 1031, + "canvasX": 1784, + "canvasY": 2062, + "cursor": "auto" + }, + { + "timeInSeconds": 2.0567999997138977, + "clientX": 815, + "clientY": 1032, + "pageX": 815, + "pageY": 1032, + "canvasX": 1630, + "canvasY": 2064, + "cursor": "auto" + }, + { + "timeInSeconds": 2.0745, + "clientX": 769, + "clientY": 1032, + "pageX": 769, + "pageY": 1032, + "canvasX": 1538, + "canvasY": 2064, + "cursor": "auto" + }, + { + "timeInSeconds": 2.0905, + "clientX": 731, + "clientY": 1031, + "pageX": 731, + "pageY": 1031, + "canvasX": 1462, + "canvasY": 2062, + "cursor": "auto" + }, + { + "timeInSeconds": 2.107699999809265, + "clientX": 715, + "clientY": 1031, + "pageX": 715, + "pageY": 1031, + "canvasX": 1430, + "canvasY": 2062, + "cursor": "auto" + }, + { + "timeInSeconds": 2.1232999997138977, + "clientX": 709, + "clientY": 1031, + "pageX": 709, + "pageY": 1031, + "canvasX": 1418, + "canvasY": 2062, + "cursor": "auto" + }, + { + "timeInSeconds": 2.1400999999046326, + "clientX": 707, + "clientY": 1031, + "pageX": 707, + "pageY": 1031, + "canvasX": 1414, + "canvasY": 2062, + "cursor": "auto" + }, + { + "timeInSeconds": 2.2240999999046327, + "clientX": 706, + "clientY": 1031, + "pageX": 706, + "pageY": 1031, + "canvasX": 1412, + "canvasY": 2062, + "cursor": "auto" + }, + { + "timeInSeconds": 2.2415, + "clientX": 706, + "clientY": 1031, + "pageX": 706, + "pageY": 1031, + "canvasX": 1412, + "canvasY": 2062, + "cursor": "auto" + }, + { + "timeInSeconds": 2.2575, + "clientX": 706, + "clientY": 1031, + "pageX": 706, + "pageY": 1031, + "canvasX": 1412, + "canvasY": 2062, + "cursor": "auto" + }, + { + "timeInSeconds": 2.274199999809265, + "clientX": 706, + "clientY": 1030, + "pageX": 706, + "pageY": 1030, + "canvasX": 1412, + "canvasY": 2060, + "cursor": "auto" + }, + { + "timeInSeconds": 2.284799999713898, + "clientX": 706, + "clientY": 1030, + "pageX": 706, + "pageY": 1030, + "canvasX": 1412, + "canvasY": 2060, + "cursor": "auto" + }, + { + "timeInSeconds": 2.2907999997138977, + "clientX": 706, + "clientY": 1028, + "pageX": 706, + "pageY": 1028, + "canvasX": 1412, + "canvasY": 2056, + "cursor": "auto" + }, + { + "timeInSeconds": 2.3073999996185304, + "clientX": 706, + "clientY": 1027, + "pageX": 706, + "pageY": 1027, + "canvasX": 1412, + "canvasY": 2054, + "cursor": "auto" + }, + { + "timeInSeconds": 2.3241999998092653, + "clientX": 708, + "clientY": 1025, + "pageX": 708, + "pageY": 1025, + "canvasX": 1416, + "canvasY": 2050, + "cursor": "auto" + }, + { + "timeInSeconds": 2.3407999997138975, + "clientX": 709, + "clientY": 1021, + "pageX": 709, + "pageY": 1021, + "canvasX": 1418, + "canvasY": 2042, + "cursor": "auto" + }, + { + "timeInSeconds": 2.3575, + "clientX": 711, + "clientY": 1017, + "pageX": 711, + "pageY": 1017, + "canvasX": 1422, + "canvasY": 2034, + "cursor": "auto" + }, + { + "timeInSeconds": 2.375199999809265, + "clientX": 712, + "clientY": 1015, + "pageX": 712, + "pageY": 1015, + "canvasX": 1424, + "canvasY": 2030, + "cursor": "auto" + }, + { + "timeInSeconds": 2.391, + "clientX": 712, + "clientY": 1014, + "pageX": 712, + "pageY": 1014, + "canvasX": 1424, + "canvasY": 2028, + "cursor": "auto" + }, + { + "timeInSeconds": 2.4075999999046327, + "clientX": 713, + "clientY": 1010, + "pageX": 713, + "pageY": 1010, + "canvasX": 1426, + "canvasY": 2020, + "cursor": "auto" + }, + { + "timeInSeconds": 2.424299999713898, + "clientX": 714, + "clientY": 1006, + "pageX": 714, + "pageY": 1006, + "canvasX": 1428, + "canvasY": 2012, + "cursor": "auto" + }, + { + "timeInSeconds": 2.4407999997138976, + "clientX": 715, + "clientY": 1004, + "pageX": 715, + "pageY": 1004, + "canvasX": 1430, + "canvasY": 2008, + "cursor": "auto" + }, + { + "timeInSeconds": 2.4575, + "clientX": 715, + "clientY": 1000, + "pageX": 715, + "pageY": 1000, + "canvasX": 1430, + "canvasY": 2000, + "cursor": "auto" + }, + { + "timeInSeconds": 2.474199999809265, + "clientX": 715, + "clientY": 999, + "pageX": 715, + "pageY": 999, + "canvasX": 1430, + "canvasY": 1998, + "cursor": "auto" + }, + { + "timeInSeconds": 2.4908999996185304, + "clientX": 715, + "clientY": 997, + "pageX": 715, + "pageY": 997, + "canvasX": 1430, + "canvasY": 1994, + "cursor": "auto" + }, + { + "timeInSeconds": 2.507599999904633, + "clientX": 716, + "clientY": 997, + "pageX": 716, + "pageY": 997, + "canvasX": 1432, + "canvasY": 1994, + "cursor": "auto" + }, + { + "timeInSeconds": 2.5242999997138975, + "clientX": 717, + "clientY": 994, + "pageX": 717, + "pageY": 994, + "canvasX": 1434, + "canvasY": 1988, + "cursor": "auto" + }, + { + "timeInSeconds": 2.5410999999046324, + "clientX": 718, + "clientY": 992, + "pageX": 718, + "pageY": 992, + "canvasX": 1436, + "canvasY": 1984, + "cursor": "auto" + }, + { + "timeInSeconds": 2.55789999961853, + "clientX": 719, + "clientY": 990, + "pageX": 719, + "pageY": 990, + "canvasX": 1438, + "canvasY": 1980, + "cursor": "auto" + }, + { + "timeInSeconds": 2.5745999999046325, + "clientX": 720, + "clientY": 989, + "pageX": 720, + "pageY": 989, + "canvasX": 1440, + "canvasY": 1978, + "cursor": "auto" + }, + { + "timeInSeconds": 2.5912999997138977, + "clientX": 721, + "clientY": 988, + "pageX": 721, + "pageY": 988, + "canvasX": 1442, + "canvasY": 1976, + "cursor": "auto" + }, + { + "timeInSeconds": 2.608, + "clientX": 721, + "clientY": 987, + "pageX": 721, + "pageY": 987, + "canvasX": 1442, + "canvasY": 1974, + "cursor": "auto" + }, + { + "timeInSeconds": 2.6246999998092653, + "clientX": 723, + "clientY": 985, + "pageX": 723, + "pageY": 985, + "canvasX": 1446, + "canvasY": 1970, + "cursor": "auto" + }, + { + "timeInSeconds": 2.6412999997138975, + "clientX": 723, + "clientY": 984, + "pageX": 723, + "pageY": 984, + "canvasX": 1446, + "canvasY": 1968, + "cursor": "auto" + }, + { + "timeInSeconds": 2.6577999997138977, + "clientX": 723, + "clientY": 984, + "pageX": 723, + "pageY": 984, + "canvasX": 1446, + "canvasY": 1968, + "cursor": "auto" + }, + { + "timeInSeconds": 2.6745, + "clientX": 724, + "clientY": 983, + "pageX": 724, + "pageY": 983, + "canvasX": 1448, + "canvasY": 1966, + "cursor": "auto" + }, + { + "timeInSeconds": 2.692199999809265, + "clientX": 724, + "clientY": 983, + "pageX": 724, + "pageY": 983, + "canvasX": 1448, + "canvasY": 1966, + "cursor": "auto" + }, + { + "timeInSeconds": 2.7077999997138975, + "clientX": 724, + "clientY": 982, + "pageX": 724, + "pageY": 982, + "canvasX": 1448, + "canvasY": 1964, + "cursor": "auto" + }, + { + "timeInSeconds": 2.7245999999046324, + "clientX": 724, + "clientY": 982, + "pageX": 724, + "pageY": 982, + "canvasX": 1448, + "canvasY": 1964, + "cursor": "auto" + }, + { + "timeInSeconds": 2.741199999809265, + "clientX": 724, + "clientY": 982, + "pageX": 724, + "pageY": 982, + "canvasX": 1448, + "canvasY": 1964, + "cursor": "auto" + }, + { + "timeInSeconds": 2.7578999996185303, + "clientX": 724, + "clientY": 982, + "pageX": 724, + "pageY": 982, + "canvasX": 1448, + "canvasY": 1964, + "cursor": "auto" + }, + { + "timeInSeconds": 2.7745999999046327, + "clientX": 725, + "clientY": 981, + "pageX": 725, + "pageY": 981, + "canvasX": 1450, + "canvasY": 1962, + "cursor": "auto" + }, + { + "timeInSeconds": 2.791299999713898, + "clientX": 725, + "clientY": 980, + "pageX": 725, + "pageY": 980, + "canvasX": 1450, + "canvasY": 1960, + "cursor": "auto" + }, + { + "timeInSeconds": 2.80789999961853, + "clientX": 726, + "clientY": 980, + "pageX": 726, + "pageY": 980, + "canvasX": 1452, + "canvasY": 1960, + "cursor": "auto" + }, + { + "timeInSeconds": 2.824699999809265, + "clientX": 726, + "clientY": 980, + "pageX": 726, + "pageY": 980, + "canvasX": 1452, + "canvasY": 1960, + "cursor": "auto" + }, + { + "timeInSeconds": 2.8415, + "clientX": 726, + "clientY": 979, + "pageX": 726, + "pageY": 979, + "canvasX": 1452, + "canvasY": 1958, + "cursor": "auto" + }, + { + "timeInSeconds": 2.8585999999046328, + "clientX": 726, + "clientY": 979, + "pageX": 726, + "pageY": 979, + "canvasX": 1452, + "canvasY": 1958, + "cursor": "auto" + }, + { + "timeInSeconds": 2.8745999999046328, + "clientX": 726, + "clientY": 978, + "pageX": 726, + "pageY": 978, + "canvasX": 1452, + "canvasY": 1956, + "cursor": "auto" + }, + { + "timeInSeconds": 2.8913999996185304, + "clientX": 726, + "clientY": 978, + "pageX": 726, + "pageY": 978, + "canvasX": 1452, + "canvasY": 1956, + "cursor": "auto" + }, + { + "timeInSeconds": 2.9087999997138976, + "clientX": 727, + "clientY": 978, + "pageX": 727, + "pageY": 978, + "canvasX": 1454, + "canvasY": 1956, + "cursor": "auto" + }, + { + "timeInSeconds": 2.925, + "clientX": 727, + "clientY": 978, + "pageX": 727, + "pageY": 978, + "canvasX": 1454, + "canvasY": 1956, + "cursor": "auto" + }, + { + "timeInSeconds": 2.942, + "clientX": 727, + "clientY": 978, + "pageX": 727, + "pageY": 978, + "canvasX": 1454, + "canvasY": 1956, + "cursor": "auto" + }, + { + "timeInSeconds": 2.959199999809265, + "clientX": 727, + "clientY": 978, + "pageX": 727, + "pageY": 978, + "canvasX": 1454, + "canvasY": 1956, + "cursor": "auto" + }, + { + "timeInSeconds": 2.974799999713898, + "clientX": 727, + "clientY": 977, + "pageX": 727, + "pageY": 977, + "canvasX": 1454, + "canvasY": 1954, + "cursor": "auto" + }, + { + "timeInSeconds": 2.9925, + "clientX": 727, + "clientY": 977, + "pageX": 727, + "pageY": 977, + "canvasX": 1454, + "canvasY": 1954, + "cursor": "auto" + }, + { + "timeInSeconds": 3.008199999809265, + "clientX": 727, + "clientY": 977, + "pageX": 727, + "pageY": 977, + "canvasX": 1454, + "canvasY": 1954, + "cursor": "auto" + }, + { + "timeInSeconds": 3.0250999999046324, + "clientX": 727, + "clientY": 976, + "pageX": 727, + "pageY": 976, + "canvasX": 1454, + "canvasY": 1952, + "cursor": "auto" + }, + { + "timeInSeconds": 3.041699999809265, + "clientX": 727, + "clientY": 975, + "pageX": 727, + "pageY": 975, + "canvasX": 1454, + "canvasY": 1950, + "cursor": "auto" + }, + { + "timeInSeconds": 3.0588999996185304, + "clientX": 727, + "clientY": 975, + "pageX": 727, + "pageY": 975, + "canvasX": 1454, + "canvasY": 1950, + "cursor": "auto" + }, + { + "timeInSeconds": 3.0750999999046327, + "clientX": 727, + "clientY": 975, + "pageX": 727, + "pageY": 975, + "canvasX": 1454, + "canvasY": 1950, + "cursor": "auto" + }, + { + "timeInSeconds": 3.0915, + "clientX": 727, + "clientY": 975, + "pageX": 727, + "pageY": 975, + "canvasX": 1454, + "canvasY": 1950, + "cursor": "auto" + }, + { + "timeInSeconds": 3.1082999997138976, + "clientX": 727, + "clientY": 975, + "pageX": 727, + "pageY": 975, + "canvasX": 1454, + "canvasY": 1950, + "cursor": "auto" + }, + { + "timeInSeconds": 3.125, + "clientX": 727, + "clientY": 975, + "pageX": 727, + "pageY": 975, + "canvasX": 1454, + "canvasY": 1950, + "cursor": "auto" + }, + { + "timeInSeconds": 3.3180999999046326, + "clientX": 728, + "clientY": 974, + "pageX": 728, + "pageY": 974, + "canvasX": 1456, + "canvasY": 1948, + "cursor": "auto" + }, + { + "timeInSeconds": 3.3375, + "clientX": 728, + "clientY": 974, + "pageX": 728, + "pageY": 974, + "canvasX": 1456, + "canvasY": 1948, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.358199999809265, + "clientX": 728, + "clientY": 974, + "pageX": 728, + "pageY": 974, + "canvasX": 1456, + "canvasY": 1948, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.3775999999046324, + "clientX": 728, + "clientY": 974, + "pageX": 728, + "pageY": 974, + "canvasX": 1456, + "canvasY": 1948, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.3913999996185304, + "clientX": 728, + "clientY": 973, + "pageX": 728, + "pageY": 973, + "canvasX": 1456, + "canvasY": 1946, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.408, + "clientX": 728, + "clientY": 973, + "pageX": 728, + "pageY": 973, + "canvasX": 1456, + "canvasY": 1946, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.4255999999046325, + "clientX": 728, + "clientY": 973, + "pageX": 728, + "pageY": 973, + "canvasX": 1456, + "canvasY": 1946, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.8666999998092653, + "clientX": 728, + "clientY": 973, + "pageX": 728, + "pageY": 973, + "canvasX": 1456, + "canvasY": 1946, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.8915999999046327, + "clientX": 727, + "clientY": 967, + "pageX": 727, + "pageY": 967, + "canvasX": 1454, + "canvasY": 1934, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.909, + "clientX": 723, + "clientY": 949, + "pageX": 723, + "pageY": 949, + "canvasX": 1446, + "canvasY": 1898, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.925699999809265, + "clientX": 721, + "clientY": 930, + "pageX": 721, + "pageY": 930, + "canvasX": 1442, + "canvasY": 1860, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.9422999997138977, + "clientX": 720, + "clientY": 906, + "pageX": 720, + "pageY": 906, + "canvasX": 1440, + "canvasY": 1812, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.958799999713898, + "clientX": 718, + "clientY": 881, + "pageX": 718, + "pageY": 881, + "canvasX": 1436, + "canvasY": 1762, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.9755999999046328, + "clientX": 714, + "clientY": 847, + "pageX": 714, + "pageY": 847, + "canvasX": 1428, + "canvasY": 1694, + "cursor": "row-resize" + }, + { + "timeInSeconds": 3.992199999809265, + "clientX": 713, + "clientY": 821, + "pageX": 713, + "pageY": 821, + "canvasX": 1426, + "canvasY": 1642, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.008899999618531, + "clientX": 713, + "clientY": 804, + "pageX": 713, + "pageY": 804, + "canvasX": 1426, + "canvasY": 1608, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.025199999809265, + "clientX": 713, + "clientY": 788, + "pageX": 713, + "pageY": 788, + "canvasX": 1426, + "canvasY": 1576, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.042299999713897, + "clientX": 713, + "clientY": 778, + "pageX": 713, + "pageY": 778, + "canvasX": 1426, + "canvasY": 1556, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.059, + "clientX": 713, + "clientY": 765, + "pageX": 713, + "pageY": 765, + "canvasX": 1426, + "canvasY": 1530, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.075799999713897, + "clientX": 713, + "clientY": 759, + "pageX": 713, + "pageY": 759, + "canvasX": 1426, + "canvasY": 1518, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.092099999904632, + "clientX": 713, + "clientY": 754, + "pageX": 713, + "pageY": 754, + "canvasX": 1426, + "canvasY": 1508, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.109, + "clientX": 713, + "clientY": 751, + "pageX": 713, + "pageY": 751, + "canvasX": 1426, + "canvasY": 1502, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.125699999809265, + "clientX": 713, + "clientY": 750, + "pageX": 713, + "pageY": 750, + "canvasX": 1426, + "canvasY": 1500, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.14239999961853, + "clientX": 713, + "clientY": 750, + "pageX": 713, + "pageY": 750, + "canvasX": 1426, + "canvasY": 1500, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.241799999713898, + "clientX": 713, + "clientY": 751, + "pageX": 713, + "pageY": 751, + "canvasX": 1426, + "canvasY": 1502, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.258899999618531, + "clientX": 715, + "clientY": 760, + "pageX": 715, + "pageY": 760, + "canvasX": 1430, + "canvasY": 1520, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.275799999713898, + "clientX": 717, + "clientY": 776, + "pageX": 717, + "pageY": 776, + "canvasX": 1434, + "canvasY": 1552, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.292399999618531, + "clientX": 718, + "clientY": 814, + "pageX": 718, + "pageY": 814, + "canvasX": 1436, + "canvasY": 1628, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.308299999713897, + "clientX": 718, + "clientY": 845, + "pageX": 718, + "pageY": 845, + "canvasX": 1436, + "canvasY": 1690, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.325699999809265, + "clientX": 717, + "clientY": 886, + "pageX": 717, + "pageY": 886, + "canvasX": 1434, + "canvasY": 1772, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.3423999996185305, + "clientX": 717, + "clientY": 921, + "pageX": 717, + "pageY": 921, + "canvasX": 1434, + "canvasY": 1842, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.3590999999046325, + "clientX": 717, + "clientY": 946, + "pageX": 717, + "pageY": 946, + "canvasX": 1434, + "canvasY": 1892, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.375899999618531, + "clientX": 716, + "clientY": 965, + "pageX": 716, + "pageY": 965, + "canvasX": 1432, + "canvasY": 1930, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.3925, + "clientX": 716, + "clientY": 977, + "pageX": 716, + "pageY": 977, + "canvasX": 1432, + "canvasY": 1954, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.409099999904632, + "clientX": 715, + "clientY": 993, + "pageX": 715, + "pageY": 993, + "canvasX": 1430, + "canvasY": 1986, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.425799999713898, + "clientX": 715, + "clientY": 1002, + "pageX": 715, + "pageY": 1002, + "canvasX": 1430, + "canvasY": 2004, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.442099999904633, + "clientX": 715, + "clientY": 1011, + "pageX": 715, + "pageY": 1011, + "canvasX": 1430, + "canvasY": 2022, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.459, + "clientX": 715, + "clientY": 1015, + "pageX": 715, + "pageY": 1015, + "canvasX": 1430, + "canvasY": 2030, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.475799999713898, + "clientX": 715, + "clientY": 1016, + "pageX": 715, + "pageY": 1016, + "canvasX": 1430, + "canvasY": 2032, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.4922999997138975, + "clientX": 715, + "clientY": 1018, + "pageX": 715, + "pageY": 1018, + "canvasX": 1430, + "canvasY": 2036, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.509199999809265, + "clientX": 715, + "clientY": 1018, + "pageX": 715, + "pageY": 1018, + "canvasX": 1430, + "canvasY": 2036, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.5255, + "clientX": 715, + "clientY": 1018, + "pageX": 715, + "pageY": 1018, + "canvasX": 1430, + "canvasY": 2036, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.5425, + "clientX": 715, + "clientY": 1018, + "pageX": 715, + "pageY": 1018, + "canvasX": 1430, + "canvasY": 2036, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.559299999713898, + "clientX": 715, + "clientY": 1018, + "pageX": 715, + "pageY": 1018, + "canvasX": 1430, + "canvasY": 2036, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.5761999998092655, + "clientX": 715, + "clientY": 1018, + "pageX": 715, + "pageY": 1018, + "canvasX": 1430, + "canvasY": 2036, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.592599999904633, + "clientX": 714, + "clientY": 1018, + "pageX": 714, + "pageY": 1018, + "canvasX": 1428, + "canvasY": 2036, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.604599999904632, + "clientX": 714, + "clientY": 1018, + "pageX": 714, + "pageY": 1018, + "canvasX": 1428, + "canvasY": 2036, + "cursor": "row-resize" + }, + { + "timeInSeconds": 4.6085, + "clientX": 714, + "clientY": 1017, + "pageX": 714, + "pageY": 1017, + "canvasX": 1428, + "canvasY": 2034, + "cursor": "auto" + }, + { + "timeInSeconds": 4.62539999961853, + "clientX": 714, + "clientY": 1017, + "pageX": 714, + "pageY": 1017, + "canvasX": 1428, + "canvasY": 2034, + "cursor": "auto" + }, + { + "timeInSeconds": 4.641899999618531, + "clientX": 714, + "clientY": 1017, + "pageX": 714, + "pageY": 1017, + "canvasX": 1428, + "canvasY": 2034, + "cursor": "auto" + }, + { + "timeInSeconds": 4.658699999809265, + "clientX": 714, + "clientY": 1017, + "pageX": 714, + "pageY": 1017, + "canvasX": 1428, + "canvasY": 2034, + "cursor": "auto" + } + ] +} \ No newline at end of file diff --git a/packages/example/src/CanvasCapturePreview.tsx b/packages/example/src/CanvasCapturePreview.tsx new file mode 100644 index 00000000000..6999a34242b --- /dev/null +++ b/packages/example/src/CanvasCapturePreview.tsx @@ -0,0 +1,434 @@ +import {Video} from '@remotion/media'; +import React from 'react'; +import { + AbsoluteFill, + CalculateMetadataFunction, + Img, + staticFile, + useCurrentFrame, + useVideoConfig, +} from 'remotion'; +import {getMediaMetadata} from './get-media-metadata'; + +type MouseMovement = { + readonly timeInSeconds: number; + readonly clientX: number; + readonly clientY: number; + readonly pageX: number; + readonly pageY: number; + readonly canvasX?: number | null; + readonly canvasY?: number | null; + readonly cursor: string; +}; + +type CursorRecording = { + readonly startedAt: number; + readonly endedAt: number; + readonly captureMetadata?: { + readonly density: number; + readonly contentRect: { + readonly left: number; + readonly top: number; + readonly width: number; + readonly height: number; + }; + readonly canvasSize: { + readonly width: number; + readonly height: number; + }; + } | null; + readonly mouseMovements: MouseMovement[]; +}; + +export type CanvasCapturePreviewProps = { + readonly videoFile: string; + readonly cursorFile: string; + readonly cursorScale: number; + readonly cursorOffsetX: number; + readonly cursorOffsetY: number; + readonly cursorAssetBasePath: string | null; + readonly cursorData?: CursorRecording; +}; + +export const canvasCapturePreviewDefaultProps: CanvasCapturePreviewProps = { + videoFile: 'https://remotion.media/remotion-studio-canvas-recording.webm', + cursorFile: 'remotion-studio-canvas-recording.json', + cursorScale: 3, + cursorOffsetX: 0, + cursorOffsetY: 0, + cursorAssetBasePath: 'https://remotion.media/mac-cursors', +}; + +const macCursorFilenameByCssValue: Record = { + alias: 'makealias.svg', + 'all-scroll': 'move.svg', + auto: 'default.svg', + beachball: 'beachball.svg', + busy: 'busy.svg', + cell: 'cell.svg', + 'col-resize': 'resizeleftright.svg', + 'context-menu': 'contextualmenu.svg', + contextualmenu: 'contextualmenu.svg', + copy: 'copy.svg', + crosshair: 'cross.svg', + cross: 'cross.svg', + default: 'default.svg', + 'e-resize': 'resizeeast.svg', + 'ew-resize': 'resizeleftright.svg', + grab: 'handopen.svg', + grabbing: 'handgrabbing.svg', + handgrabbing: 'handgrabbing.svg', + handopen: 'handopen.svg', + handpointing: 'handpointing.svg', + help: 'help.svg', + makealias: 'makealias.svg', + move: 'move.svg', + 'n-resize': 'resizenorth.svg', + 'ne-resize': 'resizenortheast.svg', + 'nesw-resize': 'resizenortheastsouthwest.svg', + 'no-drop': 'notallowed.svg', + none: null, + 'not-allowed': 'notallowed.svg', + 'ns-resize': 'resizenorthsouth.svg', + 'nw-resize': 'resizenorthwest.svg', + 'nwse-resize': 'resizenorthwestsoutheast.svg', + pointer: 'handpointing.svg', + poof: 'poof.svg', + progress: 'busy.svg', + 'resize northsouth': 'resize northsouth.svg', + resizedown: 'resizedown.svg', + resizeeast: 'resizeeast.svg', + resizeleft: 'resizeleft.svg', + resizeleftright: 'resizeleftright.svg', + resizenorth: 'resizenorth.svg', + resizenortheast: 'resizenortheast.svg', + resizenortheastsouthwest: 'resizenortheastsouthwest.svg', + resizenorthsouth: 'resizenorthsouth.svg', + resizenorthwest: 'resizenorthwest.svg', + resizenorthwestsoutheast: 'resizenorthwestsoutheast.svg', + resizeright: 'resizeright.svg', + resizesouth: 'resizesouth.svg', + resizesoutheast: 'resizesoutheast.svg', + resizesouthwest: 'resizesouthwest.svg', + resizeup: 'resizeup.svg', + resizeupdown: 'resizeupdown.svg', + resizewest: 'resizewest.svg', + resizewesteast: 'resizewesteast.svg', + 'row-resize': 'resizenorthsouth.svg', + 's-resize': 'resizesouth.svg', + screenshotselection: 'screenshotselection.svg', + screenshotwindow: 'screenshotwindow.svg', + 'se-resize': 'resizesoutheast.svg', + 'sw-resize': 'resizesouthwest.svg', + text: 'textcursor.svg', + textcursor: 'textcursor.svg', + textcursorvertical: 'textcursorvertical.svg', + 'vertical-text': 'textcursorvertical.svg', + 'w-resize': 'resizewest.svg', + wait: 'busy.svg', + 'zoom-in': 'zoomin.svg', + 'zoom-out': 'zoomout.svg', +}; + +const resolveAsset = (src: string) => { + return src.startsWith('http://') || src.startsWith('https://') + ? src + : staticFile(src); +}; + +const resolveCursorAsset = (basePath: string, filename: string) => { + const normalizedBasePath = basePath.endsWith('/') + ? basePath.slice(0, -1) + : basePath; + + return normalizedBasePath.startsWith('http://') || + normalizedBasePath.startsWith('https://') + ? `${normalizedBasePath}/${encodeURIComponent(filename)}` + : staticFile(`${normalizedBasePath}/${filename}`); +}; + +const getNormalizedCursor = (cursor: string) => { + return cursor.split(',').at(-1)?.trim().toLowerCase() ?? 'auto'; +}; + +const getMacCursorFilename = (cursor: string) => { + return ( + macCursorFilenameByCssValue[getNormalizedCursor(cursor)] ?? 'default.svg' + ); +}; + +const findCursorAtTime = ( + mouseMovements: readonly MouseMovement[], + timeInSeconds: number, +) => { + let low = 0; + let high = mouseMovements.length - 1; + let latest: MouseMovement | null = null; + + while (low <= high) { + const middle = Math.floor((low + high) / 2); + const movement = mouseMovements[middle]; + + if (movement.timeInSeconds <= timeInSeconds) { + latest = movement; + low = middle + 1; + } else { + high = middle - 1; + } + } + + return latest; +}; + +const ArrowCursor: React.FC<{readonly scale: number}> = ({scale}) => { + const size = 24 * scale; + + return ( + + + + ); +}; + +const TextCursor: React.FC<{readonly scale: number}> = ({scale}) => { + const height = 28 * scale; + + return ( +
+ ); +}; + +const ResizeCursor: React.FC<{ + readonly scale: number; + readonly direction: 'horizontal' | 'vertical'; +}> = ({direction, scale}) => { + const size = 26 * scale; + const rotation = direction === 'horizontal' ? 90 : 0; + + return ( + + + + ); +}; + +const CursorGlyph: React.FC<{ + readonly cursorAssetBasePath: string | null; + readonly cursor: string; + readonly scale: number; +}> = ({cursor, cursorAssetBasePath, scale}) => { + const macCursorFilename = getMacCursorFilename(cursor); + + if (macCursorFilename === null) { + return null; + } + + if (cursorAssetBasePath) { + return ( + + ); + } + + if (cursor.includes('text')) { + return ; + } + + if (cursor === 'row-resize' || cursor === 'ns-resize') { + return ; + } + + if (cursor === 'col-resize' || cursor === 'ew-resize') { + return ; + } + + return ; +}; + +const CursorOverlay: React.FC<{ + readonly cursorAssetBasePath: string | null; + readonly cursorData: CursorRecording; + readonly cursorOffsetX: number; + readonly cursorOffsetY: number; + readonly cursorScale: number; +}> = ({ + cursorAssetBasePath, + cursorData, + cursorOffsetX, + cursorOffsetY, + cursorScale, +}) => { + const frame = useCurrentFrame(); + const {fps} = useVideoConfig(); + const cursor = findCursorAtTime(cursorData.mouseMovements, frame / fps); + + if (!cursor) { + return null; + } + + const metadata = cursorData.captureMetadata; + const scale = metadata?.density ?? cursorScale; + const x = + typeof cursor.canvasX === 'number' + ? cursor.canvasX + : metadata + ? (cursor.clientX - metadata.contentRect.left) * metadata.density + : (cursor.pageX - cursorOffsetX) * cursorScale; + const y = + typeof cursor.canvasY === 'number' + ? cursor.canvasY + : metadata + ? (cursor.clientY - metadata.contentRect.top) * metadata.density + : (cursor.pageY - cursorOffsetY) * cursorScale; + + return ( +
+ +
+ ); +}; + +export const calculateCanvasCapturePreviewMetadata: CalculateMetadataFunction< + CanvasCapturePreviewProps +> = async ({props}) => { + const fps = 30; + const videoSrc = resolveAsset(props.videoFile); + const [{durationInSeconds, dimensions}, cursorData] = await Promise.all([ + getMediaMetadata(videoSrc), + fetch(staticFile(props.cursorFile)).then((res) => { + if (!res.ok) { + throw new Error(`Could not load cursor data: ${res.status}`); + } + + return res.json() as Promise; + }), + ]); + + if (!dimensions) { + throw new Error('Could not determine canvas capture video dimensions.'); + } + + return { + durationInFrames: Math.ceil(durationInSeconds * fps), + fps, + width: dimensions.width, + height: dimensions.height, + props: { + ...props, + cursorData, + }, + }; +}; + +export const CanvasCapturePreview: React.FC = ({ + cursorAssetBasePath, + cursorData, + cursorFile, + cursorOffsetX, + cursorOffsetY, + cursorScale, + videoFile, +}) => { + if (!cursorData) { + throw new Error(`Cursor data from ${cursorFile} was not loaded.`); + } + + if (!Number.isFinite(cursorScale) || cursorScale <= 0) { + throw new Error('Cursor scale must be greater than 0.'); + } + + return ( + + + ); +}; diff --git a/packages/example/src/Root.tsx b/packages/example/src/Root.tsx index 10dbd0fcaad..87b67325fed 100644 --- a/packages/example/src/Root.tsx +++ b/packages/example/src/Root.tsx @@ -182,6 +182,11 @@ import {AudioSmoothnessTrimButtonComp} from './AudioSmoothness/TrimButton'; import Amplify from './AudioTesting/Amplify'; import {Issue7568} from './AudioTesting/Issue7568'; import {BrowserTest} from './BrowserTest'; +import { + CanvasCapturePreview, + calculateCanvasCapturePreviewMetadata, + canvasCapturePreviewDefaultProps, +} from './CanvasCapturePreview'; import {EdgeBlur} from './EdgeBlur/EdgeBlur'; import {EffectsTestbed} from './EffectsTestbed/EffectsTestbed'; import {HalftoneGradient} from './EffectsTestbed/HalftoneGradient'; @@ -410,6 +415,16 @@ export const Index: React.FC = () => { fps={30} durationInFrames={120} /> + @@ -125,4 +127,10 @@ export const Editor: React.FC<{ ); + + return CANVAS_CAPTURE_ENABLED ? ( + {editor} + ) : ( + editor + ); }; diff --git a/packages/studio/src/components/StudioCanvasCapture.tsx b/packages/studio/src/components/StudioCanvasCapture.tsx new file mode 100644 index 00000000000..8d7eb1ff1f0 --- /dev/null +++ b/packages/studio/src/components/StudioCanvasCapture.tsx @@ -0,0 +1,55 @@ +import { + HtmlInCanvasCapture, + type HtmlInCanvasCaptureHandle, + isHtmlInCanvasAvailable, +} from '@remotion/canvas-capture'; +import React, {useEffect, useMemo, useRef} from 'react'; +import {useKeybinding} from '../helpers/use-keybinding'; + +const logCaptureError = (message: string, err: unknown) => { + // eslint-disable-next-line no-console + console.error(message, err instanceof Error ? err.message : String(err)); +}; + +export const StudioCanvasCapture: React.FC<{ + readonly children: React.ReactNode; + readonly density: number; +}> = ({children, density}) => { + const captureRef = useRef(null); + const isSupported = useMemo(() => isHtmlInCanvasAvailable(), []); + const keybindings = useKeybinding(); + + useEffect(() => { + if (!isSupported) { + return; + } + + const binding = keybindings.registerKeybinding({ + event: 'keydown', + key: 'p', + commandCtrlKey: true, + callback: () => { + captureRef.current?.toggleRecording().catch((err) => { + logCaptureError('Could not toggle Studio canvas recording:', err); + }); + }, + preventDefault: true, + triggerIfInputFieldFocused: true, + keepRegisteredWhenNotHighestContext: true, + }); + + return () => { + binding.unregister(); + }; + }, [isSupported, keybindings]); + + return ( + + {children} + + ); +}; diff --git a/packages/studio/src/components/canvas-capture-enabled.ts b/packages/studio/src/components/canvas-capture-enabled.ts new file mode 100644 index 00000000000..3732eb5fbba --- /dev/null +++ b/packages/studio/src/components/canvas-capture-enabled.ts @@ -0,0 +1 @@ +export const CANVAS_CAPTURE_ENABLED = false; diff --git a/packages/studio/src/test/canvas-capture-enabled.test.ts b/packages/studio/src/test/canvas-capture-enabled.test.ts new file mode 100644 index 00000000000..bdfab6ea3ae --- /dev/null +++ b/packages/studio/src/test/canvas-capture-enabled.test.ts @@ -0,0 +1,6 @@ +import {expect, test} from 'bun:test'; +import {CANVAS_CAPTURE_ENABLED} from '../components/canvas-capture-enabled'; + +test('Canvas capture should be disabled by default', () => { + expect(CANVAS_CAPTURE_ENABLED).toBe(false); +}); diff --git a/packages/studio/tsconfig.json b/packages/studio/tsconfig.json index f33441fa87d..d46897976ad 100644 --- a/packages/studio/tsconfig.json +++ b/packages/studio/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../core" }, + { + "path": "../canvas-capture" + }, { "path": "../media-utils" }, diff --git a/tsconfig.json b/tsconfig.json index d42d90231d1..d620fecfb69 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -255,6 +255,9 @@ { "path": "./packages/rounded-text-box" }, + { + "path": "./packages/canvas-capture" + }, { "path": "./packages/light-leaks" }, From d0ad369be81faffff09d72428581608b1ab5a4c2 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 9 Jun 2026 16:48:47 +0200 Subject: [PATCH 3/8] @remotion/studio: Fix marquee selection from empty timeline area (#8279) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Timeline/TimelineScrollable.tsx | 27 ++++++++++++++++++- .../components/Timeline/TimelineTracks.tsx | 24 +---------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/packages/studio/src/components/Timeline/TimelineScrollable.tsx b/packages/studio/src/components/Timeline/TimelineScrollable.tsx index 48f135e3642..92b742ea28a 100644 --- a/packages/studio/src/components/Timeline/TimelineScrollable.tsx +++ b/packages/studio/src/components/Timeline/TimelineScrollable.tsx @@ -1,7 +1,10 @@ import React, {useMemo} from 'react'; import {HORIZONTAL_SCROLLBAR_CLASSNAME} from '../Menu/is-menu-item'; import {scrollableRef} from './timeline-refs'; -import {TIMELINE_BACKGROUND} from './TimelineSelection'; +import { + TIMELINE_BACKGROUND, + useTimelineMarqueeSelection, +} from './TimelineSelection'; const outer: React.CSSProperties = { width: '100%', @@ -12,9 +15,19 @@ const outer: React.CSSProperties = { backgroundColor: TIMELINE_BACKGROUND, }; +const marqueeStyle: React.CSSProperties = { + backgroundColor: 'rgba(70, 130, 255, 0.16)', + border: '1px solid rgba(70, 130, 255, 0.75)', + boxSizing: 'border-box', + pointerEvents: 'none', + position: 'fixed', + zIndex: 10, +}; + export const TimelineScrollable: React.FC<{ readonly children: React.ReactNode; }> = ({children}) => { + const {marqueeRect, onPointerDownCapture} = useTimelineMarqueeSelection(); const containerStyle: React.CSSProperties = useMemo(() => { return { width: '100%', @@ -27,8 +40,20 @@ export const TimelineScrollable: React.FC<{ ref={scrollableRef} style={outer} className={HORIZONTAL_SCROLLBAR_CLASSNAME} + onPointerDownCapture={onPointerDownCapture} >
{children}
+ {marqueeRect === null ? null : ( +
+ )}
); }; diff --git a/packages/studio/src/components/Timeline/TimelineTracks.tsx b/packages/studio/src/components/Timeline/TimelineTracks.tsx index 738430cc4f7..415b7c931f4 100644 --- a/packages/studio/src/components/Timeline/TimelineTracks.tsx +++ b/packages/studio/src/components/Timeline/TimelineTracks.tsx @@ -2,7 +2,6 @@ import React, {useMemo} from 'react'; import type {TrackWithHash} from '../../helpers/get-timeline-sequence-sort-key'; import {TIMELINE_PADDING} from '../../helpers/timeline-layout'; import {MaxTimelineTracksReached} from './MaxTimelineTracks'; -import {useTimelineMarqueeSelection} from './TimelineSelection'; import {TimelineTimePadding} from './TimelineTimeIndicators'; import {TimelineTrack} from './TimelineTrack'; @@ -15,20 +14,10 @@ const timelineContent: React.CSSProperties = { minHeight: '100%', }; -const marqueeStyle: React.CSSProperties = { - backgroundColor: 'rgba(70, 130, 255, 0.16)', - border: '1px solid rgba(70, 130, 255, 0.75)', - boxSizing: 'border-box', - pointerEvents: 'none', - position: 'fixed', - zIndex: 10, -}; - const TimelineTracksInner: React.FC<{ readonly timeline: TrackWithHash[]; readonly hasBeenCut: boolean; }> = ({timeline, hasBeenCut}) => { - const {marqueeRect, onPointerDownCapture} = useTimelineMarqueeSelection(); const timelineStyle: React.CSSProperties = useMemo(() => { return { ...timelineContent, @@ -37,7 +26,7 @@ const TimelineTracksInner: React.FC<{ }, []); return ( -
+
{timeline.map((track) => ( @@ -45,17 +34,6 @@ const TimelineTracksInner: React.FC<{ ))}
{hasBeenCut ? : null} - {marqueeRect === null ? null : ( -
- )}
); }; From 0e3cf197adb1c1c8818184b556d05bca6bf2a254 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 9 Jun 2026 16:57:52 +0200 Subject: [PATCH 4/8] `@remotion/media`: Prevent initial double seek (#8278) --- .../media/src/test/trim-change-seek.test.ts | 2 +- packages/media/src/video-iterator-manager.ts | 15 ++++++- .../media/src/video/video-preview-iterator.ts | 45 +++++++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/packages/media/src/test/trim-change-seek.test.ts b/packages/media/src/test/trim-change-seek.test.ts index d4a8ab973d8..a818e57eaad 100644 --- a/packages/media/src/test/trim-change-seek.test.ts +++ b/packages/media/src/test/trim-change-seek.test.ts @@ -40,7 +40,7 @@ test('setTrimBefore and setTrimAfter should update frame when paused', async () const framesAfterTrimBefore = player.videoIteratorManager!.getFramesRendered(); await player.setTrimAfter(90, 0); - expect(player.videoIteratorManager!.getFramesRendered()).toBeGreaterThan( + expect(player.videoIteratorManager!.getFramesRendered()).toBe( framesAfterTrimBefore, ); diff --git a/packages/media/src/video-iterator-manager.ts b/packages/media/src/video-iterator-manager.ts index d33591913ab..4d0a7e7150e 100644 --- a/packages/media/src/video-iterator-manager.ts +++ b/packages/media/src/video-iterator-manager.ts @@ -7,6 +7,7 @@ import type { } from 'remotion'; import {Internals} from 'remotion'; import type {DelayPlaybackIfNotPremounting} from './delay-playback-if-not-premounting'; +import {roundTo4Digits} from './helpers/round-to-4-digits'; import type {Nonce} from './nonce-manager'; import {makePrewarmedVideoIteratorCache} from './prewarm-iterator-for-looping'; import { @@ -51,6 +52,7 @@ export const videoIteratorManager = async ({ let framesRendered = 0; let currentDelayHandle: {unblock: () => void} | null = null; let lastDrawnFrame: WrappedCanvas | null = null; + let currentSeek: number | null = null; const clearLastDrawnFrame = () => { lastDrawnFrame = null; @@ -143,6 +145,7 @@ export const videoIteratorManager = async ({ videoFrameIterator?.destroy(); using delayHandle = delayPlaybackHandleIfNotPremounting(); currentDelayHandle = delayHandle; + currentSeek = timeToSeek; const iterator = await createVideoIterator( timeToSeek, @@ -176,6 +179,15 @@ export const videoIteratorManager = async ({ return; } + if ( + currentSeek !== null && + roundTo4Digits(currentSeek) === roundTo4Digits(newTime) + ) { + return; + } + + currentSeek = newTime; + if (getIsLooping()) { // If less than 1 second from the end away, we pre-warm a new iterator if (getLoopSegmentMediaEndTimestamp() - newTime < 1) { @@ -185,7 +197,8 @@ export const videoIteratorManager = async ({ } } - const videoSatisfyResult = videoFrameIterator.tryToSatisfySeek(newTime); + const videoSatisfyResult = + await videoFrameIterator.tryToSatisfySeek(newTime); // Doing this before the staleness check, because // frame might be better than what we currently have diff --git a/packages/media/src/video/video-preview-iterator.ts b/packages/media/src/video/video-preview-iterator.ts index f5ef19f1c18..211cba9a237 100644 --- a/packages/media/src/video/video-preview-iterator.ts +++ b/packages/media/src/video/video-preview-iterator.ts @@ -17,7 +17,33 @@ export const createVideoIterator = async ( : await firstAwait.wait(); let lastReturnedFrame = initialFrame; + let peekedFrame: WrappedCanvas | null = null; + + const peek = async () => { + if (peekedFrame) { + return peekedFrame; + } + + const next = iterator.next(); + if (next.type === 'ready') { + peekedFrame = next.frame; + } else { + peekedFrame = await next.wait(); + } + + return peekedFrame; + }; + const getNextOrNullIfNotAvailable = () => { + if (peekedFrame) { + const retValue = { + type: 'got-frame-or-end' as const, + frame: peekedFrame, + }; + peekedFrame = null; + return retValue; + } + const next = iterator.next(); if (next.type === 'pending') { @@ -54,9 +80,9 @@ export const createVideoIterator = async ( iterator.closeIterator().catch(() => undefined); }; - const tryToSatisfySeek = ( + const tryToSatisfySeek = async ( time: number, - ): + ): Promise< | { type: 'not-satisfied'; reason: string; @@ -64,7 +90,8 @@ export const createVideoIterator = async ( | { type: 'satisfied'; frame: WrappedCanvas; - } => { + } + > => { if (lastReturnedFrame) { const frameTimestamp = roundTo4Digits(lastReturnedFrame.timestamp); @@ -87,9 +114,18 @@ export const createVideoIterator = async ( }; } + let lastFrameDuration = lastReturnedFrame.duration; + if (lastFrameDuration === 0) { + const peeked = await peek(); + if (peeked) { + lastFrameDuration = peeked.timestamp - lastReturnedFrame.timestamp; + } + } + const frameEndTimestamp = roundTo4Digits( - lastReturnedFrame.timestamp + lastReturnedFrame.duration, + lastReturnedFrame.timestamp + lastFrameDuration, ); + const timestamp = roundTo4Digits(time); if (frameTimestamp <= timestamp && frameEndTimestamp > timestamp) { return { @@ -115,6 +151,7 @@ export const createVideoIterator = async ( while (true) { const frame = getNextOrNullIfNotAvailable(); + if (frame.type === 'need-to-wait-for-it') { return { type: 'not-satisfied' as const, From 5689717d4e8ed915e01bebb296e72f905405c707 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 9 Jun 2026 16:58:52 +0200 Subject: [PATCH 5/8] Delete packages/template-blank/public/Screenshot 2026-05-28 at 14.02.20.png --- .../public/Screenshot 2026-05-28 at 14.02.20.png | Bin 2964 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/template-blank/public/Screenshot 2026-05-28 at 14.02.20.png diff --git a/packages/template-blank/public/Screenshot 2026-05-28 at 14.02.20.png b/packages/template-blank/public/Screenshot 2026-05-28 at 14.02.20.png deleted file mode 100644 index f078728c2e6826d6006d569da09dba5047c9f1fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2964 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 GfBYNxz&(@z From 44e960599c714e74e0bbf2cc0afe567a34570588 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 9 Jun 2026 16:59:06 +0200 Subject: [PATCH 6/8] Delete packages/template-blank/public/withtitle.png --- packages/template-blank/public/withtitle.png | Bin 32275 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/template-blank/public/withtitle.png diff --git a/packages/template-blank/public/withtitle.png b/packages/template-blank/public/withtitle.png deleted file mode 100644 index 265c340825752c978f648576bbee2ace001f7bda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32275 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 Yv7DvJ$jIk1yEA?tDyb_LE110be_jqp1poj5 From 8665dbbb28c1ddb96cad09f6b696fe40b6bc145b Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 9 Jun 2026 17:16:53 +0200 Subject: [PATCH 7/8] @remotion/studio: Exclude hidden timeline elements from canvas selection (#8282) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/components/SelectedOutlineOverlay.tsx | 5 +- .../src/test/timeline-selection.test.ts | 48 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/studio/src/components/SelectedOutlineOverlay.tsx b/packages/studio/src/components/SelectedOutlineOverlay.tsx index 8ddbea84af3..15ae00a80ad 100644 --- a/packages/studio/src/components/SelectedOutlineOverlay.tsx +++ b/packages/studio/src/components/SelectedOutlineOverlay.tsx @@ -512,7 +512,10 @@ export const getSequencesWithSelectableOutlines = ({ return false; } - return track.nodePathInfo.auxiliaryKeys.length === 0; + return ( + track.sequence.showInTimeline && + track.nodePathInfo.auxiliaryKeys.length === 0 + ); }) .filter((track) => track.sequence.refForOutline !== null) .sort((a, b) => a.depth - b.depth) diff --git a/packages/studio/src/test/timeline-selection.test.ts b/packages/studio/src/test/timeline-selection.test.ts index 7236449224b..04e59d2bfab 100644 --- a/packages/studio/src/test/timeline-selection.test.ts +++ b/packages/studio/src/test/timeline-selection.test.ts @@ -146,6 +146,7 @@ const makeTimelineSequence = ({ duration = 100, from = 0, type = 'sequence', + showInTimeline = true, }: { readonly schema: SequenceSchema; readonly effects?: readonly {readonly schema: SequenceSchema}[]; @@ -156,6 +157,7 @@ const makeTimelineSequence = ({ readonly duration?: number; readonly from?: number; readonly type?: TSequence['type']; + readonly showInTimeline?: boolean; }): TSequence => ({ type, @@ -166,7 +168,7 @@ const makeTimelineSequence = ({ documentationLink: null, parent: parentId, rootId: 'root', - showInTimeline: true, + showInTimeline, nonce: [[0, 0]], loopDisplay: undefined, getStack: () => null, @@ -1241,6 +1243,50 @@ test('Canvas outline hit targets render nested sequences above parents', () => { ]); }); +test('Canvas outline hit targets exclude sequences hidden from the timeline', () => { + const schema = {} satisfies SequenceSchema; + const refForOutline = {current: null}; + const hiddenNodePathInfo = makeNodePathInfo(['body', 0], []); + const visibleNodePathInfo = makeNodePathInfo(['body', 1], []); + const childNodePathInfo = makeNodePathInfo(['body', 0, 'children', 0], []); + const outlines = getSequencesWithSelectableOutlines({ + sequences: [ + makeTimelineSequence({ + schema, + id: 'hidden', + overrideId: 'hidden', + refForOutline, + showInTimeline: false, + }), + makeTimelineSequence({ + schema, + id: 'visible', + overrideId: 'visible', + refForOutline, + }), + makeTimelineSequence({ + schema, + id: 'child', + overrideId: 'child', + parentId: 'hidden', + refForOutline, + }), + ], + overrideIdsToNodePaths: { + hidden: hiddenNodePathInfo.sequenceSubscriptionKey, + visible: visibleNodePathInfo.sequenceSubscriptionKey, + child: childNodePathInfo.sequenceSubscriptionKey, + }, + }); + + expect(outlines.map((outline) => outline.key).sort()).toEqual( + [ + getTimelineSequenceSelectionKey(visibleNodePathInfo), + getTimelineSequenceSelectionKey(childNodePathInfo), + ].sort(), + ); +}); + test('UV handles project semantic outline corners', () => { const points = [ {x: 200, y: 200}, From 0ee48a09cc8bcb2bd7763eda84da52f922fbc297 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 9 Jun 2026 17:20:28 +0200 Subject: [PATCH 8/8] cool --- bun.lock | 1 - packages/example/src/CanvasCapturePreview.tsx | 60 ++++++++++--------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/bun.lock b/bun.lock index a8d05400418..357f3542671 100644 --- a/bun.lock +++ b/bun.lock @@ -446,7 +446,6 @@ "@remotion/animation-utils": "workspace:*", "@remotion/babel-loader": "workspace:*", "@remotion/bundler": "workspace:*", - "@remotion/canvas-capture": "workspace:*", "@remotion/captions": "workspace:*", "@remotion/cli": "workspace:*", "@remotion/cloudrun": "workspace:*", diff --git a/packages/example/src/CanvasCapturePreview.tsx b/packages/example/src/CanvasCapturePreview.tsx index 6999a34242b..27ca5e38142 100644 --- a/packages/example/src/CanvasCapturePreview.tsx +++ b/packages/example/src/CanvasCapturePreview.tsx @@ -4,6 +4,7 @@ import { AbsoluteFill, CalculateMetadataFunction, Img, + Interactive, staticFile, useCurrentFrame, useVideoConfig, @@ -48,6 +49,8 @@ export type CanvasCapturePreviewProps = { readonly cursorOffsetY: number; readonly cursorAssetBasePath: string | null; readonly cursorData?: CursorRecording; + width: number | null; + height: number | null; }; export const canvasCapturePreviewDefaultProps: CanvasCapturePreviewProps = { @@ -272,7 +275,7 @@ const CursorGlyph: React.FC<{ marginLeft: -16 * scale, marginTop: -16 * scale, width: 32 * scale, - scale: 3.23, + scale: 5.97, }} /> ); @@ -371,11 +374,13 @@ export const calculateCanvasCapturePreviewMetadata: CalculateMetadataFunction< return { durationInFrames: Math.ceil(durationInSeconds * fps), fps, - width: dimensions.width, - height: dimensions.height, + width: 1920, + height: 1080, props: { ...props, cursorData, + width: dimensions.width, + height: dimensions.height, }, }; }; @@ -388,6 +393,8 @@ export const CanvasCapturePreview: React.FC = ({ cursorOffsetY, cursorScale, videoFile, + width, + height, }) => { if (!cursorData) { throw new Error(`Cursor data from ${cursorFile} was not loaded.`); @@ -399,36 +406,31 @@ export const CanvasCapturePreview: React.FC = ({ return ( - ); };