Push through
A scene arriving by pushing the last one out of frame.
Customize
import {Scene, Video} from "odori";
import {PushThrough} from "../components/push-through/push-through";
<Video>
<Scene id="push-through" duration="3s">
<PushThrough
direction="up"
recede={0.94}
/>
</Scene>
</Video>Installation
pnpm odori add component push-throughnpx odori add component push-throughbunx odori add component push-throughEdit the source in videos/components/push-through/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/push-through/push-through.tsx
import type {ReactNode} from "react";
import {Easing, Fill, interpolate, useSceneTransition} from "odori";
export type PushThroughProps = {
children?: ReactNode;
/** Which way the incoming scene travels. */
direction?: "up" | "down" | "left" | "right";
/** How far the outgoing scene recedes, as a scale. */
recede?: number;
};
/**
* One scene pushing the last one out of the frame.
*
* This is the transition the timeline could not express before: it needs both
* scenes on screen at once, so the scene it lives in declares an `overlap` and
* this reads how far through the join it is. The same component handles both
* halves — arriving, it slides in and pushes; leaving, it recedes and is
* pushed — because a transition written as two components has two places to
* get the timing wrong.
*
* ```tsx
* <Scene id="proof" duration="4s" overlap="0.5s">
* <PushThrough>{content}</PushThrough>
* </Scene>
* ```
*/
export const PushThrough = ({children, direction = "up", recede = 0.94}: PushThroughProps) => {
const {entering, leaving} = useSceneTransition();
const arriving = interpolate(entering, [0, 1], [0, 1], {easing: Easing.standard});
const departing = interpolate(leaving, [0, 1], [0, 1], {easing: Easing.standard});
const axis = direction === "up" || direction === "down" ? "Y" : "X";
const sign = direction === "up" || direction === "left" ? 1 : -1;
// Arriving: travel in from off-frame. Leaving: give way in the same
// direction, and shrink slightly so the incoming scene reads as in front.
const offset = arriving < 1 ? (1 - arriving) * 100 * sign : departing * 22 * sign;
const scale = arriving < 1 ? 1 : 1 - departing * (1 - recede);
const dim = arriving < 1 ? 0 : departing * 0.35;
return (
<Fill style={{transform: `translate${axis}(${offset}%) scale(${scale})`, transformOrigin: "center"}}>
{children}
{dim > 0 ? (
<div aria-hidden style={{background: "#000", inset: 0, opacity: dim, position: "absolute"}} />
) : null}
</Fill>
);
};
videos/components/push-through/push-through.preview.tsx
import {useBrand} from "odori";
import {defineComponentPreview} from "odori/preview";
import {PushThrough} from "./push-through";
const Panel = ({label}: {label: string}) => {
const brand = useBrand();
return (
<div
style={{
alignItems: "center",
background: brand.colors.background,
color: brand.colors.foreground,
display: "flex",
fontFamily: "ui-sans-serif, system-ui",
fontSize: 88,
inset: 0,
justifyContent: "center",
letterSpacing: "-0.04em",
position: "absolute",
}}
>
{label}
</div>
);
};
export default defineComponentPreview({
title: "Push through",
category: "Motion",
description: "A scene arriving by pushing the last one out of frame.",
component: PushThrough,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
direction: {type: "select", defaultValue: "up", options: ["up", "down", "left", "right"]},
recede: {type: "number", defaultValue: 0.94, min: 0.8, max: 1, step: 0.01},
},
examples: [
{name: "Default", props: {children: <Panel label="Arriving" />}},
{name: "Sideways", props: {direction: "left", children: <Panel label="Arriving" />}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {PushThrough} from "../components/push-through/push-through";
<Video>
<Scene id="push-through" duration="3s">
<PushThrough
direction="up"
recede={0.94}
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Motion
- Recommended duration
- 90 frames · 3s
- Minimum duration
- 30 frames · 1s
- Entrance
- 15 frames
- Exit
- 15 frames
- Reduced motion
- the arriving scene appears in place, with no travel
- Requires
- Odori
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
children | ReactNode | — | — |
directionWhich way the incoming scene travels. | "up" | "down" | "left" | "right" | "up" | — |
recedeHow far the outgoing scene recedes, as a scale. | number | 0.94 | — |
Related components
All componentsCamera stageAn animated camera that moves through your scene.Cell revealTiles slide into place to reveal your content.Charged borderA moving glow around a rounded frame.Dissolve revealA noise threshold sweeps across the frame, so content arrives as a torn edge.Gradient fieldA looping gradient in your brand colors.Halftone revealContent revealed through expanding dots.