Marks starter
Placeholder marks that draw themselves on, as editable SVG source.
Customize
import {Scene, Video} from "odori";
import {MarksStarter} from "../components/marks-starter/marks-starter";
<Video>
<Scene id="marks-starter" duration="2s">
<MarksStarter
size={220}
drawFrames={24}
/>
</Scene>
</Video>Installation
pnpm odori add component marks-starternpx odori add component marks-starterbunx odori add component marks-starterEdit the source in videos/components/marks-starter/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/marks-starter/marks-starter.tsx
import type {CSSProperties} from "react";
import {useBrand, useFrame, useDesignScale, Easing, interpolate} from "odori";
export type MarkProps = {
/** Defaults to the brand accent, so a mark inherits the palette. */
color?: string;
/** Size in design pixels, scaled for the format. */
size?: number;
/** Draw the mark on across this many frames. Zero renders it complete. */
drawFrames?: number;
style?: CSSProperties;
};
/**
* Placeholder marks, as source. Replace the paths with your own and the motion
* stays: a stroked mark draws itself on from the frame clock, which is the one
* thing an image file can never do.
*
* These are deliberately generic. A registry that shipped other companies'
* logos would be making a trademark decision on your behalf.
*/
const useMark = (drawFrames: number, color?: string) => {
const frame = useFrame();
const brand = useBrand();
const progress =
drawFrames <= 0 ? 1 : interpolate(frame, [0, drawFrames], [0, 1], {easing: Easing.standard});
return {stroke: color ?? brand.colors.accent, progress};
};
/** A closed shape that draws its outline, then fills. */
export const MarkAperture = ({color, size = 72, drawFrames = 24, style}: MarkProps) => {
const scale = useDesignScale();
const {stroke, progress} = useMark(drawFrames, color);
const length = 240;
return (
<svg
viewBox="0 0 100 100"
style={{height: size * scale, width: size * scale, ...style}}
aria-hidden="true"
>
<circle
cx="50"
cy="50"
r="38"
fill={stroke}
fillOpacity={Math.max(0, (progress - 0.7) / 0.3) * 0.16}
stroke={stroke}
strokeWidth="8"
strokeDasharray={length}
strokeDashoffset={length * (1 - progress)}
strokeLinecap="round"
/>
</svg>
);
};
/** Three ascending bars, staggered so the mark builds rather than appears. */
export const MarkStack = ({color, size = 72, drawFrames = 24, style}: MarkProps) => {
const scale = useDesignScale();
const frame = useFrame();
const {stroke} = useMark(drawFrames, color);
const bars = [
{x: 18, height: 30},
{x: 42, height: 52},
{x: 66, height: 74},
];
return (
<svg
viewBox="0 0 100 100"
style={{height: size * scale, width: size * scale, ...style}}
aria-hidden="true"
>
{bars.map((bar, index) => {
const start = index * Math.max(1, drawFrames / 4);
const grow =
drawFrames <= 0
? 1
: interpolate(frame, [start, start + drawFrames], [0, 1], {easing: Easing.standard});
return (
<rect
key={bar.x}
x={bar.x}
y={88 - bar.height * grow}
width="16"
height={bar.height * grow}
rx="4"
fill={stroke}
/>
);
})}
</svg>
);
};
videos/components/marks-starter/marks-starter.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {MarkAperture} from "./marks-starter";
export default defineComponentPreview({
title: "Marks starter",
category: "Brand",
description: "Placeholder marks that draw themselves on, as editable SVG source.",
component: MarkAperture,
canvas: {width: 1920, height: 1080, duration: "2s"},
controls: {
size: {type: "number", defaultValue: 220, min: 48, max: 400},
drawFrames: {type: "number", defaultValue: 24, min: 0, max: 60},
},
examples: [
{name: "Aperture", props: {size: 220}},
{name: "Instant", props: {size: 220, drawFrames: 0}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {MarksStarter} from "../components/marks-starter/marks-starter";
<Video>
<Scene id="marks-starter" duration="2s">
<MarksStarter
size={220}
drawFrames={24}
/>
</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
- 60 frames · 2s
- Minimum duration
- 24 frames · 0.8s
- Entrance
- 24 frames
- Exit
- 0 frames
- Reduced motion
- the mark renders complete, with no draw on
- Requires
- Odori
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
colorDefaults to the brand accent, so a mark inherits the palette. | string | — | — |
sizeSize in design pixels, scaled for the format. | number | 72 | — |
drawFramesDraw the mark on across this many frames. Zero renders it complete. | number | 24 | — |
style | CSSProperties | — | — |
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.Logo orbitProduct identity surrounded by connected integrations.Absolute stageFull-frame surface with stable clipping and positioning.AccordionSections opening one at a time, the outgoing one closing as the next grows.