Logo orbit
Product identity surrounded by connected integrations.
Customize
import {Scene, Video} from "odori";
import {LogoOrbit} from "../components/logo-orbit/logo-orbit";
<Video>
<Scene id="logo-orbit" duration="5s">
<LogoOrbit
center="odori"
around={[
"Next.js",
"React",
"Studio",
"FFmpeg",
"CI",
"Slack"
]}
detail="one ecosystem"
/>
</Scene>
</Video>Installation
pnpm odori add component logo-orbitnpx odori add component logo-orbitbunx odori add component logo-orbitEdit the source in videos/components/logo-orbit/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/logo-orbit/logo-orbit.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type LogoOrbitProps = {
/** Center wordmark. Defaults to the brand name. */
center?: string;
/** Integration names placed evenly around the ring. */
around: string[];
detail?: string;
};
/**
* One identity surrounded by what connects to it. The ring is static and the
* marks arrive in order, because a spinning orbit is unreadable at any
* distance a viewer will actually watch from.
*/
export const LogoOrbit = ({center, around, detail}: LogoOrbitProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const name = center ?? brand.name;
const ring = interpolate(frame, [6, 40], [0, 1], {easing: Easing.standard});
const radius = 300 * scale;
return (
<Fill style={{alignItems: "center", justifyContent: "center"}}>
<div style={{height: radius * 2, position: "relative", width: radius * 2}}>
<div
style={{
border: `1px solid ${brand.colors.border}`,
borderRadius: 999,
inset: 0,
opacity: ring,
position: "absolute",
transform: `scale(${0.9 + ring * 0.1})`,
}}
/>
<div
style={{
alignItems: "center",
display: "flex",
flexDirection: "column",
gap: 10 * scale,
inset: 0,
justifyContent: "center",
position: "absolute",
}}
>
<span style={{fontSize: 72 * scale, fontWeight: 600, letterSpacing: "-0.05em", opacity: ring}}>{name}</span>
{detail ? <span style={{color: brand.colors.muted, fontSize: 26 * scale, opacity: ring}}>{detail}</span> : null}
</div>
{around.map((label, index) => {
const angle = (index / around.length) * Math.PI * 2 - Math.PI / 2;
const at = 20 + index * 8;
const enter = interpolate(frame, [at, at + 18], [0, 1], {easing: Easing.standard});
return (
<div
key={label}
style={{
background: brand.colors.surface,
border: `1px solid ${brand.colors.border}`,
borderRadius: 999,
fontFamily: brand.typography.mono,
fontSize: 24 * scale,
left: radius + Math.cos(angle) * radius,
opacity: enter,
padding: `${12 * scale}px ${20 * scale}px`,
position: "absolute",
top: radius + Math.sin(angle) * radius,
transform: `translate(-50%, -50%) scale(${0.85 + enter * 0.15})`,
}}
>
{label}
</div>
);
})}
</div>
</Fill>
);
};
videos/components/logo-orbit/logo-orbit.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {LogoOrbit} from "./logo-orbit";
export default defineComponentPreview({
title: "Logo orbit",
category: "Brand",
description: "Product identity surrounded by connected integrations.",
component: LogoOrbit,
canvas: {width: 1920, height: 1080, duration: "5s"},
controls: {
center: {type: "text", defaultValue: "odori", maxLength: 18},
detail: {type: "text", defaultValue: "one ecosystem"},
},
examples: [
{name: "Ecosystem", props: {around: ["Next.js", "React", "Studio", "FFmpeg", "CI", "Slack"]}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {LogoOrbit} from "../components/logo-orbit/logo-orbit";
<Video>
<Scene id="logo-orbit" duration="5s">
<LogoOrbit
center="odori"
around={[
"Next.js",
"React",
"Studio",
"FFmpeg",
"CI",
"Slack"
]}
detail="one ecosystem"
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Brand
- Recommended duration
- 150 frames · 5s
- Minimum duration
- 75 frames · 2.5s
- Entrance
- 68 frames
- Exit
- 0 frames
- Reduced motion
- marks appear without scaling
- Requires
- sans font, mono font
- Content limits
- around ≤ 8, labelLength ≤ 16
Props
| Prop | Type | Default | Required |
|---|---|---|---|
centerCenter wordmark. Defaults to the brand name. | string | — | — |
aroundIntegration names placed evenly around the ring. | string[] | — | Yes |
detail | string | — | — |
Related components
All componentsBrand providerThe resolved brand rendered as a sheet: color, type, and motion.Icon setA stroked icon set that draws itself on, as editable SVG source.Identity revealAn animated product logo and name.Marks starterPlaceholder marks that draw themselves on, as editable SVG source.Absolute stageFull-frame surface with stable clipping and positioning.AccordionSections opening one at a time, the outgoing one closing as the next grows.