diff --git a/content/homepage/timelines/introduction.md b/content/homepage/timelines/introduction.md index 30018ab..c2bbcc8 100644 --- a/content/homepage/timelines/introduction.md +++ b/content/homepage/timelines/introduction.md @@ -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. +
-## 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 - +
-## 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. +
diff --git a/package.json b/package.json index 0fdddee..5b8eb6d 100644 --- a/package.json +++ b/package.json @@ -84,5 +84,6 @@ }, "resolutions": { "string-width": "4.2.3" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/app/globals.css b/src/app/globals.css index 6a28b90..49efba8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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 */ @@ -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 */ diff --git a/src/app/page.tsx b/src/app/page.tsx index e2e1187..fd0b0d8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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"; @@ -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 ( - + ); } diff --git a/src/components/homepage/community-cta.tsx b/src/components/homepage/community-cta.tsx new file mode 100644 index 0000000..ad07bdd --- /dev/null +++ b/src/components/homepage/community-cta.tsx @@ -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 ( +
+ {links.map((link) => ( + + ))} +
+ ); +} diff --git a/src/components/homepage/feature-cards.tsx b/src/components/homepage/feature-cards.tsx new file mode 100644 index 0000000..97711ab --- /dev/null +++ b/src/components/homepage/feature-cards.tsx @@ -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 ( +
+ {features.map((feature) => ( + + + + {feature.title} + + + + {feature.description} + + + + ))} +
+ ); +} diff --git a/src/components/homepage/integration-grid.tsx b/src/components/homepage/integration-grid.tsx new file mode 100644 index 0000000..8e1497e --- /dev/null +++ b/src/components/homepage/integration-grid.tsx @@ -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 ( +
+
+ {integrations.map((integration) => ( +
+
+ {integration.name} +
+ + {integration.name} + +
+ ))} +
+
+ + View All Apps & Tools → + +
+
+ ); +} diff --git a/src/components/nle/content-renderer.tsx b/src/components/nle/content-renderer.tsx index 082ce17..9ba26ff 100644 --- a/src/components/nle/content-renderer.tsx +++ b/src/components/nle/content-renderer.tsx @@ -1,9 +1,14 @@ 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; @@ -11,6 +16,7 @@ interface ContentRendererProps { sections?: Section[]; currentTimeMs?: number; syncWithPlayhead?: boolean; + integrations?: Integration[]; } // Memoize ContentRenderer to prevent unnecessary re-renders @@ -20,6 +26,7 @@ export const ContentRenderer = memo(function ContentRenderer({ sections = [], currentTimeMs = 0, syncWithPlayhead = false, + integrations = [], }: ContentRendererProps) { const [ast, setAst] = useState(null); @@ -178,6 +185,11 @@ export const ContentRenderer = memo(function ContentRenderer({ ))} ); + } else if (element.type === "widget" && element.widgetName) { + if (element.widgetName === "feature-cards") return ; + if (element.widgetName === "integration-grid") return ; + if (element.widgetName === "community-cta") return ; + return null; } else if (element.type.startsWith("h")) { const level = parseInt(element.type[1]); const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements; @@ -274,6 +286,13 @@ export const ContentRenderer = memo(function ContentRenderer({ // Regular link - YouTube links are handled at the paragraph level return {children}; }, + div: ({ node, ...props }: any) => { + const component = props["data-component"]; + if (component === "feature-cards") return ; + if (component === "integration-grid") return ; + if (component === "community-cta") return ; + return
; + }, code: ({ className, children, ...props }) => { const match = /language-(\w+)/.exec(className || ""); return match ? ( @@ -295,6 +314,7 @@ export const ContentRenderer = memo(function ContentRenderer({ }, }} remarkPlugins={[]} + rehypePlugins={[rehypeRaw]} > {cleanMarkdown} diff --git a/src/components/nle/index.tsx b/src/components/nle/index.tsx index 6f2dc1d..712f42d 100644 --- a/src/components/nle/index.tsx +++ b/src/components/nle/index.tsx @@ -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); @@ -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); } @@ -612,11 +613,12 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { }} />
*/} - {
-
{""}
+
{"media"}
-
-
{""}
-
- - -
-
{/* Scrollable timeline area */} diff --git a/src/components/nle/sequence.tsx b/src/components/nle/sequence.tsx index 8537be5..42cccda 100644 --- a/src/components/nle/sequence.tsx +++ b/src/components/nle/sequence.tsx @@ -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: "

", name: "Header 1", type: "h1" as const }, { label: "

", name: "Header 2", type: "h2" as const }, { label: "

", name: "Header 3", type: "h3" as const }, - { label: "", name: "Image", type: "img" as const }, + { label: "media", name: "Media", type: "media" as const }, { label: "

", name: "Paragraph", type: "p" as const }, { label: "

    ", name: "List", type: "ul" as const }, - { label: "", name: "Embed", type: "embed" as const }, ]; interface ClipRendererProps { @@ -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"; } diff --git a/src/components/nle/utils/markdown-parser.ts b/src/components/nle/utils/markdown-parser.ts index 36c4801..b53df43 100644 --- a/src/components/nle/utils/markdown-parser.ts +++ b/src/components/nle/utils/markdown-parser.ts @@ -6,7 +6,7 @@ import { Root, Content, Heading, Paragraph, Image, Text, List } from "mdast"; * Element extracted from markdown AST */ export interface ParsedElement { - type: "h1" | "h2" | "h3" | "p" | "img" | "embed" | "ul"; + type: "h1" | "h2" | "h3" | "p" | "img" | "embed" | "ul" | "widget"; content: string; node: Content; imageUrl?: string; @@ -14,6 +14,7 @@ export interface ParsedElement { embedUrl?: string; embedType?: "youtube"; listItems?: string[]; + widgetName?: string; } /** @@ -33,8 +34,8 @@ export interface TrackItem { id: string; content: string; name: string; - track: number; // 0 = h1, 1 = h2, 2 = h3, 3 = img, 4 = p, 5 = embed, 6 = ul - type: "h1" | "h2" | "h3" | "img" | "p" | "embed" | "ul"; + track: number; // 0 = h1, 1 = h2, 2 = h3, 3 = media (img/embed/widget), 4 = p, 5 = ul + type: "h1" | "h2" | "h3" | "img" | "p" | "embed" | "ul" | "widget"; start: number; // milliseconds end: number; // milliseconds node?: Content; // AST node reference @@ -43,17 +44,19 @@ export interface TrackItem { embedUrl?: string; embedType?: "youtube"; listItems?: string[]; + widgetName?: string; } -// Track mapping: h1=0, h2=1, h3=2, img=3, p=4, embed=5, ul=6 +// Track mapping: h1=0, h2=1, h3=2, media=3 (img/embed/widget), p=4, ul=5 const TRACK_MAP: Record = { h1: 0, h2: 1, h3: 2, img: 3, + embed: 3, + widget: 3, p: 4, ul: 5, - embed: 6, }; /** @@ -142,6 +145,21 @@ function extractYouTubeEmbed(node: Content): ParsedElement | null { * Parse markdown AST node to ParsedElement */ function parseNode(node: Content): ParsedElement | null { + // Detect HTML nodes with data-component attribute (widget markers) + if (node.type === "html") { + const htmlNode = node as any; + const value = (htmlNode.value || "") as string; + const match = value.match(/data-component="([^"]+)"/); + if (match) { + return { + type: "widget", + content: match[1], + node, + widgetName: match[1], + }; + } + } + if (node.type === "heading") { const heading = node as Heading; const level = heading.depth; @@ -231,8 +249,10 @@ function calculateSectionDuration(section: Section): number { const imageTime = section.elements.filter((e) => e.type === "img").length * 3000; // Embeds add extra time (10 seconds per embed for video preview) const embedTime = section.elements.filter((e) => e.type === "embed").length * 10000; + // Widgets add 5 seconds each + const widgetTime = section.elements.filter((e) => e.type === "widget").length * 5000; - return Math.max(minDuration, readingTime + imageTime + embedTime); + return Math.max(minDuration, readingTime + imageTime + embedTime + widgetTime); } /** @@ -408,6 +428,8 @@ export function generateClipsFromSections(sections: Section[]): TrackItem[] { ? element.alt || "Image" : element.type === "embed" ? element.content || "Embed" + : element.type === "widget" + ? element.widgetName || "Widget" : element.type === "ul" ? `List (${element.listItems?.length || 0} items)` : element.content.substring(0, 30) + (element.content.length > 30 ? "..." : ""), @@ -421,6 +443,7 @@ export function generateClipsFromSections(sections: Section[]): TrackItem[] { embedUrl: element.embedUrl, embedType: element.embedType, listItems: element.listItems, + widgetName: element.widgetName, }); } } diff --git a/src/styles/nle.css b/src/styles/nle.css index 7704af1..29fee3f 100644 --- a/src/styles/nle.css +++ b/src/styles/nle.css @@ -262,8 +262,8 @@ background: var(--track-label-h3); } -.track-label[data-track="img"] { - background: var(--track-label-img); +.track-label[data-track="media"] { + background: var(--track-label-media); } .track-label[data-track="p"] { @@ -272,9 +272,6 @@ .track-label[data-track="ul"] { background: var(--track-label-ul); } -.track-label[data-track="embed"] { - background: var(--track-label-embed); -} .track-controls { display: flex;