Transitions (View/Rescaler)
This guide will show a few basic examples of animated transitions on View
/Rescaler
components.
Configure inputs and output
Start the compositor and configure 2 input streams and a single output stream as described in the "Simple scene" guide in the "Configure inputs and output" section.
Transition that changes the width
of an input stream
- React
- HTTP
- Membrane Framework
function App() {
const [beforeTransition, setBeforeTransition] = useState(true);
useEffect(() => {
setTimeout(() => setBeforeTransition(false), 2000);
}, []);
return (
<View backgroundColor="#4d4d4d">
<Rescaler
width={beforeTransition ? 480 : 1280}
transition={{ durationMs: 2000 }}>
<InputStream inputId="input_1" />
</Rescaler>
</View>
)
}
After 2 seconds component changes state where width changes from 480 to 1280.
Set initial scene for the transition:
POST: /api/output/output_1/update
Content-Type: application/json
{
"video": {
"root": {
"type": "view",
"background_color_rgba": "#4d4d4dff",
"children": [
{
"type": "rescaler",
"id": "rescaler_1",
"width": 480,
"child": { "type": "input_stream", "input_id": "input_1" },
}
]
}
}
}
A few seconds latter update a scene with a different width
:
POST: /api/output/output_1/update
Content-Type: application/json
{
"video": {
"root": {
"type": "view",
"background_color_rgba": "#4d4d4dff",
"children": [
{
"type": "rescaler",
"id": "rescaler_1",
"width": 1280,
"transition": { "duration_ms": 2000 },
"child": { "type": "input_stream", "input_id": "input_1" },
}
]
}
}
}
In the first update request, you can see that the rescaler has a width of 480, and in the second one, it is changed
to 1280 and transition.duration_ms: 2000
was added.
The component must have the same "id"
in both the initial state and the update that starts the
transition, otherwise it will switch immediately to the new state without a transition.
Set initial scene for the transition and after few seconds update a component
with a different width
:
def handle_setup(ctx, state) do
request = %LiveCompositor.Request.UpdateVideoOutput{
output_id: "output_1",
root: %{
type: :view,
background_color_rgba: "#4d4d4dff",
children: [
%{
type: :rescaler,
id: "rescaler_1",
width: 480,
child: %{ type: :input_stream, input_id: :input_1 },
}
]
}
}
Process.send_after(self(), :start_transition, 2000)
{[notify_child: {:live_compositor, request}], state}
end
def handle_info(:start_transition, _ctx, state)
request = %LiveCompositor.Request.UpdateVideoOutput{
output_id: "output_1",
root: %{
type: :view,
background_color_rgba: "#4d4d4dff",
children: [
%{
type: :rescaler,
id: "rescaler_1",
width: 1280,
transition: %{ duration_ms: 2000 },
child: %{ type: :input_stream, input_id: :input_1 },
}
]
}
}
{[notify_child: {:live_compositor, request}], state}
end
In the first update request, you can see that the rescaler has a width of 480, and in the second one, it is changed
to 1280 and transition.duration_ms: 2000
was added.
The component must have the same "id"
in both the initial state and the update that starts the
transition, otherwise it will switch immediately to the new state without a transition.
Output stream
Transition on one of the sibling components
In the above scenario you saw how transition on a single component behaves, but let's see what happens with components that are not a part of the transition, but their size and position still depend on other components.
Add a second input stream wrapped with Rescaler
, but without any transition options.
- React
- HTTP
- Membrane Framework
function App() {
const [beforeTransition, setBeforeTransition] = useState(true);
useEffect(() => {
setTimeout(() => setBeforeTransition(false), 2000);
}, []);
return (
<View backgroundColor="#4d4d4d">
<Rescaler
width={beforeTransition ? 480 : 1280}
transition={{ durationMs: 2000 }}>
<InputStream inputId="input_1" />
</Rescaler>
<Rescaler>
<InputStream inputId="input_2" />
</Rescaler>
</View>
)
}
POST: /api/output/output_1/update
Content-Type: application/json
{
"video": {
"root": {
"type": "view",
"background_color_rgba": "#4d4d4dff",
"children": [
{
"type": "rescaler",
"id": "rescaler_1",
"width": 480,
"child": { "type": "input_stream", "input_id": "input_1" },
},
{
"type": "rescaler",
"child": { "type": "input_stream", "input_id": "input_2" },
}
]
}
}
}
Update a scene with a different width
:
POST: /api/output/output_1/update
Content-Type: application/json
{
"video": {
"root": {
"type": "view",
"background_color_rgba": "#4d4d4dff",
"children": [
{
"type": "rescaler",
"id": "rescaler_1",
"width": 1280,
"transition": { "duration_ms": 2000 },
"child": { "type": "input_stream", "input_id": "input_1" },
},
{
"type": "rescaler",
"child": { "type": "input_stream", "input_id": "input_2" },
}
]
}
}
}
def handle_setup(ctx, state) do
request = %LiveCompositor.Request.UpdateVideoOutput{
output_id: "output_1",
root: %{
type: :view,
background_color_rgba: "#4d4d4dff",
children: [
%{
type: :rescaler,
id: "rescaler_1",
width: 480,
child: %{ type: :input_stream, input_id: :input_1 },
},
%{
type: :rescaler,
child: %{ type: :input_stream, input_id: :input_2 },
}
]
}
}
Process.send_after(self(), :start_transition, 2000)
{[notify_child: {:live_compositor, request}], state}
end
def handle_info(:start_transition, _ctx, state)
request = %LiveCompositor.Request.UpdateVideoOutput{
output_id: "output_1",
root: %{
type: :view,
background_color_rgba: "#4d4d4dff",
children: [
%{
type: :rescaler,
id: "rescaler_1",
width: 1280,
transition: %{ duration_ms: 2000 },
child: %{ type: :input_stream, input_id: :input_1 },
},
%{
type: :rescaler,
child: %{ type: :input_stream, input_id: :input_2 },
}
]
}
}
{[notify_child: {:live_compositor, request}], state}
end
Output stream
Transition between different modes
Currently, a state before the transition and after needs to use the same type of configuration. In particular:
- It is not possible to transition a component between static and absolute positioning.
- It is not possible to transition a component between using
top
andbottom
fields (the same forleft
/right
). - It is not possible to transition a component with known
width
/height
to a state with dynamicwidth
/height
based on the parent layout.
Let's try the same example as in the first scenario with a single input, but instead, change the Rescaler
component to be absolutely positioned in the second update.
- React
- HTTP
- Membrane Framework
function App() {
const [beforeTransition, setBeforeTransition] = useState(true);
useEffect(() => {
setTimeout(() => setBeforeTransition(false), 2000);
}, []);
return (
<View backgroundColor="#4d4d4d">
{beforeTransition ? (
<Rescaler width={480}>
<InputStream inputId="input_1" />
</Rescaler>
) : (
<Rescaler width={1280} top={0} left={0} transition={{ durationMs: 2000 }} >
<InputStream inputId="input_1" />
</Rescaler>
)}
</View>
);
}
POST: /api/output/output_1/update
Content-Type: application/json
{
"video": {
"root": {
"type": "view",
"background_color_rgba": "#4d4d4dff",
"children": [
{
"type": "rescaler",
"id": "rescaler_1",
"width": 480,
"child": { "type": "input_stream", "input_id": "input_1" },
}
]
}
}
}
POST: /api/output/output_1/update
Content-Type: application/json
{
"video": {
"root": {
"type": "view",
"background_color_rgba": "#4d4d4dff",
"children": [
{
"type": "rescaler",
"id": "rescaler_1",
"width": 1280,
"top": 0,
"left": 0,
"transition": { "duration_ms": 2000 },
"child": { "type": "input_stream", "input_id": "input_1" },
}
]
}
}
}
def handle_setup(ctx, state) do
request = %LiveCompositor.Request.UpdateVideoOutput{
output_id: "output_1",
root: %{
type: :view,
background_color_rgba: "#4d4d4dff",
children: [
%{
type: :rescaler,
id: "rescaler_1",
width: 480,
child: %{ type: :input_stream, input_id: :input_1 },
}
]
}
}
Process.send_after(self(), :start_transition, 2000)
{[notify_child: {:live_compositor, request}], state}
end
def handle_info(:start_transition, _ctx, state)
request = %LiveCompositor.Request.UpdateVideoOutput{
output_id: "output_1",
root: %{
type: :view,
background_color_rgba: "#4d4d4dff",
children: [
%{
type: :rescaler,
id: "rescaler_1",
width: 1280,
top: 0,
left: 0,
transition: %{ duration_ms: 2000 },
child: %{ type: :input_stream, input_id: :input_1 },
}
]
}
}
{[notify_child: {:live_compositor, request}], state}
end
As you can see on the resulting stream, the transition did not happen because the Rescaler
component
in the initial scene was using static positioning and after the update it was positioned absolutely.
Output stream
Different interpolation functions
All of the above examples use default linear interpolation, but there are also a few other modes available.
- React
- HTTP
- Membrane Framework
function App() {
const [beforeTransition, setBeforeTransition] = useState(true);
useEffect(() => {
setTimeout(() => setBeforeTransition(false), 2000);
}, []);
const top = beforeTransition ? 0 : 540;
return (
<View backgroundColor="#4d4d4d">
<Rescaler
width={320} height={180} top={top} left={0}
transition={{ durationMs: 2000 }}>
<InputStream inputId="input_1" />
</Rescaler>
<Rescaler
width={320} height={180} top={top} left={320}
transition={{ durationMs: 2000, easingFunction: 'bounce' }}>
<InputStream inputId="input_2" />
</Rescaler>
<Rescaler
width={320} height={180} top={top} left={640}
transition={{
durationMs: 2000,
easingFunction: {
functionName: 'cubic_bezier',
points: [0.65, 0, 0.35, 1],
},
}}>
<InputStream inputId="input_3" />
</Rescaler>
<Rescaler
width={320} height={180} top={top} left={960}
transition={{
durationMs: 2000,
easingFunction: {
functionName: 'cubic_bezier',
points: [0.33, 1, 0.68, 1],
},
}}>
<InputStream inputId="input_4" />
</Rescaler>
</View>
);
}
POST: /api/output/output_1/update
Content-Type: application/json
{
"video": {
"root": {
"type": "view",
"background_color_rgba": "#4d4d4dff",
"children": [
{
"type": "rescaler",
"id": "rescaler_1",
"width": 320, "height": 180, "top": 0, "left": 0,
"child": { "type": "input_stream", "input_id": "input_1" },
},
{
"type": "rescaler",
"id": "rescaler_2",
"width": 320, "height": 180, "top": 0, "left": 320,
"child": { "type": "input_stream", "input_id": "input_2" },
},
{
"type": "rescaler",
"id": "rescaler_3",
"width": 320, "height": 180, "top": 0, "left": 640,
"child": { "type": "input_stream", "input_id": "input_3" },
},
{
"type": "rescaler",
"id": "rescaler_4",
"width": 320, "height": 180, "top": 0, "left": 960,
"child": { "type": "input_stream", "input_id": "input_4" },
},
]
}
}
}
POST: /api/output/output_1/update
Content-Type: application/json
{
"video": {
"root": {
"type": "view",
"background_color_rgba": "#4d4d4dff",
"children": [
{
"type": "rescaler",
"id": "rescaler_1",
"width": 320, "height": 180, "top": 540, "left": 0,
"child": { "type": "input_stream", "input_id": "input_1" },
"transition": { "duration_ms": 2000 },
},
{
"type": "rescaler",
"id": "rescaler_2",
"width": 320, "height": 180, "top": 540, "left": 320,
"child": { "type": "input_stream", "input_id": "input_2" },
"transition": {
"duration_ms": 2000, "easing_function": {"function_name": "bounce"}
},
},
{
"type": "rescaler",
"id": "rescaler_3",
"width": 320, "height": 180, "top": 540, "left": 640,
"child": { "type": "input_stream", "input_id": "input_3" },
"transition": {
"duration_ms": 2000,
"easing_function": {
"function_name": "cubic_bezier",
"points": [0.65, 0, 0.35, 1]
}
}
},
{
"type": "rescaler",
"id": "rescaler_4",
"width": 320, "height": 180, "top": 540, "left": 960,
"child": { "type": "input_stream", "input_id": "input_4" },
"transition": {
"duration_ms": 2000,
"easing_function": {
"function_name": "cubic_bezier",
"points": [0.33, 1, 0.68, 1]
}
}
}
]
}
}
}
def handle_setup(ctx, state) do
request = %LiveCompositor.Request.UpdateVideoOutput{
output_id: "output_1",
root: %{
type: :view,
background_color_rgba: "#4d4d4dff",
children: [
%{
type: :rescaler,
id: "rescaler_1",
width: 320, height: 180, top: 0, left: 0,
child: %{ type: :input_stream, input_id: :input_1 },
},
%{
type: :rescaler,
id: "rescaler_2",
width: 320, height: 180, top: 0, left: 320,
child: %{ type: :input_stream, input_id: :input_2 },
},
%{
type: :rescaler,
id: "rescaler_3",
width: 320, height: 180, top: 0, left: 640,
child: %{ type: :input_stream, input_id: :input_3 },
},
%{
type: :rescaler,
id: "rescaler_4",
width: 320, height: 180, top: 0, left: 960,
child: %{ type: :input_stream, input_id: :input_4 },
}
]
}
}
Process.send_after(self(), :start_transition, 2000)
{[notify_child: {:live_compositor, request}], state}
end
def handle_info(:start_transition, _ctx, state)
request = %LiveCompositor.Request.UpdateVideoOutput{
output_id: "output_1",
root: %{
type: :view,
background_color_rgba: "#4d4d4dff",
children: [
%{
type: :rescaler,
id: "rescaler_1",
width: 320, height: 180, top: 0, left: 0,
child: %{ type: :input_stream, input_id: :input_1 },
transition: %{ duration_ms: 2000 },
},
%{
type: :rescaler,
id: "rescaler_2",
width: 320, height: 180, top: 0, left: 320,
child: %{ type: :input_stream, input_id: :input_2 },
transition: %{
duration_ms: 2000
easing_function: %{ function_name: :bounce}
},
},
%{
type: :rescaler,
id: "rescaler_3",
width: 320, height: 180, top: 0, left: 640,
child: %{ type: :input_stream, input_id: :input_3 },
transition: %{
duration_ms: 2000
easing_function: %{
function_name: :cubic_bezier,
points: [0.65, 0, 0.35, 1]
}
}
},
%{
type: :rescaler,
id: "rescaler_4",
width: 320, height: 180, top: 0, left: 960,
child: %{ type: :input_stream, input_id: :input_4 },
transition: %{
duration_ms: 2000
easing_function: %{
function_name: :cubic_bezier,
points: [0.33, 1, 0.68, 1]
}
}
}
]
}
}
{[notify_child: {:live_compositor, request}], state}
end
Output stream
Input 1
- Linear transitionInput 2
- Bounce transitionInput 3
- Cubic Bézier transition with[0.65, 0, 0.35, 1]
points (easeInOutCubic
)Input 4
- Cubic Bézier transition with[0.33, 1, 0.68, 1]
points (easeOutCubic
)
Check out other popular Cubic Bézier curves on https://easings.net.