1const SVG_FILE = 2 typeof dataFlowAnimation !== "undefined" && dataFlowAnimation.svgUrl 3 ? dataFlowAnimation.svgUrl 4 : "/wp-content/uploads/2026/02/How-We-Help-Companies-Succeed.svg"; 5const SVG_NS = "http://www.w3.org/2000/svg"; 6const DESKTOP_QUERY = "(min-width: 1024px)"; 7const FLOW_COLOR = "#00AAE5"; 8const SPEED = 170; 9const HIGHLIGHT_DURATION = 0.65; 10const CYCLE_GAP = 0.35; 11 12const WAVE_COUNT = 1; 13const WAVE_INTERVAL = 1.9; 14const MIN_SEGMENT_DURATION = 1.1; // seconds 15const JUNCTION_OVERLAP = 0.18; // seconds 16const PACKET_FADE = 0.06; // portion of duration used for fade-in/out (0..0.5) 17const PACKET_STROKE_SCALE = 1; // base line thickness 18const PACKET_LENGTH_SCALE = 1.18; // make moving lines ~18% longer 19const FINISH_SYNC_TERMINALS = true; 20 21const prefersReduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 22const desktopMatch = window.matchMedia ? window.matchMedia(DESKTOP_QUERY) : { matches: true }; 23 24let mount = null; 25let svgRoot = null; 26let inView = false; 27let rafId = null; 28let animationState = null; 29 30function copyAttributes(fromEl, toEl) { 31 for (const attr of Array.from(fromEl.attributes || [])) { 32 const name = attr.name; 33 if (name === "xmlns" || name.startsWith("xmlns:")) continue; 34 toEl.setAttribute(name, attr.value); 35 } 36} 37 38function buildSvgTree(templateSvg) { 39 const svg = document.createElementNS(SVG_NS, "svg"); 40 copyAttributes(templateSvg, svg); 41 svg.setAttribute("xmlns", SVG_NS); 42 43 const rebuild = (srcNode, dstParent) => { 44 for (const child of Array.from(srcNode.childNodes)) { 45 if (child.nodeType === Node.ELEMENT_NODE) { 46 const tag = child.tagName; 47 if (tag && tag.toLowerCase() === "script") continue; 48 const el = document.createElementNS(SVG_NS, tag); 49 copyAttributes(child, el); 50 dstParent.appendChild(el); 51 rebuild(child, el); 52 } else if (child.nodeType === Node.TEXT_NODE) { 53 const txt = child.nodeValue; 54 if (txt && txt.trim().length) dstParent.appendChild(document.createTextNode(txt)); 55 } 56 } 57 }; 58 59 rebuild(templateSvg, svg); 60 return svg; 61} 62 63function ensurePacketGlow(svg) { 64 let defs = svg.querySelector("defs"); 65 if (!defs) { 66 defs = document.createElementNS(SVG_NS, "defs"); 67 svg.insertBefore(defs, svg.firstChild); 68 } 69 if (svg.querySelector("#packetGlow")) return; 70 71 const filter = document.createElementNS(SVG_NS, "filter"); 72 filter.setAttribute("id", "packetGlow"); 73 filter.setAttribute("x", "-40%"); 74 filter.setAttribute("y", "-40%"); 75 filter.setAttribute("width", "180%"); 76 filter.setAttribute("height", "180%"); 77 78 const blur = document.createElementNS(SVG_NS, "feGaussianBlur"); 79 blur.setAttribute("in", "SourceGraphic"); 80 blur.setAttribute("stdDeviation", "2.2"); 81 blur.setAttribute("result", "blur"); 82 83 const merge = document.createElementNS(SVG_NS, "feMerge"); 84 const m1 = document.createElementNS(SVG_NS, "feMergeNode"); 85 m1.setAttribute("in", "blur"); 86 const m2 = document.createElementNS(SVG_NS, "feMergeNode"); 87 m2.setAttribute("in", "SourceGraphic"); 88 merge.appendChild(m1); 89 merge.appendChild(m2); 90 91 filter.appendChild(blur); 92 filter.appendChild(merge); 93 defs.appendChild(filter); 94 95 // (Gradient for moving packets removed to restore base style.) 96} 97 98function canAnimate() { 99 return !prefersReduced && desktopMatch.matches; 100} 101 102// ---------- geometry ---------- 103function dist(ax, ay, bx, by) { 104 const dx = ax - bx; 105 const dy = ay - by; 106 return Math.sqrt(dx * dx + dy * dy); 107} 108 109function getCentralPoint(svg) { 110 // Heuristic: pick the largest white-filled circle/ellipse as the "center node". 111 const isWhite = (fill) => { 112 const f = (fill || "").trim().toLowerCase(); 113 return f === "white" || f === "#fff" || f === "#ffffff"; 114 }; 115 116 let best = null; 117 118 for (const c of Array.from(svg.querySelectorAll("circle"))) { 119 if (!isWhite(c.getAttribute("fill"))) continue; 120 const r = parseFloat(c.getAttribute("r") || "0"); 121 const area = Math.PI * r * r; 122 if (!best || area > best.area) { 123 best = { 124 cx: parseFloat(c.getAttribute("cx") || "0"), 125 cy: parseFloat(c.getAttribute("cy") || "0"), 126 r, 127 area,
128 }; 129 } 130 } 131 132 for (const e of Array.from(svg.querySelectorAll("ellipse"))) { 133 if (!isWhite(e.getAttribute("fill"))) continue; 134 const rx = parseFloat(e.getAttribute("rx") || "0"); 135 const ry = parseFloat(e.getAttribute("ry") || "0"); 136 const area = Math.PI * rx * ry; 137 if (!best || area > best.area) { 138 best = { 139 cx: parseFloat(e.getAttribute("cx") || "0"), 140 cy: parseFloat(e.getAttribute("cy") || "0"), 141 r: Math.max(rx, ry), 142 area, 143 }; 144 } 145 } 146 147 if (best) return { x: best.cx, y: best.cy, r: best.r }; 148 149 const vb = (svg.getAttribute("viewBox") || "").trim().split(/\s+/).map(Number); 150 if (vb.length === 4 && vb.every((n) => Number.isFinite(n))) { 151 const [x, y, w, h] = vb; 152 return { x: x + w / 2, y: y + h / 2, r: Math.min(w, h) / 8 }; 153 } 154 return { x: 0, y: 0, r: 0 }; 155} 156 157function polarToCartesian(cx, cy, r, angleDeg) { 158 const angleRad = (angleDeg * Math.PI) / 180; 159 return { 160 x: cx + r * Math.cos(angleRad), 161 y: cy + r * Math.sin(angleRad), 162 }; 163} 164 165function arcPathD(cx, cy, r, startAngle, endAngle, sweepFlag) { 166 const start = polarToCartesian(cx, cy, r, startAngle); 167 const end = polarToCartesian(cx, cy, r, endAngle); 168 const largeArcFlag = 0; 169 const sweep = sweepFlag ? 1 : 0; 170 return `M ${start.x.toFixed(2)} ${start.y.toFixed(2)} A ${r.toFixed(2)} ${r.toFixed( 171 2, 172 )} 0 ${largeArcFlag} ${sweep} ${end.x.toFixed(2)} ${end.y.toFixed(2)}`; 173} 174 175function parsePathStart(path) { 176 const d = path.getAttribute("d") || ""; 177 const match = d.match(/M\\s*([-\\d.]+)[,\\s]+([-\\d.]+)/i); 178 if (!match) return null; 179 return { x: parseFloat(match[1]), y: parseFloat(match[2]) }; 180} 181 182function getPathEnd(path) { 183 const len = path.getTotalLength(); 184 const pt = path.getPointAtLength(len); 185 return { x: pt.x, y: pt.y }; 186} 187 188function isConnectorPath(path) { 189 const stroke = (path.getAttribute("stroke") || "").trim().toLowerCase(); 190 if (stroke !== FLOW_COLOR.toLowerCase()) return false; 191 const op = path.getAttribute("stroke-opacity"); 192 if (op && parseFloat(op) < 0.15) return false; 193 return true; 194} 195 196function getDestinationCircles(svg, center) { 197 // Destination icons are white circles away from the center. We keep the size range 198 // broad enough to include the larger left destination circle. 199 return Array.from(svg.querySelectorAll("circle")) 200 .map((c) => ({ 201 el: c, 202 cx: parseFloat(c.getAttribute("cx") || "NaN"), 203 cy: parseFloat(c.getAttribute("cy") || "NaN"), 204 r: parseFloat(c.getAttribute("r") || "NaN"), 205 fill: (c.getAttribute("fill") || "").trim().toLowerCase(), 206 })) 207 .filter((c) => Number.isFinite(c.cx) && Number.isFinite(c.cy) && Number.isFinite(c.r)) 208 .filter((c) => c.fill === "white" || c.fill === "#fff" || c.fill === "#ffffff") 209 .filter((c) => c.r >= 18 && c.r <= 70) 210 .filter((c) => dist(c.cx, c.cy, center.x, center.y) > Math.max(60, center.r + 30)); 211} 212 213function nearestDestination(destinations, x, y) { 214 let best = null; 215 let bestD = Infinity; 216 for (const c of destinations) { 217 const d = dist(c.cx, c.cy, x, y); 218 if (d < bestD) { 219 bestD = d; 220 best = c; 221 } 222 } 223 return best ? { circle: best, d: bestD } : null; 224} 225 226function getGraphNodes(svg) { 227 // Nodes are white circles (destination icons + intermediate junction circles). 228 // We use these to connect path segments even when the path endpoints sit inside the circle. 229 const nodes = []; 230 for (const c of Array.from(svg.querySelectorAll("circle"))) { 231 const fill = (c.getAttribute("fill") || "").trim().toLowerCase(); 232 if (fill !== "white" && fill !== "#fff" && fill !== "#ffffff") continue; 233 const cx = parseFloat(c.getAttribute("cx") || "NaN"); 234 const cy = parseFloat(c.getAttribute("cy") || "NaN"); 235 const r = parseFloat(c.getAttribute("r") || "NaN"); 236 if (!Number.isFinite(cx) || !Number.isFinite(cy) || !Number.isFinite(r)) continue; 237 if (r < 18 || r > 90) continue; 238 const isDestination = r <= 45; 239 nodes.push({ cx, cy, r, el: c, isDestination }); 240 } 241 return nodes; 242} 243 244function findNodeForPoint(nodes, x, y, pad = 10) { 245 let best = null; 246 let bestD = Infinity; 247 for (const n of nodes) { 248 const d = dist(n.cx, n.cy, x, y); 249 if (d <= n.r + pad && d < bestD) { 250 bestD = d; 251 best = n; 252 } 253 } 254 return best; 255} 256
257function topLevelChildUnderRoot(svgRoot, el) { 258 let cur = el; 259 while (cur && cur.parentNode && cur.parentNode !== svgRoot) cur = cur.parentNode; 260 return cur && cur.parentNode === svgRoot ? cur : null; 261} 262 263function findEarliestIconContainer(svgRoot) { 264 const center = getCentralPoint(svgRoot); 265 const destCircles = getDestinationCircles(svgRoot, center); 266 if (!destCircles.length) return null; 267 268 const candidates = new Set(); 269 for (const c of destCircles) { 270 const top = topLevelChildUnderRoot(svgRoot, c.el); 271 if (top) candidates.add(top); 272 } 273 if (!candidates.size) return null; 274 275 const children = Array.from(svgRoot.childNodes).filter((n) => n.nodeType === Node.ELEMENT_NODE); 276 let best = null; 277 let bestIdx = Infinity; 278 for (const node of candidates) { 279 const idx = children.indexOf(node); 280 if (idx !== -1 && idx < bestIdx) { 281 bestIdx = idx; 282 best = node; 283 } 284 } 285 return best; 286} 287 288function getOuterRing(svg) { 289 const circles = Array.from(svg.querySelectorAll("circle")).filter((c) => c.getAttribute("stroke")); 290 let best = null; 291 for (const c of circles) { 292 const r = parseFloat(c.getAttribute("r") || "0"); 293 if (!best || r > best.r) { 294 best = { 295 cx: parseFloat(c.getAttribute("cx") || "0"), 296 cy: parseFloat(c.getAttribute("cy") || "0"), 297 r, 298 }; 299 } 300 } 301 return best; 302} 303 304function findPathsStartingNear(paths, target, maxDist = 30) { 305 return paths.filter((p) => { 306 const start = parsePathStart(p); 307 if (!start) return false; 308 return dist(start.x, start.y, target.x, target.y) <= maxDist; 309 }); 310} 311 312function findClosestFilledCircle(svg, x, y, maxDist = 65) { 313 const circles = Array.from(svg.querySelectorAll("circle")).filter((c) => { 314 const fill = (c.getAttribute("fill") || "").toLowerCase(); 315 return fill === "white"; 316 }); 317 318 let best = null; 319 let bestD = Infinity; 320 for (const c of circles) { 321 const cx = parseFloat(c.getAttribute("cx") || "0"); 322 const cy = parseFloat(c.getAttribute("cy") || "0"); 323 const d = dist(cx, cy, x, y); 324 if (d < bestD) { 325 bestD = d; 326 best = c; 327 } 328 } 329 330 if (best && bestD <= maxDist) { 331 return { 332 cx: parseFloat(best.getAttribute("cx") || "0"), 333 cy: parseFloat(best.getAttribute("cy") || "0"), 334 r: parseFloat(best.getAttribute("r") || "6"), 335 }; 336 } 337 return null; 338} 339 340function addCenterPulse(svg) { 341 const whiteCircles = Array.from(svg.querySelectorAll("circle")).filter((c) => { 342 const fill = (c.getAttribute("fill") || "").trim().toLowerCase(); 343 return fill === "white" || fill === "#fff" || fill === "#ffffff"; 344 }); 345 const whiteEllipses = Array.from(svg.querySelectorAll("ellipse")).filter((e) => { 346 const fill = (e.getAttribute("fill") || "").trim().toLowerCase(); 347 return fill === "white" || fill === "#fff" || fill === "#ffffff"; 348 }); 349 350 let best = null; 351 for (const c of whiteCircles) { 352 const r = parseFloat(c.getAttribute("r") || "0"); 353 const area = Math.PI * r * r; 354 if (!best || area > best.area) best = { el: c, cx: +c.getAttribute("cx"), cy: +c.getAttribute("cy"), r, area }; 355 } 356 for (const e of whiteEllipses) { 357 const rx = parseFloat(e.getAttribute("rx") || "0"); 358 const ry = parseFloat(e.getAttribute("ry") || "0"); 359 const area = Math.PI * rx * ry; 360 if (!best || area > best.area) { 361 best = { 362 el: e, 363 cx: parseFloat(e.getAttribute("cx") || "0"), 364 cy: parseFloat(e.getAttribute("cy") || "0"), 365 r: Math.max(rx, ry), 366 area, 367 }; 368 } 369 } 370 371 if (!best?.el || !Number.isFinite(best.cx) || !Number.isFinite(best.cy) || !Number.isFinite(best.r)) return; 372 373 best.el.classList.add("center-core-pulse"); 374 375 const ringEls = Array.from(svg.querySelectorAll("circle[stroke], ellipse[stroke]")).filter((el) => { 376 const cx = parseFloat(el.getAttribute("cx") || "NaN"); 377 const cy = parseFloat(el.getAttribute("cy") || "NaN"); 378 if (!Number.isFinite(cx) || !Number.isFinite(cy)) return false;
379 return dist(cx, cy, best.cx, best.cy) <= 6; 380 }); 381 const ringRadii = ringEls 382 .map((el) => { 383 if (el.tagName.toLowerCase() === "circle") { 384 return parseFloat(el.getAttribute("r") || "NaN"); 385 } 386 const rx = parseFloat(el.getAttribute("rx") || "NaN"); 387 const ry = parseFloat(el.getAttribute("ry") || "NaN"); 388 return Math.max(rx, ry); 389 }) 390 .filter((r) => Number.isFinite(r) && r > 0) 391 .sort((a, b) => a - b); 392 393 const innerR = ringRadii.length ? ringRadii[0] : best.r + 10; 394 const outerR = ringRadii.length ? ringRadii[ringRadii.length - 1] : best.r + 50; 395 const sweepScale = Math.max(1.05, outerR / Math.max(1, innerR)); 396 397 const pulseLayer = document.createElementNS(SVG_NS, "g"); 398 pulseLayer.setAttribute("data-center-pulse", "1"); 399 400 const mkRing = (delaySeconds) => { 401 const ring = document.createElementNS(SVG_NS, "circle"); 402 ring.classList.add("center-pulse-sweep"); 403 ring.setAttribute("cx", String(best.cx)); 404 ring.setAttribute("cy", String(best.cy)); 405 ring.setAttribute("r", String(innerR)); 406 ring.style.animationDelay = `${delaySeconds}s`; 407 ring.style.setProperty("--pulse-scale", String(sweepScale)); 408 return ring; 409 }; 410 411 pulseLayer.appendChild(mkRing(0)); 412 pulseLayer.appendChild(mkRing(-1.1)); 413 414 svg.insertBefore(pulseLayer, svg.firstElementChild); 415} 416 417function shrinkCircles(svg, delta = 3) { 418 for (const c of Array.from(svg.querySelectorAll("circle"))) { 419 const r = parseFloat(c.getAttribute("r") || "0"); 420 if (!Number.isFinite(r)) continue; 421 const nr = Math.max(1, r - delta); 422 c.setAttribute("r", String(nr)); 423 } 424 for (const e of Array.from(svg.querySelectorAll("ellipse"))) { 425 const rx = parseFloat(e.getAttribute("rx") || "0"); 426 const ry = parseFloat(e.getAttribute("ry") || "0"); 427 if (!Number.isFinite(rx) || !Number.isFinite(ry)) continue; 428 const nrx = Math.max(1, rx - delta); 429 const nry = Math.max(1, ry - delta); 430 e.setAttribute("rx", String(nrx)); 431 e.setAttribute("ry", String(nry)); 432 } 433} 434 435function addNodePulseRings(svg) { 436 const isWhite = (fill) => { 437 const f = (fill || "").trim().toLowerCase(); 438 return f === "white" || f === "#fff" || f === "#ffffff"; 439 }; 440 441 const center = getCentralPoint(svg); 442 const nodeCircles = Array.from(svg.querySelectorAll("circle")) 443 .map((c) => ({ 444 el: c, 445 cx: parseFloat(c.getAttribute("cx") || "NaN"), 446 cy: parseFloat(c.getAttribute("cy") || "NaN"), 447 r: parseFloat(c.getAttribute("r") || "NaN"), 448 fill: c.getAttribute("fill"), 449 })) 450 .filter((c) => isWhite(c.fill)) 451 .filter((c) => Number.isFinite(c.cx) && Number.isFinite(c.cy) && Number.isFinite(c.r)) 452 // avoid tiny decorative circle 453 .filter((c) => c.r >= 18 && c.r <= 70) 454 // skip anything too close to the center 455 .filter((c) => dist(c.cx, c.cy, center.x, center.y) > Math.max(55, center.r * 0.75)); 456 457 if (!nodeCircles.length) return; 458 459 const ringLayer = document.createElementNS(SVG_NS, "g"); 460 ringLayer.setAttribute("data-node-pulse-rings", "1"); 461 462 for (const n of nodeCircles) { 463 const mk = (delay) => { 464 const ring = document.createElementNS(SVG_NS, "circle"); 465 ring.classList.add("node-pulse-ring"); 466 ring.setAttribute("cx", String(n.cx)); 467 ring.setAttribute("cy", String(n.cy)); 468 ring.setAttribute("r", String(n.r + 7)); 469 const dur = 2.6 + Math.random() * 1.6; 470 ring.style.setProperty("--dur", `${dur.toFixed(2)}s`); 471 ring.style.animationDelay = `${delay.toFixed(2)}s`; 472 return ring; 473 }; 474 475 const baseDelay = -Math.random() * 2.6; 476 ringLayer.appendChild(mk(baseDelay)); 477 ringLayer.appendChild(mk(baseDelay - 1.2)); 478 } 479 480 const iconAnchor = findEarliestIconContainer(svg); 481 if (iconAnchor) svg.insertBefore(ringLayer, iconAnchor); 482 else svg.appendChild(ringLayer); 483} 484 485function createFlowPath(d, strokeWidth) { 486 const path = document.createElementNS(SVG_NS, "path");
487 path.classList.add("moving-packet"); 488 path.setAttribute("d", d); 489 path.setAttribute("fill", "none"); 490 path.setAttribute("stroke", FLOW_COLOR); 491 const base = parseFloat(strokeWidth || "3"); 492 path.setAttribute("stroke-width", String(base * PACKET_STROKE_SCALE)); 493 path.setAttribute("stroke-linecap", "round"); 494 path.setAttribute("stroke-linejoin", "round"); 495 path.setAttribute("filter", "url(#packetGlow)"); 496 return path; 497} 498 499function durationForLen(len) { 500 return Math.max(MIN_SEGMENT_DURATION, len / SPEED); 501} 502 503function createLoopPacket(pathEl, opts = {}) { 504 const len = pathEl.getTotalLength(); 505 const baseSeg = opts.seg ?? len * 0.08; 506 const seg = Math.max(18, Math.min(baseSeg * PACKET_LENGTH_SCALE, 90)); 507 508 const gap = Math.max(1, len); 509 pathEl.setAttribute("stroke-dasharray", `${seg} ${gap}`); 510 pathEl.setAttribute("stroke-dashoffset", "0"); 511 pathEl.setAttribute("opacity", "0"); 512 const baseDuration = durationForLen(len); 513 return { 514 el: pathEl, 515 len, 516 baseDuration, 517 duration: baseDuration, 518 fade: opts.fade ?? 0.08, 519 start: opts.start ?? 0, 520 reverse: !!opts.reverse, 521 isTerminal: !!opts.isTerminal, 522 }; 523} 524 525function concatPathD(d1, d2) { 526 const d2c = (d2 || "").replace(/^M\s*([-\d.]+)[,\s]+([-\d.]+)/i, "L $1 $2"); 527 return `${d1} ${d2c}`.trim(); 528} 529 530function buildBranchLoopTimeline(svg, layer) { 531 const outer = getOuterRing(svg); 532 const center = outer ? { x: outer.cx, y: outer.cy, r: outer.r } : getCentralPoint(svg); 533 534 const connectors = Array.from(svg.querySelectorAll("path[stroke]")).filter(isConnectorPath); 535 if (!connectors.length) return null; 536 537 const NEAR_CENTER = outer?.r ? outer.r + 26 : Math.max(170, center.r + 90); 538 const EPS_JOIN = 3.0; 539 const nodes = getGraphNodes(svg); 540 541 const segments = connectors 542 .map((p) => { 543 const len = p.getTotalLength(); 544 if (len < 30) return null; 545 const a = p.getPointAtLength(0); 546 const b = p.getPointAtLength(len); 547 const nodeA = findNodeForPoint(nodes, a.x, a.y); 548 const nodeB = findNodeForPoint(nodes, b.x, b.y); 549 return { 550 path: p, 551 d: p.getAttribute("d") || "", 552 width: parseFloat(p.getAttribute("stroke-width") || "3"), 553 len, 554 a, 555 b, 556 da: dist(a.x, a.y, center.x, center.y), 557 db: dist(b.x, b.y, center.x, center.y), 558 nodeA, 559 nodeB, 560 }; 561 }) 562 .filter(Boolean); 563 564 const roots = segments 565 .filter((s) => Math.min(s.da, s.db) <= NEAR_CENTER) 566 .map((s) => { 567 const reverse = s.db < s.da; // path direction is a->b if b is closer, we travel b->a 568 const startPt = reverse ? s.b : s.a; 569 const endPt = reverse ? s.a : s.b; 570 return { ...s, reverse, startPt, endPt }; 571 }); 572 573 if (!roots.length) return null; 574 575 const leftRoots = roots 576 .filter((r) => r.endPt.x < center.x) 577 .sort((a, b) => a.endPt.y - b.endPt.y) 578 .slice(0, 3); 579 const rightRoots = roots 580 .filter((r) => r.endPt.x > center.x) 581 .sort((a, b) => a.endPt.y - b.endPt.y) 582 .slice(0, 3); 583 const selectedRoots = [...leftRoots, ...rightRoots]; 584 if (!selectedRoots.length) return null; 585 586 const chosen = new Map(); // pathEl -> {start, reverse, segOverride} 587 588 const addPacketFor = (seg, startTime, reverse, segOverride = null) => { 589 const prev = chosen.get(seg.path); 590 if (prev && prev.start <= startTime) return; 591 chosen.set(seg.path, { start: startTime, reverse, segOverride }); 592 }; 593 594 const rootSegBase = selectedRoots.length ? selectedRoots.reduce((sum, r) => sum + r.len * 0.08, 0) / selectedRoots.length : null; 595 596 for (const r of selectedRoots) { 597 addPacketFor(r, 0, r.reverse, rootSegBase); 598 } 599 600 const maxDepth = 3; 601 for (let depth = 0; depth < maxDepth; depth++) { 602 let addedAny = false;
603 const current = Array.from(chosen.entries()) 604 .map(([pathEl, info]) => { 605 const s = segments.find((x) => x.path === pathEl); 606 if (!s) return null; 607 const reverse = info.reverse; 608 const endPt = reverse ? s.a : s.b; 609 return { seg: s, endPt, start: info.start, duration: durationForLen(s.len) }; 610 }) 611 .filter(Boolean); 612 613 for (const item of current) { 614 const node = item.endPt; 615 const childStart = Math.max(0, item.start + item.duration - JUNCTION_OVERLAP); 616 const nodeAtEnd = findNodeForPoint(nodes, node.x, node.y); 617 618 if (nodeAtEnd?.isDestination) continue; 619 620 for (const cand of segments) { 621 if (chosen.has(cand.path)) continue; 622 623 const hitNodeA = nodeAtEnd && cand.nodeA === nodeAtEnd; 624 const hitNodeB = nodeAtEnd && cand.nodeB === nodeAtEnd; 625 const hitA = dist(cand.a.x, cand.a.y, node.x, node.y) <= EPS_JOIN; 626 const hitB = dist(cand.b.x, cand.b.y, node.x, node.y) <= EPS_JOIN; 627 if (!hitNodeA && !hitNodeB && !hitA && !hitB) continue; 628 629 const reverse = hitNodeB || (!hitNodeA && hitB && !hitA); 630 const startPt = reverse ? cand.b : cand.a; 631 const endPt = reverse ? cand.a : cand.b; 632 633 addPacketFor(cand, childStart, reverse); 634 addedAny = true; 635 } 636 } 637 638 if (!addedAny) break; 639 } 640 641 const packets = []; 642 for (const [pathEl, info] of chosen.entries()) { 643 const seg = segments.find((x) => x.path === pathEl); 644 if (!seg?.d) continue; 645 const endNode = info.reverse ? seg.nodeA : seg.nodeB; 646 const isTerminal = !!endNode?.isDestination; 647 for (let w = 0; w < WAVE_COUNT; w++) { 648 const flow = createFlowPath(seg.d, seg.width); 649 layer.appendChild(flow); 650 const packet = createLoopPacket(flow, { 651 start: info.start + w * WAVE_INTERVAL, 652 reverse: info.reverse, 653 fade: PACKET_FADE, 654 isTerminal, 655 seg: info.segOverride ?? undefined, 656 }); 657 packets.push(packet); 658 } 659 } 660 661 if (!packets.length) return null; 662 663 if (FINISH_SYNC_TERMINALS) { 664 const terminalEnds = packets.filter((p) => p.isTerminal).map((p) => p.start + p.baseDuration); 665 const baseline = terminalEnds.length ? terminalEnds : packets.map((p) => p.start + p.baseDuration); 666 const targetEnd = Math.max(...baseline); 667 668 for (const p of packets) { 669 if (p.isTerminal) p.duration = Math.max(MIN_SEGMENT_DURATION, targetEnd - p.start); 670 else p.duration = p.baseDuration; 671 } 672 673 return { packets, cycleDuration: targetEnd + CYCLE_GAP }; 674 } 675 676 const cycleEnd = Math.max(...packets.map((p) => p.start + p.duration)); 677 return { packets, cycleDuration: cycleEnd + CYCLE_GAP }; 678} 679 680function createPacket(pathEl, start) { 681 const len = pathEl.getTotalLength(); 682 const seg = Math.max(16, Math.min(len * 0.08, 60)); 683 const gap = Math.max(1, len - seg); 684 pathEl.setAttribute("stroke-dasharray", `${seg} ${gap}`); 685 pathEl.setAttribute("stroke-dashoffset", "0"); 686 pathEl.setAttribute("opacity", "0"); 687 return { el: pathEl, len, start, duration: len / SPEED }; 688} 689 690function createHighlight(layer, circle, startTime) { 691 const ring = document.createElementNS(SVG_NS, "circle"); 692 const baseR = circle.r + 8; 693 ring.classList.add("arrival-ring"); 694 ring.setAttribute("cx", String(circle.cx)); 695 ring.setAttribute("cy", String(circle.cy)); 696 ring.setAttribute("r", String(baseR)); 697 ring.setAttribute("opacity", "0"); 698 layer.appendChild(ring); 699 return { el: ring, start: startTime, duration: HIGHLIGHT_DURATION, baseR }; 700} 701 702function buildTimeline(svg, layer) { 703 const ring = getOuterRing(svg); 704 if (!ring) return null; 705 706 const connectors = Array.from(svg.querySelectorAll("path[stroke]")).filter(isConnectorPath); 707 if (!connectors.length) return null; 708
709 const leftJunction = { x: ring.cx - ring.r, y: ring.cy }; 710 const rightJunction = { x: ring.cx + ring.r, y: ring.cy }; 711 712 const leftBranches = findPathsStartingNear(connectors, leftJunction, 30); 713 const rightPrimary = findPathsStartingNear(connectors, rightJunction, 30); 714 715 if (leftBranches.length < 3 || rightPrimary.length < 3) return null; 716 717 leftBranches.sort((a, b) => parsePathStart(a).y - parsePathStart(b).y); 718 rightPrimary.sort((a, b) => parsePathStart(a).y - parsePathStart(b).y); 719 720 const rightMid = rightPrimary[1]; 721 const rightSplitPoint = getPathEnd(rightMid); 722 const rightSecondary = connectors.filter((p) => { 723 if (rightPrimary.includes(p)) return false; 724 const start = parsePathStart(p); 725 if (!start) return false; 726 return dist(start.x, start.y, rightSplitPoint.x, rightSplitPoint.y) <= 30; 727 }); 728 rightSecondary.sort((a, b) => getPathEnd(a).y - getPathEnd(b).y); 729 730 const packets = []; 731 const highlights = []; 732 733 const arcLeft = createFlowPath(arcPathD(ring.cx, ring.cy, ring.r, -90, 180, false), 3); 734 const arcRight = createFlowPath(arcPathD(ring.cx, ring.cy, ring.r, 90, 0, false), 3); 735 layer.appendChild(arcLeft); 736 layer.appendChild(arcRight); 737 738 const leftArcPacket = createPacket(arcLeft, 0); 739 const rightArcPacket = createPacket(arcRight, 0); 740 packets.push(leftArcPacket, rightArcPacket); 741 742 const leftStart = leftArcPacket.duration; 743 const rightStart = rightArcPacket.duration; 744 745 for (const path of leftBranches.slice(0, 3)) { 746 const d = path.getAttribute("d") || ""; 747 const width = parseFloat(path.getAttribute("stroke-width") || "3"); 748 const flow = createFlowPath(d, width); 749 layer.appendChild(flow); 750 const packet = createPacket(flow, leftStart); 751 packets.push(packet); 752 753 const end = getPathEnd(path); 754 const circle = findClosestFilledCircle(svg, end.x, end.y, 70); 755 if (circle) highlights.push(createHighlight(layer, circle, leftStart + packet.duration)); 756 } 757 758 const rightTop = rightPrimary[0]; 759 const rightMidPath = rightPrimary[1]; 760 const rightBottom = rightPrimary[2]; 761 762 const rightTopFlow = createFlowPath(rightTop.getAttribute("d") || "", parseFloat(rightTop.getAttribute("stroke-width") || "3")); 763 const rightBottomFlow = createFlowPath(rightBottom.getAttribute("d") || "", parseFloat(rightBottom.getAttribute("stroke-width") || "3")); 764 const rightMidFlow = createFlowPath(rightMidPath.getAttribute("d") || "", parseFloat(rightMidPath.getAttribute("stroke-width") || "3")); 765 766 layer.appendChild(rightTopFlow); 767 layer.appendChild(rightMidFlow); 768 layer.appendChild(rightBottomFlow); 769 770 const rightTopPacket = createPacket(rightTopFlow, rightStart); 771 const rightBottomPacket = createPacket(rightBottomFlow, rightStart); 772 const rightMidPacket = createPacket(rightMidFlow, rightStart); 773 packets.push(rightTopPacket, rightMidPacket, rightBottomPacket); 774 775 for (const branch of [rightTop, rightBottom]) { 776 const end = getPathEnd(branch); 777 const circle = findClosestFilledCircle(svg, end.x, end.y, 70); 778 if (circle) { 779 highlights.push( 780 createHighlight(layer, circle, rightStart + (branch === rightTop ? rightTopPacket.duration : rightBottomPacket.duration)), 781 ); 782 } 783 } 784 785 const secondSplitStart = rightStart + rightMidPacket.duration; 786 const secondaryPackets = []; 787 for (const path of rightSecondary.slice(0, 3)) { 788 const d = path.getAttribute("d") || ""; 789 const width = parseFloat(path.getAttribute("stroke-width") || "3"); 790 const flow = createFlowPath(d, width); 791 layer.appendChild(flow); 792 const packet = createPacket(flow, secondSplitStart); 793 packets.push(packet); 794 secondaryPackets.push({ packet, path }); 795 } 796 797 for (const item of secondaryPackets) { 798 const end = getPathEnd(item.path); 799 const circle = findClosestFilledCircle(svg, end.x, end.y, 70); 800 if (circle) highlights.push(createHighlight(layer, circle, secondSplitStart + item.packet.duration)); 801 } 802 803 const maxPacketEnd = Math.max(...packets.map((p) => p.start + p.duration), 0); 804 const maxHighlightEnd = Math.max(...highlights.map((h) => h.start + h.duration), maxPacketEnd); 805 806 return { 807 packets, 808 highlights, 809 cycleDuration: maxHighlightEnd + CYCLE_GAP, 810 }; 811} 812 813function animate(now) { 814 if (!animationState) return; 815 const t = (now - animationState.startTime) / 1000; 816 const loopTime = t % animationState.cycleDuration; 817 818 for (const p of animationState.packets) { 819 if (loopTime < p.start || loopTime > p.start + p.duration) {
820 p.el.setAttribute("opacity", "0"); 821 continue; 822 } 823 824 const progress = (loopTime - p.start) / p.duration; 825 const fade = p.fade; 826 const opacity = progress < fade ? progress / fade : progress > 1 - fade ? (1 - progress) / fade : 1; 827 p.el.setAttribute("opacity", String(opacity.toFixed(3))); 828 829 const s = p.reverse ? (1 - progress) * p.len : progress * p.len; 830 p.el.setAttribute("stroke-dashoffset", String(-s)); 831 } 832 833 rafId = requestAnimationFrame(animate); 834} 835 836function startAnimation() { 837 if (!svgRoot || !canAnimate() || animationState) return; 838 ensurePacketGlow(svgRoot); 839 840 const layer = document.createElementNS(SVG_NS, "g"); 841 layer.setAttribute("data-flow-layer", "1"); 842 843 const iconAnchor = findEarliestIconContainer(svgRoot); 844 if (iconAnchor) svgRoot.insertBefore(layer, iconAnchor); 845 else svgRoot.appendChild(layer); 846 847 const timeline = buildBranchLoopTimeline(svgRoot, layer); 848 if (!timeline) return; 849 850 animationState = { 851 layer, 852 packets: timeline.packets, 853 cycleDuration: timeline.cycleDuration, 854 startTime: performance.now(), 855 }; 856 rafId = requestAnimationFrame(animate); 857} 858 859function stopAnimation() { 860 if (rafId) cancelAnimationFrame(rafId); 861 rafId = null; 862 if (animationState?.layer) animationState.layer.remove(); 863 animationState = null; 864} 865 866function setupObserver() { 867 if (!mount) return; 868 const observer = new IntersectionObserver( 869 (entries) => { 870 const entry = entries[0]; 871 inView = !!entry?.isIntersecting; 872 if (inView && canAnimate()) startAnimation(); 873 if (!inView) stopAnimation(); 874 }, 875 { threshold: 0.35 }, 876 ); 877 observer.observe(mount); 878 879 if (desktopMatch.addEventListener) { 880 desktopMatch.addEventListener("change", () => { 881 if (!canAnimate()) stopAnimation(); 882 if (canAnimate() && inView) startAnimation(); 883 }); 884 } 885} 886 887(async function init() { 888 mount = document.getElementById("svgMount"); 889 if (!mount) return; 890 891 const res = await fetch(SVG_FILE); 892 const svgText = await res.text(); 893 894 const parser = new DOMParser(); 895 const doc = parser.parseFromString(svgText, "image/svg+xml"); 896 const templateSvg = doc.querySelector("svg"); 897 if (!templateSvg) return; 898 899 svgRoot = buildSvgTree(templateSvg); 900 mount.innerHTML = ""; 901 mount.appendChild(svgRoot); 902 903 shrinkCircles(svgRoot, 3); 904 905 addCenterPulse(svgRoot); 906 addNodePulseRings(svgRoot); 907 908 if (prefersReduced) return; 909 setupObserver(); 910})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.