This is an automated email from the ASF dual-hosted git repository.

davidzollo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/seatunnel-website.git


The following commit(s) were added to refs/heads/main by this push:
     new a033bdbdbee4 [Docs] Replace homepage motion iframe with native cards 
(#538)
a033bdbdbee4 is described below

commit a033bdbdbee4f75841a6e0ca9aebaf01469fac88
Author: Daniel <[email protected]>
AuthorDate: Mon Sep 21 22:17:59 2026 -0400

    [Docs] Replace homepage motion iframe with native cards (#538)
---
 src/pages/home/MotionMechanismGrid.jsx | 225 +++++++++++++++++++
 src/pages/home/index.jsx               | 175 ++-------------
 src/pages/home/index.less              | 388 +++++++++++++++++++++------------
 static/home/seatunnel-motion.html      | 329 ----------------------------
 4 files changed, 500 insertions(+), 617 deletions(-)

diff --git a/src/pages/home/MotionMechanismGrid.jsx 
b/src/pages/home/MotionMechanismGrid.jsx
new file mode 100644
index 000000000000..cfce5436a2ec
--- /dev/null
+++ b/src/pages/home/MotionMechanismGrid.jsx
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+
+function Arrow({x, y}) {
+    return <path d={`M${x - 6} ${y - 5}l6 5-6 5`} 
className="st-home-motion-diagram-arrow" fill="none" />;
+}
+
+function ParallelReadsDiagram() {
+    return (
+        <svg viewBox="0 0 520 166" className="st-home-motion-diagram" 
aria-hidden="true">
+            <text x="18" y="20" 
className="st-home-motion-diagram-label">SOURCE SPLITS</text>
+            <text x="207" y="20" 
className="st-home-motion-diagram-label">READERS</text>
+            <text x="438" y="20" 
className="st-home-motion-diagram-label">TARGET</text>
+            <rect x="16" y="32" width="118" height="112" rx="10" 
className="st-home-motion-diagram-node" />
+            {['S1', 'S2', 'S3', 'S4', 'S5', 'S6'].map((split, index) => {
+                const x = 28 + (index % 2) * 49;
+                const y = 45 + Math.floor(index / 2) * 31;
+                const active = index === 1 || index === 3;
+                return (
+                    <g key={split}>
+                        <rect x={x} y={y} width="39" height="22" rx="5" 
className={active ? 'st-home-motion-diagram-packet' : 
'st-home-motion-diagram-chip'} />
+                        <text
+                            x={x + 19.5}
+                            y={y + 15}
+                            textAnchor="middle"
+                            className={
+                                active
+                                    ? 'st-home-motion-diagram-on-blue'
+                                    : 'st-home-motion-diagram-chip-text'
+                            }
+                        >
+                            {split}
+                        </text>
+                    </g>
+                );
+            })}
+            {[54, 87, 120].map((y, index) => (
+                <g key={y}>
+                    <path d={`M134 88 C168 88 174 ${y} 204 ${y}`} 
className="st-home-motion-diagram-line" fill="none" />
+                    <rect x="205" y={y - 13} width="121" height="26" rx="7" 
className="st-home-motion-diagram-node" />
+                    <circle cx="220" cy={y} r="3.5" className={index === 1 ? 
'st-home-motion-diagram-teal-fill' : 'st-home-motion-diagram-blue-fill'} />
+                    <text x="232" y={y + 5} 
className="st-home-motion-diagram-copy">Reader {index + 1}</text>
+                    <path d={`M326 ${y} C364 ${y} 384 88 428 88`} 
className="st-home-motion-diagram-line" fill="none" />
+                </g>
+            ))}
+            <Arrow x="427" y="88" />
+            <ellipse cx="466" cy="67" rx="27" ry="7" 
className="st-home-motion-diagram-node" />
+            <path d="M439 67v43c0 9 54 9 54 0V67" 
className="st-home-motion-diagram-node" />
+            <path d="M439 89c0 9 54 9 54 0M439 110c0 9 54 9 54 0" 
className="st-home-motion-diagram-line" fill="none" />
+        </svg>
+    );
+}
+
+function SnapshotCdcDiagram() {
+    return (
+        <svg viewBox="0 0 520 166" className="st-home-motion-diagram" 
aria-hidden="true">
+            <text x="18" y="20" className="st-home-motion-diagram-label">MYSQL 
/ ORDERS</text>
+            <text x="373" y="20" 
className="st-home-motion-diagram-label">TARGET / ORDERS</text>
+            <rect x="16" y="31" width="136" height="103" rx="9" 
className="st-home-motion-diagram-node" />
+            <rect x="17" y="32" width="134" height="24" rx="7" 
className="st-home-motion-diagram-soft" />
+            <text x="29" y="48" 
className="st-home-motion-diagram-label">id</text><text x="76" y="48" 
className="st-home-motion-diagram-label">status</text>
+            <text x="29" y="77" 
className="st-home-motion-diagram-copy">41</text><text x="76" y="77" 
className="st-home-motion-diagram-copy">NEW</text>
+            <text x="29" y="104" 
className="st-home-motion-diagram-copy">42</text><text x="76" y="104" 
className="st-home-motion-diagram-copy">NEW</text>
+            <rect x="201" y="24" width="98" height="23" rx="12" 
className="st-home-motion-diagram-soft" />
+            <text x="250" y="40" textAnchor="middle" 
className="st-home-motion-diagram-accent-text">SNAPSHOT</text>
+            <path d="M153 82H367" className="st-home-motion-diagram-line" 
fill="none" />
+            <Arrow x="367" y="82" />
+            <rect x="235" y="70" width="48" height="23" rx="7" 
className="st-home-motion-diagram-teal" />
+            <text x="259" y="86" textAnchor="middle" 
className="st-home-motion-diagram-on-teal">S:42</text>
+            <text x="260" y="116" textAnchor="middle" 
className="st-home-motion-diagram-copy">SeaTunnel</text>
+            <rect x="370" y="31" width="136" height="103" rx="9" 
className="st-home-motion-diagram-node" />
+            <rect x="371" y="32" width="134" height="24" rx="7" 
className="st-home-motion-diagram-soft" />
+            <text x="383" y="48" 
className="st-home-motion-diagram-label">id</text><text x="430" y="48" 
className="st-home-motion-diagram-label">status</text>
+            <text x="383" y="77" 
className="st-home-motion-diagram-copy">41</text><text x="430" y="77" 
className="st-home-motion-diagram-copy">NEW</text>
+        </svg>
+    );
+}
+
+function TransformDiagram() {
+    return (
+        <svg viewBox="0 0 520 166" className="st-home-motion-diagram" 
aria-hidden="true">
+            <text x="18" y="20" 
className="st-home-motion-diagram-label">INPUT</text>
+            <text x="218" y="20" className="st-home-motion-diagram-label">ROW 
SQL</text>
+            <text x="421" y="20" 
className="st-home-motion-diagram-label">OUTPUT</text>
+            <rect x="16" y="31" width="125" height="91" rx="9" 
className="st-home-motion-diagram-node" />
+            <text x="28" y="57" 
className="st-home-motion-diagram-copy">1&nbsp;&nbsp; Ada</text>
+            <text x="28" y="82" 
className="st-home-motion-diagram-copy">0&nbsp;&nbsp; Temp</text>
+            <text x="28" y="107" 
className="st-home-motion-diagram-copy">2&nbsp;&nbsp; Lin</text>
+            <path d="M142 76H206M315 76h84" 
className="st-home-motion-diagram-line" fill="none" />
+            <Arrow x="399" y="76" />
+            <rect x="207" y="48" width="108" height="56" rx="13" 
className="st-home-motion-diagram-blue" />
+            <text x="261" y="82" textAnchor="middle" 
className="st-home-motion-diagram-on-blue st-home-motion-diagram-sql">SQL</text>
+            <rect x="402" y="31" width="102" height="91" rx="9" 
className="st-home-motion-diagram-node" />
+            <rect x="365" y="64" width="24" height="24" rx="7" 
className="st-home-motion-diagram-teal" />
+            <text x="377" y="81" textAnchor="middle" 
className="st-home-motion-diagram-on-teal">1</text>
+            <text x="413" y="77" 
className="st-home-motion-diagram-muted-copy">waiting…</text>
+            <text x="18" y="144" 
className="st-home-motion-diagram-code">CONCAT(name, '_')</text>
+        </svg>
+    );
+}
+
+function RoutingDiagram() {
+    const rows = ['users', 'orders', 'items'];
+    return (
+        <svg viewBox="0 0 520 166" className="st-home-motion-diagram" 
aria-hidden="true">
+            {rows.map((table, index) => {
+                const y = 39 + index * 43;
+                return (
+                    <g key={table}>
+                        <rect x="16" y={y} width="127" height="30" rx="7" 
className="st-home-motion-diagram-node" />
+                        <rect x="377" y={y} width="127" height="30" rx="7" 
className="st-home-motion-diagram-node" />
+                        <rect x="28" y={y + 8} width="16" height="14" rx="2" 
className="st-home-motion-diagram-table" />
+                        <rect x="389" y={y + 8} width="16" height="14" rx="2" 
className="st-home-motion-diagram-table" />
+                        <text x="55" y={y + 20} 
className="st-home-motion-diagram-copy">{table}</text>
+                        <text x="416" y={y + 20} 
className="st-home-motion-diagram-copy">{table}</text>
+                        <path d={`M144 ${y + 15} C179 ${y + 15} 189 82 221 
82M299 82 C333 82 343 ${y + 15} 376 ${y + 15}`} 
className="st-home-motion-diagram-line" fill="none" />
+                        <Arrow x="375" y={y + 15} />
+                    </g>
+                );
+            })}
+            <rect x="221" y="52" width="78" height="61" rx="12" 
className="st-home-motion-diagram-blue" />
+            <text x="260" y="78" textAnchor="middle" 
className="st-home-motion-diagram-on-blue">Table</text>
+            <text x="260" y="98" textAnchor="middle" 
className="st-home-motion-diagram-on-blue">route</text>
+        </svg>
+    );
+}
+
+function SchemaDiagram() {
+    const rows = [['id', 'BIGINT'], ['name', 'VARCHAR'], ['region', 
'VARCHAR']];
+    return (
+        <svg viewBox="0 0 520 166" className="st-home-motion-diagram" 
aria-hidden="true">
+            <text x="18" y="20" className="st-home-motion-diagram-label">MYSQL 
SOURCE</text>
+            <text x="371" y="20" className="st-home-motion-diagram-label">JDBC 
MYSQL SINK</text>
+            {[16, 369].map((x) => <rect key={x} x={x} y="31" width="135" 
height="104" rx="9" className="st-home-motion-diagram-node" />)}
+            {[16, 369].map((x) => <g key={x}><rect x={x + 1} y="32" 
width="133" height="24" rx="7" className="st-home-motion-diagram-soft" /><text 
x={x + 14} y="48" className="st-home-motion-diagram-copy">▦&nbsp; 
customers</text></g>)}
+            {rows.map(([name, type], index) => {
+                const y = 76 + index * 24;
+                return (
+                    <g key={name}>
+                        {index === 2 ? <rect x="22" y={y - 16} width="123" 
height="22" rx="4" className="st-home-motion-diagram-highlight" /> : null}
+                        <text x="29" y={y} className={index === 2 ? 
'st-home-motion-diagram-teal-copy' : 
'st-home-motion-diagram-copy'}>{name}</text>
+                        <text x="92" y={y} className={index === 2 ? 
'st-home-motion-diagram-teal-copy' : 
'st-home-motion-diagram-code'}>{type}</text>
+                        <text x="382" y={y} className={index === 2 ? 
'st-home-motion-diagram-teal-copy' : 
'st-home-motion-diagram-copy'}>{name}</text>
+                        <text x="445" y={y} className={index === 2 ? 
'st-home-motion-diagram-teal-copy' : 
'st-home-motion-diagram-code'}>{type}</text>
+                    </g>
+                );
+            })}
+            <path d="M152 82H364" className="st-home-motion-diagram-line" 
fill="none" /><Arrow x="364" y="82" />
+            <rect x="225" y="48" width="58" height="22" rx="11" 
className="st-home-motion-diagram-soft" /><text x="254" y="64" 
textAnchor="middle" className="st-home-motion-diagram-accent-text">DDL</text>
+            <rect x="217" y="74" width="42" height="23" rx="7" 
className="st-home-motion-diagram-blue" /><text x="238" y="90" 
textAnchor="middle" className="st-home-motion-diagram-on-blue">ADD</text>
+        </svg>
+    );
+}
+
+function CheckpointDiagram() {
+    return (
+        <svg viewBox="0 0 520 166" className="st-home-motion-diagram" 
aria-hidden="true">
+            <path d="M122 70h70M327 70h70" 
className="st-home-motion-diagram-line" fill="none" /><Arrow x="192" y="70" 
/><Arrow x="397" y="70" />
+            {['Source', 'Writer', 'Sink'].map((label, index) => {
+                const x = [16, 198, 403][index];
+                return <g key={label}><rect x={x} y="47" width="104" 
height="43" rx="10" className="st-home-motion-diagram-node" /><text x={x + 52} 
y="74" textAnchor="middle" 
className="st-home-motion-diagram-strong">{label}</text></g>;
+            })}
+            <rect x="232" y="20" width="70" height="21" rx="10" 
className="st-home-motion-diagram-soft" /><text x="267" y="35" 
textAnchor="middle" 
className="st-home-motion-diagram-accent-text">BARRIER</text>
+            <path d="M68 91C68 130 157 137 215 137M250 91v25" 
className="st-home-motion-diagram-line" fill="none" />
+            <rect x="215" y="114" width="134" height="38" rx="9" 
className="st-home-motion-diagram-node" />
+            <text x="282" y="135" textAnchor="middle" 
className="st-home-motion-diagram-copy">Checkpoint state</text>
+            <text x="282" y="147" textAnchor="middle" 
className="st-home-motion-diagram-label">COMPLETED</text>
+        </svg>
+    );
+}
+
+function MotionDiagram({type}) {
+    const diagrams = {
+        parallel: <ParallelReadsDiagram />,
+        cdc: <SnapshotCdcDiagram />,
+        transform: <TransformDiagram />,
+        routing: <RoutingDiagram />,
+        schema: <SchemaDiagram />,
+        checkpoint: <CheckpointDiagram />,
+    };
+
+    return diagrams[type];
+}
+
+export default function MotionMechanismGrid({caption, gridLabel, mechanisms}) {
+    return (
+        <>
+            <ul className="st-home-motion-grid" aria-label={gridLabel}>
+                {mechanisms.map((mechanism, index) => (
+                    <li key={mechanism.title} className={`st-home-motion-card 
st-home-motion-card-${mechanism.diagram}`}>
+                        <article>
+                            <div className="st-home-motion-card-heading">
+                                <span>{String(index + 1).padStart(2, 
'0')}</span>
+                                <div>
+                                    <h3>{mechanism.title}</h3>
+                                    <p>{mechanism.description}</p>
+                                </div>
+                            </div>
+                            <div 
className="st-home-motion-visual"><MotionDiagram type={mechanism.diagram} 
/></div>
+                            <p 
className="st-home-motion-card-detail">{mechanism.detail}</p>
+                            <p 
className="st-home-motion-card-note">{mechanism.note}</p>
+                        </article>
+                    </li>
+                ))}
+            </ul>
+            <p className="st-home-motion-caption">{caption}</p>
+        </>
+    );
+}
diff --git a/src/pages/home/index.jsx b/src/pages/home/index.jsx
index 6d9f656faa8b..70bab5d4458b 100644
--- a/src/pages/home/index.jsx
+++ b/src/pages/home/index.jsx
@@ -15,12 +15,13 @@
  * limitations under the License.
  */
 
-import React, {useEffect, useRef, useState} from 'react';
+import React, {useEffect, useRef} from 'react';
 import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
 import useIsBrowser from '@docusaurus/useIsBrowser';
 import useBaseUrl from '@docusaurus/useBaseUrl';
 import './index.less';
 import systemConfiguration from '../../js/sysConfig';
+import MotionMechanismGrid from './MotionMechanismGrid';
 
 const versions = require('../../../versions.json');
 
@@ -260,24 +261,17 @@ const HOME_COPY = {
             ],
         },
         motion: {
-            eyebrow: 'See it in motion',
-            title: 'Six mechanisms. One integration layer.',
-            lead: 'Explore independent examples of parallel reads, CDC, row 
transforms, table routing, schema evolution, and checkpoint recovery.',
-            openButton: 'Open interactive demo',
-            frameTitle: 'Interactive SeaTunnel data integration mechanisms 
demo',
-            controls: {
-                label: 'Motion demo controls',
-                play: 'Play animation',
-                pause: 'Pause animation',
-                overview: 'All mechanisms',
-            },
+            title: 'Data integration, in motion.',
+            lead: 'Six mechanisms that keep data moving through parallel 
workloads, continuous change, and recoverable failures.',
+            gridLabel: 'SeaTunnel data integration mechanisms',
+            caption: 'Illustrative flows—not a deployment blueprint. 
Production behavior depends on the selected connector, engine, and 
configuration.',
             mechanisms: [
-                {title: 'Parallel reads', description: 'Assign independent 
source splits to parallel readers.'},
-                {title: 'Snapshot + CDC', description: 'Start with a snapshot, 
then capture changes.'},
-                {title: 'Row transforms', description: 'Apply expressions and 
filters to individual records.'},
-                {title: 'Multi-table routing', description: 'Route each table 
identity to its matching destination.'},
-                {title: 'Schema evolution', description: 'Apply supported 
schema changes before data moves downstream.'},
-                {title: 'Checkpoint recovery', description: 'Resume from 
completed checkpoint state after a failure.'},
+                {diagram: 'parallel', title: 'Parallel batch reads', 
description: 'Divide the work. Read independent splits in parallel.', detail: 
'Independent readers process assigned source splits.', note: 'JDBC example · 
Split assignment is source-specific.'},
+                {diagram: 'cdc', title: 'Snapshot + CDC', description: 
'Bootstrap a snapshot, then keep changes flowing.', detail: 'Capture existing 
rows before streaming changes.', note: 'MySQL-CDC example · startup.mode = 
initial.'},
+                {diagram: 'transform', title: 'Row-level transforms', 
description: 'Shape, filter, and enrich records before they land.', detail: 
'Apply row expressions and filters in the pipeline.', note: 'SQL example · 
expression + row filter.'},
+                {diagram: 'routing', title: 'Multi-table routing', 
description: 'Keep every record on its intended table path.', detail: 'Table 
identity selects the matching destination writer.', note: 'Logical routing · 
Physical connections may differ.'},
+                {diagram: 'schema', title: 'Schema evolution', description: 
'Apply supported table changes before new data moves downstream.', detail: 
'Propagate an added column through the compatible path.', note: 'Zeta example · 
MySQL-CDC → JDBC MySQL.'},
+                {diagram: 'checkpoint', title: 'Checkpoint recovery', 
description: 'Resume from a completed checkpoint after a failure.', detail: 
'Capture state with a checkpoint barrier.', note: 'Exactly-once requires 
compatible source and sink support.'},
             ],
         },
         connectors: {
@@ -528,24 +522,17 @@ const HOME_COPY = {
             ],
         },
         motion: {
-            eyebrow: 'See it in motion',
-            title: 'Six mechanisms. One integration layer.',
-            lead: 'Explore independent examples of parallel reads, CDC, row 
transforms, table routing, schema evolution, and checkpoint recovery.',
-            openButton: 'Open interactive demo',
-            frameTitle: 'Interactive SeaTunnel data integration mechanisms 
demo',
-            controls: {
-                label: 'Motion demo controls',
-                play: 'Play animation',
-                pause: 'Pause animation',
-                overview: 'All mechanisms',
-            },
+            title: 'Data integration, in motion.',
+            lead: 'Six mechanisms that keep data moving through parallel 
workloads, continuous change, and recoverable failures.',
+            gridLabel: 'SeaTunnel data integration mechanisms',
+            caption: 'Illustrative flows—not a deployment blueprint. 
Production behavior depends on the selected connector, engine, and 
configuration.',
             mechanisms: [
-                {title: 'Parallel reads', description: 'Assign independent 
source splits to parallel readers.'},
-                {title: 'Snapshot + CDC', description: 'Start with a snapshot, 
then capture changes.'},
-                {title: 'Row transforms', description: 'Apply expressions and 
filters to individual records.'},
-                {title: 'Multi-table routing', description: 'Route each table 
identity to its matching destination.'},
-                {title: 'Schema evolution', description: 'Apply supported 
schema changes before data moves downstream.'},
-                {title: 'Checkpoint recovery', description: 'Resume from 
completed checkpoint state after a failure.'},
+                {diagram: 'parallel', title: 'Parallel batch reads', 
description: 'Divide the work. Read independent splits in parallel.', detail: 
'Independent readers process assigned source splits.', note: 'JDBC example · 
Split assignment is source-specific.'},
+                {diagram: 'cdc', title: 'Snapshot + CDC', description: 
'Bootstrap a snapshot, then keep changes flowing.', detail: 'Capture existing 
rows before streaming changes.', note: 'MySQL-CDC example · startup.mode = 
initial.'},
+                {diagram: 'transform', title: 'Row-level transforms', 
description: 'Shape, filter, and enrich records before they land.', detail: 
'Apply row expressions and filters in the pipeline.', note: 'SQL example · 
expression + row filter.'},
+                {diagram: 'routing', title: 'Multi-table routing', 
description: 'Keep every record on its intended table path.', detail: 'Table 
identity selects the matching destination writer.', note: 'Logical routing · 
Physical connections may differ.'},
+                {diagram: 'schema', title: 'Schema evolution', description: 
'Apply supported table changes before new data moves downstream.', detail: 
'Propagate an added column through the compatible path.', note: 'Zeta example · 
MySQL-CDC → JDBC MySQL.'},
+                {diagram: 'checkpoint', title: 'Checkpoint recovery', 
description: 'Resume from a completed checkpoint after a failure.', detail: 
'Capture state with a checkpoint barrier.', note: 'Exactly-once requires 
compatible source and sink support.'},
             ],
         },
         connectors: {
@@ -670,17 +657,6 @@ function handleFeatureGlow(event) {
     card.style.setProperty('--my', `${((event.clientY - rect.top) / 
rect.height) * 100}%`);
 }
 
-function prepareMotionDemo(event) {
-    const frameDocument = event.currentTarget.contentDocument;
-    if (!frameDocument) {
-        return;
-    }
-
-    frameDocument.querySelector('main')?.style.setProperty('padding', '0');
-    frameDocument.querySelector('nav')?.setAttribute('hidden', '');
-    frameDocument.querySelector('#hint')?.setAttribute('hidden', '');
-}
-
 function ArrowRightIcon() {
     return (
         <svg viewBox="0 0 24 24" aria-hidden="true">
@@ -754,10 +730,6 @@ export default function Home() {
     const architectureEngineRef = useRef(null);
     const architectureSourceRefs = useRef([]);
     const architectureTargetRefs = useRef([]);
-    const motionFrameRef = useRef(null);
-    const [motionDemoReady, setMotionDemoReady] = useState(false);
-    const [motionPlaying, setMotionPlaying] = useState(true);
-    const [motionFocus, setMotionFocus] = useState(-1);
 
     const language = isBrowser
         ? (window.location.pathname.startsWith('/zh-CN/') ? 'zh-CN' : 'en')
@@ -770,48 +742,6 @@ export default function Home() {
     const quickStartPath = useBaseUrl(localizePath(language, 
`/docs/${version}/getting-started/locally/quick-start-seatunnel-engine`));
     const connectorsPath = useBaseUrl(localizePath(language, 
`/docs/${version}/connectors/source`));
     const logoPath = `${assetRoot}image/logo.png`;
-    const motionDemoPath = 
`${useBaseUrl('/home/seatunnel-motion.html')}?locale=${language}`;
-
-    const getMotionDemo = () => 
motionFrameRef.current?.contentWindow?.SeaTunnelMotion;
-
-    const syncMotionDemoState = () => {
-        const motionDemo = getMotionDemo();
-        if (!motionDemo) {
-            return;
-        }
-        setMotionPlaying(motionDemo.state.playing);
-        setMotionFocus(motionDemo.state.focus);
-    };
-
-    const handleMotionDemoLoad = (event) => {
-        prepareMotionDemo(event);
-        setMotionDemoReady(true);
-        syncMotionDemoState();
-
-        const frameDocument = event.currentTarget.contentDocument;
-        const scheduleMotionDemoStateSync = () => 
window.requestAnimationFrame(syncMotionDemoState);
-        frameDocument?.addEventListener('click', scheduleMotionDemoStateSync, 
true);
-        frameDocument?.addEventListener('keydown', 
scheduleMotionDemoStateSync, true);
-    };
-
-    const toggleMotionDemoPlayback = () => {
-        const motionDemo = getMotionDemo();
-        if (!motionDemo) {
-            return;
-        }
-        motionDemo.state.playing = !motionDemo.state.playing;
-        setMotionPlaying(motionDemo.state.playing);
-    };
-
-    const focusMotionDemo = (index) => {
-        const motionDemo = getMotionDemo();
-        if (!motionDemo) {
-            return;
-        }
-        motionDemo.setFocus(index);
-        setMotionFocus(index);
-    };
-
     useEffect(() => {
         if (!isBrowser || !pageRef.current) {
             return undefined;
@@ -1699,68 +1629,9 @@ export default function Home() {
 
             <section className="st-home-section st-home-motion-section">
                 <div className="st-home-container">
-                    <p className="st-home-eyebrow 
st-home-rv">{content.motion.eyebrow}</p>
                     <h2 className="st-home-section-title 
st-home-rv">{content.motion.title}</h2>
                     <p className="st-home-section-lead 
st-home-rv">{content.motion.lead}</p>
-                    <a
-                        href={motionDemoPath}
-                        className="st-home-motion-open st-home-rv"
-                        target="_blank"
-                        rel="noreferrer"
-                    >
-                        {content.motion.openButton} <ArrowRightIcon />
-                    </a>
-                    <div className="st-home-motion-controls st-home-rv" 
role="group" aria-label={content.motion.controls.label}>
-                        <button
-                            type="button"
-                            className="st-home-motion-control"
-                            disabled={!motionDemoReady}
-                            onClick={toggleMotionDemoPlayback}
-                        >
-                            {motionPlaying ? content.motion.controls.pause : 
content.motion.controls.play}
-                        </button>
-                        <button
-                            type="button"
-                            className={`st-home-motion-control${motionFocus 
=== -1 ? ' is-active' : ''}`}
-                            disabled={!motionDemoReady}
-                            aria-pressed={motionFocus === -1}
-                            onClick={() => focusMotionDemo(-1)}
-                        >
-                            {content.motion.controls.overview}
-                        </button>
-                        {content.motion.mechanisms.map((mechanism, index) => (
-                            <button
-                                type="button"
-                                
className={`st-home-motion-control${motionFocus === index ? ' is-active' : ''}`}
-                                disabled={!motionDemoReady}
-                                aria-pressed={motionFocus === index}
-                                key={mechanism.title}
-                                onClick={() => focusMotionDemo(index)}
-                            >
-                                {mechanism.title}
-                            </button>
-                        ))}
-                    </div>
-                    <div className="st-home-motion-frame st-home-rv">
-                        <iframe
-                            ref={motionFrameRef}
-                            className="st-home-motion-iframe"
-                            src={motionDemoPath}
-                            title={content.motion.frameTitle}
-                            loading="lazy"
-                            allowFullScreen
-                            onLoad={handleMotionDemoLoad}
-                        ></iframe>
-                    </div>
-                    <ul className="st-home-motion-mobile-grid">
-                        {content.motion.mechanisms.map((mechanism, index) => (
-                            <li className="st-home-motion-mobile-card" 
key={mechanism.title}>
-                                <span>{String(index + 1).padStart(2, 
'0')}</span>
-                                <h3>{mechanism.title}</h3>
-                                <p>{mechanism.description}</p>
-                            </li>
-                        ))}
-                    </ul>
+                    <MotionMechanismGrid {...content.motion} />
                 </div>
             </section>
 
diff --git a/src/pages/home/index.less b/src/pages/home/index.less
index 4e780d50a804..e6b860ed8238 100644
--- a/src/pages/home/index.less
+++ b/src/pages/home/index.less
@@ -967,87 +967,261 @@ html[data-theme='light'] .st-home {
 }
 
 .st-home-motion-section {
+  --motion-card: rgba(16, 29, 57, 0.88);
+  --motion-stage: rgba(6, 15, 32, 0.62);
+  --motion-node: rgba(18, 34, 65, 0.96);
+  --motion-ink: #eef4ff;
+  --motion-muted: #a9bdd2;
+  --motion-line: rgba(166, 203, 232, 0.28);
+  --motion-soft: rgba(93, 184, 226, 0.14);
+  --motion-blue: #5db8e2;
+  --motion-teal: #52cbbf;
+  --motion-on-blue: #082137;
+  --motion-on-teal: #061f2a;
+  --motion-teal-text: #6ee5d7;
+  --motion-highlight: rgba(82, 203, 191, 0.14);
   background: var(--bg1);
 }
 
-.st-home-motion-open {
-  display: inline-flex;
-  align-items: center;
-  gap: 0.5rem;
-  margin-top: 1.5rem;
-  color: var(--t1);
-  font-size: 0.94rem;
-  font-weight: 700;
-  transition: color 0.2s ease, transform 0.2s ease;
+.st-home-motion-section .st-home-section-title {
+  max-width: 860px;
 }
 
-.st-home-motion-open:hover {
-  color: #7dd3fc;
-  transform: translateX(3px);
+.st-home-motion-section .st-home-section-lead {
+  max-width: 760px;
 }
 
-.st-home-motion-open svg {
-  width: 1.125rem;
-  height: 1.125rem;
+.st-home-motion-grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 1.15rem;
+  margin: 3rem 0 0;
+  padding: 0;
+  list-style: none;
 }
 
-.st-home-motion-controls {
+.st-home-motion-card {
+  min-width: 0;
+  border: 1px solid var(--motion-line);
+  border-radius: 1.2rem;
+  background: var(--motion-card);
+  box-shadow: 0 16px 32px rgba(0, 0, 0, 0.16);
+}
+
+.st-home-motion-card-schema {
+  border-color: var(--motion-teal);
+}
+
+.st-home-motion-card article {
   display: flex;
-  flex-wrap: wrap;
-  gap: 0.625rem;
-  margin-top: 1.5rem;
+  min-height: 100%;
+  flex-direction: column;
+  padding: 1.25rem;
 }
 
-.st-home-motion-control {
-  padding: 0.5rem 0.75rem;
-  border: 1px solid rgba(137, 161, 199, 0.26);
-  border-radius: 999px;
-  background: rgba(255, 255, 255, 0.06);
-  color: var(--t2);
-  cursor: pointer;
-  font: inherit;
-  font-size: 0.82rem;
-  font-weight: 600;
-  line-height: 1.3;
-  transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
+.st-home-motion-card-heading {
+  display: grid;
+  grid-template-columns: auto minmax(0, 1fr);
+  gap: 0.7rem;
+  align-items: start;
 }
 
-.st-home-motion-control:hover,
-.st-home-motion-control:focus-visible,
-.st-home-motion-control.is-active {
-  border-color: rgba(107, 198, 242, 0.72);
-  background: rgba(93, 184, 226, 0.16);
-  color: var(--t1);
-  outline: none;
+.st-home-motion-card-heading > span {
+  display: grid;
+  width: 2rem;
+  height: 2rem;
+  place-items: center;
+  border-radius: 0.6rem;
+  background: var(--motion-soft);
+  color: var(--motion-blue);
+  font-family: var(--ifm-font-family-monospace);
+  font-size: 0.76rem;
+  font-weight: 700;
+}
+
+.st-home-motion-card h3,
+.st-home-motion-card-heading p,
+.st-home-motion-card-detail,
+.st-home-motion-card-note,
+.st-home-motion-caption {
+  margin: 0;
 }
 
-.st-home-motion-control:disabled {
-  cursor: wait;
-  opacity: 0.55;
+.st-home-motion-card h3 {
+  color: var(--motion-ink);
+  font-size: 1.16rem;
+  font-weight: 750;
+  letter-spacing: -0.025em;
+  line-height: 1.2;
 }
 
-.st-home-motion-frame {
-  aspect-ratio: 16 / 9;
-  margin-top: 3rem;
-  padding: 0.5rem;
+.st-home-motion-card-heading p {
+  margin-top: 0.28rem;
+  color: var(--motion-muted);
+  font-size: 0.86rem;
+  line-height: 1.45;
+}
+
+.st-home-motion-visual {
+  display: flex;
+  align-items: center;
+  min-height: 10.35rem;
+  margin: 1rem 0 0.85rem;
+  padding: 0.45rem;
   overflow: hidden;
-  border: 1px solid rgba(108, 177, 235, 0.24);
-  border-radius: 1.75rem;
-  background: #e9f0f4;
-  box-shadow: 0 28px 64px rgba(2, 8, 19, 0.3);
+  border-radius: 0.8rem;
+  background: var(--motion-stage);
 }
 
-.st-home-motion-iframe {
+.st-home-motion-diagram {
   display: block;
   width: 100%;
-  height: 100%;
-  border: 0;
-  border-radius: 1.3rem;
-  background: #e9f0f4;
+  height: auto;
 }
 
-.st-home-motion-mobile-grid {
-  display: none;
+.st-home-motion-diagram text {
+  font-family: var(--ifm-font-family-base);
+}
+
+.st-home-motion-diagram-label {
+  fill: var(--motion-muted);
+  font-size: 10px;
+  font-weight: 700;
+}
+
+.st-home-motion-diagram-copy,
+.st-home-motion-diagram-strong {
+  fill: var(--motion-ink);
+  font-size: 13px;
+  font-weight: 550;
+}
+
+.st-home-motion-diagram-strong {
+  font-size: 15px;
+  font-weight: 700;
+}
+
+.st-home-motion-diagram-muted-copy {
+  fill: var(--motion-muted);
+  font-size: 12px;
+}
+
+.st-home-motion-diagram-code {
+  fill: var(--motion-muted);
+  font-family: var(--ifm-font-family-monospace) !important;
+  font-size: 11px;
+}
+
+.st-home-motion-diagram-teal-copy {
+  fill: var(--motion-teal-text);
+  font-size: 12px;
+  font-weight: 700;
+}
+
+.st-home-motion-diagram-accent-text {
+  fill: var(--motion-blue);
+  font-size: 10px;
+  font-weight: 750;
+}
+
+.st-home-motion-diagram-on-blue,
+.st-home-motion-diagram-on-teal,
+.st-home-motion-diagram-chip-text {
+  font-size: 11px;
+  font-weight: 700;
+}
+
+.st-home-motion-diagram-on-blue {
+  fill: var(--motion-on-blue);
+}
+
+.st-home-motion-diagram-on-teal {
+  fill: var(--motion-on-teal);
+}
+
+.st-home-motion-diagram-chip-text {
+  fill: var(--motion-blue);
+}
+
+.st-home-motion-diagram-sql {
+  font-size: 20px;
+}
+
+.st-home-motion-diagram-node {
+  fill: var(--motion-node);
+  stroke: var(--motion-line);
+  stroke-width: 1.2;
+}
+
+.st-home-motion-diagram-line,
+.st-home-motion-diagram-arrow {
+  stroke: var(--motion-line);
+  stroke-linecap: round;
+  stroke-linejoin: round;
+  stroke-width: 1.6;
+}
+
+.st-home-motion-diagram-soft,
+.st-home-motion-diagram-chip {
+  fill: var(--motion-soft);
+}
+
+.st-home-motion-diagram-packet,
+.st-home-motion-diagram-blue {
+  fill: var(--motion-blue);
+}
+
+.st-home-motion-diagram-teal {
+  fill: var(--motion-teal);
+}
+
+.st-home-motion-diagram-blue-fill {
+  fill: var(--motion-blue);
+}
+
+.st-home-motion-diagram-teal-fill {
+  fill: var(--motion-teal);
+}
+
+.st-home-motion-diagram-table {
+  fill: none;
+  stroke: var(--motion-blue);
+  stroke-width: 1.4;
+}
+
+.st-home-motion-diagram-highlight {
+  fill: var(--motion-highlight);
+}
+
+.st-home-motion-card-detail {
+  color: var(--motion-blue);
+  font-size: 0.82rem;
+  font-weight: 700;
+  line-height: 1.45;
+}
+
+.st-home-motion-card-detail::before {
+  display: inline-block;
+  margin-right: 0.45rem;
+  color: var(--motion-teal);
+  content: '•';
+}
+
+.st-home-motion-card-note {
+  margin-top: 0.55rem;
+  padding-top: 0.55rem;
+  border-top: 1px solid var(--motion-line);
+  color: var(--motion-muted);
+  font-size: 0.75rem;
+  line-height: 1.4;
+}
+
+.st-home-motion-caption {
+  max-width: 760px;
+  margin-top: 1.25rem;
+  color: var(--motion-muted);
+  font-size: 0.8rem;
+  line-height: 1.5;
 }
 
 .st-home-architecture .st-home-gradient {
@@ -2523,34 +2697,24 @@ html[data-theme='light'] .st-home 
.st-home-features-section {
 }
 
 html[data-theme='light'] .st-home .st-home-motion-section {
+  --motion-card: rgba(255, 255, 255, 0.88);
+  --motion-stage: #f4f8fb;
+  --motion-node: #fff;
+  --motion-ink: #17374b;
+  --motion-muted: #526c7c;
+  --motion-line: #d7e5eb;
+  --motion-soft: #eaf6f7;
+  --motion-blue: #2469a6;
+  --motion-teal: #30adb2;
+  --motion-on-blue: #fff;
+  --motion-on-teal: #0d3945;
+  --motion-teal-text: #07777b;
+  --motion-highlight: #dff3f1;
   background: linear-gradient(180deg, #e7eef8 0%, #f4f8fd 100%);
 }
 
-html[data-theme='light'] .st-home .st-home-motion-open {
-  color: #315a83;
-}
-
-html[data-theme='light'] .st-home .st-home-motion-open:hover {
-  color: #1d78a8;
-}
-
-html[data-theme='light'] .st-home .st-home-motion-control {
-  border-color: rgba(90, 150, 230, 0.2);
-  background: rgba(255, 255, 255, 0.72);
-  color: #4e6886;
-}
-
-html[data-theme='light'] .st-home .st-home-motion-control:hover,
-html[data-theme='light'] .st-home .st-home-motion-control:focus-visible,
-html[data-theme='light'] .st-home .st-home-motion-control.is-active {
-  border-color: rgba(72, 153, 211, 0.58);
-  background: rgba(216, 239, 253, 0.88);
-  color: #254f78;
-}
-
-html[data-theme='light'] .st-home .st-home-motion-frame {
-  border-color: rgba(90, 150, 230, 0.26);
-  box-shadow: 0 28px 58px rgba(87, 122, 177, 0.18);
+html[data-theme='light'] .st-home .st-home-motion-card {
+  box-shadow: 0 14px 30px rgba(87, 122, 177, 0.1);
 }
 
 html[data-theme='light'] .st-home .st-home-architecture {
@@ -2841,71 +3005,23 @@ html[data-theme='light'] .st-home .st-home-rv {
     padding: 5.5rem 1rem;
   }
 
-  .st-home-motion-frame {
-    margin-top: 2.25rem;
-    padding: 0.35rem;
-    border-radius: 1.1rem;
-  }
-
-  .st-home-motion-iframe {
-    border-radius: 0.8rem;
-  }
-
-  .st-home-motion-open,
-  .st-home-motion-controls,
-  .st-home-motion-frame {
-    display: none;
-  }
-
-  .st-home-motion-mobile-grid {
+  .st-home-motion-grid {
     display: grid;
     grid-template-columns: repeat(2, minmax(0, 1fr));
     gap: 0.875rem;
     margin: 2.25rem 0 0;
-    padding: 0;
-    list-style: none;
   }
 
-  .st-home-motion-mobile-card {
-    min-height: 9.5rem;
+  .st-home-motion-card article {
     padding: 1rem;
-    border: 1px solid rgba(137, 161, 199, 0.18);
-    border-radius: 1rem;
-    background: rgba(255, 255, 255, 0.05);
-  }
-
-  .st-home-motion-mobile-card span {
-    display: block;
-    margin-bottom: 0.65rem;
-    color: #69c2ea;
-    font-family: var(--ifm-font-family-monospace);
-    font-size: 0.72rem;
-    font-weight: 700;
-    letter-spacing: 0.1em;
-  }
-
-  .st-home-motion-mobile-card h3 {
-    margin: 0;
-    color: var(--t1);
-    font-size: 0.95rem;
-    line-height: 1.35;
   }
 
-  .st-home-motion-mobile-card p {
-    margin: 0.5rem 0 0;
-    color: var(--t2);
-    font-size: 0.8rem;
-    line-height: 1.5;
-  }
-
-  html[data-theme='light'] .st-home .st-home-motion-mobile-card {
-    border-color: rgba(90, 150, 230, 0.16);
-    background: rgba(255, 255, 255, 0.78);
-    box-shadow: 0 14px 30px rgba(87, 122, 177, 0.1);
+  .st-home-motion-visual {
+    min-height: auto;
   }
 
-  html[data-theme='light'] .st-home .st-home-motion-mobile-card span {
-    color: #4f9bc6;
+  .st-home-motion-card h3 {
+    font-size: 1.06rem;
   }
 
   .st-home-cta-section {
@@ -2930,7 +3046,7 @@ html[data-theme='light'] .st-home .st-home-rv {
     grid-template-columns: 1fr;
   }
 
-  .st-home-motion-mobile-grid {
+  .st-home-motion-grid {
     grid-template-columns: 1fr;
   }
 }
diff --git a/static/home/seatunnel-motion.html 
b/static/home/seatunnel-motion.html
deleted file mode 100644
index 5c9050596062..000000000000
--- a/static/home/seatunnel-motion.html
+++ /dev/null
@@ -1,329 +0,0 @@
-<!--
-  Licensed to the Apache Software Foundation (ASF) under one or more
-  contributor license agreements.  See the NOTICE file distributed with
-  this work for additional information regarding copyright ownership.
-  The ASF licenses this file to You under the Apache License, Version 2.0
-  (the "License"); you may not use this file except in compliance with
-  the License.  You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
--->
-<!doctype html><html lang="en"><head><meta charset="utf-8"><meta 
name="viewport" content="width=device-width,initial-scale=1"><meta 
name="color-scheme" content="light"><title>Apache SeaTunnel · Data integration 
in motion</title><style>
-*{box-sizing:border-box}body{margin:0;background:#e9f0f4;color:#17374b;font-family:Inter,Arial,"PingFang
 SC","Microsoft 
YaHei",sans-serif}main{max-width:1920px;margin:auto;padding:20px}#player{background:#f4f8fa;border:1px
 solid #d5e3e9;border-radius:14px;overflow:hidden;box-shadow:0 16px 60px 
#16364714}#film{display:block;width:100%;height:auto;aspect-ratio:16/9;cursor:zoom-in}#player:fullscreen{width:100vw;height:100vh;display:flex;align-items:center;border:none;background:#f4f8fa}#pla
 [...]
-
-</style></head><body><main><section id="player" aria-label="SeaTunnel motion 
player"><canvas id="film" width="1920" height="1080" tabindex="0" role="img" 
aria-label="Apache SeaTunnel six-panel motion graphic: parallel reads, snapshot 
and CDC, row transforms, multi-table routing, schema evolution, and checkpoint 
recovery. Select any panel to enlarge it."></canvas></section><nav 
aria-label="Playback controls"><button id="play">Pause</button><button 
id="restart">Restart</button><button id=" [...]
- * Original vector animation; every frame is determined by time t and supports 
offline playback, seeking, and frame export.
- * Technical illustration, not runtime metrics. The six panels are independent 
examples, not six sequential job stages.
- * Uses system fonts only; no font files are bundled or downloaded.
- */
-'use strict';
-const W = 1920, H = 1080, DURATION = 24, PW = 584, PH = 326;
-const C = {ink:'#17374B',muted:'#526C7C',subtle:'#75909E',line:'#D7E5EB',
-  
blue:'#2469A6',teal:'#30ADB2',dark:'#215F91',soft:'#EAF6F7',blueSoft:'#EDF3FA',
-  paper:'#FFFFFF',bg:'#F4F8FA',stage:'#F7FAFC'};
-const TITLES = ['Parallel batch reads','Snapshot + CDC','Row-level 
transforms','Multi-table routing','Schema evolution','Checkpoint recovery'];
-const SUBTITLES = ['Divide the work. Read splits in parallel.','Snapshot 
first. Then capture binlog changes.','Compute fields and filter individual 
rows.','Keep each record on the right table path.','Propagate supported table 
changes.','Restart from a completed checkpoint.'];
-const TAGS = ['BATCH','CDC','TRANSFORM','MULTI-TABLE','DDL','RECOVERY'];
-const NOTES = ['JDBC example · split assignment is source-specific.',
- 'MySQL-CDC example · startup.mode = initial',
- 'SQL example · expression + row filter',
- 'Logical routing · physical connections may differ.',
- 'Zeta example · MySQL-CDC → JDBC MySQL',
- 'Exactly-once requires compatible source / sink support.'];
-const CAPTIONS = [
- ['PARALLELISM','Split the input, not the meaning.','Independent readers 
process assigned source splits.'],
- ['CHANGE CAPTURE','Bootstrap once. Keep the changes moving.','The example 
includes INSERT, UPDATE and DELETE.'],
- ['ROW TRANSFORMATION','Shape every record before it lands.','A field 
expression and a filter operate on individual rows.'],
- ['TABLE IDENTITY','One job. Distinct table destinations.','The table identity 
determines the matching sink writer.'],
- ['SCHEMA CHANGE','A table changes. The supported path adapts.','DDL is 
applied before the example sends a row with the new column.'],
- ['RECOVERY','A failure is not a reason to start from zero.','Restore 
completed checkpoint state; source history must still be available.']
-];
-const canvas = document.getElementById('film'), ctx = 
canvas.getContext('2d',{alpha:false});
-const logo = new Image(); logo.src = LOGO_DATA;
-const MOTION_LOCALE = 'en';
-const UI = {
-  title: 'Apache SeaTunnel · Data integration in motion', player: 'SeaTunnel 
motion player',
-  canvas: 'Apache SeaTunnel animated overview: parallel reads, snapshot and 
CDC, row transforms, multi-table routing, schema evolution, and checkpoint 
recovery. Click or use number keys to focus a card.',
-  controls: 'Playback controls', pause: 'Pause', play: 'Play', restart: 
'Restart', overview: 'All mechanisms', fullscreen: 'Fullscreen', speed: 
'Speed', timeline: 'Playback progress',
-  hint: 'Click a card to focus it; click again to return to the overview. 
Space: pause / play; R: restart; 1–6: focus. This offline animation does not 
connect to a database.',
-  fullscreenUnavailable: 'Fullscreen is unavailable in this browser; playback 
is still available.', assetLoadFailed: 'Unable to load animation assets: '
-};
-document.documentElement.lang = MOTION_LOCALE;
-document.title = UI.title;
-document.getElementById('player').setAttribute('aria-label', UI.player);
-canvas.setAttribute('aria-label', UI.canvas);
-document.querySelector('nav').setAttribute('aria-label', UI.controls);
-document.getElementById('play').textContent = UI.pause;
-document.getElementById('restart').textContent = UI.restart;
-document.getElementById('back').textContent = UI.overview;
-document.getElementById('fullscreen').textContent = UI.fullscreen;
-document.getElementById('speed').setAttribute('aria-label', UI.speed);
-document.getElementById('timeline').setAttribute('aria-label', UI.timeline);
-const speedLabel = document.getElementById('speed').closest('label');
-if (speedLabel?.firstChild) speedLabel.firstChild.textContent = UI.speed + ' ';
-document.getElementById('hint').textContent = UI.hint;
-const IS_EXPORT=Boolean(window.__MOTION_EXPORT__) || new 
URLSearchParams(location.search).has('export');
-const state = 
{time:0,playing:!IS_EXPORT&&!window.matchMedia('(prefers-reduced-motion: 
reduce)').matches,speed:1,focus:-1,last:0,raf:0};
-const telemetry = {errors:[],textOverflow:[],ready:false};
-let staticCanvas=null,staticKey='';
-function clamp(n,a=0,b=1){return Math.min(b,Math.max(a,n));}
-function smooth(n){n=clamp(n);return n*n*(3-2*n);}
-function lerp(a,b,p){return a+(b-a)*p;}
-function phase(t){return ((t%8)+8)%8;}
-function cycleOpacity(q){return smooth(q/.25)*(1-smooth((q-7.55)/.4));}
-function rr(g,x,y,w,h,r=10,fill=C.paper,stroke=null,lw=1.5){
-  
if(w<=0||h<=0)return;g.beginPath();g.roundRect(x,y,w,h,r);if(fill){g.fillStyle=fill;g.fill();}if(stroke){g.strokeStyle=stroke;g.lineWidth=lw;g.stroke();}
-}
-function 
line(g,x1,y1,x2,y2,color=C.line,lw=2,dash=[]){g.beginPath();g.setLineDash(dash);g.moveTo(x1,y1);g.lineTo(x2,y2);g.strokeStyle=color;g.lineWidth=lw;g.stroke();g.setLineDash([]);}
-function 
txt(g,s,x,y,size=18,color=C.ink,weight=400,align='left',max=null,mono=false){
-  g.font=`${weight} ${size}px ${mono?'"DejaVu Sans 
Mono",Consolas,monospace':'"Inter Display",Inter,Arial,sans-serif'}`;
-  g.textAlign=align;g.textBaseline='alphabetic';g.fillStyle=color;
-  const width=g.measureText(s).width;
-  if(max&&width>max+.6){const key=s+' @ 
'+size;if(!telemetry.textOverflow.includes(key))telemetry.textOverflow.push(key);}
-  g.fillText(s,x,y);
-}
-function 
circle(g,x,y,r,fill,stroke=null,lw=1.5){g.beginPath();g.arc(x,y,r,0,Math.PI*2);if(fill){g.fillStyle=fill;g.fill();}if(stroke){g.strokeStyle=stroke;g.lineWidth=lw;g.stroke();}}
-function 
check(g,x,y,s=1,col=C.teal){g.beginPath();g.moveTo(x-5*s,y);g.lineTo(x-1*s,y+4*s);g.lineTo(x+7*s,y-5*s);g.lineCap='round';g.lineJoin='round';g.lineWidth=2.4*s;g.strokeStyle=col;g.stroke();}
-function 
arrowHead(g,x,y,angle=0,col=C.subtle,size=5){g.save();g.translate(x,y);g.rotate(angle);g.beginPath();g.moveTo(-size,-size*.72);g.lineTo(0,0);g.lineTo(-size,size*.72);g.strokeStyle=col;g.lineWidth=1.8;g.stroke();g.restore();}
-function bezierPoint(p,a){const u=1-p;return 
{x:u*u*u*a[0]+3*u*u*p*a[2]+3*u*p*p*a[4]+p*p*p*a[6],y:u*u*u*a[1]+3*u*u*p*a[3]+3*u*p*p*a[5]+p*p*p*a[7]};}
-function 
curve(g,a,col=C.line,lw=2,dash=[]){g.beginPath();g.moveTo(a[0],a[1]);g.bezierCurveTo(a[2],a[3],a[4],a[5],a[6],a[7]);g.lineWidth=lw;g.strokeStyle=col;g.setLineDash(dash);g.stroke();g.setLineDash([]);}
-function packet(g,a,p,label='',col=C.blue,w=39){
-  if(p<0||p>1)return;const d=bezierPoint(p,a);g.save();
-  if(!label){circle(g,d.x,d.y,7,col);circle(g,d.x,d.y,3,C.paper);}
-  
else{rr(g,d.x-w/2,d.y-12,w,24,7,col);txt(g,label,d.x,d.y+5,13.5,C.paper,650,'center',w-5);}
-  g.restore();
-}
-function 
pill(g,label,x,y,w,col=C.blue,fill=C.blueSoft,size=14){rr(g,x,y,w,26,13,fill);txt(g,label,x+w/2,y+18,size,col,650,'center',w-12);}
-function statusLine(g,label,kind='blue'){
- const color=kind==='teal'?C.teal:C.blue;
- circle(g,38,281,3.7,color);txt(g,label,49,287,16,color,600,'left',504);
-}
-function dbIcon(g,x,y,w=62,h=75,col=C.blue){
- const 
eh=12;g.beginPath();g.moveTo(x,y+eh/2);g.lineTo(x,y+h-eh/2);g.ellipse(x+w/2,y+h-eh/2,w/2,eh/2,0,Math.PI,0,true);g.lineTo(x+w,y+eh/2);g.fillStyle=C.blueSoft;g.fill();g.strokeStyle=col;g.lineWidth=2;g.stroke();
- 
g.beginPath();g.ellipse(x+w/2,y+eh/2,w/2,eh/2,0,0,Math.PI*2);g.fillStyle=C.paper;g.fill();g.stroke();
- for(let 
i=1;i<=2;i++){g.beginPath();g.ellipse(x+w/2,y+eh/2+(h-eh)*i/3,w/2,eh/2,0,0,Math.PI);g.stroke();}
-}
-function 
tableIcon(g,x,y,col=C.blue){rr(g,x,y,20,17,3,null,col,1.4);line(g,x,y+6,x+20,y+6,col,1.2);line(g,x+7,y+6,x+7,y+17,col,1.2);line(g,x+13,y+6,x+13,y+17,col,1.2);}
-const CARD_POS = 
Array.from({length:6},(_,i)=>[64+(i%3)*604,251+Math.floor(i/3)*350]);
-function activeIndex(t){return Math.floor(((t%DURATION)+DURATION)%DURATION/4);}
-function drawBrand(g){
- // The repository logo is cropped only to remove fully transparent padding; 
it is neither redrawn nor recolored.
- if(logo.complete&&logo.naturalWidth)g.drawImage(logo,1535,45,77,96);
- 
txt(g,'Apache',1628,79,21,C.muted,500);txt(g,'SeaTunnel',1626,122,43,C.ink,700,'left',239);
-}
-function drawHeader(g,focus=-1){
- txt(g,'APACHE SEATUNNEL   /   ENGINEERING IN MOTION',66,58,17,C.blue,650);
- txt(g,focus<0?'Data integration, in 
motion.':TITLES[focus],62,133,66,C.ink,650,'left',1436);
- txt(g,focus<0?'Six mechanisms behind the data integration 
layer.':SUBTITLES[focus],66,182,25,C.muted,400,'left',1420);
- drawBrand(g);line(g,64,217,1856,217,C.line,1.5);
-}
-function drawBasePanel(g,i){
- // Shadows and information layers are intentionally restrained and cached 
separately from the internal data animation.
- 
g.save();g.shadowColor='#244D6910';g.shadowBlur=15;g.shadowOffsetY=4;rr(g,0,0,PW,PH,18,C.paper);g.restore();rr(g,0,0,PW,PH,18,null,C.line,1.4);
- 
rr(g,24,24,34,34,10,C.soft);txt(g,String(i+1).padStart(2,'0'),41,47,17,C.blue,700,'center');
- txt(g,TITLES[i],71,49,27,C.ink,650,'left',421);
- txt(g,SUBTITLES[i],26,81,18.5,C.muted,400,'left',534);
- rr(g,22,103,PW-44,161,11,C.stage);
- line(g,26,300,PW-26,300,C.line,1);
- txt(g,NOTES[i],27,318,15,C.muted,450,'left',PW-54);
-}
-function staticBackground(focus){
- const key=`${canvas.width}x${canvas.height}:${focus}`;
- if(staticCanvas&&staticKey===key)return staticCanvas;
- 
staticKey=key;staticCanvas=document.createElement('canvas');staticCanvas.width=canvas.width;staticCanvas.height=canvas.height;
- const 
g=staticCanvas.getContext('2d',{alpha:false});g.scale(canvas.width/W,canvas.height/H);
- 
g.fillStyle=C.bg;g.fillRect(0,0,W,H);g.fillStyle=C.paper;g.fillRect(0,0,W,218);drawHeader(g,focus);
- if(focus<0){for(let 
i=0;i<6;i++){g.save();g.translate(...CARD_POS[i]);drawBasePanel(g,i);g.restore();}}
- else{const 
sc=2.03;g.save();g.translate(75,269);g.scale(sc,sc);drawBasePanel(g,focus);g.restore();}
- line(g,64,955,1856,955,C.line,1.5);
- txt(g,'Apache SeaTunnel is an open-source, distributed data integration 
platform.',65,995,22,C.ink,500,'left',1780);
- txt(g,'Independent conceptual examples. Capabilities and delivery guarantees 
depend on connectors, engine and 
configuration.',65,1034,17.5,C.muted,400,'left',1660);
- txt(g,'24s LOOP',1854,1034,15,C.blue,650,'right');
- return staticCanvas;
-}
-function javaHash(s){let n=0;for(const ch of 
s)n=(Math.imul(31,n)+ch.charCodeAt(0))|0;return n;}
-const 
splitRecords=Array.from({length:6},(_,i)=>({id:'S'+(i+1),owner:(javaHash('split-'+i)&2147483647)%3,start:.35+i*.62}));
-function drawParallel(g,q){
- txt(g,'SOURCE 
SPLITS',37,124,12.5,C.muted,650);txt(g,'READERS',230,124,12.5,C.muted,650);txt(g,'TARGET',464,124,12.5,C.muted,650);
- const ys=[152,190,228],colors=[C.blue,C.teal,C.dark];
- for(let r=0;r<3;r++){
-  curve(g,[150,189,177,189,186,ys[r],218,ys[r]],C.line,2);
-  const 
out=[343,ys[r],385,ys[r],405,190,456,190];curve(g,out,C.line,2);arrowHead(g,452,190,0,C.subtle,4);
-  
rr(g,219,ys[r]-14,123,28,8,C.paper,colors[r],1.4);circle(g,235,ys[r],3.3,colors[r]);txt(g,'Reader
 '+(r+1),248,ys[r]+5,16,C.ink,500);
- }
- dbIcon(g,464,160,59,58);rr(g,34,133,111,119,9,C.paper,C.line,1);
- let done=0;
- for(let i=0;i<6;i++){
-  const 
s=splitRecords[i],x=43+(i%2)*49,y=146+Math.floor(i/2)*35,p=(q-s.start)/1.25,arrive=s.start+2.5;
-  const waiting=q<s.start,finished=q>=arrive;
-  rr(g,x,y,41,26,6,waiting?C.blueSoft:C.stage,waiting?C.line:null,1);
-  txt(g,s.id,x+20.5,y+18,14,waiting?colors[s.owner]:C.subtle,650,'center');
-  const a=[150,189,177,189,186,ys[s.owner],218,ys[s.owner]];
-  const b=[343,ys[s.owner],385,ys[s.owner],405,190,456,190];
-  
packet(g,a,p,s.id,colors[s.owner]);packet(g,b,(q-s.start-1.25)/1.25,s.id,colors[s.owner]);
-  if(finished)done++;
- }
- pill(g,`${done} / 6`,458,227,72,C.blue,C.blueSoft,13.5);
- statusLine(g,done===6?'6 illustrated splits delivered.':'Assigned splits move 
through 3 readers.',done===6?'teal':'blue');
-}
-const CDC_EVENTS=[
- {start:.4,arrive:1.3,op:'S',id:41,value:'NEW'},
- {start:1.45,arrive:2.35,op:'S',id:42,value:'NEW'},
- {start:3.0,arrive:3.9,op:'I',id:43,value:'NEW'},
- {start:4.2,arrive:5.1,op:'U',id:42,value:'PAID'},
- {start:5.55,arrive:6.45,op:'D',id:41,value:null}
-];
-function cdcModel(q){
- const source=new Map([[41,'NEW'],[42,'NEW']]),target=new Map();
- for(const e of 
CDC_EVENTS){if(e.op!=='S'&&q>=e.start){if(e.op==='D')source.delete(e.id);else 
source.set(e.id,e.value);}if(q>=e.arrive){if(e.op==='D')target.delete(e.id);else
 target.set(e.id,e.value);}}
- return {source:[...source],target:[...target]};
-}
-function recordsTable(g,x,y,w,label,rows){
- txt(g,label,x,y-9,14,C.muted,600);rr(g,x,y,w,108,9,C.paper,C.line,1.2);
- 
rr(g,x+1,y+1,w-2,25,7,C.blueSoft);txt(g,'id',x+12,y+18,13.5,C.muted,600);txt(g,'status',x+58,y+18,13.5,C.muted,600);
- for(let i=0;i<rows.length;i++){const 
[id,value]=rows[i],yy=y+45+i*25;if(value==='PAID')rr(g,x+5,yy-17,w-10,23,5,C.soft);txt(g,''+id,x+12,yy,15,C.ink,500,'left',40,true);txt(g,value,x+58,yy,14.5,value==='PAID'?C.teal:C.ink,value==='PAID'?650:450);}
- if(!rows.length)txt(g,'waiting…',x+12,y+56,14,C.subtle,400);
-}
-function drawCDC(g,q){
- const m=cdcModel(q);recordsTable(g,33,137,148,'MySQL / 
orders',m.source);recordsTable(g,396,137,154,'Target / orders',m.target);
- const 
a=[208,187,254,187,321,187,367,187];line(g,191,187,386,187,C.line,2.2);arrowHead(g,385,187,0,C.subtle,6);
- for(const e of 
CDC_EVENTS)packet(g,a,(q-e.start)/(e.arrive-e.start),e.op+':'+e.id,e.op==='I'||e.op==='S'?C.teal:C.blue,53);
- 
pill(g,q<2.8?'SNAPSHOT':'BINLOG',235,124,108,q<2.8?C.blue:C.teal,q<2.8?C.blueSoft:C.soft,13);
- txt(g,'SeaTunnel',291,223,14.5,C.muted,500,'center');
- statusLine(g,q<2.8?'Read existing rows.':q<4.1?'INSERT · new row 
43':q<5.55?'UPDATE · row 42 becomes PAID':q<6.45?'DELETE · remove row 
41':'DELETE · row 41 is removed.',q>=6.45?'teal':'blue');
-}
-const 
TRANSFORMS=[{id:1,name:'Ada',start:.45},{id:0,name:'Temp',start:2.35},{id:2,name:'Lin',start:4.25}];
-function transformModel(q){return 
TRANSFORMS.filter(r=>r.id>0&&q>=r.start+1.65).map(r=>[r.id,r.name+'_']);}
-function drawTransform(g,q){
- txt(g,'INPUT',36,124,13,C.muted,650);txt(g,'ROW 
SQL',241,124,13,C.muted,650);txt(g,'OUTPUT',440,124,13,C.muted,650);
- const 
a=[168,173,190,173,204,173,228,173],b=[347,173,367,173,396,173,419,173];curve(g,a);curve(g,b);arrowHead(g,418,173,0,C.subtle);
- 
rr(g,33,137,132,93,9,C.paper,C.line,1.2);rr(g,422,137,128,93,9,C.paper,C.line,1.2);
- for(let i=0;i<3;i++){const r=TRANSFORMS[i];txt(g,`${r.id}  
${r.name}`,44,159+27*i,15.5,r.id===0?C.muted:C.ink,450,'left',111,true);}
- rr(g,231,146,113,57,12,C.blue);txt(g,'Sql',287.5,182,25,C.paper,650,'center');
- let filtered=false;
- for(const r of TRANSFORMS){
-  packet(g,a,(q-r.start)/.65,`${r.id}`,C.blue,28);
-  if(r.id>0)packet(g,b,(q-r.start-.85)/.8,`${r.id}`,C.teal,28);
-  else if(q>=r.start+.65){filtered=true;const 
p=(q-r.start-.65)/.7;packet(g,[287.5,205,287.5,225,287.5,234,287.5,247],p,'0',C.dark,28);}
- }
- const out=transformModel(q);out.forEach((r,i)=>txt(g,`${r[0]} 
${r[1]}`,433,162+i*29,15,C.teal,600,'left',106,true));
- if(!out.length)txt(g,'waiting…',434,169,14,C.subtle);
- 
if(filtered){circle(g,287,239,8,C.paper,C.blue);line(g,282,244,292,234,C.blue,1.7);txt(g,'id
 = 0 filtered',327,244,12.5,C.muted,450);}
- else{txt(g,'CONCAT(name,\'_\')',32,252,13.5,C.muted,450,'left',245,true);}
- statusLine(g,'WHERE id > 0 · CONCAT(name, \'_\')');
-}
-function drawRouting(g,q){
- const 
tables=['users','orders','items'],colors=[C.blue,C.teal,C.dark],ys=[143,192,241];
- for(let i=0;i<3;i++){
-  const 
y=ys[i],a=[168,y,204,y,205,192,247,192],b=[335,192,375,192,381,y,421,y];curve(g,a,C.line,1.7);curve(g,b,C.line,1.7);arrowHead(g,417,y,0,C.subtle,4);
-  
rr(g,32,y-17,135,34,8,C.paper,C.line,1.2);tableIcon(g,43,y-9,colors[i]);txt(g,tables[i],73,y+5,16,C.ink,500,'left',81);
-  
rr(g,423,y-17,130,34,8,C.paper,C.line,1.2);tableIcon(g,434,y-9,colors[i]);txt(g,tables[i],463,y+5,15.5,C.ink,500,'left',82);
-  for(let j=0;j<2;j++){const 
start=.35+i*.5+j*3.0;packet(g,a,(q-start)/1.15,tables[i][0].toUpperCase(),colors[i],25);packet(g,b,(q-start-1.15)/1.15,tables[i][0].toUpperCase(),colors[i],25);}
- }
- 
rr(g,247,151,88,82,13,C.blue);txt(g,'Table',291,180,18,C.paper,650,'center');txt(g,'route',291,204,18,C.paper,650,'center');
- statusLine(g,'Table ID → matching destination writer.');
-}
-function schemaModel(q){return 
{sourceColumns:q>=1.1?3:2,sinkColumns:q>=3.8?3:2,newRowSent:q>=5.0,newRowArrived:q>=6.1};}
-function schemaTable(g,x,y,w,label,cols,added){
- 
txt(g,label,x,y-11,14,C.muted,600,'left',w+20);rr(g,x,y,w,110,9,C.paper,C.line,1.2);
- 
rr(g,x+1,y+1,w-2,25,7,C.blueSoft);tableIcon(g,x+10,y+5,C.blue);txt(g,'customers',x+39,y+19,13.5,C.ink,600,'left',w-43);
- const names=['id       BIGINT','name     VARCHAR','region   VARCHAR'];
- for(let 
i=0;i<cols;i++){if(i===2)rr(g,x+5,y+81,w-10,24,5,C.soft);txt(g,names[i],x+11,y+46+i*26,12.2,i===2?C.teal:C.muted,i===2?650:400,'left',w-16,true);}
- if(added)check(g,x+w-12,y-14,.65,C.teal);
-}
-function drawSchema(g,q){
- const m=schemaModel(q);schemaTable(g,32,138,177,'MySQL 
source',m.sourceColumns,m.sourceColumns===3);schemaTable(g,376,138,177,'JDBC 
MySQL sink',m.sinkColumns,m.sinkColumns===3);
- const 
path=[232,190,266,190,321,190,354,190];curve(g,[219,190,266,190,321,190,366,190],C.line,2);arrowHead(g,365,190,0,C.subtle,5);
- pill(g,'DDL',264,126,60,C.blue,C.blueSoft,13);
- packet(g,path,(q-1.4)/2.4,'ADD',C.blue,43);
- if(q>=5){packet(g,path,(q-5)/1.1,'ROW',C.teal,44);}
- if(q>3.8)txt(g,'applied',294,237,13,C.teal,600,'center');else 
txt(g,'region',294,236,13,C.muted,500,'center');
- statusLine(g,'schema-changes.enabled = true',m.sinkColumns===3?'teal':'blue');
-}
-function checkpointModel(q){return 
{completed:q>=2.7,failed:q>=3.65&&q<5.55,restoring:q>=4.65&&q<5.55,resumed:q>=5.55};}
-function drawCheckpoint(g,q){
- const m=checkpointModel(q),lost=m.failed&&!m.restoring;
- const xs=[34,229,430],ww=[121,129,122];
- 
line(g,156,161,228,161,C.line,2);arrowHead(g,222,161,0,C.subtle,5);line(g,359,161,429,161,C.line,2);arrowHead(g,424,161,0,C.subtle,5);
- for(let i=0;i<3;i++){
-  const 
failed=i===1&&lost;rr(g,xs[i],138,ww[i],47,10,failed?C.ink:C.paper,failed?C.ink:C.line,1.5);
-  txt(g,failed?'Worker 
lost':['Source','Writer','Sink'][i],xs[i]+ww[i]/2,168,failed?15:18,failed?C.paper:C.ink,600,'center',ww[i]-12);
- }
- if(q<1.1||(q>=5.55&&q<7.4)){
-  const p=(q<1.1?q:q-5.55)/.9;for(let 
j=0;j<2;j++){packet(g,[164,161,182,161,202,161,220,161],(p-j*.48)%1,'',C.teal);packet(g,[367,161,381,161,402,161,419,161],(p-j*.48+.2)%1,'',C.teal);}
- }
- // Simplified illustration of distributed barrier propagation and completion; 
it does not model a complete two-phase commit protocol.
- if(q>=1.1&&q<2.7){const 
x=lerp(93,488,(q-1.1)/1.6);line(g,x,126,x,196,C.blue,2.8,[4,3]);pill(g,'BARRIER',x-38,107,76,C.blue,C.blueSoft,11.5);}
- const store=[207,210,177,45];
- 
curve(g,[94,187,94,217,152,232,207,232],m.restoring?C.blue:C.line,1.6,m.restoring?[4,3]:[]);
- curve(g,[294,187,294,197,294,200,294,210],C.line,1.6);
- 
if(m.restoring){packet(g,[207,232,152,232,94,217,94,187],(q-4.65)/.9,'',C.blue);}
- rr(g,...store,9,m.completed?C.soft:C.paper,m.completed?C.teal:C.line,1.3);
- txt(g,m.completed?'Checkpoint #42':'Checkpoint 
state',295.5,230,15,m.completed?C.dark:C.muted,600,'center');
- 
txt(g,m.completed?'COMPLETED':'PENDING',295.5,247,11.5,m.completed?C.teal:C.subtle,650,'center');
- if(m.completed)check(g,367,227,.65,C.teal);
- let message=q<1.1?'Running · source and sink connected.':q<2.7?'Capture state 
with a checkpoint barrier.':q<3.65?'Checkpoint #42 completed and 
retained.':q<4.65?'A worker fails after checkpoint #42.':q<5.55?'Restore saved 
source state from #42.':'Resumed · replay after the saved position.';
- statusLine(g,message,m.resumed?'teal':'blue');
-}
-const 
DRAWS=[drawParallel,drawCDC,drawTransform,drawRouting,drawSchema,drawCheckpoint];
-function drawDynamicPanel(g,i,t){
- let q=phase(t);
- g.save();g.beginPath();g.rect(23,104,PW-46,192);g.clip();
- // At the end of the demo loop, the final and initial states transition 
smoothly; the first frame always shows the complete illustration.
- if(q<7.5){DRAWS[i](g,q);}else{const 
blend=smooth((q-7.5)/.5);g.globalAlpha=1-blend;DRAWS[i](g,7.49);g.globalAlpha=blend;DRAWS[i](g,0);}
- g.restore();
-}
-function drawFocusNotes(g,i,t){
- const x=1344,q=phase(t),copy=CAPTIONS[i];
- pill(g,copy[0],x,299,420,C.blue,C.blueSoft,15);
- const 
lines=wrap(copy[1],22);lines.forEach((s,j)=>txt(g,s,x,391+j*45,35,C.ink,600,'left',440));
- const 
lines2=wrap(copy[2],36);lines2.forEach((s,j)=>txt(g,s,x,554+j*31,22,C.muted,450,'left',446));
- txt(g,'INDEPENDENT MECHANISM',x,773,14,C.blue,650);
- txt(g,`${String(i+1).padStart(2,'0')} / 06`,x,833,41,C.ink,500);
- line(g,x,860,x+420,860,C.line,4);line(g,x,860,x+420*(q/8),860,C.teal,4);
-}
-function wrap(s,n){const out=[];let l='';for(const w of s.split(' ')){if((l+' 
'+w).trim().length>n&&l){out.push(l);l=w;}else l+=(l?' 
':'')+w;}if(l)out.push(l);return out;}
-function renderAt(t){
- if(!Number.isFinite(t))throw new TypeError('time must be finite');
- state.time=((t%DURATION)+DURATION)%DURATION;
- 
ctx.setTransform(1,0,0,1,0,0);ctx.drawImage(staticBackground(state.focus),0,0);ctx.setTransform(canvas.width/W,0,0,canvas.height/H,0,0);
- if(state.focus<0){
-  const ai=activeIndex(state.time),local=state.time%4;
-  for(let 
i=0;i<6;i++){ctx.save();ctx.translate(...CARD_POS[i]);drawDynamicPanel(ctx,i,state.time);
-   if(i===ai){const 
a=.48+.42*Math.sin(Math.PI*local/4);ctx.globalAlpha=a;rr(ctx,0,0,PW,PH,18,null,C.teal,2);ctx.globalAlpha=1;rr(ctx,26,PH-1,532*clamp(local/4),3,1.5,C.teal);}
-   ctx.restore();}
-
- 
}else{ctx.save();ctx.translate(75,269);ctx.scale(2.03,2.03);drawDynamicPanel(ctx,state.focus,state.time);ctx.restore();drawFocusNotes(ctx,state.focus,state.time);}
- const 
progress=document.getElementById('timeline');if(progress&&document.activeElement!==progress)progress.value=state.time;
- 
document.getElementById('time').textContent=`${state.time.toFixed(1).padStart(4,'0')}
 / ${DURATION}s`;
- return state.time;
-}
-function setResolution(w,h){
- 
if(!Number.isInteger(w)||!Number.isInteger(h)||w<320||w>7680||h<180||h>4320)throw
 new RangeError('invalid resolution');
- if(Math.abs(w/h-W/H)>.005)throw new RangeError('16:9 resolution required');
- canvas.width=w;canvas.height=h;staticKey='';renderAt(state.time);
-}
-function setFocus(i){if(!Number.isInteger(i)||i< -1||i>5)throw new 
RangeError('invalid 
focus');state.focus=i;staticKey='';document.getElementById('back').disabled=i<0;renderAt(state.time);}
-function 
tick(now){if(state.playing&&state.last){state.time+=(now-state.last)/1000*state.speed;}state.last=now;renderAt(state.time);state.raf=requestAnimationFrame(tick);}
-function 
playPause(){state.playing=!state.playing;document.getElementById('play').textContent=state.playing?UI.pause:UI.play;}
-function updateResolution(){if(IS_EXPORT)return;const 
r=canvas.getBoundingClientRect();const 
width=Math.max(1280,Math.min(3840,Math.round(r.width*devicePixelRatio/16)*16));setResolution(width,width*9/16);}
-function getModels(t){const q=phase(t);return 
{cdc:cdcModel(q),transform:transformModel(q),schema:schemaModel(q),checkpoint:checkpointModel(q),splits:splitRecords};}
-window.SeaTunnelMotion={renderAt,setResolution,setFocus,getModels,state,telemetry,duration:DURATION};
-window.addEventListener('error',e=>telemetry.errors.push(e.message));
-document.getElementById('play').onclick=playPause;
-document.getElementById('restart').onclick=()=>renderAt(0);
-document.getElementById('back').onclick=()=>setFocus(-1);
-document.getElementById('speed').onchange=e=>{state.speed=Number(e.target.value);};
-document.getElementById('timeline').oninput=e=>{renderAt(Number(e.target.value));};
-document.getElementById('fullscreen').onclick=async()=>{try{if(document.fullscreenElement)await
 document.exitFullscreen();else await 
document.getElementById('player').requestFullscreen();}catch(e){document.getElementById('hint').textContent=UI.fullscreenUnavailable;}};
-canvas.onclick=e=>{if(state.focus>=0){setFocus(-1);return;}const 
r=canvas.getBoundingClientRect(),x=(e.clientX-r.left)*W/r.width,y=(e.clientY-r.top)*H/r.height;const
 
i=CARD_POS.findIndex(p=>x>=p[0]&&x<=p[0]+PW&&y>=p[1]&&y<=p[1]+PH);if(i>=0)setFocus(i);};
-window.addEventListener('keydown',e=>{if(['INPUT','SELECT','TEXTAREA'].includes(e.target.tagName))return;if(e.code==='Space'){e.preventDefault();playPause();}if(e.code==='Escape')setFocus(-1);if(e.key==='r'||e.key==='R')renderAt(0);if(/^[1-6]$/.test(e.key))setFocus(Number(e.key)-1);});
-window.addEventListener('resize',updateResolution);
-Promise.all([logo.decode(),document.fonts.ready]).then(()=>{staticKey='';setResolution(1920,1080);updateResolution();telemetry.ready=true;document.getElementById('play').textContent=state.playing?UI.pause:UI.play;renderAt(0);if(!IS_EXPORT)state.raf=requestAnimationFrame(tick);}).catch(e=>{telemetry.errors.push(e.message);document.getElementById('hint').textContent=UI.assetLoadFailed+e.message;});
-</script></body></html>

Reply via email to