Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions content/homepage/timelines/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,31 @@ framerate: 24
---
# OpenTimelineIO

## Bridging Editorial Workflows Across Platforms
**An open-source API and interchange format for managing timeline data in media production pipelines.**
## Bridging Editorial Workflows
**The open-source interchange format for editorial timeline data in film, animation, and VFX.**

<!-- -->

### What is OpenTimelineIO?
OpenTimelineIO (OTIO) is an open-source tool built for managing and exchanging editorial timeline data across a variety of editing tools and media formats, enabling seamless data interchange in film, animation, and VFX production.
### About OpenTimelineIO
A format and API for exchanging editorial timeline data between tools enabling seamless workflows across editing, compositing, and review applications.

[Intro Video](https://www.youtube.com/watch?v=nb6MELswKKk)

<!-- -->

## Key Features
- **Interchange Format**: Effortlessly exchange timeline data across platforms.
- **Python & C++ Integrations**: Robust support for developers.
- **Adaptable**: Custom adapters for flexible workflows.

<!-- -->

## OTIO Suporting Apps & Integrations
OTIO integrates with popular editing and production tools, offering a streamlined data exchange solution in modern pipelines.
<div data-component="feature-cards"></div>

<!-- -->

## Community and Contribution
Join a collaborative community of developers and media professionals! Contribute new adapters, offer support, and help shape the future of OTIO.
## Apps & Integrations

<!-- -->
<div data-component="integration-grid"></div>

## Resources & Documentation
Access the full [documentation](#), API references, and community forums to unlock OTIO’s full potential.
<!-- -->

---
## Get Involved
Join developers and media professionals shaping the future of editorial interchange.

<div data-component="community-cta"></div>
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,6 @@
},
"resolutions": {
"string-width": "4.2.3"
}
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
8 changes: 3 additions & 5 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,13 @@
--track-height: 38px;
--track-header-width: 120px;

/* Track label colors - light mode matching clip colors (blue-250, teal-250, violet-250, rose-250, slate-250) */
/* Track label colors - light mode matching clip colors */
--track-label-h1: oklch(0.75 0.12 250); /* Blue - matches blue-200/300 gradient */
--track-label-h2: oklch(0.75 0.10 180); /* Teal - matches teal-200/300 gradient */
--track-label-h3: oklch(0.75 0.12 300); /* Violet - matches violet-200/300 gradient */
--track-label-img: oklch(0.75 0.10 15); /* Rose - matches rose-200/300 gradient */
--track-label-media: oklch(0.75 0.10 15); /* Rose - consolidated media track */
--track-label-p: oklch(0.70 0.02 250); /* Slate - matches slate-200/300 gradient */
--track-label-ul: oklch(0.75 0.12 85); /* Amber - matches amber-200/300 gradient */
--track-label-embed: oklch(0.75 0.15 25); /* Red - matches red-200/300 gradient */

/* Track header background - light mode */
--track-header-bg: oklch(0.85 0.01 240); /* Light gray-blue */
Expand Down Expand Up @@ -235,10 +234,9 @@
--track-label-h1: oklch(0.50 0.15 250); /* Blue - matches dark mode blue-600/40-20 */
--track-label-h2: oklch(0.50 0.12 180); /* Teal - matches dark mode teal-600/40-20 */
--track-label-h3: oklch(0.50 0.15 300); /* Violet - matches dark mode violet-600/40-20 */
--track-label-img: oklch(0.50 0.12 15); /* Rose - matches dark mode rose-600/40-20 */
--track-label-media: oklch(0.50 0.12 15); /* Rose - consolidated media track */
--track-label-p: oklch(0.45 0.08 250); /* Slate - matches dark mode slate-600/40-20 */
--track-label-ul: oklch(0.50 0.15 85); /* Amber - matches dark mode amber-600/40-20 */
--track-label-embed: oklch(0.50 0.18 25); /* Red - matches dark mode red-600/40-20 */

/* Track header background - dark mode */
--track-header-bg: oklch(0.18 0.02 240); /* Darker gray-blue */
Expand Down
4 changes: 3 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { EditorialInterfaceComponent } from "@/components/nle/index";
import { NavWidthSetter } from "@/components/layout/nav-width-setter";
import { getSiteUrl } from "@/lib/site-config";
import { getIntegrations } from "@/lib/integrations";
import { promises as fs } from "fs";
import path from "path";

Expand Down Expand Up @@ -30,10 +31,11 @@ export const metadata: Metadata = {

export default async function NonLinearEditor() {
const markdown = await getMarkdownContent();
const integrations = getIntegrations().filter((i) => i.logo);

return (
<NavWidthSetter width="full">
<EditorialInterfaceComponent markdown={markdown} />
<EditorialInterfaceComponent markdown={markdown} integrations={integrations} />
</NavWidthSetter>
);
}
35 changes: 35 additions & 0 deletions src/components/homepage/community-cta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Github, ExternalLink, Users } from "lucide-react";
import { Button } from "@/components/ui/button";

const links = [
{
icon: Github,
label: "GitHub",
href: "https://github.com/AcademySoftwareFoundation/OpenTimelineIO",
},
{
icon: ExternalLink,
label: "ASWF Project",
href: "https://www.aswf.io/projects/opentimelineio/",
},
{
icon: Users,
label: "Contributing",
href: "https://github.com/AcademySoftwareFoundation/OpenTimelineIO/blob/main/CONTRIBUTING.md",
},
];

export function CommunityCta() {
return (
<div className="flex flex-wrap gap-3 my-6">
{links.map((link) => (
<Button key={link.label} variant="outline" asChild>
<a href={link.href} target="_blank" rel="noopener noreferrer">
<link.icon className="h-4 w-4" />
{link.label}
</a>
</Button>
))}
</div>
);
}
43 changes: 43 additions & 0 deletions src/components/homepage/feature-cards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ArrowLeftRight, Code, Puzzle } from "lucide-react";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";

const features = [
{
icon: ArrowLeftRight,
title: "Interchange Format",
description:
"Effortlessly exchange timeline data across editing, compositing, and review platforms.",
},
{
icon: Code,
title: "Developer APIs",
description:
"Robust Python and C++ APIs for building pipeline tools and custom integrations.",
},
{
icon: Puzzle,
title: "Extensible Adapters",
description:
"Write custom adapters for any editorial format — flexible, open, and community-driven.",
},
];

export function FeatureCards() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 my-6">
{features.map((feature) => (
<Card key={feature.title} className="flex flex-col">
<CardHeader className="pb-2">
<feature.icon className="h-8 w-8 mb-2 text-primary" />
<CardTitle className="text-lg">{feature.title}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-sm">
{feature.description}
</CardDescription>
</CardContent>
</Card>
))}
</div>
);
}
42 changes: 42 additions & 0 deletions src/components/homepage/integration-grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Image from "next/image";
import Link from "next/link";
import { Integration } from "@/types/integrations";

interface IntegrationGridProps {
integrations: Integration[];
}

export function IntegrationGrid({ integrations }: IntegrationGridProps) {
return (
<div className="my-6">
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-6">
{integrations.map((integration) => (
<div
key={integration.id ?? integration.name}
className="flex flex-col items-center gap-2"
>
<div className="relative w-12 h-12">
<Image
src={integration.logo}
alt={integration.name}
fill
className="object-contain"
/>
</div>
<span className="text-xs text-center text-muted-foreground leading-tight">
{integration.name}
</span>
</div>
))}
</div>
<div className="mt-6">
<Link
href="/apps-and-tools"
className="text-primary text-sm font-medium hover:underline"
>
View All Apps & Tools &rarr;
</Link>
</div>
</div>
);
}
20 changes: 20 additions & 0 deletions src/components/nle/content-renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { useState, useMemo, memo } from "react";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Card } from "@/components/ui/card";
import { Section } from "@/components/nle/utils/markdown-parser";
import { Integration } from "@/types/integrations";
import { FeatureCards } from "@/components/homepage/feature-cards";
import { IntegrationGrid } from "@/components/homepage/integration-grid";
import { CommunityCta } from "@/components/homepage/community-cta";

interface ContentRendererProps {
markdown: string;
onAstUpdate?: (ast: any) => void;
sections?: Section[];
currentTimeMs?: number;
syncWithPlayhead?: boolean;
integrations?: Integration[];
}

// Memoize ContentRenderer to prevent unnecessary re-renders
Expand All @@ -20,6 +26,7 @@ export const ContentRenderer = memo(function ContentRenderer({
sections = [],
currentTimeMs = 0,
syncWithPlayhead = false,
integrations = [],
}: ContentRendererProps) {
const [ast, setAst] = useState<any>(null);

Expand Down Expand Up @@ -178,6 +185,11 @@ export const ContentRenderer = memo(function ContentRenderer({
))}
</ul>
);
} else if (element.type === "widget" && element.widgetName) {
if (element.widgetName === "feature-cards") return <FeatureCards key={elemIndex} />;
if (element.widgetName === "integration-grid") return <IntegrationGrid key={elemIndex} integrations={integrations} />;
if (element.widgetName === "community-cta") return <CommunityCta key={elemIndex} />;
return null;
} else if (element.type.startsWith("h")) {
const level = parseInt(element.type[1]);
const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements;
Expand Down Expand Up @@ -274,6 +286,13 @@ export const ContentRenderer = memo(function ContentRenderer({
// Regular link - YouTube links are handled at the paragraph level
return <a href={href} className="text-primary underline hover:no-underline" {...props}>{children}</a>;
},
div: ({ node, ...props }: any) => {
const component = props["data-component"];
if (component === "feature-cards") return <FeatureCards />;
if (component === "integration-grid") return <IntegrationGrid integrations={integrations} />;
if (component === "community-cta") return <CommunityCta />;
return <div {...props} />;
},
code: ({ className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || "");
return match ? (
Expand All @@ -295,6 +314,7 @@ export const ContentRenderer = memo(function ContentRenderer({
},
}}
remarkPlugins={[]}
rehypePlugins={[rehypeRaw]}
>
{cleanMarkdown}
</ReactMarkdown>
Expand Down
25 changes: 8 additions & 17 deletions src/components/nle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import { Sequence } from "@/components/nle/sequence";
import { SequenceSelector } from "@/components/nle/sequence-selector";
import { parseMarkdownToClips } from "@/components/nle/utils/markdown-parser";
import { msToFrames, percentageToMs } from "@/lib/time-utils";
import { Integration } from "@/types/integrations";
import "@/styles/nle.css";

const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => {
const EditorialInterfaceComponent = ({ markdown, integrations = [] }: { markdown: string; integrations?: Integration[] }) => {
const [scrollPercentage, setScrollPercentage] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const [playheadPosition, setPlayheadPosition] = useState(0);
Expand Down Expand Up @@ -226,9 +227,9 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => {
setTimelineWidth(width);
setContainerWidth(viewportWidth);

// Calculate playhead height: ticks (32px) + tracks (7 × 32px = 224px) = 256px
// Calculate playhead height: ticks (32px) + tracks (6 × 32px = 192px) = 224px
const ticksHeight = 32;
const tracksHeight = 7 * 32;
const tracksHeight = 6 * 32;
const totalHeight = ticksHeight + tracksHeight;
setTimelineHeight(totalHeight);
}
Expand Down Expand Up @@ -612,11 +613,12 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => {
}}
/>
</div> */}
<ContentRenderer
markdown={markdown}
<ContentRenderer
markdown={markdown}
sections={sections}
currentTimeMs={percentageToMs(scrollPercentage, totalDuration)}
syncWithPlayhead={false}
integrations={integrations}
/>
</div>
<KeyboardShortcutDisplay
Expand Down Expand Up @@ -827,7 +829,7 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => {
</div>
</div>
<div className="track-header">
<div className="track-label" data-track="img">{"<img>"}</div>
<div className="track-label" data-track="media">{"media"}</div>
<div className="track-controls">
<button>
<Monitor size={16} />
Expand Down Expand Up @@ -859,17 +861,6 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => {
</button>
</div>
</div>
<div className="track-header">
<div className="track-label" data-track="embed">{"<vid>"}</div>
<div className="track-controls">
<button>
<Monitor size={16} />
</button>
<button>
<Eye size={16} />
</button>
</div>
</div>
</div>

{/* Scrollable timeline area */}
Expand Down
7 changes: 4 additions & 3 deletions src/components/nle/sequence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ import { msToPercentage } from "@/lib/time-utils";
import { useMemo, memo } from "react";
import "@/styles/nle.css";

// Track configuration: h1=0, h2=1, h3=2, img=3, p=4, embed=5, ul=6
// Track configuration: h1=0, h2=1, h3=2, media=3, p=4, ul=5
const TRACK_CONFIG = [
{ label: "<h1>", name: "Header 1", type: "h1" as const },
{ label: "<h2>", name: "Header 2", type: "h2" as const },
{ label: "<h3>", name: "Header 3", type: "h3" as const },
{ label: "<img>", name: "Image", type: "img" as const },
{ label: "media", name: "Media", type: "media" as const },
{ label: "<p>", name: "Paragraph", type: "p" as const },
{ label: "<ul>", name: "List", type: "ul" as const },
{ label: "<embed>", name: "Embed", type: "embed" as const },
];

interface ClipRendererProps {
Expand Down Expand Up @@ -49,6 +48,8 @@ const ClipRenderer = memo(({ item, totalDurationMs, timelineWidth }: ClipRendere
return "bg-gradient-to-b from-red-200/90 to-red-300/90 border-red-400/80 text-red-900 dark:from-red-600/40 dark:to-red-600/20 dark:border-red-400/60 dark:text-red-50";
case "ul":
return "bg-gradient-to-b from-amber-200/90 to-amber-300/90 border-amber-400/80 text-amber-900 dark:from-amber-600/40 dark:to-amber-600/20 dark:border-amber-400/60 dark:text-amber-50";
case "widget":
return "bg-gradient-to-b from-emerald-200/90 to-emerald-300/90 border-emerald-400/80 text-emerald-900 dark:from-emerald-600/40 dark:to-emerald-600/20 dark:border-emerald-400/60 dark:text-emerald-50";
default:
return "bg-gradient-to-b from-gray-200/90 to-gray-300/90 border-gray-400/80 text-gray-900 dark:from-gray-600/40 dark:to-gray-600/20 dark:border-gray-400/60 dark:text-gray-50";
}
Expand Down
Loading