marks-starter
Placeholder marks that draw themselves on, as editable SVG source.
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>The same runtime that renders the export. Scrub to any frame, switch format, and edit the fixture props.
Installation
pnpm odori add @odori/marks-starternpx odori add @odori/marks-starterbunx odori add @odori/marks-starterSource is copied into videos/components/marks-starter/ and belongs to your repository. odori diff compares it with upstream later; odori update applies the change you accept. It has no registry dependencies.
Copy and paste the following code 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: "Foundation",
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}},
],
});
Use it in 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>Nothing else to install.
The component imports only from odori, so there are no dependencies to add and no import paths to rewrite.
Timing contract
Every registry component declares how it behaves in time.odori test fails a cut that violates these.
- Family
- Foundation
- Aspect ratios
- 16:9 · 9:16 · 1:1
- 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
- nothing beyond the runtime
- 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 | — | — |