From 103c109acfb360b14eb4ecb0eda0d3577c254163 Mon Sep 17 00:00:00 2001 From: vatsalpatel Date: Thu, 30 Jul 2026 18:47:57 +0530 Subject: [PATCH] fix: restore consensus series SVG animations The Fumadocs migration removed the GSAP animation layer and rewrote each into a plain markdown image, so all 16 animations in the consensus series now render every state at once. Restore svg-animator.ts, AnimatedSVG.tsx, the 14 per-figure timelines and the gsap dependency from ac5d0a5^, and reconnect them by overriding the MDX img component. Each figure is matched by its caption, so no content changes are needed and both the blog and docs copies are fixed. AnimatedSVG's wrappers become spans: markdown images are paragraph-wrapped, and a div inside a p closes the paragraph early and breaks hydration. Signed-off-by: vatsalpatel --- package.json | 1 + pnpm-lock.yaml | 8 + src/components/AnimatedSVG.tsx | 277 ++++++++ src/components/animated-figure.tsx | 38 ++ src/components/consensus-animations.ts | 219 +++++++ src/components/mdx.tsx | 2 + src/components/part06Fig1.ts | 53 ++ src/components/part06Fig2Scenario1.ts | 68 ++ src/components/part06Fig2Scenario2.ts | 82 +++ src/components/part06Fig2Scenario3.ts | 106 ++++ src/components/part06Fig3.ts | 222 +++++++ src/components/part07Fig3Raft.ts | 143 +++++ src/components/part07Fig3Scenario2.ts | 145 +++++ src/components/part07Fig3Scenario3.ts | 154 +++++ src/components/part07Fig3Scenario4.ts | 144 +++++ src/components/part07Fig3Scenario5.ts | 218 +++++++ src/components/part08Fig3a.ts | 113 ++++ src/components/part08Fig3b.ts | 338 ++++++++++ src/components/part08Fig3c.ts | 232 +++++++ src/components/requestProcessing.ts | 472 ++++++++++++++ src/lib/svg-animator.ts | 841 +++++++++++++++++++++++++ 21 files changed, 3876 insertions(+) create mode 100644 src/components/AnimatedSVG.tsx create mode 100644 src/components/animated-figure.tsx create mode 100644 src/components/consensus-animations.ts create mode 100644 src/components/part06Fig1.ts create mode 100644 src/components/part06Fig2Scenario1.ts create mode 100644 src/components/part06Fig2Scenario2.ts create mode 100644 src/components/part06Fig2Scenario3.ts create mode 100644 src/components/part06Fig3.ts create mode 100644 src/components/part07Fig3Raft.ts create mode 100644 src/components/part07Fig3Scenario2.ts create mode 100644 src/components/part07Fig3Scenario3.ts create mode 100644 src/components/part07Fig3Scenario4.ts create mode 100644 src/components/part07Fig3Scenario5.ts create mode 100644 src/components/part08Fig3a.ts create mode 100644 src/components/part08Fig3b.ts create mode 100644 src/components/part08Fig3c.ts create mode 100644 src/components/requestProcessing.ts create mode 100644 src/lib/svg-animator.ts diff --git a/package.json b/package.json index e360e96..755a1dc 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "fumadocs-core": "16.9.3", "fumadocs-mdx": "15.0.10", "fumadocs-ui": "16.9.3", + "gsap": "^3.15.0", "lucide-react": "^1.17.0", "posthog-js": "^1.376.6", "radix-ui": "^1.4.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df89cad..975541d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: fumadocs-ui: specifier: 16.9.3 version: 16.9.3(@tailwindcss/oxide@4.3.0)(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(fumadocs-core@16.9.3(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@1.17.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(tailwindcss@4.3.0) + gsap: + specifier: ^3.15.0 + version: 3.15.0 lucide-react: specifier: ^1.17.0 version: 1.17.0(react@19.2.6) @@ -2726,6 +2729,9 @@ packages: resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gsap@3.15.0: + resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==} + h3@2.0.1-rc.20: resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} engines: {node: '>=20.11.1'} @@ -6842,6 +6848,8 @@ snapshots: graphql@16.14.0: {} + gsap@3.15.0: {} + h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.16)): dependencies: rou3: 0.8.1 diff --git a/src/components/AnimatedSVG.tsx b/src/components/AnimatedSVG.tsx new file mode 100644 index 0000000..9b7e986 --- /dev/null +++ b/src/components/AnimatedSVG.tsx @@ -0,0 +1,277 @@ +/** + * AnimatedSVG Component + * + * A React component wrapper for SVG animations using GSAP. + * Provides an easy way to add animations to SVG diagrams in Docusaurus. + * + * @example + * ```tsx + * import AnimatedSVG from '@site/src/components/AnimatedSVG'; + * + * { + * animator + * .fadeIn('g[id="node-1"]', { duration: 1 }) + * .drawPath('g[id="arrow-1"] path', { duration: 2 }) + * .play(); + * }} + * autoPlay={true} + * /> + * ``` + */ + +import React, { useEffect, useRef, useState } from "react"; +import { SVGAnimator } from "../lib/svg-animator"; + +interface AnimatedSVGProps { + /** URL or path to the SVG file */ + src: string; + /** Animation callback that receives the animator instance */ + onAnimate?: (animator: SVGAnimator) => void; + /** Whether to auto-play the animation on mount */ + autoPlay?: boolean; + /** Whether to show manual controls (forward/reverse buttons) */ + showControls?: boolean; + /** Whether to show restart button only */ + showRestartButton?: boolean; + /** Additional CSS classes */ + className?: string; + /** Additional inline styles */ + style?: React.CSSProperties; + /** Alt text for accessibility */ + alt?: string; + /** Width of the SVG container */ + width?: string | number; + /** Height of the SVG container */ + height?: string | number; +} + +const AnimatedSVG: React.FC = ({ + src, + onAnimate, + autoPlay = false, + showControls = false, + showRestartButton = false, + className = "", + style = {}, + alt = "Animated SVG", + width, + height, +}) => { + const containerRef = useRef(null); + const animatorRef = useRef(null); + const [isLoaded, setIsLoaded] = useState(false); + const [canGoNext, setCanGoNext] = useState(true); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + // Fetch and inject SVG + const loadSVG = async () => { + try { + const response = await fetch(src); + const svgText = await response.text(); + container.innerHTML = svgText; + + const svgElement = container.querySelector("svg"); + if (!svgElement) { + console.error("No SVG element found in the loaded content"); + return; + } + + // Set dimensions if provided + if (width) + svgElement.style.width = + typeof width === "number" ? `${width}px` : width; + if (height) + svgElement.style.height = + typeof height === "number" ? `${height}px` : height; + + // Create animator instance + animatorRef.current = new SVGAnimator(svgElement); + setIsLoaded(true); + + // Call animation callback + if (onAnimate && animatorRef.current) { + onAnimate(animatorRef.current); + + if (autoPlay) { + animatorRef.current.play(); + } + } + } catch (error) { + console.error("Error loading SVG:", error); + } + }; + + loadSVG(); + + // Cleanup + return () => { + if (animatorRef.current) { + animatorRef.current.kill(); + } + }; + }, [src, onAnimate, autoPlay, width, height]); + + const updateButtonStates = () => { + if (animatorRef.current) { + setCanGoNext(animatorRef.current.hasNextStep()); + } + }; + + const handleNext = () => { + if (animatorRef.current) { + animatorRef.current.nextStep(); + updateButtonStates(); + } + }; + + const handleReset = () => { + // Reload the component by toggling isLoaded + setIsLoaded(false); + setCanGoNext(true); + + // Re-trigger the useEffect by incrementing a key would be cleaner, + // but we can also just re-run the SVG loading logic + const container = containerRef.current; + if (!container) return; + + // Kill existing animator + if (animatorRef.current) { + animatorRef.current.kill(); + animatorRef.current = null; + } + + // Reload SVG + const loadSVG = async () => { + try { + const response = await fetch(src); + const svgText = await response.text(); + container.innerHTML = svgText; + + const svgElement = container.querySelector("svg"); + if (!svgElement) { + console.error("No SVG element found in the loaded content"); + return; + } + + // Set dimensions if provided + if (width) + svgElement.style.width = + typeof width === "number" ? `${width}px` : width; + if (height) + svgElement.style.height = + typeof height === "number" ? `${height}px` : height; + + // Create animator instance + animatorRef.current = new SVGAnimator(svgElement); + setIsLoaded(true); + + // Call animation callback + if (onAnimate && animatorRef.current) { + onAnimate(animatorRef.current); + + // Auto-play if enabled + if (autoPlay) { + animatorRef.current.play(); + } + } + } catch (error) { + console.error("Error loading SVG:", error); + } + }; + + loadSVG(); + }; + + /* + * Markdown images are wrapped in a paragraph, and a
anywhere inside a + *

makes the browser close the paragraph early — which desyncs the parsed + * DOM from React's tree and breaks hydration. Every wrapper here is therefore + * a carrying an explicit display. + */ + return ( + + + {showControls && isLoaded && ( + + + + + )} + {showRestartButton && !showControls && isLoaded && ( + + + + )} + + ); +}; + +export default AnimatedSVG; diff --git a/src/components/animated-figure.tsx b/src/components/animated-figure.tsx new file mode 100644 index 0000000..38e2818 --- /dev/null +++ b/src/components/animated-figure.tsx @@ -0,0 +1,38 @@ +import defaultMdxComponents from 'fumadocs-ui/mdx'; +import type { ImgHTMLAttributes } from 'react'; +import AnimatedSVG from '@/components/AnimatedSVG'; +import { getFigureAnimation } from '@/components/consensus-animations'; + +const DefaultImg = defaultMdxComponents.img as ( + props: ImgHTMLAttributes, +) => React.ReactNode; + +/** + * Renders markdown images normally, except for the consensus figures whose SVG + * is a stack of animation states. Those are handed to AnimatedSVG so the states + * are revealed one step at a time instead of all at once. + */ +export function AnimatedFigure(props: ImgHTMLAttributes) { + const { src, alt } = props; + + if (typeof src === 'string' && typeof alt === 'string') { + const spec = getFigureAnimation(src, alt); + if (spec) { + return ( + + ); + } + } + + return ; +} diff --git a/src/components/consensus-animations.ts b/src/components/consensus-animations.ts new file mode 100644 index 0000000..34cba25 --- /dev/null +++ b/src/components/consensus-animations.ts @@ -0,0 +1,219 @@ +import type { CSSProperties } from 'react'; +import type { SVGAnimator } from '@/lib/svg-animator'; +import { part06Fig1 } from '@/components/part06Fig1'; +import { part06Fig2Scenario1 } from '@/components/part06Fig2Scenario1'; +import { part06Fig2Scenario2 } from '@/components/part06Fig2Scenario2'; +import { part06Fig2Scenario3 } from '@/components/part06Fig2Scenario3'; +import { part06Fig3 } from '@/components/part06Fig3'; +import { part07Fig3Raft } from '@/components/part07Fig3Raft'; +import { part07Fig3Scenario2 } from '@/components/part07Fig3Scenario2'; +import { part07Fig3Scenario3 } from '@/components/part07Fig3Scenario3'; +import { part07Fig3Scenario4 } from '@/components/part07Fig3Scenario4'; +import { part07Fig3Scenario5 } from '@/components/part07Fig3Scenario5'; +import { part08Fig3a } from '@/components/part08Fig3a'; +import { part08Fig3b } from '@/components/part08Fig3b'; +import { part08Fig3c } from '@/components/part08Fig3c'; +import { requestProcessing } from '@/components/requestProcessing'; + +/** + * Mirrors the props each figure was authored with before the Fumadocs + * migration. Kept as data so the restored AnimatedSVG receives exactly what it + * used to: most figures step manually, one autoplays with a replay button, and + * the two 2000px-wide ones are deliberately shifted left. + */ +type FigureAnimation = { + /** Base name of the SVG the animation drives, without directory or content hash */ + svg: string; + animate: (animator: SVGAnimator) => void; + autoPlay: boolean; + showControls: boolean; + showRestartButton?: boolean; + width?: number; + height?: number; + style?: CSSProperties; +}; + +const BLOCK: CSSProperties = { + display: 'block', + margin: '1rem 0', + overflow: 'visible', +}; + +/** part06-fig3 is 2000px wide and authored to hang left of the text column */ +const SHIFTED_LEFT: CSSProperties = { + display: 'inline-block', + margin: '1rem 0', + overflow: 'visible', + transform: 'translateX(-450px)', +}; + +/** + * The consensus figures are single SVGs holding every state of a sequence, + * driven by a GSAP timeline that reveals one step at a time. Several figures + * share one SVG and differ only in the timeline, so the caption identifies them. + */ +const FIGURE_ANIMATIONS: Record = { + 'Multigres consensus and replication diagram': { + svg: 'requestProcessing', + animate: requestProcessing, + autoPlay: false, + showControls: true, + }, + 'Figure 1: Revocation methods': { + svg: 'part06-fig1', + animate: part06Fig1, + autoPlay: true, + showControls: false, + showRestartButton: true, + width: 900, + height: 300, + style: BLOCK, + }, + 'Figure 2: Scenario 1 - No race': { + svg: 'part06-fig2', + animate: part06Fig2Scenario1, + autoPlay: false, + showControls: true, + width: 1200, + height: 400, + style: BLOCK, + }, + 'Figure 2: Scenario 2 - Newer term steals nodes': { + svg: 'part06-fig2', + animate: part06Fig2Scenario2, + autoPlay: false, + showControls: true, + width: 1200, + height: 400, + style: BLOCK, + }, + 'Figure 2: Scenario 3 - Newer term starts after scenario 1': { + svg: 'part06-fig2', + animate: part06Fig2Scenario3, + autoPlay: false, + showControls: true, + width: 1200, + height: 400, + style: BLOCK, + }, + 'Figure 3: All possible leaders': { + svg: 'part06-fig3', + animate: part06Fig3, + autoPlay: false, + showControls: true, + width: 2000, + height: 700, + style: SHIFTED_LEFT, + }, + 'Figure 1: Term number competition': { + svg: 'part06-fig3', + animate: part06Fig3, + autoPlay: false, + showControls: true, + width: 2000, + height: 700, + style: SHIFTED_LEFT, + }, + 'Figure 3: Raft timeline propagation': { + svg: 'part07-fig3', + animate: part07Fig3Raft, + autoPlay: false, + showControls: true, + width: 1000, + height: 500, + style: BLOCK, + }, + 'Figure 4: Initial state': { + svg: 'part07-fig3', + animate: part07Fig3Raft, + autoPlay: false, + showControls: false, + width: 1000, + height: 500, + style: BLOCK, + }, + 'Figure 5: Scenario 2': { + svg: 'part07-fig3', + animate: part07Fig3Scenario2, + autoPlay: false, + showControls: true, + width: 1000, + height: 500, + style: BLOCK, + }, + 'Figure 6: Scenario 3': { + svg: 'part07-fig3', + animate: part07Fig3Scenario3, + autoPlay: false, + showControls: true, + width: 1000, + height: 500, + style: BLOCK, + }, + 'Figure 7: Scenario 4': { + svg: 'part07-fig3', + animate: part07Fig3Scenario4, + autoPlay: false, + showControls: true, + width: 1000, + height: 500, + style: BLOCK, + }, + 'Figure 8: Scenario 5': { + svg: 'part07-fig3', + animate: part07Fig3Scenario5, + autoPlay: false, + showControls: true, + width: 1000, + height: 500, + style: BLOCK, + }, + 'Figure 3a: Ruleset change scenario 1': { + svg: 'part08-fig3', + animate: part08Fig3a, + autoPlay: false, + showControls: true, + width: 800, + height: 400, + style: BLOCK, + }, + 'Figure 3b: Ruleset change scenario 2': { + svg: 'part08-fig3', + animate: part08Fig3b, + autoPlay: false, + showControls: true, + width: 800, + height: 400, + style: BLOCK, + }, + 'Figure 3c: Ruleset change scenario 3': { + svg: 'part08-fig3', + animate: part08Fig3c, + autoPlay: false, + showControls: true, + width: 800, + height: 400, + style: BLOCK, + }, +}; + +/** + * Dev serves `/img/consensus/part07-fig3.svg`; a build emits + * `/assets/part07-fig3-DEk2p0Pr.svg`. The hash cannot be stripped lexically — + * Vite hashes may themselves contain `-` and `_` (`Bexfl0_P`, `DDkSJ-hG`) — + * so match the known name as a prefix instead. + */ +function matchesSvg(src: string, expected: string): boolean { + const file = src.split('/').pop()?.split('?')[0] ?? ''; + const base = file.replace(/\.svg$/i, ''); + return base === expected || base.startsWith(`${expected}-`); +} + +export function getFigureAnimation( + src: string, + alt: string, +): FigureAnimation | undefined { + const spec = FIGURE_ANIMATIONS[alt.trim()]; + if (!spec) return undefined; + return matchesSvg(src, spec.svg) ? spec : undefined; +} diff --git a/src/components/mdx.tsx b/src/components/mdx.tsx index 4f7ac1a..a5c1ecc 100644 --- a/src/components/mdx.tsx +++ b/src/components/mdx.tsx @@ -1,5 +1,6 @@ import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; +import { AnimatedFigure } from '@/components/animated-figure'; import { BlogAuthor } from '@/components/blog-author'; import { PgRegressCount } from '@/components/pg-regress-count'; import { YouTubeEmbed } from '@/components/youtube-embed'; @@ -23,6 +24,7 @@ export function getMDXComponents(components?: MDXComponents) { Author, PgRegressCount, YouTubeEmbed, + img: AnimatedFigure, ...components, } satisfies MDXComponents; } diff --git a/src/components/part06Fig1.ts b/src/components/part06Fig1.ts new file mode 100644 index 0000000..f9d5a3f --- /dev/null +++ b/src/components/part06Fig1.ts @@ -0,0 +1,53 @@ +/** + * Animation configuration for Part 6 Figure 1 - Revocation methods + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part06-fig1.svg + */ +export const part06Fig1 = (animator: SVGAnimator) => { + // Initial state: Hide specified elements + animator.hideElements([ + "#n1text1", + "#n1text2", + "#n1n3", + "#n1n3text", + "#c6n3", + "#c6n3text", + ]); + + animator.addLabel("start"); + + // Animate c6n3 arrow and c6n3text appearing + animator + .animateArrow("#c6n3", { duration: DURATION.normal }) + .show("#c6n3text", { autoAlpha: 1 }); + + animator.wait(DURATION.normal); + + // Change n3term to "6" with active color + animator + .morphText("#n3term", "6", { duration: DURATION.instant }) + .show("#n3term", { fill: COLORS.active }); + + animator.wait(DURATION.normal); + + // Animate n1n3 arrow and n1n3text appearing + animator + .animateArrow("#n1n3", { duration: DURATION.normal }) + .show("#n1n3text", { autoAlpha: 1 }); + + animator.wait(DURATION.normal); + + // Show n1text1 and n1text2, change n1role to "F" with active color + animator + .show("#n1text1", { autoAlpha: 1 }) + .show("#n1text2", { autoAlpha: 1 }) + .morphText("#n1role", "F", { duration: DURATION.instant }) + .show("#n1role", { fill: COLORS.active }); +}; diff --git a/src/components/part06Fig2Scenario1.ts b/src/components/part06Fig2Scenario1.ts new file mode 100644 index 0000000..94c2fa2 --- /dev/null +++ b/src/components/part06Fig2Scenario1.ts @@ -0,0 +1,68 @@ +/** + * Animation configuration for Part 6 Figure 2 - Scenario 1: no race + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part06-fig2.svg - Scenario 1 + */ +export const part06Fig2Scenario1 = (animator: SVGAnimator) => { + // Initial state: Hide all arrows and their text + animator.hideElements([ + "#c6n3", + "#c6n3text", + "#c6n4", + "#c6n4text", + "#c6n5", + "#c6n5text", + "#c6n5fail", + "#c6n5failtext", + "#c7", + "#c7text", + "#c7n2", + "#c7n2text", + "#c7n5", + "#c7n5text", + "#c7n6", + "#c7n6text", + "#n1n3", + "#n1n3text", + ]); + + // Step 1: C6 recruits N3, show descriptions + animator + .animateArrow("#c6n3", { duration: DURATION.normal }) + .show("#c6n3text", { autoAlpha: 1 }) + .changeText("#n3term", "6", COLORS.active, DURATION.fast) + .animateArrow("#n1n3", { duration: DURATION.normal }) + .show("#n1n3text", { autoAlpha: 1 }) + .show("#desc1", { fill: COLORS.active }) + .morphText("#desc1", "* Recruitment of N3 satisfies", { + duration: DURATION.normal, + }) + .show("#desc2", { fill: COLORS.active }) + .morphText("#desc2", " revocation of N1", { duration: DURATION.instant }) + .addLabel("step1"); + + // Step 2: C6 recruits N4 and N5 + animator + .animateArrow("#c6n5", { duration: DURATION.normal }) + .show("#c6n5text", { autoAlpha: 1 }) + .changeText("#n5term", "6", COLORS.active, DURATION.fast) + .animateArrow("#c6n4", { duration: DURATION.normal }) + .show("#c6n4text", { autoAlpha: 1 }) + .changeText("#n4term", "6", COLORS.active, DURATION.fast) + .show("#desc1", { fill: COLORS.inactive }) + .show("#desc2", { fill: COLORS.inactive }) + .show("#desc3", { fill: COLORS.active }) + .morphText("#desc3", "* Recruitment of N4 and N5 satisfies", { + duration: DURATION.normal, + }) + .show("#desc4", { fill: COLORS.active }) + .morphText("#desc4", " candidacy of N4", { duration: DURATION.instant }) + .addLabel("step2"); +}; diff --git a/src/components/part06Fig2Scenario2.ts b/src/components/part06Fig2Scenario2.ts new file mode 100644 index 0000000..f3b23f2 --- /dev/null +++ b/src/components/part06Fig2Scenario2.ts @@ -0,0 +1,82 @@ +/** + * Animation configuration for Part 6 Figure 2 - Scenario 2: newer term steals nodes + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part06-fig2.svg - Scenario 2 + */ +export const part06Fig2Scenario2 = (animator: SVGAnimator) => { + // Initial state: Hide all arrows and their text + animator.hideElements([ + "#c6n3", + "#c6n3text", + "#c6n4", + "#c6n4text", + "#c6n5", + "#c6n5text", + "#c6n5fail", + "#c6n5failtext", + "#c7n2", + "#c7n2text", + "#c7n5", + "#c7n5text", + "#c7n6", + "#c7n6text", + "#n1n2", + "#n1n3", + "#n1n3text", + "#np", + "#nptext1", + "#nptext2", + ]); + + // Set initial text before animation steps + animator.setText("#desc0", "Scenario 2: New term steals nodes"); + + // Step 1: C6 recruits N3, C7 recruits N2 + animator + .animateArrow("#c6n3", { duration: DURATION.normal }) + .show("#c6n3text", { autoAlpha: 1 }) + .changeText("#n3term", "6", COLORS.active, DURATION.fast) + .animateArrow("#c7n2", { duration: DURATION.normal }) + .show("#c7n2text", { autoAlpha: 1 }) + .changeText("#n2term", "7", COLORS.blue, DURATION.fast) + .show("#desc1", { fill: COLORS.active }) + .morphText("#desc1", "* C6 and C7 revoke N1 using different nodes", { + duration: DURATION.normal, + }) + .addLabel("step1"); + + // Step 2: C7 recruits N5 and N6 + animator + .show("#c6n3", { autoAlpha: 0 }) + .show("#c6n3text", { autoAlpha: 0 }) + .show("#c7n2", { autoAlpha: 0 }) + .show("#c7n2text", { autoAlpha: 0 }) + .animateArrow("#c7n5", { duration: DURATION.normal }) + .show("#c7n5text", { autoAlpha: 1 }) + .changeText("#n5term", "7", COLORS.blue, DURATION.fast) + .animateArrow("#c7n6", { duration: DURATION.normal }) + .show("#c7n6text", { autoAlpha: 1 }) + .changeText("#n6term", "7", COLORS.blue, DURATION.fast) + .show("#desc1", { fill: COLORS.inactive }) + .show("#desc2", { fill: COLORS.blue }) + .morphText("#desc2", "* C7 recruits N5 and N6") + .addLabel("step2"); + + // Step 3: C6 fails to recruit N5 + animator + .animateArrow("#c6n5fail", { duration: DURATION.normal }) + .show("#c6n5failtext", { autoAlpha: 1 }) + .show("#desc2", { fill: COLORS.inactive }) + .show("#desc3", { fill: COLORS.red }) + .morphText("#desc3", "* C6 cannot recruit N5 because") + .show("#desc4", { fill: COLORS.red }) + .morphText("#desc4", " of its higher term") + .addLabel("step3"); +}; diff --git a/src/components/part06Fig2Scenario3.ts b/src/components/part06Fig2Scenario3.ts new file mode 100644 index 0000000..b808588 --- /dev/null +++ b/src/components/part06Fig2Scenario3.ts @@ -0,0 +1,106 @@ +/** + * Animation configuration for Part 6 Figure 2 - Scenario 3: newer term starts after scenario 1 + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part06-fig2.svg - Scenario 3 + */ +export const part06Fig2Scenario3 = (animator: SVGAnimator) => { + // Initial state: Hide all arrows and their text + animator.hideElements([ + "#c6n3", + "#c6n3text", + "#c6n4", + "#c6n4text", + "#c6n5", + "#c6n5text", + "#c6n5fail", + "#c6n5failtext", + "#c7n2", + "#c7n2text", + "#c7n5", + "#c7n5text", + "#c7n6", + "#c7n6text", + "#n1n2", + "#n1n3", + "#n1n3text", + "#np", + "#nptext1", + "#nptext2", + ]); + + // Set initial text before animation steps + animator.setText("#desc0", "Scenario 3: New term starts after scenario 1"); + + // Step 1: Show scenario 1 completion - C6 recruits N3, N4, N5 + // First group: animate arrows simultaneously + animator.group((a: SVGAnimator) => { + a.animateArrow("#c6n3", { duration: DURATION.normal }) + .show("#c6n3text", { autoAlpha: 1 }) + .animateArrow("#c6n5", { duration: DURATION.normal }) + .show("#c6n5text", { autoAlpha: 1 }) + .animateArrow("#c6n4", { duration: DURATION.normal }) + .show("#c6n4text", { autoAlpha: 1 }); + }); + + // Second group: change term numbers + animator.group((a: SVGAnimator) => { + a.changeText("#n3term", "6", COLORS.active, DURATION.fast) + .changeText("#n5term", "6", COLORS.active, DURATION.fast) + .changeText("#n4term", "6", COLORS.active, DURATION.fast); + }); + + animator + .show("#desc1", { fill: COLORS.active }) + .morphText("#desc1", "* C6 recruits N3, N4, N5", { + duration: DURATION.normal, + }); + + animator.addLabel("step1"); + + // Step 2: C7 recruits N2, N5, N6 + // Hide c6 arrows first + animator + .show("#c6n3", { autoAlpha: 0 }) + .show("#c6n3text", { autoAlpha: 0 }) + .show("#c6n4", { autoAlpha: 0 }) + .show("#c6n4text", { autoAlpha: 0 }) + .show("#c6n5", { autoAlpha: 0 }) + .show("#c6n5text", { autoAlpha: 0 }); + + // First group: animate arrows simultaneously + animator.group((a: SVGAnimator) => { + a.animateArrow("#c7n2", { duration: DURATION.normal }) + .show("#c7n2text", { autoAlpha: 1 }) + .animateArrow("#c7n5", { duration: DURATION.normal }) + .show("#c7n5text", { autoAlpha: 1 }) + .animateArrow("#c7n6", { duration: DURATION.normal }) + .show("#c7n6text", { autoAlpha: 1 }); + }); + + // Second group: change term numbers to 7 with blue color + animator.group((a: SVGAnimator) => { + a.changeText("#n2term", "7", COLORS.blue, DURATION.fast) + .changeText("#n5term", "7", COLORS.blue, DURATION.fast) + .changeText("#n6term", "7", COLORS.blue, DURATION.fast); + }); + + animator + .show("#desc1", { fill: COLORS.active }) + .show("#desc2", { fill: COLORS.blue }) + .show("#desc3", { fill: COLORS.blue }) + .morphText("#desc2", "* C7 overrides C6 by recruiting N5 and N6", { + duration: DURATION.normal, + }) + .morphText("#desc3", " that were previously recruited by C6", { + duration: DURATION.instant, + }); + + animator.addLabel("step2"); +}; diff --git a/src/components/part06Fig3.ts b/src/components/part06Fig3.ts new file mode 100644 index 0000000..7ceb8bb --- /dev/null +++ b/src/components/part06Fig3.ts @@ -0,0 +1,222 @@ +/** + * Animation configuration for Part 6 Figure 3 - All possible leaders (NEW VERSION) + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const DURATION = SVGAnimator.DURATION; + +// Relative offsets for moving elements between sections +// Revocation (top-left) -> Target (right): move right and down +// Calculated from: n1 source (50, 112.738) -> target worked at (825, 435) +// Relative offset: (825-50, 435-112.738) = (775, 322.262) +const REVOCATION_TO_TARGET_OFFSET = { + x: 775, + y: 322.262, +} as const; + +// Candidacy (bottom-left) -> Target (right): move right and UP +// Calculated from: n4n6 source (338.934, 1233.763) -> target works at (1113.934, 813.763) +// Relative offset: (775, -420) +const CANDIDACY_TO_TARGET_OFFSET = { + x: 775, // Same horizontal movement as Revocation->Target + y: -420, // Move up (negative) to reach Target section +} as const; + +// Helper function to calculate target position from source + offset +const addOffset = ( + source: { x: number; y: number }, + offset: { x: number; y: number }, +) => ({ + x: source.x + offset.x, + y: source.y + offset.y, +}); + +// Helper function to extract position from SVG element's transform attribute +const getElementPosition = (selector: string): { x: number; y: number } => { + const element = document.querySelector(selector); + if (!element) { + console.warn(`Element not found: ${selector}`); + return { x: 0, y: 0 }; + } + + const transform = element.getAttribute("transform"); + if (!transform) { + return { x: 0, y: 0 }; + } + + // Extract translate(x, y) from transform attribute + const translateMatch = transform.match(/translate\(([^,\s]+)[\s,]+([^)]+)\)/); + if (translateMatch) { + return { + x: parseFloat(translateMatch[1]), + y: parseFloat(translateMatch[2]), + }; + } + + return { x: 0, y: 0 }; +}; + +/** + * Animation sequence for part06-fig3.svg - All possible leaders (NEW VERSION) + */ +export const part06Fig3 = (animator: SVGAnimator) => { + // Initial state: Hide elements + animator.hideElements(["#blue", "#purple", "#green"]); + + // Step 1: Move elements from Revocation and Candidacy to Target section + + // Get source positions from SVG + const n1Source = getElementPosition("#n1"); + const n4Source = getElementPosition("#n4"); + const n4n6Source = getElementPosition("#n4n6"); + + // Move n1 from Revocation to Target using relative offset + animator.moveTo( + "#n1", + null, + addOffset(n1Source, REVOCATION_TO_TARGET_OFFSET), + { + duration: DURATION.slow, + ease: "power2.inOut", + }, + ); + + // Move n4 from Revocation to Target using relative offset + animator.moveTo( + "#n4", + null, + addOffset(n4Source, REVOCATION_TO_TARGET_OFFSET), + { + duration: DURATION.slow, + ease: "power2.inOut", + }, + ); + + // Move n4n6 from Candidacy to Target using Candidacy-specific offset + animator.moveTo( + "#n4n6", + null, + addOffset(n4n6Source, CANDIDACY_TO_TARGET_OFFSET), + { + duration: DURATION.slow, + ease: "power2.inOut", + }, + ); + + // Show blue text/label + animator.show("#blue", { autoAlpha: 1 }); + + animator.addLabel("step1"); + + // Step 2: Restore step1 elements and move new set of elements to Target + + // First, quickly restore step1 elements to their original positions + animator.group((a: SVGAnimator) => { + a.moveTo("#n1", null, n1Source, { duration: DURATION.fast }); + a.moveTo("#n4", null, n4Source, { duration: DURATION.fast }); + a.moveTo("#n4n6", null, n4n6Source, { duration: DURATION.fast }); + }); + + // Get source positions from SVG for step2 elements + const n3Source = getElementPosition("#n3"); + const n4n5Source = getElementPosition("#n4n5"); + + // Move n3 from Revocation to Target + animator.moveTo( + "#n3", + null, + addOffset(n3Source, REVOCATION_TO_TARGET_OFFSET), + { + duration: DURATION.slow, + ease: "power2.inOut", + }, + ); + + // Move n4 from Revocation to Target (same as step1) + animator.moveTo( + "#n4", + null, + addOffset(n4Source, REVOCATION_TO_TARGET_OFFSET), + { + duration: DURATION.slow, + ease: "power2.inOut", + }, + ); + + // Move n4n5 from Candidacy to Target + animator.moveTo( + "#n4n5", + null, + addOffset(n4n5Source, CANDIDACY_TO_TARGET_OFFSET), + { + duration: DURATION.slow, + ease: "power2.inOut", + }, + ); + + // Show purple text/label + animator.show("#purple", { autoAlpha: 1 }); + + animator.addLabel("step2"); + + // Step 3: Restore step2 elements and move final set of elements to Target + + // First, quickly restore step2 elements to their original positions + animator.group((a: SVGAnimator) => { + a.moveTo("#n3", null, n3Source, { duration: DURATION.fast }); + a.moveTo("#n4", null, n4Source, { duration: DURATION.fast }); + a.moveTo("#n4n5", null, n4n5Source, { duration: DURATION.fast }); + }); + + // Get source positions from SVG for step3 elements + const n5n6Source = getElementPosition("#n5n6"); + const n1n2n3Source = getElementPosition("#n1n2n3"); + + // Move n1 from Revocation to Target + animator.moveTo( + "#n1", + null, + addOffset(n1Source, REVOCATION_TO_TARGET_OFFSET), + { + duration: DURATION.slow, + ease: "power2.inOut", + }, + ); + + // Move n5n6 from Revocation to Target + animator.moveTo( + "#n5n6", + null, + addOffset(n5n6Source, REVOCATION_TO_TARGET_OFFSET), + { + duration: DURATION.slow, + ease: "power2.inOut", + }, + ); + + // Move n1n2n3 from Candidacy to Target + animator.moveTo( + "#n1n2n3", + null, + addOffset(n1n2n3Source, CANDIDACY_TO_TARGET_OFFSET), + { + duration: DURATION.slow, + ease: "power2.inOut", + }, + ); + + // Show green text/label + animator.show("#green", { autoAlpha: 1 }); + + animator.addLabel("step3"); + + // Step 4: Restore all step3 elements to their original positions + animator.group((a: SVGAnimator) => { + a.moveTo("#n1", null, n1Source, { duration: DURATION.fast }); + a.moveTo("#n5n6", null, n5n6Source, { duration: DURATION.fast }); + a.moveTo("#n1n2n3", null, n1n2n3Source, { duration: DURATION.fast }); + }); + + animator.addLabel("step4"); +}; diff --git a/src/components/part07Fig3Raft.ts b/src/components/part07Fig3Raft.ts new file mode 100644 index 0000000..2dfe7a4 --- /dev/null +++ b/src/components/part07Fig3Raft.ts @@ -0,0 +1,143 @@ +/** + * Animation configuration for Part 7 Figure 3 - Raft timeline propagation + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part07-fig3.svg - Raft timeline propagation + * + * Available IDs in SVG (nodes N1-N6 with states t2-t5, v2-v5 and arrows between them): + * - n1t3-n1t5, n1v3-n1v5: N1's term/value at different states + * - n2t3-n2t5, n2v3-n2v5: N2's term/value at different states + * - n3t3-n3t5, n3v3-n3v5: N3's term/value at different states + * - n4t3-n4t5, n4v3-n4v5: N4's term/value at different states + * - n5t2-n5t5, n5v2-n5v5: N5's term/value at different states + * - n6t3-n6t5, n6v3-n6v5: N6's term/value at different states + * - Arrows: n1n6, n3n5, n4n1, n4n2, n4n3, n4n5, n4n6, n5n3, n5n4 + * - Arrow text: corresponding *text suffix for each arrow + */ +export const part07Fig3Raft = (animator: SVGAnimator) => { + // Initial state: Hide all elements except N1 slots 3 and 4 + animator.hideElements([ + // N1 elements (slots 3 and 4 are visible initially, only hide slot 5) + "#n1t5", + "#n1v5", + // N2 elements (slot 3 is visible initially) + "#n2t4", + "#n2v4", + "#n2t5", + "#n2v5", + // N3 elements + "#n3t3", + "#n3v3", + "#n3t4", + "#n3v4", + "#n3t5", + "#n3v5", + // N4 elements + "#n4t3", + "#n4v3", + "#n4t4", + "#n4v4", + "#n4t5", + "#n4v5", + // N5 elements + "#n5t2", + "#n5v2", + "#n5t3", + "#n5v3", + "#n5t4", + "#n5v4", + "#n5t5", + "#n5v5", + // N6 elements + "#n6t3", + "#n6v3", + "#n6t4", + "#n6v4", + "#n6t5", + "#n6v5", + // Arrows + "#n1n6", + "#n1n6text", + "#n3n5", + "#n3n5text", + "#n4n1", + "#n4n1text", + "#n4n2", + "#n4n2text", + "#n4n3", + "#n4n3text", + "#n4n5", + "#n4n5text", + "#n4n6", + "#n4n6text", + "#n5n3", + "#n5n3text", + "#n5n4", + "#n5n4text", + // Quorums + "#quorum1", + "#quorum2", + "#quorum3", + // Descriptions + "#desc1", + "#desc2", + "#desc3", + "#desc4", + "#desc5", + ]); + + + // Step 1: N1 propagates C (with term 5) to N6 + animator + .animateArrow("#n1n6", { duration: DURATION.normal }) + .show("#n1n6text", { autoAlpha: 1 }) + .wait(DURATION.fast) + .show("#n6t3", { autoAlpha: 1 }) + .show("#n6v3", { autoAlpha: 1 }) + .morphText("#desc1", "* Step 1: Append 5-C") + .show("#desc1", { autoAlpha: 1, fill: COLORS.blue, attr: { dx: -80, dy: -60 } }); + + animator.addLabel("step1"); + + // Step 2: N1 propagates D (with term 5) to N6 + animator + .unanimateArrow("#n1n6", { duration: DURATION.instant }) + .set("#n1n6text", { autoAlpha: 0 }) + .morphText("#n1n6text", "Append 5-D") + .animateArrow("#n1n6", { duration: DURATION.normal }) + .show("#n1n6text", { autoAlpha: 1 }) + .wait(DURATION.fast) + .show("#n6t4", { autoAlpha: 1 }) + .show("#n6v4", { autoAlpha: 1 }) + .morphText("#desc2", "* Step 2: Append 5-D") + .show("#desc2", { autoAlpha: 1, fill: COLORS.blue, attr: { dx: -80, dy: -60 } }); + + animator.addLabel("step2"); + + // Step 3: N1 creates new request with term 7 and replicates to N6 + animator + .wait(DURATION.fast) + .show("#n1t5", { autoAlpha: 1 }) + .show("#n1v5", { autoAlpha: 1 }) + .unanimateArrow("#n1n6", { duration: DURATION.instant }) + .set("#n1n6text", { autoAlpha: 0 }) + .morphText("#n1n6text", "Append 7-ok") + .animateArrow("#n1n6", { duration: DURATION.normal }) + .show("#n1n6text", { autoAlpha: 1 }) + .wait(DURATION.fast) + .show("#n6t5", { autoAlpha: 1 }) + .show("#n6v5", { autoAlpha: 1 }) + .morphText("#desc3", "* Step 3: Append 7-ok to N1") + .show("#desc3", { autoAlpha: 1, fill: COLORS.blue, attr: { dx: -80, dy: -60 } }) + .morphText("#desc4", " and transmit to N7") + .show("#desc4", { autoAlpha: 1, fill: COLORS.blue, attr: { dx: -80, dy: -60 } }); + + animator.addLabel("step3"); + +}; diff --git a/src/components/part07Fig3Scenario2.ts b/src/components/part07Fig3Scenario2.ts new file mode 100644 index 0000000..ed2ef9e --- /dev/null +++ b/src/components/part07Fig3Scenario2.ts @@ -0,0 +1,145 @@ +/** + * Animation configuration for Part 7 Figure 3 - Scenario 2 + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part07-fig3.svg - Scenario 2 + * C6 recruits N3, N4 and N5 for revocation and candidacy + * + * Available IDs in SVG (nodes N1-N6 with states t2-t5, v2-v5 and arrows between them): + * - n1t3-n1t5, n1v3-n1v5: N1's term/value at different states + * - n2t3-n2t5, n2v3-n2v5: N2's term/value at different states + * - n3t3-n3t5, n3v3-n3v5: N3's term/value at different states + * - n4t3-n4t5, n4v3-n4v5: N4's term/value at different states + * - n5t2-n5t5, n5v2-n5v5: N5's term/value at different states + * - n6t3-n6t5, n6v3-n6v5: N6's term/value at different states + * - Arrows: n1n6, n3n5, n4n1, n4n2, n4n3, n4n5, n4n6, n5n3, n5n4 + * - Arrow text: corresponding *text suffix for each arrow + */ +export const part07Fig3Scenario2 = (animator: SVGAnimator) => { + // Initial state: Hide all elements except N1 slots 3 and 4 + animator.hideElements([ + // N1 elements (slots 3 and 4 are visible initially, only hide slot 5) + "#n1t5", + "#n1v5", + // N2 elements (slot 3 is visible initially) + "#n2t4", + "#n2v4", + "#n2t5", + "#n2v5", + // N3 elements + "#n3t3", + "#n3v3", + "#n3t4", + "#n3v4", + "#n3t5", + "#n3v5", + // N4 elements + "#n4t3", + "#n4v3", + "#n4t4", + "#n4v4", + "#n4t5", + "#n4v5", + // N5 elements + "#n5t2", + "#n5v2", + "#n5t3", + "#n5v3", + "#n5t4", + "#n5v4", + "#n5t5", + "#n5v5", + // N6 elements + "#n6t3", + "#n6v3", + "#n6t4", + "#n6v4", + "#n6t5", + "#n6v5", + // Arrows + "#n1n6", + "#n1n6text", + "#n3n5", + "#n3n5text", + "#n4n1", + "#n4n1text", + "#n4n2", + "#n4n2text", + "#n4n3", + "#n4n3text", + "#n4n5", + "#n4n5text", + "#n4n6", + "#n4n6text", + "#n5n3", + "#n5n3text", + "#n5n4", + "#n5n4text", + // Quorums + "#quorum1", + "#quorum2", + "#quorum3", + // Descriptions + "#desc1", + "#desc2", + "#desc3", + "#desc4", + "#desc5", + ]); + + + // Step 1: C6 recruits N3, N4 and N5 + animator + .show("#n3", { fill: COLORS.active }) + .show("#n4", { fill: COLORS.active }) + .show("#n5", { fill: COLORS.active }) + .show("#quorum1", { autoAlpha: 1 }) + .morphText("#desc1", "* C6 recruits N3, N4 and N5") + .show("#desc1", { autoAlpha: 1, fill: COLORS.active, attr: { dx: -300, dy: 450 } }); + + animator.addLabel("step1"); + + // Step 2: N3 appends 5-B to N5 + animator + .morphText("#n3n5text", "Append 5-B") + .animateArrow("#n3n5", { duration: DURATION.normal }) + .show("#n3n5text", { autoAlpha: 1 }) + .wait(DURATION.fast) + .show("#n5t2", { autoAlpha: 1 }) + .show("#n5v2", { autoAlpha: 1 }) + .morphText("#desc2", "* N3 appends 5-B to N5") + .show("#desc2", { autoAlpha: 1, fill: COLORS.active, attr: { dx: -300, dy: 450 } }); + + animator.addLabel("step2"); + + // Step 3: N3 appends 6-ok to itself and appends to N5 + animator + .wait(DURATION.fast) + .show("#n3t3", { autoAlpha: 1 }) + .show("#n3v3", { autoAlpha: 1 }) + .unanimateArrow("#n3n5", { duration: DURATION.instant }) + .set("#n3n5text", { autoAlpha: 0 }) + .morphText("#n3n5text", "Append 6-ok") + .animateArrow("#n3n5", { duration: DURATION.normal }) + .show("#n3n5text", { autoAlpha: 1 }) + .wait(DURATION.fast) + .show("#n5t3", { autoAlpha: 1 }) + .show("#n5v3", { autoAlpha: 1 }) + .morphText("#desc3", "* N3 appends 6-ok to itself and N5") + .show("#desc3", { autoAlpha: 1, fill: COLORS.active, attr: { dx: -300, dy: 450 } }); + + animator.addLabel("step3"); + + // Step 4: C6 crashes + animator + .morphText("#desc4", "* C6 crashes") + .show("#desc4", { autoAlpha: 1, fill: COLORS.red, attr: { dx: -300, dy: 450 } }); + + animator.addLabel("step4"); +}; diff --git a/src/components/part07Fig3Scenario3.ts b/src/components/part07Fig3Scenario3.ts new file mode 100644 index 0000000..f490e80 --- /dev/null +++ b/src/components/part07Fig3Scenario3.ts @@ -0,0 +1,154 @@ +/** + * Animation configuration for Part 7 Figure 3 - Scenario 3 + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part07-fig3.svg - Scenario 3 + * C7 recruits N1, N4 and N6, propagates N1 to N6, then fails + * + * Available IDs in SVG (nodes N1-N6 with states t2-t5, v2-v5 and arrows between them): + * - n1t3-n1t5, n1v3-n1v5: N1's term/value at different states + * - n2t3-n2t5, n2v3-n2v5: N2's term/value at different states + * - n3t3-n3t5, n3v3-n3v5: N3's term/value at different states + * - n4t3-n4t5, n4v3-n4v5: N4's term/value at different states + * - n5t2-n5t5, n5v2-n5v5: N5's term/value at different states + * - n6t3-n6t5, n6v3-n6v5: N6's term/value at different states + * - Arrows: n1n6, n3n5, n4n1, n4n2, n4n3, n4n5, n4n6, n5n3, n5n4 + * - Arrow text: corresponding *text suffix for each arrow + */ +export const part07Fig3Scenario3 = (animator: SVGAnimator) => { + // Initial state: Ending state of Scenario 2 (after C6 crashes) + // N1 has slots 3 and 4, N2 has slot 3, N3 has slot 3 (6-ok), N5 has slots 2 and 3 (5-B and 6-ok) + animator.hideElements([ + // N1 elements (slots 3 and 4 are visible initially, only hide slot 5) + "#n1t5", + "#n1v5", + // N2 elements (slot 3 is visible initially) + "#n2t4", + "#n2v4", + "#n2t5", + "#n2v5", + // N3 elements (slot 3 is visible - 6-ok from Scenario 2) + "#n3t4", + "#n3v4", + "#n3t5", + "#n3v5", + // N4 elements (all hidden) + "#n4t3", + "#n4v3", + "#n4t4", + "#n4v4", + "#n4t5", + "#n4v5", + // N5 elements (slots 2 and 3 are visible - 5-B and 6-ok from Scenario 2) + "#n5t4", + "#n5v4", + "#n5t5", + "#n5v5", + // N6 elements + "#n6t3", + "#n6v3", + "#n6t4", + "#n6v4", + "#n6t5", + "#n6v5", + // Arrows + "#n1n6", + "#n1n6text", + "#n3n5", + "#n3n5text", + "#n4n1", + "#n4n1text", + "#n4n2", + "#n4n2text", + "#n4n3", + "#n4n3text", + "#n4n5", + "#n4n5text", + "#n4n6", + "#n4n6text", + "#n5n3", + "#n5n3text", + "#n5n4", + "#n5n4text", + // Quorums + "#quorum1", + "#quorum2", + "#quorum3", + // Descriptions + "#desc1", + "#desc2", + "#desc3", + "#desc4", + "#desc5", + ]); + + // Step 1: C7 recruits N1, N4 and N6 + animator + .show("#n1", { fill: COLORS.blue }) + .show("#n4", { fill: COLORS.blue }) + .show("#n6", { fill: COLORS.blue }) + .show("#quorum2", { autoAlpha: 1 }) + .morphText("#desc1", "* C7 recruits N1, N4 and N6") + .show("#desc1", { autoAlpha: 1, fill: COLORS.blue, attr: { dx: -200, dy: -60 } }); + + animator.addLabel("step1"); + + // Step 2: N1 propagates C (with term 5) to N6 + animator + .morphText("#n1n6text", "Append 5-C") + .animateArrow("#n1n6", { duration: DURATION.normal }) + .show("#n1n6text", { autoAlpha: 1 }) + .wait(DURATION.fast) + .show("#n6t3", { autoAlpha: 1 }) + .show("#n6v3", { autoAlpha: 1 }) + .morphText("#desc2", "* Step 2: Append 5-C") + .show("#desc2", { autoAlpha: 1, fill: COLORS.blue, attr: { dx: -200, dy: -60 } }); + + animator.addLabel("step2"); + + // Step 3: N1 propagates D (with term 5) to N6 + animator + .unanimateArrow("#n1n6", { duration: DURATION.instant }) + .set("#n1n6text", { autoAlpha: 0 }) + .morphText("#n1n6text", "Append 5-D") + .animateArrow("#n1n6", { duration: DURATION.normal }) + .show("#n1n6text", { autoAlpha: 1 }) + .wait(DURATION.fast) + .show("#n6t4", { autoAlpha: 1 }) + .show("#n6v4", { autoAlpha: 1 }) + .morphText("#desc3", "* Step 3: Append 5-D") + .show("#desc3", { autoAlpha: 1, fill: COLORS.blue, attr: { dx: -200, dy: -60 } }); + + animator.addLabel("step3"); + + // Step 4: N1 creates new request with term 7 and replicates to N6 + animator + .wait(DURATION.fast) + .show("#n1t5", { autoAlpha: 1 }) + .show("#n1v5", { autoAlpha: 1 }) + .unanimateArrow("#n1n6", { duration: DURATION.instant }) + .set("#n1n6text", { autoAlpha: 0 }) + .morphText("#n1n6text", "Append 7-ok") + .animateArrow("#n1n6", { duration: DURATION.normal }) + .show("#n1n6text", { autoAlpha: 1 }) + .wait(DURATION.fast) + .show("#n6t5", { autoAlpha: 1 }) + .show("#n6v5", { autoAlpha: 1 }) + .morphText("#desc4", "* Step 4: Append 7-ok to N1 and transmit") + .show("#desc4", { autoAlpha: 1, fill: COLORS.blue, attr: { dx: -200, dy: -60 } }); + + animator.addLabel("step4"); + + // Step 5: C7 crashes + animator + .morphText("#desc5", "* C7 crashes") + .show("#desc5", { autoAlpha: 1, fill: COLORS.red, attr: { dx: -200, dy: -60 } }); + + animator.addLabel("step5"); +}; diff --git a/src/components/part07Fig3Scenario4.ts b/src/components/part07Fig3Scenario4.ts new file mode 100644 index 0000000..b3b5ba4 --- /dev/null +++ b/src/components/part07Fig3Scenario4.ts @@ -0,0 +1,144 @@ +/** + * Animation configuration for Part 7 Figure 3 - Scenario 4 + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part07-fig3.svg - Scenario 4 + * Initial state matches ending state of Scenario 3 + * + * Available IDs in SVG (nodes N1-N6 with states t2-t5, v2-v5 and arrows between them): + * - n1t3-n1t5, n1v3-n1v5: N1's term/value at different states + * - n2t3-n2t5, n2v3-n2v5: N2's term/value at different states + * - n3t3-n3t5, n3v3-n3v5: N3's term/value at different states + * - n4t3-n4t5, n4v3-n4v5: N4's term/value at different states + * - n5t2-n5t5, n5v2-n5v5: N5's term/value at different states + * - n6t3-n6t5, n6v3-n6v5: N6's term/value at different states + * - Arrows: n1n6, n3n5, n4n1, n4n2, n4n3, n4n5, n4n6, n5n3, n5n4 + * - Arrow text: corresponding *text suffix for each arrow + */ +export const part07Fig3Scenario4 = (animator: SVGAnimator) => { + // Initial state: Ending state of Scenario 3 (after C7 propagates to N6) + // N1 has slots 3, 4, 5 (C, D, 7-ok), N2 has slot 3, N3 has slot 3 (6-ok), + // N5 has slots 2 and 3 (5-B and 6-ok), N6 has slots 3, 4, 5 (C, D, 7-ok) + animator.hideElements([ + // N1 elements (slots 3, 4, and 5 are visible initially) + // N2 elements (slot 3 is visible initially) + "#n2t4", + "#n2v4", + "#n2t5", + "#n2v5", + // N3 elements (slot 3 is visible - 6-ok from Scenario 2) + "#n3t4", + "#n3v4", + "#n3t5", + "#n3v5", + // N4 elements (all hidden) + "#n4t3", + "#n4v3", + "#n4t4", + "#n4v4", + "#n4t5", + "#n4v5", + // N5 elements (slots 2 and 3 are visible - 5-B and 6-ok from Scenario 2) + "#n5t4", + "#n5v4", + "#n5t5", + "#n5v5", + // N6 elements (slots 3, 4, and 5 are visible - C, D, 7-ok from Scenario 3) + // Arrows + "#n1n6", + "#n1n6text", + "#n3n5", + "#n3n5text", + "#n4n1", + "#n4n1text", + "#n4n2", + "#n4n2text", + "#n4n3", + "#n4n3text", + "#n4n5", + "#n4n5text", + "#n4n6", + "#n4n6text", + "#n5n3", + "#n5n3text", + "#n5n4", + "#n5n4text", + // Quorums + "#quorum1", + "#quorum2", + "#quorum3", + // Descriptions + "#desc1", + "#desc2", + "#desc3", + "#desc4", + "#desc5", + ]); + + // Step 1: C8 recruits N3, N4 and N5 + animator + .show("#n3", { fill: COLORS.orange }) + .show("#n4", { fill: COLORS.orange }) + .show("#n5", { fill: COLORS.orange }) + .show("#quorum1", { autoAlpha: 1, stroke: COLORS.orange }) + .morphText("#desc1", "* C8 recruits N3, N4 and N5") + .show("#desc1", { autoAlpha: 1, fill: COLORS.orange, attr: { dx: -300, dy: 450 } }); + + animator.addLabel("step1"); + + // Step 2: N5 appends 6-ok to N4 + animator + .morphText("#n5n4text", "Append 6-ok") + .animateArrow("#n5n4", { duration: DURATION.normal }) + .show("#n5n4text", { autoAlpha: 1 }) + .wait(DURATION.fast) + .show("#n4t3", { autoAlpha: 1 }) + .show("#n4v3", { autoAlpha: 1 }) + .morphText("#desc2", "* N5 appends 6-ok to N4") + .show("#desc2", { autoAlpha: 1, fill: COLORS.orange, attr: { dx: -300, dy: 450 } }); + + animator.addLabel("step2"); + + // Step 3: N5 applies and propagates 8-ok to N3 and N4 + animator + .wait(DURATION.fast) + .show("#n5t4", { autoAlpha: 1 }) + .show("#n5v4", { autoAlpha: 1 }) + .unanimateArrow("#n5n4", { duration: DURATION.instant }) + .set("#n5n4text", { autoAlpha: 0 }) + .group(() => { + animator + .morphText("#n5n3text", "Append 8-ok") + .animateArrow("#n5n3", { duration: DURATION.normal }) + .show("#n5n3text", { autoAlpha: 1 }); + animator + .morphText("#n5n4text", "Append 8-ok") + .animateArrow("#n5n4", { duration: DURATION.normal }) + .show("#n5n4text", { autoAlpha: 1 }); + }) + .wait(DURATION.fast) + .show("#n3t4", { autoAlpha: 1 }) + .show("#n3v4", { autoAlpha: 1 }) + .show("#n4t4", { autoAlpha: 1 }) + .show("#n4v4", { autoAlpha: 1 }) + .morphText("#desc3", "* N5 appends 8-ok to itself") + .show("#desc3", { autoAlpha: 1, fill: COLORS.orange, attr: { dx: -300, dy: 450 } }) + .morphText("#desc4", " and propagates to N3 and N4") + .show("#desc4", { autoAlpha: 1, fill: COLORS.orange, attr: { dx: -300, dy: 450 } }); + + animator.addLabel("step3"); + + // Step 4: 8-ok reaches durability but C8 crashes + animator + .wait(DURATION.fast) + .morphText("#desc5", "* Durability reached, but C8 crashes") + .show("#desc5", { autoAlpha: 1, fill: COLORS.red, attr: { dx: -300, dy: 450 } }); + + animator.addLabel("step4"); +}; diff --git a/src/components/part07Fig3Scenario5.ts b/src/components/part07Fig3Scenario5.ts new file mode 100644 index 0000000..f8fe52d --- /dev/null +++ b/src/components/part07Fig3Scenario5.ts @@ -0,0 +1,218 @@ +/** + * Animation configuration for Part 7 Figure 3 - Scenario 5 + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part07-fig3.svg - Scenario 5 + * Initial state matches ending state of Scenario 4 + * + * Available IDs in SVG (nodes N1-N6 with states t2-t5, v2-v5 and arrows between them): + * - n1t3-n1t5, n1v3-n1v5: N1's term/value at different states + * - n2t3-n2t5, n2v3-n2v5: N2's term/value at different states + * - n3t3-n3t5, n3v3-n3v5: N3's term/value at different states + * - n4t3-n4t5, n4v3-n4v5: N4's term/value at different states + * - n5t2-n5t5, n5v2-n5v5: N5's term/value at different states + * - n6t3-n6t5, n6v3-n6v5: N6's term/value at different states + * - Arrows: n1n6, n3n5, n4n1, n4n2, n4n3, n4n5, n4n6, n5n3, n5n4 + * - Arrow text: corresponding *text suffix for each arrow + */ +export const part07Fig3Scenario5 = (animator: SVGAnimator) => { + // Initial state: Ending state of Scenario 4 (after C8 crashes with 8-ok durable) + // N1 has slots 3, 4, 5 (C, D, 7-ok), N2 has slot 3 (C), + // N3 has slots 3, 4 (6-ok, 8-ok), N4 has slots 3, 4 (6-ok, 8-ok), + // N5 has slots 2, 3, 4 (5-B, 6-ok, 8-ok), N6 has slots 3, 4, 5 (C, D, 7-ok) + animator.hideElements([ + // N1 elements (slots 3, 4, and 5 are visible initially) + // N2 elements (slot 3 is visible initially) + "#n2t4", + "#n2v4", + "#n2t5", + "#n2v5", + // N3 elements (slots 3 and 4 are visible - 6-ok and 8-ok from Scenario 4) + "#n3t5", + "#n3v5", + // N4 elements (slots 3 and 4 are visible - 6-ok and 8-ok from Scenario 4) + "#n4t5", + "#n4v5", + // N5 elements (slots 2, 3, and 4 are visible - 5-B, 6-ok, 8-ok from Scenario 4) + "#n5t5", + "#n5v5", + // N6 elements (slots 3, 4, and 5 are visible - C, D, 7-ok from Scenario 4) + // Arrows + "#n1n6", + "#n1n6text", + "#n3n5", + "#n3n5text", + "#n4n1", + "#n4n1text", + "#n4n2", + "#n4n2text", + "#n4n3", + "#n4n3text", + "#n4n5", + "#n4n5text", + "#n4n6", + "#n4n6text", + "#n5n3", + "#n5n3text", + "#n5n4", + "#n5n4text", + // Quorums + "#quorum1", + "#quorum2", + "#quorum3", + // Descriptions + "#desc1", + "#desc2", + "#desc3", + "#desc4", + "#desc5", + ]); + + // Step 1: C9 discovers all nodes + animator + .show("#n1", { fill: COLORS.purple }) + .show("#n2", { fill: COLORS.purple }) + .show("#n3", { fill: COLORS.purple }) + .show("#n4", { fill: COLORS.purple }) + .show("#n5", { fill: COLORS.purple }) + .show("#n6", { fill: COLORS.purple }) + .show("#quorum3", { autoAlpha: 1 }) + .morphText("#desc1", "* C9 discovers all nodes") + .show("#desc1", { autoAlpha: 1, fill: COLORS.purple, attr: { dx: -90, dy: -120 } }); + + animator.addLabel("step1"); + + // Step 2: N1 appends 6-ok to N1, N2 and N6 + animator + .group(() => { + animator + .morphText("#n4n1text", "Append 6-ok") + .animateArrow("#n4n1", { duration: DURATION.normal }) + .morphText("#n4n2text", "Append 6-ok") + .animateArrow("#n4n2", { duration: DURATION.normal }) + .morphText("#n4n6text", "Append 6-ok") + .animateArrow("#n4n6", { duration: DURATION.normal }); + }) + .show("#n4n1text", { autoAlpha: 1 }) + .show("#n4n2text", { autoAlpha: 1 }) + .show("#n4n6text", { autoAlpha: 1 }) + .changeText("#n1t3", "", COLORS.active, DURATION.instant) + .changeText("#n1v3", "", COLORS.active, DURATION.instant) + .changeText("#n1t4", "", COLORS.active, DURATION.instant) + .changeText("#n1v4", "", COLORS.active, DURATION.instant) + .changeText("#n1t5", "", COLORS.active, DURATION.instant) + .changeText("#n1v5", "", COLORS.active, DURATION.instant) + .changeText("#n2t3", "", COLORS.active, DURATION.instant) + .changeText("#n2v3", "", COLORS.active, DURATION.instant) + .changeText("#n6t3", "", COLORS.active, DURATION.instant) + .changeText("#n6v3", "", COLORS.active, DURATION.instant) + .changeText("#n6t4", "", COLORS.active, DURATION.instant) + .changeText("#n6v4", "", COLORS.active, DURATION.instant) + .changeText("#n6t5", "", COLORS.active, DURATION.instant) + .changeText("#n6v5", "", COLORS.active, DURATION.instant) + .group(() => { + animator + .changeText("#n1t3", "6", COLORS.active, DURATION.fast) + .changeText("#n1v3", "ok", COLORS.active, DURATION.fast) + .changeText("#n2t3", "6", COLORS.active, DURATION.fast) + .changeText("#n2v3", "ok", COLORS.active, DURATION.fast) + .changeText("#n6t3", "6", COLORS.active, DURATION.fast) + .changeText("#n6v3", "ok", COLORS.active, DURATION.fast); + }) + .morphText("#desc2", "* N4 appends 6-ok to N1, N2 and N6") + .show("#desc2", { autoAlpha: 1, fill: COLORS.purple, attr: { dx: -90, dy: -120 } }) + .morphText("#desc3", " truncating logs as needed") + .show("#desc3", { autoAlpha: 1, fill: COLORS.purple, attr: { dx: -90, dy: -120 } }); + + animator.addLabel("step2"); + + // Step 3: N1 appends 8-ok to N1, N2 and N6 + animator + .unanimateArrow("#n4n1", { duration: DURATION.instant }) + .unanimateArrow("#n4n2", { duration: DURATION.instant }) + .unanimateArrow("#n4n6", { duration: DURATION.instant }) + .group(() => { + animator + .morphText("#n4n1text", "Append 8-ok") + .animateArrow("#n4n1", { duration: DURATION.normal }) + .morphText("#n4n2text", "Append 8-ok") + .animateArrow("#n4n2", { duration: DURATION.normal }) + .morphText("#n4n6text", "Append 8-ok") + .animateArrow("#n4n6", { duration: DURATION.normal }); + }) + .show("#n4n1text", { autoAlpha: 1 }) + .show("#n4n2text", { autoAlpha: 1 }) + .show("#n4n6text", { autoAlpha: 1 }) + .group(() => { + animator + .changeText("#n1t4", "8", COLORS.orange, DURATION.fast) + .changeText("#n1v4", "ok", COLORS.orange, DURATION.fast) + .changeText("#n2t4", "8", COLORS.orange, DURATION.fast) + .show("#n2t4", { autoAlpha: 1 }) + .changeText("#n2v4", "ok", COLORS.orange, DURATION.fast) + .show("#n2v4", { autoAlpha: 1 }) + .changeText("#n6t4", "8", COLORS.orange, DURATION.fast) + .changeText("#n6v4", "ok", COLORS.orange, DURATION.fast); + }) + .morphText("#desc4", "* N4 appends 8-ok to N1, N2 and N6") + .show("#desc4", { autoAlpha: 1, fill: COLORS.purple, attr: { dx: -90, dy: -120 } }); + + animator.addLabel("step3"); + + // Step 3: N4 appends 9-ok to itself and transmits to all + animator + .wait(DURATION.fast) + .show("#n4t5", { autoAlpha: 1 }) + .show("#n4v5", { autoAlpha: 1 }) + .unanimateArrow("#n4n1", { duration: DURATION.instant }) + .set("#n4n1text", { autoAlpha: 0 }) + .unanimateArrow("#n4n2", { duration: DURATION.instant }) + .set("#n4n2text", { autoAlpha: 0 }) + .unanimateArrow("#n4n5", { duration: DURATION.instant }) + .set("#n4n5text", { autoAlpha: 0 }) + .unanimateArrow("#n4n6", { duration: DURATION.instant }) + .set("#n4n6text", { autoAlpha: 0 }) + .group(() => { + animator + .morphText("#n4n1text", "Append 9-ok") + .animateArrow("#n4n1", { duration: DURATION.normal }) + .show("#n4n1text", { autoAlpha: 1 }); + animator + .morphText("#n4n2text", "Append 9-ok") + .animateArrow("#n4n2", { duration: DURATION.normal }) + .show("#n4n2text", { autoAlpha: 1 }); + animator + .morphText("#n4n3text", "Append 9-ok") + .animateArrow("#n4n3", { duration: DURATION.normal }) + .show("#n4n3text", { autoAlpha: 1 }); + animator + .morphText("#n4n5text", "Append 9-ok") + .animateArrow("#n4n5", { duration: DURATION.normal }) + .show("#n4n5text", { autoAlpha: 1 }); + animator + .morphText("#n4n6text", "Append 9-ok") + .animateArrow("#n4n6", { duration: DURATION.normal }) + .show("#n4n6text", { autoAlpha: 1 }); + }) + .wait(DURATION.fast) + .changeText("#n1t5", "9", COLORS.purple, DURATION.instant) + .changeText("#n1v5", "ok", COLORS.purple, DURATION.instant) + .show("#n2t5", { autoAlpha: 1 }) + .show("#n2v5", { autoAlpha: 1 }) + .show("#n3t5", { autoAlpha: 1 }) + .show("#n3v5", { autoAlpha: 1 }) + .show("#n5t5", { autoAlpha: 1 }) + .show("#n5v5", { autoAlpha: 1 }) + .changeText("#n6t5", "9", COLORS.purple, DURATION.instant) + .changeText("#n6v5", "ok", COLORS.purple, DURATION.instant) + .morphText("#desc5", "* N4 appends 9-ok to itself and transmits") + .show("#desc5", { autoAlpha: 1, fill: COLORS.purple, attr: { dx: -90, dy: -120 } }); + + animator.addLabel("step4"); +}; diff --git a/src/components/part08Fig3a.ts b/src/components/part08Fig3a.ts new file mode 100644 index 0000000..c48adb3 --- /dev/null +++ b/src/components/part08Fig3a.ts @@ -0,0 +1,113 @@ +/** + * Animation configuration for Part 8 Figure 3a - Ruleset change scenario 1 + * C7 recruits N2 and discovers a more progressed timeline + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part08-fig3.svg - Scenario 1 + * Shows C7 recruiting N2, discovering progressed timeline, + * and delegating leadership back to N1 + */ +export const part08Fig3a = (animator: SVGAnimator) => { + // Initial state: Hide all animated elements except c, ctext, and node rules + animator.hideElements([ + "#cn1", + "#cn1text", + "#cn2", + "#cn2text", + "#cn3", + "#cn3text", + "#desc1", + "#desc2", + "#desc3", + "#desc4", + "#desc5", + "#n1applied", + "#n1t2", + "#n1t3", + "#n1t4", + "#n1v2", + "#n1v3", + "#n1v4", + "#n2applied", + "#n2t2", + "#n2t3", + "#n2t4", + "#n2v2", + "#n2v3", + "#n2v4", + "#n3applied", + "#n3t2", + "#n3t3", + "#n3t4", + "#n3v2", + "#n3v2hi", + "#n3v3", + "#n3v4", + ]); + + // Step 1: C revokes N1 leadership (orange) + animator + .show("#c", { stroke: COLORS.orange }) + .show("#ctext", { fill: COLORS.orange }) + .show("#cn1", { stroke: COLORS.orange }) + .animateArrow("#cn1", { duration: DURATION.normal }) + .morphText("#cn1text", "Revoke", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#desc1", "* C6 revokes N1's leadership", { + duration: DURATION.instant, + }) + .show("#desc1", { autoAlpha: 1, fill: COLORS.orange }) + .wait(DURATION.pause); + + animator.addLabel("step1"); + + // Step 2: C6 appends ruleset change 6-rs2 to logs of N1, N2, N3 + animator + .unanimateArrow("#cn1", { duration: DURATION.instant }) + .show("#desc1", { fill: COLORS.inactive }) + .show("#cn1text", { autoAlpha: 0 }) + .show("#cn2", { stroke: COLORS.orange }) + .show("#cn3", { stroke: COLORS.orange }) + .group(() => { + animator.animateArrow("#cn1", { duration: DURATION.normal }); + animator.animateArrow("#cn2", { duration: DURATION.normal }); + animator.animateArrow("#cn3", { duration: DURATION.normal }); + }) + .morphText("#cn1text", "rs2", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#cn2text", "rs2", { duration: DURATION.instant }) + .show("#cn2text", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#cn3text", "rs2", { duration: DURATION.instant }) + .show("#cn3text", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n1t2", "6", { duration: DURATION.instant }) + .show("#n1t2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n1v2", "rs2", { duration: DURATION.instant }) + .show("#n1v2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n2t2", "6", { duration: DURATION.instant }) + .show("#n2t2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n2v2", "rs2", { duration: DURATION.instant }) + .show("#n2v2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n3t2", "6", { duration: DURATION.instant }) + .show("#n3t2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n3v2", "rs2", { duration: DURATION.instant }) + .show("#n3v2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n1term", "6", { duration: DURATION.instant }) + .show("#n1term", { fill: COLORS.orange }) + .morphText("#n2term", "6", { duration: DURATION.instant }) + .show("#n2term", { fill: COLORS.orange }) + .morphText("#n3term", "6", { duration: DURATION.instant }) + .show("#n3term", { fill: COLORS.orange }) + .morphText("#desc2", "* C6 appends 6-rs2 to N1, N2, N3", { + duration: DURATION.instant, + }) + .show("#desc2", { autoAlpha: 1, fill: COLORS.orange }) + .wait(DURATION.pause); + + animator.addLabel("step2"); +}; diff --git a/src/components/part08Fig3b.ts b/src/components/part08Fig3b.ts new file mode 100644 index 0000000..7bb0f3c --- /dev/null +++ b/src/components/part08Fig3b.ts @@ -0,0 +1,338 @@ +/** + * Animation configuration for Part 8 Figure 3b - Ruleset change scenario 2 + * C7 recruits N2 and discovers timelines have not progressed + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part08-fig3.svg - Scenario 2 + * Shows C7 recruiting N2, discovering no progress, + * and propagating using combined ruleset + */ +export const part08Fig3b = (animator: SVGAnimator) => { + // Hide elements that will be animated in this scenario + animator.hideElements([ + "#cn1", + "#cn1text", + "#cn2", + "#cn2text", + "#cn3", + "#cn3text", + "#desc1", + "#desc2", + "#desc3", + "#desc4", + "#desc5", + "#n1applied", + "#n1t3", + "#n1t4", + "#n1v3", + "#n1v4", + "#n2applied", + "#n2t3", + "#n2t4", + "#n2v3", + "#n2v4", + "#n3applied", + "#n3t3", + "#n3t4", + "#n3v2hi", + "#n3v3", + "#n3v4", + ]); + + // Initial state: Start from ending state of fig3a (but with arrows hidden) + // Show elements that were visible at end of fig3a + animator + .morphText("#n1t2", "6", { duration: 0 }) + .show("#n1t2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n1v2", "rs2", { duration: 0 }) + .show("#n1v2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n2t2", "6", { duration: 0 }) + .show("#n2t2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n2v2", "rs2", { duration: 0 }) + .show("#n2v2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n3t2", "6", { duration: 0 }) + .show("#n3t2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n3v2", "rs2", { duration: 0 }) + .show("#n3v2", { autoAlpha: 1, fill: COLORS.orange }) + .show("#n1term", { fill: COLORS.orange }) + .morphText("#n1term", "6", { duration: 0 }) + .show("#n2term", { fill: COLORS.orange }) + .morphText("#n2term", "6", { duration: 0 }) + .show("#n3term", { fill: COLORS.orange }) + .morphText("#n3term", "6", { duration: 0 }) + .show("#ctext", { fill: COLORS.orange }) + .show("#c", { stroke: COLORS.orange }); + + // Step 1: C6 promotes N1 as leader + animator + .show("#cn1", { stroke: COLORS.orange }) + .animateArrow("#cn1", { duration: DURATION.normal }) + .morphText("#cn1text", "Promote", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#desc1", "* C6 promotes N1 as leader", { + duration: DURATION.instant, + }) + .show("#desc1", { autoAlpha: 1, fill: COLORS.orange }) + .show("#n3applied", { autoAlpha: 1 }); + + animator.addLabel("step1"); + + // Step 2: N1 and N2 apply 5-a and 6-rs2 + animator + .unanimateArrow("#cn1", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 0 }) + .show("#desc1", { fill: COLORS.inactive }) + .show("#n1applied", { autoAlpha: 1, stroke: COLORS.orange }) + .show("#n2applied", { autoAlpha: 1, stroke: COLORS.orange }); + + animator.group(() => { + animator.moveTo( + "#n1applied", + null, + { x: 80, y: 0 }, + { duration: DURATION.normal }, + ); + animator.moveTo( + "#n2applied", + null, + { x: 80, y: 0 }, + { duration: DURATION.normal }, + ); + }); + + animator + .morphText("#n1rule", "rs2", { duration: DURATION.instant }) + .show("#n1rule", { fill: COLORS.orange }) + .morphText("#n2rule", "rs2", { duration: DURATION.instant }) + .show("#n2rule", { fill: COLORS.orange }); + + animator + .morphText("#desc2", "* N1 and N2 apply 5-a and 6-rs2", { + duration: DURATION.instant, + }) + .show("#desc2", { autoAlpha: 1, fill: COLORS.orange }); + + animator.addLabel("step2"); + + // Step 3: N1 accepts request 6-B, propagates to N2 and applies it + animator + .show("#desc2", { fill: COLORS.inactive }) + .show("#n1t3", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n1t3", "6", { duration: DURATION.instant }) + .show("#n1v3", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n1v3", "B", { duration: DURATION.instant }) + .show("#n2t3", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n2t3", "6", { duration: DURATION.instant }) + .show("#n2v3", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n2v3", "B", { duration: DURATION.instant }); + + animator.group(() => { + animator.moveTo( + "#n1applied", + null, + { x: 120, y: 0 }, + { duration: DURATION.normal }, + ); + animator.moveTo( + "#n2applied", + null, + { x: 120, y: 0 }, + { duration: DURATION.normal }, + ); + }); + + animator + .morphText("#desc3", "* N1 accepts request 6-B,", { + duration: DURATION.instant, + }) + .show("#desc3", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#desc4", " propagates to N2", { + duration: DURATION.instant, + }) + .show("#desc4", { autoAlpha: 1, fill: COLORS.orange }); + + animator.addLabel("step3"); + + // Step 4: C7 discovers N3 + animator + .show("#desc1", { autoAlpha: 0 }) + .morphText("#desc1", "", { duration: DURATION.instant }) + .show("#desc2", { autoAlpha: 0 }) + .morphText("#desc2", "", { duration: DURATION.instant }) + .show("#desc3", { autoAlpha: 0 }) + .morphText("#desc3", "", { duration: DURATION.instant }) + .show("#desc4", { autoAlpha: 0 }) + .morphText("#desc4", "", { duration: DURATION.instant }) + .morphText("#ctext", "", { duration: DURATION.instant }) + .show("#c", { stroke: COLORS.blue }) + .show("#ctext", { fill: COLORS.blue }) + .show("#cn3", { stroke: COLORS.blue }) + .morphText("#ctext", "C7", { duration: DURATION.normal }) + .animateArrow("#cn3", { duration: DURATION.normal }) + .morphText("#cn3text", "Recruit", { duration: DURATION.instant }) + .show("#cn3text", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n3term", "7", { duration: DURATION.instant }) + .show("#n3term", { fill: COLORS.blue }) + .morphText("#desc1", "* C7 recruits N3", { + duration: DURATION.instant, + }) + .show("#desc1", { autoAlpha: 1, fill: COLORS.blue }); + + animator.addLabel("step4"); + + // Step 5: C7 discovers rs2, must recruit N2 + animator + .show("#desc1", { fill: COLORS.inactive }) + .show("#n3v2hi", { autoAlpha: 1, stroke: COLORS.blue }) + .morphText("#desc2", "* C7 discovers rs2,", { + duration: DURATION.instant, + }) + .show("#desc2", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#desc3", " must recruit N2", { + duration: DURATION.instant, + }) + .show("#desc3", { autoAlpha: 1, fill: COLORS.blue }); + + animator.addLabel("step5"); + + // Step 6: C7 recruits N2, discovers progressed timeline + animator + .show("#n3v2hi", { autoAlpha: 0 }) + .show("#desc2", { fill: COLORS.inactive }) + .show("#desc3", { fill: COLORS.inactive }) + .show("#cn2", { stroke: COLORS.blue }) + .animateArrow("#cn2", { duration: DURATION.normal }) + .morphText("#cn2text", "Recruit", { duration: DURATION.instant }) + .show("#cn2text", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n2term", "7", { duration: DURATION.instant }) + .show("#n2term", { fill: COLORS.blue }) + .morphText("#desc4", "* C7 recruits N2,", { + duration: DURATION.instant, + }) + .show("#desc4", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#desc5", " discovers progressed timeline", { + duration: DURATION.instant, + }) + .show("#desc5", { autoAlpha: 1, fill: COLORS.blue }); + + animator.addLabel("step6"); + + // Step 7: C7 promotes N1 into term 7, propagates 7-ok to all nodes + animator + .show("#desc1", { autoAlpha: 0 }) + .morphText("#desc1", "", { duration: DURATION.instant }) + .show("#desc2", { autoAlpha: 0 }) + .morphText("#desc2", "", { duration: DURATION.instant }) + .show("#desc3", { autoAlpha: 0 }) + .morphText("#desc3", "", { duration: DURATION.instant }) + .show("#desc4", { autoAlpha: 0 }) + .morphText("#desc4", "", { duration: DURATION.instant }) + .show("#desc5", { autoAlpha: 0 }) + .morphText("#desc5", "", { duration: DURATION.instant }) + .show("#cn1", { stroke: COLORS.blue }) + .unanimateArrow("#cn1", { duration: DURATION.instant }) + .unanimateArrow("#cn2", { duration: DURATION.instant }) + .unanimateArrow("#cn3", { duration: DURATION.instant }) + .show("#cn3text", { autoAlpha: 0 }) + .morphText("#cn3text", "", { duration: DURATION.instant }); + + animator.group(() => { + animator.animateArrow("#cn1", { duration: DURATION.normal }); + animator.animateArrow("#cn2", { duration: DURATION.normal }); + }); + + animator + .morphText("#cn1text", "ok", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#cn2text", "ok", { duration: DURATION.instant }) + .show("#cn2text", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n1term", "7", { duration: DURATION.instant }) + .show("#n1term", { fill: COLORS.blue }) + .morphText("#n1t4", "7", { duration: DURATION.fast }) + .show("#n1t4", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n1v4", "ok", { duration: DURATION.fast }) + .show("#n1v4", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n2t4", "7", { duration: DURATION.fast }) + .show("#n2t4", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n2v4", "ok", { duration: DURATION.fast }) + .show("#n2v4", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#desc1", "* C7 appends 7-ok to N1 and N2", { + duration: DURATION.instant, + }) + .show("#desc1", { autoAlpha: 1, fill: COLORS.blue }); + + animator.addLabel("step7"); + + animator + .unanimateArrow("#cn2", { duration: DURATION.instant }) + .show("#cn2text", { autoAlpha: 0 }) + .morphText("#cn2text", "", { duration: DURATION.instant }) + .unanimateArrow("#cn1", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 0 }) + .morphText("#cn1text", "", { duration: DURATION.instant }) + .animateArrow("#cn1", { duration: DURATION.normal }) + .morphText("#cn1text", "Promote", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 1 }) + .show("#desc1", { autoAlpha: 1, fill: COLORS.inactive }) + .morphText("#desc2", "* C7 promotes N1", { + duration: DURATION.instant, + }) + .show("#desc2", { autoAlpha: 1, fill: COLORS.blue }); + + animator.addLabel("step8"); + + animator + .unanimateArrow("#cn1", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 0 }) + .morphText("#cn1text", "", { duration: DURATION.instant }) + .show("#c", { stroke: COLORS.inactive }) + .show("#ctext", { fill: COLORS.inactive }) + .show("#n1applied", { stroke: COLORS.blue }) + .show("#n2applied", { stroke: COLORS.blue }); + + animator.group(() => { + animator.moveTo( + "#n1applied", + null, + { x: 160, y: 0 }, + { duration: DURATION.normal }, + ); + animator.moveTo( + "#n2applied", + null, + { x: 160, y: 0 }, + { duration: DURATION.normal }, + ); + }); + + animator + .show("#desc2", { autoAlpha: 1, fill: COLORS.inactive }) + .morphText("#desc3", "* N1 and N2 apply 7-ok", { + duration: DURATION.instant, + }) + .show("#desc3", { autoAlpha: 1, fill: COLORS.blue }); + + animator + .morphText("#n3t3", "6", { duration: DURATION.fast }) + .show("#n3t3", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n3v3", "B", { duration: DURATION.fast }) + .show("#n3v3", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n3t4", "7", { duration: DURATION.fast }) + .show("#n3t4", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n3v4", "ok", { duration: DURATION.fast }) + .show("#n3v4", { autoAlpha: 1, fill: COLORS.blue }) + .show("#n3applied", { stroke: COLORS.blue }) + .moveTo("#n3applied", null, { x: 160, y: 0 }, { duration: DURATION.normal }) + .morphText("#desc4", "* N1 propagates events to N3", { + duration: DURATION.instant, + }) + .show("#desc4", { autoAlpha: 1, fill: COLORS.blue }); + + animator.addLabel("step9"); +}; diff --git a/src/components/part08Fig3c.ts b/src/components/part08Fig3c.ts new file mode 100644 index 0000000..8158ba3 --- /dev/null +++ b/src/components/part08Fig3c.ts @@ -0,0 +1,232 @@ +/** + * Animation configuration for Part 8 Figure 3c - Ruleset change scenario 3 + * Starting from ending state of fig3b + */ + +import { SVGAnimator } from "../lib/svg-animator"; + +const COLORS = SVGAnimator.COLORS; +const DURATION = SVGAnimator.DURATION; + +/** + * Animation sequence for part08-fig3.svg - Scenario 3 + * Shows propagation using combined ruleset + */ +export const part08Fig3c = (animator: SVGAnimator) => { + // Hide elements that will be animated in this scenario + animator.hideElements([ + "#cn1", + "#cn1text", + "#cn2", + "#cn2text", + "#cn3", + "#cn3text", + "#desc1", + "#desc2", + "#desc3", + "#desc4", + "#desc5", + "#n1applied", + "#n1t3", + "#n1t4", + "#n1v3", + "#n1v4", + "#n2applied", + "#n2t3", + "#n2t4", + "#n2v3", + "#n2v4", + "#n3applied", + "#n3t3", + "#n3t4", + "#n3v2hi", + "#n3v3", + "#n3v4", + ]); + + // Initial state: Start from ending state of fig3b + animator + .morphText("#n1t2", "6", { duration: 0 }) + .show("#n1t2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n1v2", "rs2", { duration: 0 }) + .show("#n1v2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n2t2", "6", { duration: 0 }) + .show("#n2t2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n2v2", "rs2", { duration: 0 }) + .show("#n2v2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n3t2", "6", { duration: 0 }) + .show("#n3t2", { autoAlpha: 1, fill: COLORS.orange }) + .morphText("#n3v2", "rs2", { duration: 0 }) + .show("#n3v2", { autoAlpha: 1, fill: COLORS.orange }) + .show("#n1term", { fill: COLORS.orange }) + .morphText("#n1term", "6", { duration: 0 }) + .show("#n2term", { fill: COLORS.orange }) + .morphText("#n2term", "6", { duration: 0 }) + .show("#n3term", { fill: COLORS.orange }) + .morphText("#n3term", "7", { duration: 0 }) + .show("#ctext", { fill: COLORS.blue }) + .show("#c", { stroke: COLORS.blue }) + .morphText("#ctext", "C7", { duration: 0 }); + + // Step 1: C7 recruits N3 + animator + .show("#cn3", { stroke: COLORS.blue }) + .animateArrow("#cn3", { duration: DURATION.normal }) + .morphText("#cn3text", "Recruit", { duration: DURATION.instant }) + .show("#cn3text", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n3term", "7", { duration: DURATION.instant }) + .show("#n3term", { fill: COLORS.blue }) + .morphText("#desc1", "* C7 recruits N3", { + duration: DURATION.instant, + }) + .show("#desc1", { autoAlpha: 1, fill: COLORS.blue }) + .wait(DURATION.pause); + + animator.addLabel("step1"); + + // Step 2: C7 discovers rs2, must recruit N2 + animator + .show("#desc1", { fill: COLORS.inactive }) + .show("#n3v2hi", { autoAlpha: 1, stroke: COLORS.blue }) + .morphText("#desc2", "* C7 discovers rs2,", { + duration: DURATION.instant, + }) + .show("#desc2", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#desc3", " must recruit N2", { + duration: DURATION.instant, + }) + .show("#desc3", { autoAlpha: 1, fill: COLORS.blue }) + .wait(DURATION.pause); + + animator.addLabel("step2"); + + // Step 3: C7 recruits N2, discovers timeline has not progressed + animator + .show("#n3v2hi", { autoAlpha: 0 }) + .show("#desc2", { fill: COLORS.inactive }) + .show("#desc3", { fill: COLORS.inactive }) + .show("#cn2", { stroke: COLORS.blue }) + .animateArrow("#cn2", { duration: DURATION.normal }) + .morphText("#cn2text", "Recruit", { duration: DURATION.instant }) + .show("#cn2text", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n2term", "7", { duration: DURATION.instant }) + .show("#n2term", { fill: COLORS.blue }) + .morphText("#desc2", "* C7 recruits N2", { + duration: DURATION.instant, + }) + .show("#desc2", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#desc3", "* C7 discovers same timeline", { + duration: DURATION.instant, + }) + .show("#desc3", { autoAlpha: 1, fill: COLORS.blue }) + .wait(DURATION.pause); + + animator.addLabel("step3"); + + // Step 4: C7 propagates 7-ok to all three nodes, satisfying rs1 and rs2 + animator + .show("#desc2", { fill: COLORS.inactive }) + .show("#desc3", { fill: COLORS.inactive }) + .show("#cn1", { stroke: COLORS.blue }); + + animator.group(() => { + animator.animateArrow("#cn1", { duration: DURATION.normal }); + animator.animateArrow("#cn2", { duration: DURATION.normal }); + animator.animateArrow("#cn3", { duration: DURATION.normal }); + }); + + animator + .morphText("#cn1text", "ok", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#cn2text", "ok", { duration: DURATION.instant }) + .show("#cn2text", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#cn3text", "ok", { duration: DURATION.instant }) + .show("#cn3text", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n1term", "7", { duration: DURATION.instant }) + .show("#n1term", { fill: COLORS.blue }) + .morphText("#n1t3", "7", { duration: DURATION.instant }) + .show("#n1t3", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n1v3", "ok", { duration: DURATION.instant }) + .show("#n1v3", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n2t3", "7", { duration: DURATION.instant }) + .show("#n2t3", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n2v3", "ok", { duration: DURATION.instant }) + .show("#n2v3", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n3t3", "7", { duration: DURATION.instant }) + .show("#n3t3", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#n3v3", "ok", { duration: DURATION.instant }) + .show("#n3v3", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#desc4", "* C7 propagates 7-ok to all three", { + duration: DURATION.instant, + }) + .show("#desc4", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#desc5", " nodes, satisfying rs1 and rs2", { + duration: DURATION.instant, + }) + .show("#desc5", { autoAlpha: 1, fill: COLORS.blue }) + .wait(DURATION.pause); + + animator.addLabel("step4"); + + // Step 5: C7 promotes N1, N1/N2/N3 apply pending requests + animator + .show("#desc1", { autoAlpha: 0 }) + .morphText("#desc1", "", { duration: DURATION.instant }) + .show("#desc2", { autoAlpha: 0 }) + .morphText("#desc2", "", { duration: DURATION.instant }) + .show("#desc3", { autoAlpha: 0 }) + .morphText("#desc3", "", { duration: DURATION.instant }) + .show("#desc4", { autoAlpha: 0 }) + .morphText("#desc4", "", { duration: DURATION.instant }) + .show("#desc5", { autoAlpha: 0 }) + .morphText("#desc5", "", { duration: DURATION.instant }) + .unanimateArrow("#cn1", { duration: DURATION.instant }) + .unanimateArrow("#cn2", { duration: DURATION.instant }) + .unanimateArrow("#cn3", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 0 }) + .show("#cn2text", { autoAlpha: 0 }) + .show("#cn3text", { autoAlpha: 0 }) + .show("#cn1", { stroke: COLORS.blue }) + .animateArrow("#cn1", { duration: DURATION.normal }) + .morphText("#cn1text", "Promote", { duration: DURATION.instant }) + .show("#cn1text", { autoAlpha: 1, fill: COLORS.blue }) + .morphText("#desc1", "* C7 promotes N1", { + duration: DURATION.instant, + }) + .show("#desc1", { autoAlpha: 1, fill: COLORS.blue }); + + animator + .show("#n1applied", { autoAlpha: 1, stroke: COLORS.blue }) + .show("#n2applied", { autoAlpha: 1, stroke: COLORS.blue }) + .show("#n3applied", { autoAlpha: 1, stroke: COLORS.blue }); + + animator.group(() => { + animator.moveTo( + "#n1applied", + null, + { x: 120, y: 0 }, + { duration: DURATION.normal }, + ); + animator.moveTo( + "#n2applied", + null, + { x: 120, y: 0 }, + { duration: DURATION.normal }, + ); + animator.moveTo( + "#n3applied", + null, + { x: 120, y: 0 }, + { duration: DURATION.normal }, + ); + }); + + animator + .morphText("#desc2", "* N1, N2, N3 apply pending requests", { + duration: DURATION.instant, + }) + .show("#desc2", { autoAlpha: 1, fill: COLORS.blue }) + .wait(DURATION.pause); + + animator.addLabel("step5"); +}; diff --git a/src/components/requestProcessing.ts b/src/components/requestProcessing.ts new file mode 100644 index 0000000..0aafd69 --- /dev/null +++ b/src/components/requestProcessing.ts @@ -0,0 +1,472 @@ +/** + * Animation configuration for the Multigres Architecture Overview diagram + * This defines the animation sequence for /img/site/req1.svg + */ + +import type { SVGAnimator } from "../lib/svg-animator"; +import { SVGAnimator as SVGAnimatorClass } from "../lib/svg-animator"; + +// Reuse constants from SVGAnimator +const COLORS = SVGAnimatorClass.COLORS; +const DURATION = SVGAnimatorClass.DURATION; + +// Helper function to hide multiple elements +const hideElements = (animator: SVGAnimator, elements: string[]) => { + elements.forEach((el) => animator.set(el, { autoAlpha: 0 })); + return animator; +}; + +// Helper to update description text +const updateDescriptions = ( + animator: SVGAnimator, + descriptions: { [key: string]: string }, + activeColor = COLORS.active, +) => { + Object.entries(descriptions).forEach(([id, text]) => { + if (text) { + animator + .show(id, { fill: activeColor }) + .morphText(id, text, { duration: DURATION.instant }); + } else { + animator.morphText(id, "", { duration: DURATION.instant }); + } + }); + return animator; +}; + +// Helper to clear all descriptions +const clearDescriptions = (animator: SVGAnimator) => { + return updateDescriptions(animator, { + "#desc1": "", + "#desc2": "", + "#desc3": "", + "#desc4": "", + "#desc5": "", + }); +}; + +// Helper to send message to multiple nodes +const sendToFollowers = ( + animator: SVGAnimator, + message: string, + nodes: string[] = ["#n1n2", "#n1n3", "#n1n4"], +) => { + animator.group((a: SVGAnimator) => { + nodes.forEach((node) => { + a.morphText(`${node}text`, message, { duration: DURATION.instant }) + .show(`${node}text`, { autoAlpha: 1 }) + .animateArrow(node, { duration: DURATION.normal }); + }); + }); + return animator; +}; + +// Helper to acknowledge from nodes +const acknowledgeFrom = ( + animator: SVGAnimator, + message: string, + nodes: string[] = ["#n2n1", "#n4n1"], +) => { + animator.group((a: SVGAnimator) => { + nodes.forEach((node) => { + a.morphText(`${node}text`, message, { duration: DURATION.instant }) + .show(node, { stroke: COLORS.active }) + .show(`${node}text`, { autoAlpha: 1, fill: COLORS.active }) + .animateArrow(node, { duration: DURATION.normal }); + }); + }); + return animator; +}; + +// Helper to reset colors for elements +const resetColors = ( + animator: SVGAnimator, + elements: string[], + type: "fill" | "stroke" = "fill", +) => { + elements.forEach((el) => { + animator.show(el, { [type]: COLORS.inactive }); + }); + return animator; +}; + +// Helper to hide and unanimate arrows +const hideArrows = (animator: SVGAnimator, arrows: string[]) => { + arrows.forEach((arrow) => { + animator + .show(`${arrow}text`, { autoAlpha: 0 }) + .unanimateArrow(arrow, { duration: DURATION.instant }); + }); + return animator; +}; + +export const requestProcessing = (animator: SVGAnimator) => { + // Initial setup - hide all dynamic elements + const elementsToHide = [ + "#appa", + "#appatext", + "#n1a", + "#aapp", + "#aapptext", + "#appb", + "#appbtext", + "#bapp", + "#bapptext", + "#n1b", + "#n1n2", + "#n1n2text", + "#n1n3", + "#n1n3text", + "#n1n4", + "#n1n4text", + "#n2n1", + "#n2n1text", + "#n3n1", + "#n3n1text", + "#n4n1", + "#n4n1text", + "#n2a", + "#n2b", + "#n3a", + "#n3b", + "#n4a", + "#n4b", + "#n1o", + "#n1otext", + ]; + hideElements(animator, elementsToHide); + + animator + // === Step 1: Request A === + // Step 1a: App issues request A + .animateArrow("#appa", { duration: DURATION.normal }) + .show("#appatext", { autoAlpha: 1 }); + + updateDescriptions(animator, { "#desc1": "* App issues request A" }); + animator.addLabel("step1a"); + + // Step 1b: N1 saves A to log + animator.show("#n1a", { autoAlpha: 1, fill: COLORS.active }); + updateDescriptions(animator, { + "#desc2": "* N1 saves A to log", + }); + animator.show("#desc1", { fill: COLORS.inactive }).addLabel("step1b"); + + // Step 1c: N1 sends A to all followers + sendToFollowers(animator, "A"); + updateDescriptions(animator, { + "#desc3": "* N1 sends A to all followers", + }); + animator.show("#desc2", { fill: COLORS.inactive }).addLabel("step1c"); + + // Step 1: Followers save A to their logs + animator + .show("#n2a", { autoAlpha: 1, fill: COLORS.active }) + .show("#n3a", { autoAlpha: 1, fill: COLORS.active }) + .show("#n4a", { autoAlpha: 1, fill: COLORS.active }); + + updateDescriptions(animator, { + "#desc4": "* Followers save A to their logs", + }); + animator.show("#desc3", { fill: COLORS.inactive }).addLabel("step1"); + + // Reset for step 2 + hideArrows(animator, ["#n1n2", "#n1n3", "#n1n4"]); + resetColors(animator, ["#n1a", "#n2a", "#n3a", "#n4a"], "fill"); + animator + .show("#appa", { stroke: COLORS.inactive }) + .show("#appatext", { fill: COLORS.inactive }); + clearDescriptions(animator); + animator.addLabel("step2a"); + + // === Step 2: Acknowledge A === + // Step 2b: N2 and N4 ack A + acknowledgeFrom(animator, "Ack A"); + updateDescriptions(animator, { + "#desc1": "* N2 and N4 ack A", + "#desc2": "* N3 ack is delayed", + }); + animator.addLabel("step2b"); + + // Step 2: N1 records A as acked for N2 + animator + .show("#n2ack", { fill: COLORS.active }) + .morphText("#n2ack", "N2: A", { duration: DURATION.instant }); + + updateDescriptions(animator, { + "#desc3": "* N1 records A as acked for N2", + "#desc4": "* Ack for N4 ignored", + "#desc5": "* Durability requirements not met", + }); + animator + .show("#desc1", { fill: COLORS.inactive }) + .show("#desc2", { fill: COLORS.inactive }) + .addLabel("step2"); + + // Reset for step 3 + animator + .show("#n2n1text", { fill: COLORS.inactive }) + .show("#n4n1text", { fill: COLORS.inactive }) + .show("#n2ack", { fill: COLORS.inactive }); + clearDescriptions(animator); + hideArrows(animator, ["#n2n1", "#n4n1"]); + animator.addLabel("step3a"); + + // === Step 3: Request B === + // Step 3b: App issues request B + animator + .show("#n2ack", { fill: COLORS.inactive }) + .animateArrow("#appb", { duration: DURATION.normal }) + .show("#appbtext", { autoAlpha: 1 }); + + updateDescriptions(animator, { "#desc1": "* App issues request B" }); + animator.addLabel("step3b"); + + // Step 3c: N1 appends B to log + animator.show("#n1b", { autoAlpha: 1 }); + updateDescriptions(animator, { + "#desc2": "* N1 appends B to log", + }); + animator.show("#desc1", { fill: COLORS.inactive }).addLabel("step3c"); + + // Step 3d: N1 sends B to all followers + sendToFollowers(animator, "B"); + updateDescriptions(animator, { + "#desc3": "* N1 sends B to all followers", + }); + animator.show("#desc2", { fill: COLORS.inactive }).addLabel("step3d"); + + // Step 3: Followers append B to their logs + animator + .show("#n2b", { autoAlpha: 1 }) + .show("#n3b", { autoAlpha: 1 }) + .show("#n4b", { autoAlpha: 1 }); + + updateDescriptions(animator, { + "#desc4": "* Followers append B to their logs", + "#desc5": "* A is still not applied", + }); + animator.show("#desc3", { fill: COLORS.inactive }).addLabel("step3"); + + // === Step 4: Acknowledge B and delayed A === + // Step 4a: N2 and N4 ack B + hideArrows(animator, ["#n1n2", "#n1n3", "#n1n4"]); + acknowledgeFrom(animator, "Ack B"); + clearDescriptions(animator); + updateDescriptions(animator, { "#desc1": "* N2 and N4 ack B" }); + animator.addLabel("step4a"); + + // Step 4b: N1 records B as acked for N2 + animator + .show("#n2ack", { fill: COLORS.active }) + .morphText("#n2ack", "N2: B", { duration: DURATION.instant }); + + updateDescriptions(animator, { + "#desc2": "* N1 records B as acked for N2", + }); + animator.show("#desc1", { fill: COLORS.inactive }).addLabel("step4b"); + + // Step 4c: N3 acks A (delayed) + animator + .show("#n3n1", { stroke: COLORS.active }) + .morphText("#n3n1text", "Ack A", { duration: DURATION.instant }) + .animateArrow("#n3n1", { duration: DURATION.normal }) + .show("#n3n1text", { autoAlpha: 1, fill: COLORS.active }); + + updateDescriptions(animator, { + "#desc3": "* N3 acks A (delayed)", + }); + animator.show("#desc2", { fill: COLORS.inactive }).addLabel("step4c"); + + // Step 4: A meets durability criteria + animator + .show("#n3ack", { fill: COLORS.active }) + .morphText("#n3ack", "N3: A", { duration: DURATION.instant }); + + updateDescriptions(animator, { + "#desc4": "* N1 records A as acked for N3.", + "#desc5": " A meets durability criteria", + }); + animator.show("#desc3", { fill: COLORS.inactive }).addLabel("step4"); + + // Reset for step 5 + animator + .unanimateArrow("#n2n1", { duration: DURATION.instant }) + .unanimateArrow("#n3n1", { duration: DURATION.instant }) + .unanimateArrow("#n4n1", { duration: DURATION.instant }) + .show("#n2n1text", { autoAlpha: 0 }) + .show("#n3n1text", { autoAlpha: 0 }) + .show("#n4n1text", { autoAlpha: 0 }); + + resetColors(animator, ["#n1b", "#n2b", "#n3b", "#n4b"], "fill"); + clearDescriptions(animator); + animator + .show("#appb", { stroke: COLORS.inactive }) + .show("#appbtext", { fill: COLORS.inactive }) + .show("#n2ack", { fill: COLORS.inactive }) + .show("#n3ack", { fill: COLORS.inactive }) + .addLabel("step5a"); + + // === Step 5: Apply A === + // Step 5b: N1 applies A + animator.group((a: SVGAnimator) => { + a.show("#n1applied", { stroke: COLORS.active }) + .show("#n1appliedtext", { fill: COLORS.active }) + .moveTo( + "#n1applied", + null, + { x: 40, y: 0 }, + { duration: DURATION.normal }, + ) + .moveTo( + "#n1appliedtext", + null, + { x: 40, y: 0 }, + { duration: DURATION.normal }, + ); + }); + + updateDescriptions(animator, { + "#desc1": "* N1 applies A", + }); + animator.addLabel("step5b"); + + // Step 5c: N1 Acks A to App + animator.group((a: SVGAnimator) => { + a.show("#appa", { autoAlpha: 0 }) + .show("#appatext", { autoAlpha: 0 }) + .animateArrow("#aapp", { duration: DURATION.normal }) + .show("#aapptext", { autoAlpha: 1 }); + }); + animator.show("#desc1", { fill: COLORS.inactive }); + updateDescriptions(animator, { + "#desc2": "* N1 Acks A to App", + }); + animator.addLabel("step5c"); + + // Step 5d: N1 sends Apply A to followers and observers + sendToFollowers(animator, "Apply A", ["#n1n2", "#n1n3", "#n1n4", "#n1o"]); + animator.show("#desc2", { fill: COLORS.inactive }); + updateDescriptions(animator, { + "#desc3": "* N1 sends Apply A to followers", + "#desc4": " and observers", + }); + animator.addLabel("step5d"); + + // Step 5: Followers and observers apply A + animator.group((a: SVGAnimator) => { + a.show("#n4applied", { stroke: COLORS.active }) + .show("#n4appliedtext", { fill: COLORS.active }) + .moveTo( + "#n4applied", + null, + { x: 40, y: 0 }, + { duration: DURATION.normal }, + ) + .moveTo( + "#n4appliedtext", + null, + { x: 40, y: 0 }, + { duration: DURATION.normal }, + ); + }); + animator + .show("#desc3", { fill: COLORS.inactive }) + .show("#desc4", { fill: COLORS.inactive }); + updateDescriptions(animator, { + "#desc5": "* Followers and observers apply A", + }); + animator.addLabel("step5"); + + // Reset for step 6 + hideArrows(animator, ["#n1n2", "#n1n3", "#n1n4", "#n1o"]); + animator + .unanimateArrow("#aapp", { duration: DURATION.instant }) + .show("#aapptext", { autoAlpha: 0 }) + .show("#n1applied", { stroke: COLORS.inactive }) + .show("#n1appliedtext", { fill: COLORS.inactive }) + .show("#n4applied", { stroke: COLORS.inactive }) + .show("#n4appliedtext", { fill: COLORS.inactive }) + .addLabel("step6a"); + + // === Step 6 & 7: Apply B === + // Step 6b: N3 acks B (delayed) + clearDescriptions(animator); + animator + .show("#n3n1", { stroke: COLORS.active }) + .morphText("#n3n1text", "Ack B", { duration: DURATION.instant }) + .animateArrow("#n3n1", { duration: DURATION.normal }) + .show("#n3n1text", { autoAlpha: 1, fill: COLORS.active }); + + updateDescriptions(animator, { + "#desc1": "* N3 acks B (delayed)", + }); + animator.addLabel("step6b"); + + // Step 6c: N1 records B as acked for N3 + animator + .show("#n3ack", { fill: COLORS.active }) + .morphText("#n3ack", "N3: B", { duration: DURATION.instant }); + animator.show("#desc1", { fill: COLORS.inactive }); + updateDescriptions(animator, { + "#desc2": "* N1 recods B as acked for N3", + }); + animator.addLabel("step6c"); + + // Step 7: N1 applies B and sends to all + animator + .show("#n3n1text", { autoAlpha: 0 }) + .unanimateArrow("#n3n1", { duration: DURATION.instant }) + .show("#n3ack", { fill: COLORS.inactive }); + + animator.group((a: SVGAnimator) => { + a.show("#n1applied", { stroke: COLORS.active }) + .show("#n1appliedtext", { fill: COLORS.active }) + .moveTo( + "#n1applied", + null, + { x: 80, y: 0 }, + { duration: DURATION.normal }, + ) + .moveTo( + "#n1appliedtext", + null, + { x: 80, y: 0 }, + { duration: DURATION.normal }, + ); + }); + + animator + .unanimateArrow("#appb", { duration: DURATION.instant }) + .show("#appbtext", { autoAlpha: 0 }) + .animateArrow("#bapp", { duration: DURATION.normal }) + .show("#bapptext", { autoAlpha: 1, fill: COLORS.active }); + + sendToFollowers(animator, "Apply B", ["#n1n2", "#n1n3", "#n1n4", "#n1o"]); + + animator.group((a: SVGAnimator) => { + a.show("#n4applied", { stroke: COLORS.active }) + .show("#n4appliedtext", { fill: COLORS.active }) + .moveTo( + "#n4applied", + null, + { x: 80, y: 0 }, + { duration: DURATION.normal }, + ) + .moveTo( + "#n4appliedtext", + null, + { x: 80, y: 0 }, + { duration: DURATION.normal }, + ); + }); + + animator.show("#desc2", { fill: COLORS.inactive }); + updateDescriptions(animator, { + "#desc3": "* N1 performs all apply steps", + "#desc4": " for B as it did for A", + "#desc5": "", + }); + + animator.addLabel("step7"); +}; diff --git a/src/lib/svg-animator.ts b/src/lib/svg-animator.ts new file mode 100644 index 0000000..abe46bf --- /dev/null +++ b/src/lib/svg-animator.ts @@ -0,0 +1,841 @@ +/** + * GSAP Animation Wrapper for SVG Components + * + * This utility provides a simple interface to animate SVG elements using GSAP. + * It can be used to create complex animations for architectural diagrams and illustrations. + * + * @example + * ```typescript + * import { SVGAnimator } from '@site/src/lib/svg-animator'; + * + * const animator = new SVGAnimator('#my-svg'); + * animator.fadeIn('.node-1', { duration: 1, delay: 0.5 }); + * animator.drawArrow('.arrow-1', { duration: 2 }); + * ``` + */ + +import gsap from "gsap"; + +export interface AnimationOptions { + duration?: number; + delay?: number; + ease?: string; + stagger?: number; + onComplete?: () => void; + onStart?: () => void; +} + +export class SVGAnimator { + /** + * Color palette for animations + * - active: Green - active/success state + * - inactive: Dark gray - inactive state + * - blue: Blue - coordinator/term color + * - orange: Orange - coordinator/term color + * - purple: Purple - coordinator/term color + * - red: Red - error/crash state + */ + static readonly COLORS = { + active: "#2f9e44", + inactive: "#ddd", + blue: "#70bafb", + orange: "#af5900", + purple: "#e99cfe", + red: "#f97f81", + } as const; + + /** + * Animation duration presets in seconds + * - instant: No animation + * - fast: Quick transitions + * - normal: Standard animation speed + * - pause: Short pause between steps + * - slow: Slow, emphasized animations + */ + static readonly DURATION = { + instant: 0, + fast: 0.5, + normal: 1, + pause: 0.5, + slow: 2, + } as const; + + private svg: SVGElement | null; + private timeline: gsap.core.Timeline; + private steps: string[] = []; + private currentStep: number = -1; + private groupStartTime: number | null = null; + + constructor(svgSelector: string | SVGElement) { + if (typeof svgSelector === "string") { + this.svg = document.querySelector(svgSelector); + } else { + this.svg = svgSelector; + } + + if (!this.svg) { + console.warn(`SVG element not found: ${svgSelector}`); + } + + this.timeline = gsap.timeline({ paused: true }); + } + + /** + * Get the GSAP timeline for custom animations + */ + getTimeline(): gsap.core.Timeline { + return this.timeline; + } + + /** + * Select elements within the SVG + */ + private select(selector: string): Element[] { + if (!this.svg) return []; + return Array.from(this.svg.querySelectorAll(selector)); + } + + /** + * Set properties on elements immediately (before timeline starts) + */ + set(selector: string, properties: gsap.TweenVars): this { + const elements = this.select(selector); + if (elements.length === 0) { + console.warn(`No elements found for selector: ${selector}`); + return this; + } + + // Use immediate gsap.set for initial setup (before timeline plays) + gsap.set(elements, properties); + return this; + } + + /** + * Hide multiple SVG elements by setting autoAlpha to 0 + * Common pattern for setting up initial state of animations + * + * @param selectors - Array of CSS selectors for elements to hide + * @returns The animator instance for chaining + */ + hideElements(selectors: string[]): this { + selectors.forEach((selector) => this.set(selector, { autoAlpha: 0 })); + return this; + } + + /** + * Set text content immediately without adding to timeline + */ + setText(selector: string, text: string): this { + const elements = this.select(selector); + if (elements.length === 0) { + console.warn(`No elements found for selector: ${selector}`); + return this; + } + + elements.forEach((element) => { + if (element instanceof SVGTextElement) { + element.textContent = text; + } + }); + return this; + } + + /** + * Change text with animation: clear, set color, then morph to new text + * Common pattern: clear text -> set color -> morph to new text + */ + changeText( + selector: string, + text: string, + fill: string, + duration: number = 0.5, + ): this { + this.morphText(selector, "", { duration: 0 }) + .show(selector, { fill }) + .morphText(selector, text, { duration }); + return this; + } + + /** + * Set properties on elements as part of the timeline animation + * Uses a very short duration so it works with reverse navigation + */ + show(selector: string, properties: gsap.TweenVars = {}): this { + const elements = this.select(selector); + if (elements.length === 0) { + console.warn(`No elements found for selector: ${selector}`); + return this; + } + + // Collect all child elements as well + const allElements: Element[] = []; + elements.forEach((element) => { + allElements.push(element); + allElements.push(...Array.from(element.querySelectorAll("*"))); + }); + + // Use a very short duration animation instead of set() so it reverses properly + this.timeline.to( + allElements, + { + ...properties, + duration: 0.01, + }, + this.groupStartTime ?? undefined, + ); + return this; + } + + /** + * Fade in elements + */ + fadeIn(selector: string, options: AnimationOptions = {}): this { + const elements = this.select(selector); + if (elements.length === 0) { + console.warn(`No elements found for selector: ${selector}`); + return this; + } + + gsap.set(elements, { opacity: 0 }); + this.timeline.to( + elements, + { + opacity: 1, + duration: options.duration ?? 0.5, + delay: options.delay ?? 0, + ease: options.ease ?? "power2.out", + stagger: options.stagger ?? 0, + onComplete: options.onComplete, + onStart: options.onStart, + }, + this.groupStartTime ?? undefined, + ); + + return this; + } + + /** + * Fade out elements + */ + fadeOut(selector: string, options: AnimationOptions = {}): this { + const elements = this.select(selector); + if (elements.length === 0) return this; + + this.timeline.to( + elements, + { + opacity: 0, + duration: options.duration ?? 0.5, + delay: options.delay ?? 0, + ease: options.ease ?? "power2.out", + stagger: options.stagger ?? 0, + onComplete: options.onComplete, + onStart: options.onStart, + }, + this.groupStartTime ?? undefined, + ); + + return this; + } + + /** + * Scale animation + */ + scale(selector: string, scale: number, options: AnimationOptions = {}): this { + const elements = this.select(selector); + if (elements.length === 0) return this; + + this.timeline.to( + elements, + { + scale, + duration: options.duration ?? 0.5, + delay: options.delay ?? 0, + ease: options.ease ?? "back.out(1.7)", + stagger: options.stagger ?? 0, + transformOrigin: "center center", + onComplete: options.onComplete, + onStart: options.onStart, + }, + this.groupStartTime ?? undefined, + ); + + return this; + } + + /** + * Draw arrow/path animation (stroke-dasharray technique) + */ + drawPath(selector: string, options: AnimationOptions = {}): this { + const elements = this.select(selector); + if (elements.length === 0) return this; + + elements.forEach((element) => { + if (element instanceof SVGGeometryElement) { + const length = element.getTotalLength(); + gsap.set(element, { + strokeDasharray: length, + strokeDashoffset: length, + }); + } + }); + + this.timeline.to( + elements, + { + strokeDashoffset: 0, + duration: options.duration ?? 1, + delay: options.delay ?? 0, + ease: options.ease ?? "power2.inOut", + stagger: options.stagger ?? 0, + onComplete: options.onComplete, + onStart: options.onStart, + }, + this.groupStartTime ?? undefined, + ); + + return this; + } + + /** + * Slide in from direction + */ + slideIn( + selector: string, + direction: "left" | "right" | "top" | "bottom" = "left", + options: AnimationOptions = {}, + ): this { + const elements = this.select(selector); + if (elements.length === 0) return this; + + const distance = 100; + const initialPosition = { + left: { x: -distance, y: 0 }, + right: { x: distance, y: 0 }, + top: { x: 0, y: -distance }, + bottom: { x: 0, y: distance }, + }[direction]; + + gsap.set(elements, { + x: initialPosition.x, + y: initialPosition.y, + opacity: 0, + }); + + this.timeline.to( + elements, + { + x: 0, + y: 0, + opacity: 1, + duration: options.duration ?? 0.8, + delay: options.delay ?? 0, + ease: options.ease ?? "power3.out", + stagger: options.stagger ?? 0, + onComplete: options.onComplete, + onStart: options.onStart, + }, + this.groupStartTime ?? undefined, + ); + + return this; + } + + /** + * Move element from one point to another + * @param selector - CSS selector for elements to move + * @param from - Starting position {x, y} or null to move from current position + * @param to - Ending position {x, y} + * @param options - Animation options + */ + moveTo( + selector: string, + from: { x: number; y: number } | null, + to: { x: number; y: number }, + options: AnimationOptions = {}, + ): this { + const elements = this.select(selector); + if (elements.length === 0) { + console.warn(`No elements found for selector: ${selector}`); + return this; + } + + // Set initial position if provided + if (from !== null) { + this.timeline.set( + elements, + { + x: from.x, + y: from.y, + }, + this.groupStartTime ?? undefined, + ); + } + + // Animate to target position + this.timeline.to( + elements, + { + x: to.x, + y: to.y, + duration: options.duration ?? 1, + delay: options.delay ?? 0, + ease: options.ease ?? "power2.inOut", + stagger: options.stagger ?? 0, + onComplete: options.onComplete, + onStart: options.onStart, + }, + this.groupStartTime ?? undefined, + ); + + return this; + } + + /** + * Highlight element with pulse animation + */ + pulse(selector: string, options: AnimationOptions = {}): this { + const elements = this.select(selector); + if (elements.length === 0) return this; + + this.timeline.to( + elements, + { + scale: 1.1, + duration: (options.duration ?? 0.6) / 2, + delay: options.delay ?? 0, + ease: "power2.inOut", + yoyo: true, + repeat: 1, + stagger: options.stagger ?? 0, + transformOrigin: "center center", + onComplete: options.onComplete, + onStart: options.onStart, + }, + this.groupStartTime ?? undefined, + ); + + return this; + } + + /** + * Morph text content from one value to another with animation + */ + morphText( + selector: string, + newText: string, + options: AnimationOptions = {}, + ): this { + const elements = this.select(selector); + if (elements.length === 0) return this; + + elements.forEach((element) => { + if (element instanceof SVGTextElement) { + // Capture the current text at the time this morphText is called + // This is what we'll restore when THIS specific tween reverses + const previousText = element.textContent || ""; + const tempObj = { value: 0 }; + + this.timeline.to( + tempObj, + { + value: 1, + duration: options.duration ?? 0.8, + delay: options.delay ?? 0, + ease: options.ease ?? "power2.inOut", + onUpdate: () => { + // Fade out and scale down + if (tempObj.value < 0.5) { + const progress = tempObj.value * 2; // 0 to 1 in first half + element.style.opacity = String(1 - progress); + element.style.transform = `scale(${1 - progress * 0.3})`; + } else { + // Change text at midpoint + if (element.textContent !== newText) { + element.textContent = newText; + } + // Fade in and scale up + const progress = (tempObj.value - 0.5) * 2; // 0 to 1 in second half + element.style.opacity = String(progress); + element.style.transform = `scale(${0.7 + progress * 0.3})`; + } + }, + onReverseComplete: () => { + // Restore the text from before this specific morphText call + element.textContent = previousText; + element.style.opacity = "1"; + element.style.transform = "scale(1)"; + }, + onComplete: () => { + element.style.opacity = "1"; + element.style.transform = "scale(1)"; + if (options.onComplete) options.onComplete(); + }, + onStart: options.onStart, + }, + this.groupStartTime ?? undefined, + ); + } + }); + + return this; + } + + /** + * Animate an arrow group where first element is the shaft and rest are arrowhead/decorations + * The shaft is drawn first, then other elements fade in + */ + animateArrow(groupSelector: string, options: AnimationOptions = {}): this { + const groups = this.select(groupSelector); + if (groups.length === 0) { + console.warn(`No arrow group found for selector: ${groupSelector}`); + return this; + } + + groups.forEach((group) => { + const children = Array.from(group.children); + if (children.length === 0) return; + + // We assume elements are hidden initially. + // Make them visible. + this.timeline.to( + group, + { + autoAlpha: 1, + duration: 0.01, + }, + this.groupStartTime ?? undefined, + ); + + // First child contains the shaft - find the actual path element + const firstChild = children[0]; + let shaftPath: SVGGeometryElement | null = null; + + // Check if first child is the path itself + if (firstChild instanceof SVGGeometryElement) { + shaftPath = firstChild; + } else { + // Look for a path element inside the first child + const pathElement = firstChild.querySelector( + "path, line, polyline, polygon", + ); + if (pathElement instanceof SVGGeometryElement) { + shaftPath = pathElement; + } + } + + // Animate the shaft using drawPath + if (shaftPath) { + let shaftSelector = ""; + if (shaftPath.id) { + shaftSelector = `#${shaftPath.id}`; + } else { + shaftSelector = `${groupSelector} > :first-child path, ${groupSelector} > :first-child line, ${groupSelector} > :first-child polyline, ${groupSelector} > :first-child polygon`; + } + + this.drawPath(shaftSelector, { + duration: options.duration ?? 1, + delay: options.delay ?? 0, + ease: options.ease ?? "none", + onStart: options.onStart, + }); + } + + // Remaining children are arrowhead/decorations - collect all descendants + const restElements: Element[] = []; + for (let i = 1; i < children.length; i++) { + restElements.push(children[i]); + // Also collect nested elements + restElements.push(...Array.from(children[i].querySelectorAll("*"))); + } + + if (restElements.length > 0) { + gsap.set(restElements, { opacity: 0 }); + // Don't use groupStartTime - arrowhead should appear AFTER shaft + this.timeline.to(restElements, { + opacity: 1, + duration: 0, + onComplete: options.onComplete, + }); + } else if (options.onComplete) { + this.timeline.call(options.onComplete); + } + }); + + return this; + } + + /** + * Reverse of animateArrow - fades out arrowhead first, then un-draws the shaft + */ + unanimateArrow(groupSelector: string, options: AnimationOptions = {}): this { + const groups = this.select(groupSelector); + if (groups.length === 0) { + console.warn(`No arrow group found for selector: ${groupSelector}`); + return this; + } + + groups.forEach((group) => { + const children = Array.from(group.children); + if (children.length === 0) return; + + // First child contains the shaft - find the actual path element + const firstChild = children[0]; + let shaftPath: SVGGeometryElement | null = null; + + // Check if first child is the path itself + if (firstChild instanceof SVGGeometryElement) { + shaftPath = firstChild; + } else { + // Look for a path element inside the first child + const pathElement = firstChild.querySelector( + "path, line, polyline, polygon", + ); + if (pathElement instanceof SVGGeometryElement) { + shaftPath = pathElement; + } + } + + // Remaining children are arrowhead/decorations - collect all descendants + const restElements: Element[] = []; + for (let i = 1; i < children.length; i++) { + restElements.push(children[i]); + // Also collect nested elements + restElements.push(...Array.from(children[i].querySelectorAll("*"))); + } + + // First, fade out the arrowhead + if (restElements.length > 0) { + this.timeline.to( + restElements, + { + opacity: 0, + duration: 0.001, + onStart: options.onStart, + }, + this.groupStartTime ?? undefined, + ); + } + + // Then un-draw the shaft + if (shaftPath) { + let shaftSelector = ""; + if (shaftPath.id) { + shaftSelector = `#${shaftPath.id}`; + } else { + shaftSelector = `${groupSelector} > :first-child path, ${groupSelector} > :first-child line, ${groupSelector} > :first-child polyline, ${groupSelector} > :first-child polygon`; + } + + const elements = this.select(shaftSelector); + elements.forEach((element) => { + if (element instanceof SVGGeometryElement) { + const length = element.getTotalLength(); + // Animate from drawn (offset 0) back to hidden (offset = length) + this.timeline.to( + element, + { + strokeDashoffset: length, + duration: options.duration ?? 1, + delay: options.delay ?? 0, + ease: options.ease ?? "none", + }, + this.groupStartTime ?? undefined, + ); + } + }); + } + + // Finally, hide the entire group + this.timeline.to(group, { + autoAlpha: 0, + duration: 0.01, + onComplete: options.onComplete, + }); + }); + + return this; + } + + /** + * Group animations to run simultaneously + * Creates a nested timeline where all animations in the callback run at the same time + */ + group(callback: (animator: SVGAnimator) => void): this { + // Mark the start position for the group + this.groupStartTime = this.timeline.duration(); + + // Execute the callback - all animations will be added at groupStartTime + callback(this); + + // Reset groupStartTime so subsequent animations continue sequentially + this.groupStartTime = null; + + return this; + } + + /** + * Add a delay to the timeline + */ + wait(duration: number): this { + this.timeline.add(() => {}, `+=${duration}`); + return this; + } + + /** + * Add a label to the timeline for seeking + */ + addLabel(label: string): this { + this.timeline.addLabel(label); + this.steps.push(label); + return this; + } + + /** + * Move to the next step in the animation + */ + nextStep(): this { + if (this.currentStep < this.steps.length - 1) { + this.currentStep++; + this.timeline.tweenTo(this.steps[this.currentStep]); + } + return this; + } + + /** + * Move to the previous step in the animation + */ + previousStep(): this { + if (this.currentStep > 0) { + this.currentStep--; + this.timeline.tweenTo(this.steps[this.currentStep]); + } else if (this.currentStep === 0) { + this.currentStep = -1; + this.timeline.tweenTo(0); + } + return this; + } + + /** + * Get the current step index + */ + getCurrentStep(): number { + return this.currentStep; + } + + /** + * Get the total number of steps + */ + getTotalSteps(): number { + return this.steps.length; + } + + /** + * Check if there's a next step + */ + hasNextStep(): boolean { + return this.currentStep < this.steps.length - 1; + } + + /** + * Check if there's a previous step + */ + hasPreviousStep(): boolean { + return this.currentStep >= 0; + } + + /** + * Play the animation + */ + play(): this { + this.timeline.play(); + return this; + } + + /** + * Pause the animation + */ + pause(): this { + this.timeline.pause(); + return this; + } + + /** + * Restart the animation + */ + restart(): this { + this.timeline.restart(); + return this; + } + + /** + * Seek to a specific time or label + */ + seek(timeOrLabel: number | string): this { + this.timeline.seek(timeOrLabel); + return this; + } + + /** + * Reverse the animation + */ + reverse(): this { + this.timeline.reverse(); + return this; + } + + /** + * Kill the animation + */ + kill(): void { + this.timeline.kill(); + } +} + +/** + * Create a new SVG animator instance + */ +export function createSVGAnimator( + svgSelector: string | SVGElement, +): SVGAnimator { + return new SVGAnimator(svgSelector); +} + +/** + * Utility function for common animation sequences + */ +export const animations = { + /** + * Fade in nodes sequentially, then draw arrows between them + */ + sequentialReveal: ( + animator: SVGAnimator, + nodeSelector: string, + arrowSelector: string, + options: { nodeDelay?: number; arrowDelay?: number } = {}, + ) => { + animator + .fadeIn(nodeSelector, { + duration: 0.5, + stagger: 0.2, + delay: options.nodeDelay ?? 0, + }) + .drawPath(arrowSelector, { + duration: 1, + stagger: 0.3, + delay: options.arrowDelay ?? 0, + }); + return animator; + }, + + /** + * Highlight a specific flow path + */ + highlightFlow: ( + animator: SVGAnimator, + flowSelector: string, + options: AnimationOptions = {}, + ) => { + animator.pulse(flowSelector, { duration: 0.8, ...options }); + return animator; + }, +};