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 527a5c349072 [Docs] Add interactive integration motion demo to 
homepage (#537)
527a5c349072 is described below

commit 527a5c3490723b90c347728ff1d402579bfc4ac7
Author: Daniel <[email protected]>
AuthorDate: Mon Sep 21 08:35:41 2026 -0400

    [Docs] Add interactive integration motion demo to homepage (#537)
---
 src/pages/home/index.jsx          | 167 ++++++++++++++++++-
 src/pages/home/index.less         | 186 +++++++++++++++++++++
 static/home/seatunnel-motion.html | 329 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 681 insertions(+), 1 deletion(-)

diff --git a/src/pages/home/index.jsx b/src/pages/home/index.jsx
index a95eea8b5d9f..6d9f656faa8b 100644
--- a/src/pages/home/index.jsx
+++ b/src/pages/home/index.jsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import React, {useEffect, useRef} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
 import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
 import useIsBrowser from '@docusaurus/useIsBrowser';
 import useBaseUrl from '@docusaurus/useBaseUrl';
@@ -259,6 +259,27 @@ 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',
+            },
+            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.'},
+            ],
+        },
         connectors: {
             eyebrow: '~200 Native Connectors',
             titleLead: 'If your data lives there,',
@@ -506,6 +527,27 @@ 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',
+            },
+            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.'},
+            ],
+        },
         connectors: {
             eyebrow: '约 200 个原生连接器',
             titleLead: '只要你的数据在那里,',
@@ -628,6 +670,17 @@ 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">
@@ -701,6 +754,10 @@ 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')
@@ -713,6 +770,47 @@ 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) {
@@ -1599,6 +1697,73 @@ export default function Home() {
                 </div>
             </section>
 
+            <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>
+                </div>
+            </section>
+
             <section className="st-home-section st-home-connectors-section">
                 <div className="st-home-container">
                     <p className="st-home-eyebrow st-home-eyebrow-blue 
st-home-rv">{content.connectors.eyebrow}</p>
diff --git a/src/pages/home/index.less b/src/pages/home/index.less
index 34381ed3826d..4e780d50a804 100644
--- a/src/pages/home/index.less
+++ b/src/pages/home/index.less
@@ -966,6 +966,90 @@ html[data-theme='light'] .st-home {
   background: var(--bg0);
 }
 
+.st-home-motion-section {
+  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-open:hover {
+  color: #7dd3fc;
+  transform: translateX(3px);
+}
+
+.st-home-motion-open svg {
+  width: 1.125rem;
+  height: 1.125rem;
+}
+
+.st-home-motion-controls {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.625rem;
+  margin-top: 1.5rem;
+}
+
+.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-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-control:disabled {
+  cursor: wait;
+  opacity: 0.55;
+}
+
+.st-home-motion-frame {
+  aspect-ratio: 16 / 9;
+  margin-top: 3rem;
+  padding: 0.5rem;
+  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);
+}
+
+.st-home-motion-iframe {
+  display: block;
+  width: 100%;
+  height: 100%;
+  border: 0;
+  border-radius: 1.3rem;
+  background: #e9f0f4;
+}
+
+.st-home-motion-mobile-grid {
+  display: none;
+}
+
 .st-home-architecture .st-home-gradient {
   background-image: var(--brand-gradient-cool);
   -webkit-background-clip: text;
@@ -2438,6 +2522,37 @@ html[data-theme='light'] .st-home 
.st-home-features-section {
   background: linear-gradient(180deg, #eff5fb 0%, #e7eef8 100%);
 }
 
+html[data-theme='light'] .st-home .st-home-motion-section {
+  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-architecture {
   background: linear-gradient(180deg, #edf4fc 0%, #e6eef8 100%);
 }
@@ -2726,6 +2841,73 @@ 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 {
+    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;
+    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);
+  }
+
+  html[data-theme='light'] .st-home .st-home-motion-mobile-card span {
+    color: #4f9bc6;
+  }
+
   .st-home-cta-section {
     padding: 7rem 1rem;
   }
@@ -2747,6 +2929,10 @@ html[data-theme='light'] .st-home .st-home-rv {
   .st-home-architecture-grid {
     grid-template-columns: 1fr;
   }
+
+  .st-home-motion-mobile-grid {
+    grid-template-columns: 1fr;
+  }
 }
 
 @media (prefers-reduced-motion: reduce) {
diff --git a/static/home/seatunnel-motion.html 
b/static/home/seatunnel-motion.html
new file mode 100644
index 000000000000..5c9050596062
--- /dev/null
+++ b/static/home/seatunnel-motion.html
@@ -0,0 +1,329 @@
+<!--
+  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