-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
remotion: Add proof of concept for nested HtmlInCanvas
#7329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
57de38f
b0f91a8
1cd7027
e82e4a4
2161674
1f79e21
ecae2b7
a3dd1e7
72d6338
74e7d45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ import React, { | |||||||||||||||||||||||||||||||||||||
| useRef, | ||||||||||||||||||||||||||||||||||||||
| useState, | ||||||||||||||||||||||||||||||||||||||
| } from 'react'; | ||||||||||||||||||||||||||||||||||||||
| import {createPortal} from 'react-dom'; | ||||||||||||||||||||||||||||||||||||||
| import type {SequenceControls} from './CompositionManager.js'; | ||||||||||||||||||||||||||||||||||||||
| import {delayRender} from './delay-render.js'; | ||||||||||||||||||||||||||||||||||||||
| import type {EffectsProp} from './effects/effect-types.js'; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -260,11 +261,16 @@ export type HtmlInCanvasProps = Omit< | |||||||||||||||||||||||||||||||||||||
| readonly children: React.ReactNode; | ||||||||||||||||||||||||||||||||||||||
| readonly onPaint?: HtmlInCanvasOnPaint; | ||||||||||||||||||||||||||||||||||||||
| readonly onInit?: HtmlInCanvasOnInit; | ||||||||||||||||||||||||||||||||||||||
| readonly nested?: boolean; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| /* eslint-enable react/require-default-props */ | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const HtmlInCanvasAncestorContext = createContext(false); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const hiddenAuxiliaryCanvasContainerStyle: React.CSSProperties = { | ||||||||||||||||||||||||||||||||||||||
| position: 'absolute', | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
JonnyBurger marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const HtmlInCanvasInner = forwardRef< | ||||||||||||||||||||||||||||||||||||||
| HTMLCanvasElement, | ||||||||||||||||||||||||||||||||||||||
| HtmlInCanvasProps & { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -282,6 +288,7 @@ const HtmlInCanvasInner = forwardRef< | |||||||||||||||||||||||||||||||||||||
| _experimentalControls: controls, | ||||||||||||||||||||||||||||||||||||||
| style, | ||||||||||||||||||||||||||||||||||||||
| durationInFrames, | ||||||||||||||||||||||||||||||||||||||
| nested = false, | ||||||||||||||||||||||||||||||||||||||
| ...sequenceProps | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| ref, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -321,7 +328,17 @@ const HtmlInCanvasInner = forwardRef< | |||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| [ref], | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| const shadowCanvasRef = useRef<HTMLCanvasElement | null>(null); | ||||||||||||||||||||||||||||||||||||||
| const [offscreenCanvas] = useState(() => new OffscreenCanvas(1, 1)); | ||||||||||||||||||||||||||||||||||||||
| const auxiliaryCanvasContainer = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||
| if (!nested || typeof document === 'undefined') { | ||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const container = document.createElement('div'); | ||||||||||||||||||||||||||||||||||||||
| Object.assign(container.style, hiddenAuxiliaryCanvasContainerStyle); | ||||||||||||||||||||||||||||||||||||||
| return container; | ||||||||||||||||||||||||||||||||||||||
| }, [nested]); | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const chainState = useEffectChainState(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -414,6 +431,19 @@ const HtmlInCanvasInner = forwardRef< | |||||||||||||||||||||||||||||||||||||
| height, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const shadowCanvas = shadowCanvasRef.current; | ||||||||||||||||||||||||||||||||||||||
| if (shadowCanvas) { | ||||||||||||||||||||||||||||||||||||||
| const shadowContext = shadowCanvas.getContext('2d'); | ||||||||||||||||||||||||||||||||||||||
| if (!shadowContext) { | ||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||
| 'Failed to acquire 2D context for <HtmlInCanvas> shadow canvas', | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| shadowContext.clearRect(0, 0, width, height); | ||||||||||||||||||||||||||||||||||||||
| shadowContext.drawImage(canvas2dRef.current!, 0, 0); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
JonnyBurger marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| continueRender(handle); | ||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||
| cancelRender(error); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -427,6 +457,18 @@ const HtmlInCanvasInner = forwardRef< | |||||||||||||||||||||||||||||||||||||
| offscreenCanvas, | ||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| useLayoutEffect(() => { | ||||||||||||||||||||||||||||||||||||||
| if (!auxiliaryCanvasContainer) { | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| document.body.appendChild(auxiliaryCanvasContainer); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||||
| auxiliaryCanvasContainer.remove(); | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| }, [auxiliaryCanvasContainer]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Set up layoutSubtree and persistent paint listener. Runs as a | ||||||||||||||||||||||||||||||||||||||
| // layout effect so the listener is attached before the resize effect | ||||||||||||||||||||||||||||||||||||||
| // below dispatches its first synthetic paint. | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -489,12 +531,25 @@ const HtmlInCanvasInner = forwardRef< | |||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| }, [width, height]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (isInsideAncestorHtmlInCanvas) { | ||||||||||||||||||||||||||||||||||||||
| if (isInsideAncestorHtmlInCanvas && !nested) { | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the component already reads |
||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||
| '<HtmlInCanvas> effects cannot be nested together. Chrome will only display the outer effect. Consider merging the effects into one if you can.', | ||||||||||||||||||||||||||||||||||||||
| '<HtmlInCanvas> cannot be nested by default. Pass the `nested` prop to the inner <HtmlInCanvas> to opt into rendering it through an auxiliary canvas, or merge the effects into one <HtmlInCanvas> if possible.', | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const layoutCanvasElement = ( | ||||||||||||||||||||||||||||||||||||||
| <canvas | ||||||||||||||||||||||||||||||||||||||
| ref={setLayoutCanvasRef} | ||||||||||||||||||||||||||||||||||||||
| width={width} | ||||||||||||||||||||||||||||||||||||||
| height={height} | ||||||||||||||||||||||||||||||||||||||
| style={style} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| <div ref={divRef} style={innerStyle}> | ||||||||||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </canvas> | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| <Sequence | ||||||||||||||||||||||||||||||||||||||
| durationInFrames={resolvedDuration} | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -505,16 +560,21 @@ const HtmlInCanvasInner = forwardRef< | |||||||||||||||||||||||||||||||||||||
| {...sequenceProps} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| <HtmlInCanvasAncestorContext.Provider value> | ||||||||||||||||||||||||||||||||||||||
| <canvas | ||||||||||||||||||||||||||||||||||||||
| ref={setLayoutCanvasRef} | ||||||||||||||||||||||||||||||||||||||
| width={width} | ||||||||||||||||||||||||||||||||||||||
| height={height} | ||||||||||||||||||||||||||||||||||||||
| style={style} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| <div ref={divRef} style={innerStyle}> | ||||||||||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </canvas> | ||||||||||||||||||||||||||||||||||||||
| {nested ? ( | ||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||
| {auxiliaryCanvasContainer | ||||||||||||||||||||||||||||||||||||||
| ? createPortal(layoutCanvasElement, auxiliaryCanvasContainer) | ||||||||||||||||||||||||||||||||||||||
| : null} | ||||||||||||||||||||||||||||||||||||||
| <canvas | ||||||||||||||||||||||||||||||||||||||
| ref={shadowCanvasRef} | ||||||||||||||||||||||||||||||||||||||
| width={width} | ||||||||||||||||||||||||||||||||||||||
| height={height} | ||||||||||||||||||||||||||||||||||||||
| style={style} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||
| layoutCanvasElement | ||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||
| </HtmlInCanvasAncestorContext.Provider> | ||||||||||||||||||||||||||||||||||||||
| </Sequence> | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import React, {useCallback} from 'react'; | ||
| import { | ||
| AbsoluteFill, | ||
| HtmlInCanvas, | ||
| type HtmlInCanvasOnPaint, | ||
| useCurrentFrame, | ||
| useVideoConfig, | ||
| } from 'remotion'; | ||
|
|
||
| const paintBlur: HtmlInCanvasOnPaint = ({canvas, element, elementImage}) => { | ||
| const ctx = canvas.getContext('2d'); | ||
| if (!ctx) { | ||
| throw new Error('Failed to acquire 2D context'); | ||
| } | ||
| ctx.reset(); | ||
| ctx.filter = 'blur(6px)'; | ||
| const transform = ctx.drawElementImage(elementImage, 0, 0); | ||
| element.style.transform = transform.toString(); | ||
| }; | ||
|
|
||
| export const HtmlInCanvasNestedEffects: React.FC = () => { | ||
| const frame = useCurrentFrame(); | ||
| const {width, height} = useVideoConfig(); | ||
|
|
||
| const paintRotation: HtmlInCanvasOnPaint = useCallback( | ||
| ({canvas, element, elementImage}) => { | ||
| const ctx = canvas.getContext('2d'); | ||
| if (!ctx) { | ||
| throw new Error('Failed to acquire 2D context'); | ||
| } | ||
| ctx.reset(); | ||
| const angle = (frame / 60) * Math.PI * 2; | ||
| ctx.translate(canvas.width / 2, canvas.height / 2); | ||
| ctx.rotate(angle); | ||
| ctx.translate(-canvas.width / 2, -canvas.height / 2); | ||
| const transform = ctx.drawElementImage(elementImage, 0, 0); | ||
| element.style.transform = transform.toString(); | ||
| }, | ||
| [frame], | ||
| ); | ||
|
|
||
| const paintTint: HtmlInCanvasOnPaint = useCallback( | ||
| ({canvas, element, elementImage}) => { | ||
| const ctx = canvas.getContext('2d'); | ||
| if (!ctx) { | ||
| throw new Error('Failed to acquire 2D context'); | ||
| } | ||
| ctx.reset(); | ||
| const transform = ctx.drawElementImage(elementImage, 0, 0); | ||
| element.style.transform = transform.toString(); | ||
| ctx.globalCompositeOperation = 'multiply'; | ||
| ctx.fillStyle = '#00aaff'; | ||
| ctx.fillRect(0, 0, canvas.width, canvas.height); | ||
| ctx.globalCompositeOperation = 'source-over'; | ||
| }, | ||
| [], | ||
| ); | ||
|
|
||
| return ( | ||
| <HtmlInCanvas width={width} height={height} onPaint={paintTint}> | ||
| <AbsoluteFill | ||
| style={{ | ||
| backgroundColor: 'white', | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| }} | ||
| > | ||
| <HtmlInCanvas width={200} height={200} onPaint={paintRotation} nested> | ||
| <AbsoluteFill | ||
| style={{ | ||
| backgroundColor: '#ffcc00', | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| }} | ||
| > | ||
| <HtmlInCanvas width={100} height={100} onPaint={paintBlur} nested> | ||
| <AbsoluteFill | ||
| style={{ | ||
| backgroundColor: '#ff0044', | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| color: 'white', | ||
| fontFamily: 'sans-serif', | ||
| fontSize: 48, | ||
| fontWeight: 700, | ||
| }} | ||
| > | ||
| B | ||
| </AbsoluteFill> | ||
| </HtmlInCanvas> | ||
| </AbsoluteFill> | ||
| </HtmlInCanvas> | ||
| </AbsoluteFill> | ||
| </HtmlInCanvas> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nestedlands on the publicly exportedHtmlInCanvasPropswith no_experimentalprefix, while this file already uses_experimentalEffects/_experimentalControlsfor unstable surfaces. Given the PR body lists flickering, visible portal nodes, and no docs as known problems, this should be_experimentalNested(or kept off the public type) so the name and semantics aren't frozen in semver before the design is settled.