Files
OmniLottie/index.html
T

1009 lines
33 KiB
HTML
Raw Normal View History

2026-02-12 21:31:55 +08:00
<!DOCTYPE html>
2026-02-28 08:48:50 +00:00
<html lang="en" data-theme="dark">
2026-02-12 21:31:55 +08:00
<head>
2026-02-28 08:48:50 +00:00
<meta charset="utf-8">
<meta name="description" content="OmniLottie: Generating Vector Animations via Parameterized Lottie Tokens">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OmniLottie: Generating Vector Animations via Parameterized Lottie Tokens</title>
<link rel="icon" href="./static/images/lottie_icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
<style>
:root {
--bg: #070913;
--bg-soft: #101526;
--panel: rgba(21, 28, 52, 0.7);
--panel-strong: rgba(14, 18, 34, 0.9);
--text: #eef3ff;
--sub: #a8b6da;
--line: rgba(148, 170, 255, 0.22);
--primary: #9f7bff;
--secondary: #37d5ff;
--pink: #ff7ad9;
--gold: #ffd37f;
--shadow: 0 28px 52px rgba(0, 0, 0, 0.42);
--chip: rgba(155, 129, 255, 0.18);
--btn-text: #0d1329;
--button-bg: linear-gradient(135deg, #b194ff, #62e6ff);
--card-bg: linear-gradient(140deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02));
--edge-mask: rgba(7, 9, 19, 1);
}
html[data-theme="light"] {
--bg: #edf2ff;
--bg-soft: #ffffff;
--panel: rgba(255, 255, 255, 0.72);
--panel-strong: rgba(255, 255, 255, 0.88);
--text: #1a2444;
--sub: #5a668b;
--line: rgba(111, 131, 223, 0.24);
--primary: #7d55ff;
--secondary: #00c9eb;
--pink: #ff5fc8;
--gold: #ffb546;
--shadow: 0 24px 42px rgba(100, 120, 186, 0.22);
--chip: rgba(126, 86, 255, 0.12);
--btn-text: #ffffff;
--button-bg: linear-gradient(135deg, #9068ff, #00c8eb);
--card-bg: linear-gradient(140deg, rgba(255, 255, 255, 0.95), rgba(239, 246, 255, 0.68));
--edge-mask: rgba(237, 242, 255, 1);
}
* { box-sizing: border-box; }
html, body { width: 100%; }
body {
margin: 0;
font-family: Inter, system-ui, -apple-system, Segoe UI, sans-serif;
color: var(--text);
min-height: 100vh;
background:
radial-gradient(900px 700px at 8% -10%, rgba(192, 120, 255, 0.32), transparent 55%),
radial-gradient(1000px 740px at 95% -12%, rgba(61, 216, 255, 0.28), transparent 58%),
radial-gradient(860px 620px at 55% 115%, rgba(255, 124, 205, 0.22), transparent 65%),
linear-gradient(160deg, var(--bg), var(--bg-soft));
transition: background 0.35s ease, color 0.25s ease;
overflow-x: hidden;
}
.bg-grid {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
background-image:
linear-gradient(rgba(130, 150, 255, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(130, 150, 255, 0.08) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.88), transparent 78%);
opacity: 0.5;
}
.aurora {
position: fixed;
border-radius: 999px;
filter: blur(44px);
pointer-events: none;
z-index: 0;
opacity: 0.44;
animation: drift 11s ease-in-out infinite;
}
.aurora.a1 {
width: 360px;
height: 360px;
left: -90px;
top: 40px;
background: rgba(175, 134, 255, 0.9);
}
.aurora.a2 {
width: 300px;
height: 300px;
right: -60px;
top: 180px;
background: rgba(81, 218, 255, 0.86);
animation-delay: -4s;
}
.aurora.a3 {
width: 340px;
height: 340px;
left: 40%;
bottom: -110px;
background: rgba(255, 126, 203, 0.74);
animation-delay: -7s;
}
@keyframes drift {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(0, -22px, 0) scale(1.05); }
}
.shell {
position: relative;
z-index: 1;
width: min(1280px, calc(100% - 28px));
margin: 18px auto 28px;
display: grid;
gap: 16px;
}
.panel {
position: relative;
border-radius: 28px;
border: 1px solid var(--line);
background: var(--panel);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: var(--shadow);
padding: 24px;
overflow: hidden;
isolation: isolate;
}
.panel::before {
content: "";
position: absolute;
inset: -40% 50% auto -20%;
height: 80%;
transform: rotate(-8deg);
background: linear-gradient(120deg, rgba(159, 123, 255, 0.16), rgba(55, 213, 255, 0.08), transparent);
z-index: -1;
pointer-events: none;
}
.hero {
text-align: center;
padding-top: 18px;
padding-bottom: 26px;
background: linear-gradient(150deg, var(--panel), rgba(255, 255, 255, 0.02));
}
.hero-toolbar {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.tag {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
color: var(--sub);
padding: 7px 14px;
font-weight: 600;
font-size: 0.85rem;
}
.theme-toggle {
border: none;
border-radius: 999px;
background: var(--button-bg);
color: var(--btn-text);
2026-02-28 12:18:05 +00:00
padding: 8px 12px;
font-size: 1.1rem;
2026-02-28 08:48:50 +00:00
font-weight: 800;
cursor: pointer;
transition: transform 0.2s ease, filter 0.2s ease;
box-shadow: 0 8px 24px rgba(87, 175, 255, 0.24);
}
.theme-toggle:hover {
transform: translateY(-2px);
filter: saturate(1.1);
}
.hero-title {
margin: 6px auto 8px;
max-width: 940px;
font-size: clamp(1.42rem, 1.08rem + 1.74vw, 2.34rem);
line-height: 1.2;
letter-spacing: 0.01em;
text-wrap: balance;
background: linear-gradient(120deg, #ffffff 15%, #bdd6ff 38%, #a695ff 58%, #74e8ff 82%, #ffc0ea);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
html[data-theme="light"] .hero-title {
background: linear-gradient(120deg, #0f1b44 12%, #2f3f87 34%, #7b53ff 60%, #0086a8 84%);
-webkit-background-clip: text;
background-clip: text;
}
.hero-sub {
margin: 0 auto;
max-width: 760px;
color: var(--sub);
line-height: 1.7;
}
.authors, .meta-row, .links {
margin-top: 14px;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.author-chip {
display: inline-flex;
align-items: center;
text-decoration: none;
color: var(--text);
border: 1px solid var(--line);
background: var(--chip);
border-radius: 999px;
padding: 8px 12px;
font-size: 0.9rem;
transition: transform 0.2s ease, border-color 0.2s ease;
}
.author-chip:hover {
transform: translateY(-2px);
border-color: rgba(157, 188, 255, 0.48);
}
.meta-row {
color: var(--sub);
font-size: 0.93rem;
gap: 8px 14px;
}
.links { gap: 10px 12px; }
.link-pill {
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: var(--text);
font-weight: 700;
font-size: 0.93rem;
border: 1px solid var(--line);
border-radius: 12px;
padding: 9px 13px;
background: linear-gradient(140deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.02));
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.link-pill:hover {
transform: translateY(-2px);
border-color: rgba(157, 188, 255, 0.48);
box-shadow: 0 10px 22px rgba(93, 98, 206, 0.24);
}
.section-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.section-head h2 {
margin: 0;
font-size: clamp(1.06rem, 0.93rem + 0.96vw, 1.6rem);
letter-spacing: 0.01em;
}
.section-head p {
margin: 0;
color: var(--sub);
font-size: 0.93rem;
}
.demo-stack {
display: grid;
gap: 14px;
}
.video-shell {
border-radius: 20px;
border: 1px solid var(--line);
background: var(--panel-strong);
padding: 12px;
}
.video-chip {
display: inline-flex;
margin-bottom: 10px;
border-radius: 999px;
border: 1px solid rgba(164, 151, 255, 0.34);
background: rgba(167, 139, 250, 0.12);
color: var(--sub);
padding: 5px 10px;
font-size: 0.79rem;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.video-frame {
border-radius: 14px;
overflow: hidden;
border: 1px solid rgba(143, 167, 255, 0.2);
background: #02040d;
aspect-ratio: 16 / 9;
min-height: 230px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}
.video-frame video {
width: 100%;
height: 100%;
display: block;
object-fit: contain;
}
.overlay-player {
position: relative;
border-radius: 14px;
overflow: hidden;
border: 1px solid rgba(143, 167, 255, 0.2);
background: #02040d;
aspect-ratio: 16 / 9;
min-height: 230px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}
.overlay-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
opacity: 0;
transition: opacity 0.8s ease;
}
.overlay-video.active { opacity: 1; }
.overlay-controls {
margin-top: 11px;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
}
.overlay-btn {
border-radius: 999px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.09);
color: var(--text);
padding: 8px 12px;
font-size: 0.86rem;
font-weight: 700;
cursor: pointer;
transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.overlay-btn.active {
border-color: rgba(144, 200, 255, 0.62);
background: linear-gradient(135deg, rgba(159, 123, 255, 0.3), rgba(55, 213, 255, 0.3));
}
.overlay-btn:hover { transform: translateY(-1px); }
.status {
color: var(--sub);
font-size: 0.92rem;
margin-bottom: 10px;
}
.lottie-rows {
display: grid;
gap: 10px;
mask-image: linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%);
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%);
}
.lottie-lane {
overflow: hidden;
border-radius: 16px;
border: 1px solid var(--line);
background: var(--card-bg);
padding: 6px 0;
}
.lane-track {
display: flex;
gap: 10px;
align-items: stretch;
will-change: transform;
padding: 2px 8px;
}
.lottie-card {
width: clamp(120px, 12.2vw, 178px);
border-radius: 14px;
border: 1px solid rgba(141, 163, 255, 0.24);
background: linear-gradient(140deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.03));
flex: 0 0 auto;
padding: 8px;
display: grid;
gap: 6px;
transition: transform 0.2s ease, border-color 0.2s ease;
}
.lottie-card:hover {
transform: translateY(-2px) scale(1.01);
border-color: rgba(157, 188, 255, 0.6);
}
.lottie-canvas {
height: clamp(70px, 7.8vw, 118px);
border-radius: 10px;
overflow: hidden;
background: rgba(8, 11, 22, 0.65);
border: 1px solid rgba(141, 163, 255, 0.2);
}
html[data-theme="light"] .lottie-canvas {
background: rgba(245, 249, 255, 0.9);
}
.lottie-name {
color: var(--sub);
font-size: 0.72rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tech-grid {
display: grid;
gap: 12px;
grid-template-columns: 1fr;
}
.tech-card {
border-radius: 18px;
border: 1px solid var(--line);
background: var(--card-bg);
padding: 14px;
}
.tech-card h3 {
margin: 0;
font-size: 1rem;
}
.tech-card p {
margin: 8px 0 10px;
color: var(--sub);
line-height: 1.7;
font-size: 0.93rem;
}
.tech-card img {
width: 100%;
border-radius: 12px;
border: 1px solid rgba(141, 163, 255, 0.18);
display: block;
background: rgba(0, 0, 0, 0.2);
}
.citation-box {
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(4, 9, 24, 0.76);
padding: 12px;
overflow: auto;
}
html[data-theme="light"] .citation-box {
background: rgba(250, 252, 255, 0.9);
}
.citation-box pre {
margin: 0;
color: var(--sub);
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.87rem;
}
.citation-actions {
margin-top: 10px;
display: flex;
justify-content: flex-end;
}
.copy-btn {
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
color: var(--text);
font-weight: 700;
padding: 7px 12px;
cursor: pointer;
}
.footer {
text-align: center;
color: var(--sub);
font-size: 0.86rem;
padding-bottom: 3px;
}
@media (max-width: 980px) {
.panel { padding: 18px; border-radius: 22px; }
.video-frame, .overlay-player { min-height: 190px; }
.tech-grid { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.shell { width: calc(100% - 16px); }
.hero-title { font-size: clamp(1.25rem, 1.05rem + 1.75vw, 1.84rem); }
.video-frame, .overlay-player { min-height: 150px; }
.lottie-card { width: clamp(116px, 35vw, 150px); }
.lottie-canvas { height: clamp(66px, 20vw, 95px); }
}
</style>
2026-02-12 21:31:55 +08:00
</head>
<body>
2026-02-28 08:48:50 +00:00
<div class="bg-grid"></div>
<div class="aurora a1"></div>
<div class="aurora a2"></div>
<div class="aurora a3"></div>
<main class="shell">
<section class="panel hero">
<div class="hero-toolbar">
<span class="tag" style="font-size:1.20em; display:inline-flex; align-items:center; gap:0.28em;">
<span style="font-weight:800;">CVPR 2026</span>
</span>
2026-02-28 12:18:05 +00:00
<button id="themeToggle" class="theme-toggle" type="button" style="margin-left:auto;" aria-label="Bright Mode"><i class="fa-solid fa-sun"></i></button>
2026-02-28 08:48:50 +00:00
</div>
<h1 class="hero-title">OmniLottie: Generating Vector Animations via Parameterized Lottie Tokens</h1>
<p class="hero-sub">
An open-source work by OpenVGLab
<a href="https://github.com/OpenVGLab" target="_blank" rel="noreferrer"
style="display:inline-block; margin-left:0.75em; vertical-align:middle;">
<img src="https://img.shields.io/github/followers/OpenVGLab?label=Follow%20%40OpenVGLab&style=social" alt="Follow @OpenVGLab on GitHub" style="height:1.2em;vertical-align:-0.18em;">
</a>
</p>
<div class="authors">
<a class="author-chip" href="https://github.com/YiyingYang12/YiyingYang12" target="_blank" rel="noreferrer">Yiying Yang<sup>1,2</sup></a>
<a class="author-chip" href="https://wchengad.github.io" target="_blank" rel="noreferrer">Wei Cheng<sup>2&dagger;</sup></a>
<a class="author-chip" href="https://ch3cook-fdu.github.io/" target="_blank" rel="noreferrer">Sijin Chen<sup>3</sup></a>
<a class="author-chip" href="https://romgai.github.io/" target="_blank" rel="noreferrer">Honghao Fu<sup>2,4</sup></a>
<a class="author-chip" href="https://scholar.google.com/citations?user=tgDc0fsAAAAJ&hl=en" target="_blank" rel="noreferrer">XianFang Zeng<sup>2</sup></a>
<a class="author-chip" href="https://vanoracai.github.io/" target="_blank" rel="noreferrer">Yujun Cai<sup>4</sup></a>
<a class="author-chip" href="https://www.skicyyu.org/" target="_blank" rel="noreferrer">Gang Yu<sup>2&ddagger;</sup></a>
<a class="author-chip" href="http://xingjunma.com/" target="_blank" rel="noreferrer">Xinjun Ma<sup>1&ddagger;</sup></a>
</div>
<div class="meta-row">
<span><sup>1</sup>Fudan University</span>
<span><sup>2</sup>StepFun</span>
<span><sup>3</sup>HKU MMLab</span>
<span><sup>4</sup>University of Queensland</span>
</div>
<div class="meta-row">
<span><sup>&dagger;</sup>Project Lead</span>
<span><sup>&ddagger;</sup>Correspondence Authors</span>
</div>
<div class="links">
2026-02-28 12:27:29 +00:00
<a class="link-pill" href="https://arxiv.org" target="_blank" rel="noreferrer">
<img src="https://cdn.jsdelivr.net/npm/simple-icons@v11/icons/arxiv.svg" alt="arXiv" style="height:1.05em;vertical-align:-0.18em;margin-right:-0.09em;">arXiv
</a>
2026-02-28 08:48:50 +00:00
<a class="link-pill" href="https://github.com/OpenVGLab/OmniLottie" target="_blank" rel="noreferrer"><i class="fa-brands fa-github"></i>Code</a>
<a class="link-pill" href="https://huggingface.co/OmniLottie/OmniLottie" target="_blank" rel="noreferrer">
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="HuggingFace" style="height:1.05em;vertical-align:-0.10em;margin-right:-0.09em;">Model
</a>
<a class="link-pill" href="https://huggingface.co/datasets/OmniLottie/MMLottie-2M" target="_blank" rel="noreferrer">
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="HuggingFace" style="height:1.05em;vertical-align:-0.10em;margin-right:-0.09em;">MMLottie-2M
</a>
<a class="link-pill" href="https://huggingface.co/datasets/OmniLottie/MMLottieBench" target="_blank" rel="noreferrer">
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="HuggingFace" style="height:1.05em;vertical-align:-0.10em;margin-right:-0.09em;">MMLottie-Bench
</a>
<a class="link-pill" href="https://huggingface.co/datasets/OmniLottie/MMLottie-2M" target="_blank" rel="noreferrer">
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="HuggingFace" style="height:1.05em;vertical-align:-0.10em;margin-right:-0.09em;">Demo
</a>
</div>
2026-02-12 21:31:55 +08:00
</section>
2026-02-28 08:48:50 +00:00
<section class="panel">
<div class="section-head">
<h2>Lottie Parametrization and Generation Process</h2>
<!-- <p>上方主 Demo + 下方 3 个叠加切换 Demo</p> -->
</div>
<div class="demo-stack">
<article class="video-shell">
<!-- <div class="video-chip">Lottie Parametrization and Generation Process</div> -->
<div class="video-frame">
2026-02-28 12:16:19 +00:00
<video src="./assets/OmniLottie-main-demo.mp4" autoplay muted loop playsinline></video>
2026-02-28 08:48:50 +00:00
</div>
</article>
<article class="video-shell">
<!-- <div class="video-chip">Overlay Demos</div> -->
<div class="overlay-player" id="overlayPlayer">
<video class="overlay-video active" src="./assets/OmniLottie-demo-1.mp4" autoplay muted loop playsinline></video>
<video class="overlay-video" src="./assets/OmniLottie-demo-2.mp4" autoplay muted loop playsinline></video>
<video class="overlay-video" src="./assets/OmniLottie-demo-3.mp4" autoplay muted loop playsinline></video>
</div>
<div class="overlay-controls" id="overlayControls">
<button class="overlay-btn active" type="button" data-index="0">Demo 1</button>
<button class="overlay-btn" type="button" data-index="1">Demo 2</button>
<button class="overlay-btn" type="button" data-index="2">Demo 3</button>
</div>
</article>
</div>
2026-02-12 21:31:55 +08:00
</section>
2026-02-28 08:48:50 +00:00
<section class="panel">
<div class="section-head">
<h2>Generated Lottie Gallery</h2>
</div>
<div id="lottieStatus" class="status">Loading...</div>
<div id="lottieRows" class="lottie-rows"></div>
2026-02-12 21:31:55 +08:00
</section>
2026-02-28 08:48:50 +00:00
<section class="panel">
<div class="section-head">
<h2>Introducing OmniLottie</h2>
<!-- <p>DatasetImage #3)与技术路径(Image #2</p> -->
</div>
<div class="tech-grid">
<article class="tech-card">
<h3>MMLottie-2M Dataset</h3>
<!-- <p>MMLottie-2M 汇集大规模专业 Lottie 动画及多模态标注,覆盖文本、关键帧和视频渲染,支持从语义到细节动作的学习。</p> -->
<p><b>Overview of the Vector Animation Data Construction Pipeline.</b>
<b>Data Collection & Conversion :</b> We convert SVG assets into static Lottie files and apply randomized animation effects to generate effect-tagged animated Lotties. In parallel, we gather professionally created Lottie animations from five online platforms and perform thorough filtering and cleaning. <b>Data Processing :</b> Each animation undergoes spatio-temporal normalization, followed by video rendering and random keyframe extraction. <b>Data Annotation :</b> Finally, we provide multi-granularity annotations emphasizing geometric structure, color attributes, and motion characteristics.</p>
<img src="./assets/OmniLottie-data-pipeline.png" alt="Dataset and data processing pipeline">
</article>
<article class="tech-card">
<h3>OmniLottie Model</h3>
<p><b>Overview of OmniLottie.</b>
<b>Lottie Structure :</b> We reorganize the Lottie JSON representation, with a particular focus on the structure of its layers, including both common layer attributes and five special layer types. <b>Lottie Parametrization :</b> The hierarchical JSON format of Lottie is flattened into a sequence of function calls, which are further parameterized to define a dedicated vocabulary and token set for Lottie. <b>OmniLottie :</b> Built upon this parameterization, OmniLottie extends Qwen2.5-VL with a new tokenizer and vocabulary for Lottie, and is trained on our curated Lottie dataset.</p>
<img src="./assets/OmniLottie-pipeline.png" alt="Technical pipeline">
</article>
</div>
2026-02-12 21:31:55 +08:00
</section>
2026-02-28 08:48:50 +00:00
<section class="panel">
<div class="section-head">
<h2>Citation</h2>
</div>
<div class="citation-box">
<pre id="citationText">@article{omnilottie2026,
title={OmniLottie: Generating Vector Animations via Parameterized Lottie Tokens},
author={Yang, Yiying and Cheng, Wei and Chen, Sijin and Fu, Honghao and Zeng, XianFang and Cai, Yujun and Yu, Gang and Ma, Xinjun},
journal={In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
year={2026}
}</pre>
</div>
<div class="citation-actions">
<button id="copyCitation" class="copy-btn" type="button">Copy Citation</button>
</div>
2026-02-12 21:31:55 +08:00
</section>
2026-02-28 12:19:47 +00:00
<div class="footer">
<a href="https://github.com/OpenVGLab/OmniLottie" target="_blank" rel="noopener" style="color: inherit; text-decoration: underline;">OpenVGLab/OmniLottie</a>
</div>
2026-02-28 08:48:50 +00:00
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.12.2/lottie.min.js"></script>
<script>
const THEME_KEY = 'omnilottie-theme';
const laneStates = [];
let overlayTimer = null;
let lanesLoopStarted = false;
function applyTheme(theme) {
const mode = theme === 'light' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', mode);
localStorage.setItem(THEME_KEY, mode);
const btn = document.getElementById('themeToggle');
if (btn) {
2026-02-28 12:18:05 +00:00
const icon = btn.querySelector('i');
if (icon) {
icon.className = mode === 'dark' ? 'fa-solid fa-sun' : 'fa-solid fa-moon';
}
btn.setAttribute('aria-label', mode === 'dark' ? 'Bright Mode' : 'Dark Mode');
2026-02-28 08:48:50 +00:00
}
}
function initTheme() {
const cached = localStorage.getItem(THEME_KEY);
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(cached || (prefersDark ? 'dark' : 'light'));
const btn = document.getElementById('themeToggle');
if (!btn) return;
btn.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
});
}
function initOverlayPlayer() {
const videos = [...document.querySelectorAll('#overlayPlayer .overlay-video')];
const buttons = [...document.querySelectorAll('#overlayControls .overlay-btn')];
if (!videos.length || !buttons.length) return;
const switchTo = (index, fromStart = false) => {
2026-02-28 08:48:50 +00:00
videos.forEach((video, i) => {
const active = i === index;
video.classList.toggle('active', active);
if (active) {
if (fromStart) video.currentTime = 0;
2026-02-28 08:48:50 +00:00
video.play().catch(() => {});
}
});
buttons.forEach((button, i) => button.classList.toggle('active', i === index));
};
let current = 0;
function restartTimer(startIndex) {
if (typeof startIndex === 'number') current = startIndex;
if (overlayTimer) clearInterval(overlayTimer);
overlayTimer = setInterval(() => {
current = (current + 1) % videos.length;
switchTo(current);
}, 10000);
2026-02-28 08:48:50 +00:00
}
buttons.forEach((button) => {
button.addEventListener('click', () => {
const idx = Number(button.dataset.index || 0);
switchTo(idx, true);
2026-02-28 08:48:50 +00:00
restartTimer(idx);
});
});
switchTo(0);
restartTimer(0);
}
function prettyName(file) {
return file
.replace('.json', '')
.replace(/_/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function normalizeJsonName(input) {
const noHash = String(input || '').split('#')[0];
const noQuery = noHash.split('?')[0];
const rawName = noQuery.split('/').pop() || '';
try {
return decodeURIComponent(rawName);
} catch (_) {
return rawName;
}
}
async function loadManifestFiles(manifestPath) {
try {
const response = await fetch(manifestPath, { cache: 'no-store' });
if (!response.ok) return [];
const data = await response.json();
if (!Array.isArray(data.files)) return [];
return data.files
.map(normalizeJsonName)
.filter((name) => name.toLowerCase().endsWith('.json') && name.toLowerCase() !== 'manifest.json');
} catch (_) {
return [];
}
}
async function scanDirectoryJsonFiles(baseDir) {
try {
const response = await fetch(`${baseDir}/`, { cache: 'no-store' });
if (!response.ok) return [];
const html = await response.text();
const links = [...html.matchAll(/href\s*=\s*["']([^"']+)["']/gi)].map((match) => match[1]);
return links
.map(normalizeJsonName)
.filter((name) => name.toLowerCase().endsWith('.json') && name.toLowerCase() !== 'manifest.json');
} catch (_) {
return [];
}
}
function uniqueSorted(files) {
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
}
async function loadAllJsonFiles() {
const baseDirs = ['./assets/demo_new_set', './assets/demo_newset'];
for (const baseDir of baseDirs) {
const [directoryFiles, manifestFiles] = await Promise.all([
scanDirectoryJsonFiles(baseDir),
loadManifestFiles(`${baseDir}/manifest.json`)
]);
const files = uniqueSorted([...directoryFiles, ...manifestFiles]);
if (files.length) {
const source = directoryFiles.length ? 'directory+manifest' : 'manifest';
return { files, baseDir, source };
}
}
return { files: [], baseDir: '', source: 'none' };
}
function createCard(file, baseDir, index, laneState) {
const card = document.createElement('article');
card.className = 'lottie-card';
const canvas = document.createElement('div');
canvas.className = 'lottie-canvas';
card.appendChild(canvas);
const animation = window.lottie.loadAnimation({
container: canvas,
renderer: 'svg',
loop: true,
autoplay: true,
path: `${baseDir}/${encodeURIComponent(file)}`,
rendererSettings: {
progressiveLoad: true,
preserveAspectRatio: 'xMidYMid meet'
}
});
animation.setSpeed(0.8 + (index % 8) * 0.04);
card.addEventListener('mouseenter', () => {
animation.pause();
laneState.paused = true;
});
card.addEventListener('mouseleave', () => {
animation.play();
laneState.paused = false;
});
return card;
}
function createLane(files, baseDir, laneIndex) {
const lane = document.createElement('div');
lane.className = 'lottie-lane';
const track = document.createElement('div');
track.className = 'lane-track';
lane.appendChild(track);
const direction = laneIndex % 2 === 0 ? 'left' : 'right';
const laneState = {
lane,
track,
laneIndex,
direction,
2026-02-28 12:04:02 +00:00
speed: (0.34 + laneIndex * 0.08) * 1.5,
2026-02-28 08:48:50 +00:00
offset: 0,
paused: false,
gap: 10
};
files.forEach((file, idx) => {
track.appendChild(createCard(file, baseDir, idx, laneState));
});
lane.addEventListener('mouseenter', () => { laneState.paused = true; });
lane.addEventListener('mouseleave', () => { laneState.paused = false; });
laneStates.push(laneState);
return lane;
}
function tickLanes(timestamp) {
laneStates.forEach((state) => {
if (state.paused) return;
const cards = state.track.children;
if (!cards.length) return;
state.gap = Number.parseFloat(getComputedStyle(state.track).gap || '10') || 10;
state.offset += state.speed;
const wave = Math.sin(timestamp * 0.001 + state.laneIndex) * 2.2;
if (state.direction === 'left') {
let first = state.track.firstElementChild;
while (first) {
const width = first.getBoundingClientRect().width + state.gap;
if (state.offset < width) break;
state.track.appendChild(first);
state.offset -= width;
first = state.track.firstElementChild;
}
state.track.style.transform = `translate3d(${-state.offset}px, ${wave}px, 0)`;
} else {
let last = state.track.lastElementChild;
while (last) {
const width = last.getBoundingClientRect().width + state.gap;
2026-02-28 12:04:02 +00:00
if (state.offset < 1) break;
2026-02-28 08:48:50 +00:00
state.track.insertBefore(last, state.track.firstElementChild);
state.offset -= width;
last = state.track.lastElementChild;
}
state.track.style.transform = `translate3d(${state.offset}px, ${-wave}px, 0)`;
}
});
requestAnimationFrame(tickLanes);
}
async function initLottieWall() {
const statusEl = document.getElementById('lottieStatus');
const rowsEl = document.getElementById('lottieRows');
if (!statusEl || !rowsEl) return;
const { files, baseDir, source } = await loadAllJsonFiles();
if (!files.length) {
statusEl.textContent = 'No Lottie files found, please check the assets/demo_new_set directory or manifest.json';
2026-02-28 08:48:50 +00:00
return;
}
2026-02-28 12:04:02 +00:00
const rows = [[], [], [], [], []];
files.forEach((file, index) => rows[index % 5].push(file));
2026-02-28 08:48:50 +00:00
rows.forEach((rowFiles, laneIndex) => {
rowsEl.appendChild(createLane(rowFiles, baseDir, laneIndex));
});
statusEl.textContent = `Loaded ${files.length} Lottie files, all generated by OmniLottie`;
if (!lanesLoopStarted) {
lanesLoopStarted = true;
requestAnimationFrame(tickLanes);
}
}
function initCitationCopy() {
const btn = document.getElementById('copyCitation');
const citationEl = document.getElementById('citationText');
if (!btn || !citationEl) return;
btn.addEventListener('click', async () => {
const text = citationEl.textContent || '';
try {
await navigator.clipboard.writeText(text);
2026-02-28 12:16:19 +00:00
btn.textContent = 'Copied Citation';
setTimeout(() => { btn.textContent = 'Copied Citation'; }, 1200);
2026-02-28 08:48:50 +00:00
} catch (_) {
2026-02-28 12:16:19 +00:00
btn.textContent = 'Copy Citation Failed';
setTimeout(() => { btn.textContent = 'Copied Citation'; }, 1200);
2026-02-28 08:48:50 +00:00
}
});
}
window.addEventListener('DOMContentLoaded', () => {
initTheme();
initOverlayPlayer();
initLottieWall();
initCitationCopy();
});
</script>
2026-02-12 21:31:55 +08:00
</body>
</html>