| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Animal.js Documentation</title>
- <style>
- :root {
- --sidebar-bg: #111;
- --main-bg: #111;
- --border-color: #222;
- --text-color: #888;
- --text-hover: #ccc;
- --active-color: #ff4b4b; /* Red/Orange for active item */
- --header-color: #666;
- --middle-col-bg: #161616;
- --tree-line-color: #333;
- --accent-color: #ff9f43;
- }
-
- body {
- margin: 0;
- padding: 0;
- height: 100vh;
- display: flex;
- background: var(--main-bg);
- color: var(--text-color);
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- overflow: hidden;
- font-size: 14px;
- }
- /* 1. LEFT SIDEBAR - MAIN NAVIGATION */
- .sidebar-left {
- width: 260px;
- background: var(--sidebar-bg);
-
- display: flex;
- flex-direction: column;
- overflow-y: auto;
- flex-shrink: 0;
- padding-bottom: 40px;
- }
- .logo-area {
- padding: 24px 20px;
- font-size: 18px;
- font-weight: bold;
- color: #fff;
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 10px;
- }
- .logo-circle {
- width: 10px;
- height: 10px;
- background: #fff;
- border-radius: 50%;
- }
- /* Tree Structure */
- .nav-tree {
- padding: 0 20px;
- }
- .tree-group {
- margin-bottom: 5px;
- }
- .tree-header {
- padding: 8px 0;
- color: var(--header-color);
- font-weight: 600;
- cursor: pointer;
- transition: color 0.2s;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
-
- .tree-header:hover {
- color: var(--text-hover);
- }
-
- .tree-header.active-header {
- color: #e69f3c; /* Highlight parent if needed, usually just children */
- }
- .tree-children {
- padding-left: 14px; /* Indent */
- border-left: 1px solid var(--tree-line-color);
- margin-left: 2px;
- display: none; /* Collapsed by default */
- flex-direction: column;
- }
-
- .tree-children.expanded {
- display: flex;
- }
- .nav-item {
- display: block;
- padding: 6px 0 6px 15px;
- color: var(--text-color);
- text-decoration: none;
- cursor: pointer;
- transition: all 0.2s;
- position: relative;
- }
- .nav-item:hover {
- color: var(--text-hover);
- }
- /* Active Item Style (Reference: Red text, maybe red border left overlaying the gray line?) */
- .nav-item.active {
- color: var(--active-color);
- }
-
- /* Optional: Active marker on the left line */
- .nav-item.active::before {
- content: '';
- position: absolute;
- left: -1px; /* Overlap the border */
- top: 0;
- bottom: 0;
- width: 2px;
- background: var(--active-color);
- }
- /* Flat menu section title */
- .nav-section-title {
- margin-top: 18px;
- padding: 10px 0 6px 0;
- color: #7a7a7a;
- font-size: 11px;
- font-weight: 800;
- letter-spacing: 1px;
- text-transform: uppercase;
- opacity: 0.9;
- }
- /* Search Box in sidebar */
- .sidebar-search {
- margin: 0 20px 20px 20px;
- background: #1a1a1a;
- border: 1px solid #333;
- border-radius: 4px;
- padding: 8px 12px;
- color: #fff;
- font-size: 13px;
- cursor: pointer;
- }
- .sidebar-search:hover {
- border-color: #555;
- }
- /* 2. MIDDLE SIDEBAR */
- .sidebar-middle {
- width: 320px;
- background: var(--middle-col-bg);
-
- display: flex;
- flex-direction: column;
- overflow: hidden;
- flex-shrink: 0;
- }
- #middle-frame {
- width: 100%;
- height: 100%;
- border: none;
- background: transparent;
- }
- /* 3. RIGHT CONTENT */
- .content-right {
- flex: 1;
- display: flex;
- flex-direction: column;
- background: #000;
- overflow: hidden;
- }
- #content-frame {
- flex: 1;
- border: none;
- width: 100%;
- height: 100%;
- }
- /* Connection line between middle and right columns */
- .column-connection-line {
- position: fixed;
- inset: 0;
- width: 100vw;
- height: 100vh;
- pointer-events: none;
- z-index: 20;
- opacity: 0.85;
- }
- .column-connection-path {
- stroke: var(--accent-color);
- stroke-width: 2;
- stroke-dasharray: 6 6;
- stroke-linecap: round;
- fill: none;
- opacity: 0.35;
- animation: dashFlow 1.1s linear infinite;
- filter: drop-shadow(0 0 2px rgba(255, 159, 67, 0.25));
- }
- @keyframes dashFlow {
- to { stroke-dashoffset: -12; }
- }
- </style>
- </head>
- <body>
- <!-- 1. Left Sidebar -->
- <div class="sidebar-left">
- <div class="logo-area">
- <div class="logo-circle"></div>
- Animal.js
- </div>
-
- <div class="sidebar-search" onclick="selectCategory('search.html', 'test_on_update.html', null)">
- Search
- </div>
-
- <div class="nav-tree" aria-label="主菜单(扁平化)">
- <div class="nav-section-title">Animal.js 动画</div>
- <div class="nav-item active" onclick="selectCategory('targets/list.html', 'targets/overview.html', this)">目标 Targets</div>
- <div class="nav-item" onclick="selectCategory('animatable_properties/list.html', 'animatable_properties/test_transforms.html', this)">可动画属性 Properties</div>
- <div class="nav-item" onclick="selectCategory('tween_value_types/list.html', 'tween_value_types/test_numerical.html', this)">补间值类型 Tween 值</div>
- <div class="nav-item" onclick="selectCategory('tween_parameters/list.html', 'tween_parameters/overview.html', this)">补间参数 Tween 参数</div>
- <div class="nav-item" onclick="selectCategory('svg/list.html', 'svg/overview.html', this)">SVG 动画</div>
- <div class="nav-item" onclick="selectCategory('list_callbacks.html', 'test_on_update.html', this)">回调 Callbacks</div>
- <div class="nav-item" onclick="selectCategory('examples/list.html', 'examples/controls.html', this)">示例 Examples</div>
- <div class="nav-section-title">Layer 弹窗</div>
- <div class="nav-item" onclick="selectCategory('examples/list.html', 'examples/layer.html', this)">交互示例(Layer + 动画)</div>
- <div class="nav-section-title">开发者</div>
- <div class="nav-item" onclick="selectCategory('search.html', 'module.html', this)">模块开发与测试指南</div>
- </div>
- </div>
- <!-- 2. Middle Sidebar -->
- <div class="sidebar-middle">
- <iframe id="middle-frame" src="targets/list.html"></iframe>
- </div>
- <!-- 3. Right Content -->
- <div class="content-right">
- <iframe id="content-frame" src="targets/test_css_selector.html"></iframe>
- </div>
- <!-- SVG overlay for the middle→right connection line -->
- <svg id="column-connection-line" class="column-connection-line" aria-hidden="true">
- <path id="column-connection-path" class="column-connection-path" d=""></path>
- </svg>
- <script>
- function selectCategory(listUrl, defaultContentUrl, el) {
- // 1. Highlight Nav Item
- document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
- if (el) el.classList.add('active');
- // 2. Load Middle Column (List)
- const middleFrame = document.getElementById('middle-frame');
- if (middleFrame.getAttribute('src') !== listUrl) {
- middleFrame.src = listUrl;
- }
- // 3. Load Right Column (Default Content)
- loadContent(defaultContentUrl);
- }
- function loadContent(url) {
- const contentFrame = document.getElementById('content-frame');
- if (contentFrame.getAttribute('src') !== url) {
- contentFrame.src = url;
- }
- // Keep middle list selection in sync when possible
- try {
- setMiddleActive(url);
- } catch {}
- // Update connection line after navigation
- scheduleConnectionLineUpdate();
- }
- // Called by content iframe pages (and also internally after navigation)
- function setMiddleActive(contentUrl) {
- const middleFrame = document.getElementById('middle-frame');
- if (!middleFrame) return;
- const win = middleFrame.contentWindow;
- if (win && typeof win.setActiveByContentUrl === 'function') {
- win.setActiveByContentUrl(contentUrl);
- }
- // Update connection line when active item changes
- scheduleConnectionLineUpdate();
- }
- // =========================================================
- // Global Prev/Next(组内顺序 + 跨组跳转,由路由表统一驱动)
- // =========================================================
- const DOC_PAGES = [
- // Targets
- { group: 'Targets', title: 'Targets 概览', url: 'targets/overview.html' },
- { group: 'Targets', title: 'CSS Selector', url: 'targets/test_css_selector.html' },
- { group: 'Targets', title: 'DOM Elements', url: 'targets/test_dom_elements.html' },
- { group: 'Targets', title: 'JavaScript Objects', url: 'targets/test_js_objects.html' },
- { group: 'Targets', title: 'Array of targets', url: 'targets/test_array_targets.html' },
- // Animatable properties
- { group: 'Properties', title: 'Transforms', url: 'animatable_properties/test_transforms.html' },
- { group: 'Properties', title: 'CSS Properties', url: 'animatable_properties/test_css_props.html' },
- { group: 'Properties', title: 'CSS Variables', url: 'animatable_properties/test_css_vars.html' },
- { group: 'Properties', title: 'JS Properties', url: 'animatable_properties/test_js_props.html' },
- // Tween value types
- { group: 'Tween 值', title: 'Numerical', url: 'tween_value_types/test_numerical.html' },
- { group: 'Tween 值', title: 'Unit', url: 'tween_value_types/test_unit.html' },
- { group: 'Tween 值', title: 'Relative', url: 'tween_value_types/test_relative.html' },
- { group: 'Tween 值', title: 'Colors', url: 'tween_value_types/test_colors.html' },
- // Tween parameters
- { group: 'Tween 参数', title: 'Tween parameters 概览', url: 'tween_parameters/overview.html' },
- { group: 'Tween 参数', title: 'duration / delay', url: 'tween_parameters/test_duration_delay.html' },
- // SVG
- { group: 'SVG', title: 'SVG 概览', url: 'svg/overview.html' },
- { group: 'SVG', title: 'draw() 路径描边', url: 'svg/test_draw_path.html' },
- { group: 'SVG', title: 'Motion Path', url: 'svg/test_motion_path.html' },
- { group: 'SVG', title: 'SVG 属性动画', url: 'svg/test_svg_attributes.html' },
- { group: 'SVG', title: 'SVG transform', url: 'svg/test_svg_transforms.html' },
- // Callbacks
- { group: 'Callbacks', title: 'onUpdate', url: 'test_on_update.html' },
- // Examples
- { group: 'Examples', title: 'Controls', url: 'examples/controls.html' },
- { group: 'Examples', title: 'Layer', url: 'examples/layer.html' },
- // Developer
- { group: 'Developer', title: '模块开发与测试指南', url: 'module.html' }
- ];
- function normalizeDocUrl(url) {
- return String(url || '')
- .replace(/^[#/]+/, '')
- .replace(/^\.\//, '')
- .replace(/^\/+/, '');
- }
- function docFindIndex(currentUrl) {
- const u = normalizeDocUrl(currentUrl);
- if (!u) return -1;
- return DOC_PAGES.findIndex(p => u === p.url || u.endsWith(p.url));
- }
- function docGetPrevNext(currentUrl) {
- const idx = docFindIndex(currentUrl);
- if (idx < 0) return { idx: -1, prev: null, next: null, current: null };
- const prev = (idx > 0) ? DOC_PAGES[idx - 1] : null;
- const next = (idx < DOC_PAGES.length - 1) ? DOC_PAGES[idx + 1] : null;
- return { idx, prev, next, current: DOC_PAGES[idx] };
- }
- function docNavigatePrev(currentUrl) {
- const { prev } = docGetPrevNext(currentUrl);
- if (prev) loadContent(prev.url);
- }
- function docNavigateNext(currentUrl) {
- const { next } = docGetPrevNext(currentUrl);
- if (next) loadContent(next.url);
- }
- // Expose to iframes (same origin)
- window.docGetPrevNext = docGetPrevNext;
- window.docNavigatePrev = docNavigatePrev;
- window.docNavigateNext = docNavigateNext;
- // =============================
- // Middle ↔ Right connection line
- // =============================
- const connectionSvg = document.getElementById('column-connection-line');
- const connectionPath = document.getElementById('column-connection-path');
- let _connRaf = 0;
- function scheduleConnectionLineUpdate() {
- if (_connRaf) return;
- _connRaf = requestAnimationFrame(() => {
- _connRaf = 0;
- updateConnectionLine();
- });
- }
- function getActiveElementInFrame(frameEl) {
- try {
- const doc = frameEl?.contentDocument;
- if (!doc) return null;
- // Middle list pages use .card.active / .header-card.active; fall back to .active
- return (
- doc.querySelector('.card.active') ||
- doc.querySelector('.header-card.active') ||
- doc.querySelector('.nav-item.active') ||
- doc.querySelector('.active')
- );
- } catch {
- return null;
- }
- }
- function getAnchorElementInContentFrame(frameEl) {
- try {
- const doc = frameEl?.contentDocument;
- if (!doc) return null;
- // Prefer a *visible* code area anchor (in-viewport), then fall back
- const candidates = [
- '.box-container .box-header',
- '.code-view.active, .html-view.active, .css-view.active',
- '.box-container',
- 'h1',
- '.container',
- 'body'
- ];
- for (const sel of candidates) {
- const el = doc.querySelector(sel);
- if (!el) continue;
- // If it's visible in the iframe viewport, use it; else keep looking
- const r = el.getBoundingClientRect();
- const vh = frameEl?.clientHeight || 0;
- if (!vh) return el;
- const visibleTop = Math.max(0, r.top);
- const visibleBottom = Math.min(vh, r.bottom);
- if (visibleBottom - visibleTop > 8) return el;
- }
- return doc.body;
- } catch {
- return null;
- }
- }
- function clamp(n, min, max) {
- return Math.min(Math.max(n, min), max);
- }
- function getVisibleCenterInFrame(frameEl, elRect) {
- const w = frameEl?.clientWidth || 0;
- const h = frameEl?.clientHeight || 0;
- if (!w || !h) return null;
- const visibleLeft = Math.max(0, elRect.left);
- const visibleRight = Math.min(w, elRect.right);
- const visibleTop = Math.max(0, elRect.top);
- const visibleBottom = Math.min(h, elRect.bottom);
- if (visibleRight - visibleLeft <= 1 || visibleBottom - visibleTop <= 1) {
- return null;
- }
- return {
- x: (visibleLeft + visibleRight) / 2,
- y: (visibleTop + visibleBottom) / 2
- };
- }
- function updateConnectionLine() {
- if (!connectionSvg || !connectionPath) return;
- const middleFrame = document.getElementById('middle-frame');
- const contentFrame = document.getElementById('content-frame');
- if (!middleFrame || !contentFrame) return;
- const midRect = middleFrame.getBoundingClientRect();
- const rightRect = contentFrame.getBoundingClientRect();
- const active = getActiveElementInFrame(middleFrame);
- const anchor = getAnchorElementInContentFrame(contentFrame);
- if (!active || !anchor) {
- connectionPath.setAttribute('d', '');
- connectionSvg.style.opacity = '0';
- return;
- }
- const activeRect = active.getBoundingClientRect();
- const anchorRect = anchor.getBoundingClientRect();
- // Use the *visible* part of the element within its iframe viewport
- const activeCenter = getVisibleCenterInFrame(middleFrame, activeRect);
- const anchorCenter = getVisibleCenterInFrame(contentFrame, anchorRect);
- if (!activeCenter || !anchorCenter) {
- // If either side isn't visible in its iframe, hide the line (prevents weird drifting)
- connectionPath.setAttribute('d', '');
- connectionSvg.style.opacity = '0';
- return;
- }
- // Convert iframe-viewport coordinates to parent viewport coordinates
- const startX = midRect.left + clamp(activeRect.right, 0, middleFrame.clientWidth);
- const startY = midRect.top + activeCenter.y;
- // Point into the code area a bit (not exactly at left edge)
- const endX = rightRect.left + clamp(anchorRect.left + 18, 12, contentFrame.clientWidth - 12);
- const endY = rightRect.top + anchorCenter.y;
- // Draw an L-shaped polyline.
- // Keep the vertical segment aligned with the column divider (middle ↔ right).
- const dividerX = rightRect.left + 0.5; // half-pixel for crisper stroke alignment
- const x1 = Math.max(startX + 12, dividerX);
- const d = [
- `M ${startX} ${startY}`,
- `L ${x1} ${startY}`,
- `L ${x1} ${endY}`,
- `L ${endX} ${endY}`
- ].join(' ');
- connectionPath.setAttribute('d', d);
- connectionSvg.style.opacity = '0.85';
- }
- function attachFrameScrollListeners(frameEl) {
- if (!frameEl) return;
- const onScroll = () => {
- markFrameScrolling(frameEl);
- scheduleConnectionLineUpdate();
- };
- try {
- // Window scroll inside iframe
- frameEl.contentWindow?.addEventListener('scroll', onScroll, { passive: true });
- // Some pages scroll on document/body; capture to be safe
- frameEl.contentDocument?.addEventListener('scroll', onScroll, true);
- frameEl.contentDocument?.documentElement?.addEventListener?.('scroll', onScroll, { passive: true });
- frameEl.contentDocument?.body?.addEventListener?.('scroll', onScroll, { passive: true });
- } catch {}
- }
- const _scrollHideTimers = new WeakMap();
- function markFrameScrolling(frameEl) {
- try {
- const doc = frameEl?.contentDocument;
- if (!doc) return;
- doc.documentElement?.classList.add('scrolling');
- doc.body?.classList.add('scrolling');
- const prev = _scrollHideTimers.get(frameEl);
- if (prev) clearTimeout(prev);
- _scrollHideTimers.set(frameEl, setTimeout(() => {
- try {
- doc.documentElement?.classList.remove('scrolling');
- doc.body?.classList.remove('scrolling');
- } catch {}
- }, 500));
- } catch {}
- }
- // Always-on lightweight loop to keep the line aligned even
- // if iframe scroll events were missed due to timing.
- let _connLoopStarted = false;
- function startConnectionLineLoop() {
- if (_connLoopStarted) return;
- _connLoopStarted = true;
- const loop = () => {
- // Only bother when we have a path (avoids work when hidden)
- if (connectionPath?.getAttribute('d')) {
- scheduleConnectionLineUpdate();
- }
- requestAnimationFrame(loop);
- };
- requestAnimationFrame(loop);
- }
- // Make initial active state robust against iframe load ordering
- window.addEventListener('load', () => {
- const contentFrame = document.getElementById('content-frame');
- const src = contentFrame?.getAttribute('src');
- if (src) setMiddleActive(src);
- const middleFrame = document.getElementById('middle-frame');
- if (middleFrame) {
- middleFrame.addEventListener('load', () => {
- const current = document.getElementById('content-frame')?.getAttribute('src');
- if (current) setMiddleActive(current);
- attachFrameScrollListeners(middleFrame);
- scheduleConnectionLineUpdate();
- });
- }
- if (contentFrame) {
- contentFrame.addEventListener('load', () => {
- const current = contentFrame.getAttribute('src');
- if (current) setMiddleActive(current);
- attachFrameScrollListeners(contentFrame);
- scheduleConnectionLineUpdate();
- });
- }
- // If the iframes were already loaded before we attached 'load' listeners,
- // still attach scroll listeners immediately.
- attachFrameScrollListeners(middleFrame);
- attachFrameScrollListeners(contentFrame);
- window.addEventListener('resize', scheduleConnectionLineUpdate, { passive: true });
- scheduleConnectionLineUpdate();
- startConnectionLineLoop();
- });
- </script>
- </body>
- </html>
|