This is an automated email from the ASF dual-hosted git repository. dpol1 pushed a commit to branch refresh in repository https://gitbox.apache.org/repos/asf/stormcrawler-site.git
commit e5d5e6e3d5628df73756189d000965e5a64afc6c Author: Davide Polato <[email protected]> AuthorDate: Tue Aug 18 16:31:01 2026 +0200 Animate the home page and crop the hero web behind the copy on mobile Load sequence: the orb web draws itself in (stroke-dashoffset over the compound paths), the spider drops onto the hub and the hero copy rises in a stagger. The pipeline rail gains a scroll-driven accent thread with a crawler dot that activates each stage node as it passes. The closing band web is built lazily when scrolled into view and rotates slowly. Below 1000px the web stays anchored to the top-right corner and bleeds off-canvas behind the copy instead of stacking underneath. Everything is gated behind prefers-reduced-motion and degrades without JavaScript. Labels now use a middot instead of an em dash and Storm, Tika and Solr are referred to by their full trademarked names. --- css/main.scss | 305 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- index.html | 32 +++--- js/site.js | 106 ++++++++++++++++---- 3 files changed, 393 insertions(+), 50 deletions(-) diff --git a/css/main.scss b/css/main.scss index 2ef819c..8cd3963 100644 --- a/css/main.scss +++ b/css/main.scss @@ -3,8 +3,9 @@ // ---------------------------------------------------------------------------- // Apache StormCrawler — site stylesheet ("Il Crawl"). -// Single file, no imports, no webfonts (ASF rule: system stacks only). -// Sections: tokens · base · header/footer · home · inner pages. +// Single file, no imports, fonts self-hosted from css/resources/ (ASF rule: +// no third-party requests). Sections: tokens · base · header/footer · home · +// inner pages. // ---------------------------------------------------------------------------- // --- Webfont: Archivo, self-hosted (precedent: css/resources/ubuntu-mono.css) @@ -85,7 +86,7 @@ --font-ui: "Archivo",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; --font-mono: SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace; --text-base: 16px; - --text-hero: 74px; + --text-hero: clamp(46px,7.5vw,74px); --text-section-h2: 38px; --text-stage-h2: 30px; --text-cta-h2: 44px; @@ -104,6 +105,7 @@ --width-content: 1080px; --width-prose: 860px; --motion-fast: .15s ease; + --ease-out: cubic-bezier(.16,1,.3,1); } // --- Base ------------------------------------------------------------------- @@ -163,6 +165,25 @@ code,pre,kbd { font-family: var(--font-mono); } } } +.skip-link { + position: absolute; + top: -48px; + left: 16px; + z-index: 100; + background: var(--icon-chip-bg); + color: var(--icon-chip-fg); + border-radius: 0 0 8px 8px; + padding: 10px 18px; + font-weight: 600; + font-size: 14px; + transition: top var(--motion-fast); +} + +.skip-link:focus { + top: 0; + text-decoration: none; +} + .only-light { display: block; } .only-dark { display: none !important; } @@ -249,6 +270,8 @@ code,pre,kbd { font-family: var(--font-mono); } transition: transform var(--motion-fast),background var(--motion-fast),color var(--motion-fast),border-color var(--motion-fast); } +.btn:active { transform: translateY(0) scale(.97); } + .btn--solid { background: var(--icon-chip-bg); color: var(--icon-chip-fg); @@ -324,13 +347,14 @@ code,pre,kbd { font-family: var(--font-mono); } letter-spacing: .08em; text-transform: uppercase; color: var(--chip-text); - transition: border-color var(--motion-fast),color var(--motion-fast); + transition: border-color var(--motion-fast),color var(--motion-fast),transform var(--motion-fast); } .chip:hover { border-color: var(--accent-ink); color: var(--text); text-decoration: none; + transform: translateY(-1px); } .chip svg { flex: 0 0 auto; } @@ -354,6 +378,7 @@ code,pre,kbd { font-family: var(--font-mono); } } .slink { + position: relative; display: inline-flex; align-items: center; gap: 8px; @@ -363,15 +388,49 @@ code,pre,kbd { font-family: var(--font-mono); } font-size: 11px; letter-spacing: .08em; color: var(--text); - border-bottom: 2px solid var(--accent); padding-bottom: 3px; } +.slink::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: var(--accent); +} + +@keyframes underline-swipe { + 0% { + transform: scaleX(1); + transform-origin: right center; + } + + 45% { + transform: scaleX(0); + transform-origin: right center; + } + + 46% { transform-origin: left center; } + + 100% { + transform: scaleX(1); + transform-origin: left center; + } +} + +.slink:hover::after { animation: underline-swipe .45s var(--ease-out); } + +.slink svg { transition: transform var(--motion-fast); } + .slink:hover { color: var(--accent-ink); text-decoration: none; } +.slink:hover svg { transform: translate(2px,-2px); } + .section { padding: var(--section-pad); border-top: 1px solid var(--border); @@ -482,7 +541,7 @@ nav.links a { white-space: nowrap; } color: var(--text); } -.site-nav-link.active::after { +.site-nav-link::after { content: ""; position: absolute; left: 0; @@ -490,8 +549,13 @@ nav.links a { white-space: nowrap; } bottom: -8px; height: 4px; background: var(--accent); + transform: scaleX(0); + transform-origin: left center; + transition: transform .2s var(--ease-out); } +.site-nav-link:hover::after,.site-nav-link.active::after { transform: scaleX(1); } + .gh-pill { display: inline-flex; align-items: center; @@ -544,6 +608,30 @@ nav.links a { white-space: nowrap; } min-height: calc(var(--web) * .85); } +// Paper grain: inline feTurbulence noise, no external asset. Bookends only — +// hero and closing band — so the flat sections between them read calmer. + +.hero::before,.cta-band::before { + content: ""; + position: absolute; + inset: 0; + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E") repeat; + background-size: 160px 160px; + opacity: .05; + mix-blend-mode: multiply; + pointer-events: none; +} + +[data-theme="dark"] .hero::before { + opacity: .05; + mix-blend-mode: screen; +} + +.cta-band::before { + opacity: .07; + mix-blend-mode: screen; +} + .hero__grid { position: relative; max-width: 1280px; @@ -617,6 +705,76 @@ nav.links a { white-space: nowrap; } max-width: none; } +// --- Home: hero load sequence ----------------------------------------------- +// Copy rises in a stagger (CSS-only, no-JS safe); the web draws itself in +// (site.js) and the spider descends onto the finished hub. Reduced motion +// disables every step via the global media rule. + +@keyframes intro-rise { + from { + opacity: 0; + transform: translateY(16px); + filter: blur(6px); + } + + to { + opacity: 1; + transform: none; + filter: none; + } +} + +.hero__copy > * { animation: intro-rise .6s var(--ease-out) backwards; } + +.hero__copy .hero__title { animation: none; } + +.hero__line { + display: inline-block; + animation: intro-rise .7s var(--ease-out) backwards; +} + +.hero__line:nth-of-type(1) { animation-delay: .06s; } + +.hero__line:nth-of-type(2) { animation-delay: .14s; } + +.hero__line:nth-of-type(3) { animation-delay: .22s; } + +.hero__copy .hero__lede { animation-delay: .3s; } + +.hero__copy .hero__ctas { animation-delay: .38s; } + +.hero__copy .hero__assurance { animation-delay: .46s; } + +@keyframes spider-drop { + 0% { + opacity: 0; + transform: translate(-50%,-150%); + } + + 60% { opacity: 1; } + + 78% { transform: translate(-50%,-44%); } + + 100% { + opacity: 1; + transform: translate(-50%,-50%); + } +} + +html:not(.no-js) .hero__webbox .hero__mark { + animation: spider-drop 1.1s cubic-bezier(.22,1,.36,1) .8s backwards; +} + +@keyframes web-node-in { + from { opacity: 0; } + + to { opacity: 1; } +} + +.web-dew { animation: web-node-in .5s ease 1.6s backwards; } + +.web-acc { animation: web-node-in .5s ease 1.9s backwards; } + // --- Home: pipeline rail 01→04 ---------------------------------------------- .rail { position: relative; } @@ -631,6 +789,50 @@ nav.links a { white-space: nowrap; } background: var(--border-strong); } +// Scroll-driven silk thread: an accent line drawn over the static rail as the +// reader crawls the pipeline, with a crawler dot riding its tip (site.js). +// Without JS or with reduced motion it stays at scaleY(0) — invisible. + +.rail__thread { + position: absolute; + left: var(--rail-x); + top: 0; + bottom: 0; + width: 2px; + background: var(--accent); + transform: scaleY(0); + transform-origin: top center; + will-change: transform; +} + +.rail__crawler { + position: absolute; + left: var(--rail-x); + top: 0; + width: 12px; + height: 12px; + margin: -6px 0 0 -5px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 0 3px var(--bg),0 0 14px rgba(195,53,64,.5); + opacity: 0; + transition: opacity .25s ease; + will-change: transform; +} + +.stage__node { transition: border-color .3s ease,color .3s ease; } + +.stage__node.is-passed { + border-color: var(--accent); + color: var(--accent-ink); +} + +.stage__node--accent.is-passed { color: #fff; } + +[data-theme="dark"] .stage__node--accent.is-passed { color: #191A16; } + +.stage:hover .stage__node { border-color: var(--accent-ink); } + .stage { position: relative; padding: 64px var(--gutter-x) 64px calc(var(--gutter-x) + 98px); @@ -707,6 +909,16 @@ nav.links a { white-space: nowrap; } .code-row .hl { color: var(--accent); } +.code-row pre::after { + content: "\258C"; + color: var(--accent); + animation: cursor-blink 1.2s steps(1) infinite; +} + +@keyframes cursor-blink { + 50% { opacity: 0; } +} + .code-row .copy-chip { position: absolute; top: 16px; @@ -853,6 +1065,11 @@ nav.links a { white-space: nowrap; } display: block; width: 100%; height: 100%; + animation: web-rot 160s linear infinite; +} + +@keyframes web-rot { + to { transform: rotate(360deg); } } .cta-band h2 { @@ -1106,6 +1323,12 @@ nav.links a { white-space: nowrap; } overflow: auto; } +.codebox .copy-chip { + position: absolute; + top: 10px; + right: 10px; +} + .codebox pre { font-family: var(--font-mono); font-size: var(--text-code); @@ -1208,6 +1431,42 @@ nav.links a { white-space: nowrap; } .contributor-legend .contributor-role { margin: 0 2px; } +// --- 404: broken thread ----------------------------------------------------- + +.lost { + max-width: 620px; + margin: 0 auto; + padding: 40px var(--page-pad-x) 110px; + text-align: center; +} + +.lost__thread { + width: 2px; + height: 110px; + margin: 0 auto; + background: repeating-linear-gradient(to bottom,var(--border-strong) 0 12px,transparent 12px 20px); +} + +.lost__mark { + width: 92px; + margin: 6px auto 26px; + transform: rotate(-8deg); +} + +.lost__title { + font-size: clamp(38px,6vw,58px); + line-height: 1; + letter-spacing: -.025em; + font-weight: 900; + text-transform: uppercase; + margin: 10px 0 18px; +} + +.lost__copy { + color: var(--text-secondary); + margin: 0 0 28px; +} + // --- Footer ----------------------------------------------------------------- .site-footer { @@ -1313,6 +1572,19 @@ nav.links a { white-space: nowrap; } flex-wrap: wrap; } +.site-footer .acevent { + display: inline-flex; + align-items: center; +} + +.site-footer .acevent:empty { display: none; } + +.site-footer .acevent img { + display: block; + border-radius: 10px; + border: 1px solid var(--invariant-border); +} + .coc-pill { display: inline-flex; align-items: center; @@ -1446,7 +1718,7 @@ nav.links a { white-space: nowrap; } nav.links .site-nav-link:first-child { border-top: 0; } - nav.links .site-nav-link.active::after { display: none; } + nav.links .site-nav-link::after { display: none; } .nav-burger { display: inline-flex; } @@ -1458,22 +1730,19 @@ nav.links a { white-space: nowrap; } } @media (max-width: 1000px) { + // Same scene, tighter crop: the web stays anchored to the top-right + // corner and bleeds off-canvas behind the copy, spider still at the hub. .hero { - display: flex; - flex-direction: column; min-height: 0; + padding-top: 84px; + padding-bottom: 72px; } - .hero__grid { order: 1; } - .hero__webbox { - order: 2; - position: relative; - inset: auto; - width: min(100%,520px); - height: auto; - aspect-ratio: 1; - margin: 26px auto 0; + --web: clamp(300px,74vw,520px); + top: calc(var(--web) * -.3); + right: calc(var(--web) * -.3); + opacity: .8; } } diff --git a/index.html b/index.html index 4b2f5d1..04c6c14 100644 --- a/index.html +++ b/index.html @@ -13,12 +13,12 @@ description: Apache StormCrawler is an open source SDK for building low-latency, </div> <div class="hero__grid"> <div class="hero__copy"> - <p class="eyebrow" style="margin-bottom:20px;">Apache StormCrawler · <span class="pill">v3.7.0</span></p> - <h1 class="hero__title">Every web<br>has its<br>spider<span class="dot">.</span></h1> + <p class="eyebrow" style="margin-bottom:20px;">Apache StormCrawler · <span class="pill">v{{ site.version }}</span></p> + <h1 class="hero__title"><span class="hero__line">Every web</span><br><span class="hero__line">has its</span><br><span class="hero__line">spider<span class="dot">.</span></span></h1> <p class="hero__lede"><strong>Apache StormCrawler</strong> is an open source SDK for building distributed web crawlers based on <a href="https://storm.apache.org/" target="_blank" rel="noopener">Apache Storm®</a>. Low-latency, scalable and polite: a collection of reusable components, written mostly in Java, under Apache License v2.</p> <div class="hero__ctas"> <a class="btn btn--solid" href="#quick-start">Get started <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style="flex:0 0 auto;"><path d="M13 2 4.5 13.5H11L9.5 22 19 10h-6.5L13 2Z"/></svg></a> - <a class="btn btn--ghost" href="{{ site.baseurl }}/download/">Download 3.7.0</a> + <a class="btn btn--ghost" href="{{ site.baseurl }}/download/">Download {{ site.version }}</a> </div> <p class="hero__assurance" style="margin:18px 0 0;"><span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12.5 9.5 18 20 6"/></svg> Apache License v2</span> <span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12.5 [...] </div> @@ -27,32 +27,34 @@ description: Apache StormCrawler is an open source SDK for building low-latency, <!-- ============================== Pipeline ============================== --> <div class="rail"> + <div class="rail__thread" aria-hidden="true"></div> + <span class="rail__crawler" aria-hidden="true"></span> <div class="stage reveal" style="border-top:0;"> <span class="stage__node stage__node--accent">01</span> - <p class="slabel">SEED — START FROM A LIBRARY</p> + <p class="slabel">SEED · START FROM A LIBRARY</p> <h2 class="stage__title">Feed it URLs, get a crawler.</h2> - <p class="stage__copy"><strong>Apache StormCrawler</strong> is a library and collection of resources that developers leverage to build their own crawlers. The good news: doing so can be pretty straightforward — seed it with your URLs and let the topology do the rest.</p> + <p class="stage__copy"><strong>Apache StormCrawler</strong> is a library and collection of resources that developers leverage to build their own crawlers. The good news: doing so can be pretty straightforward. Seed it with your URLs and let the topology do the rest.</p> <a class="slink" href="{{ site.baseurl }}/docs/latest/index.html#_quick_start">READ THE FULL GUIDE <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7M9 7h8v8"/></svg></a> </div> <div class="stage reveal"> <span class="stage__node">02</span> - <p class="slabel">FETCH — POLITE, AT SCALE</p> + <p class="slabel">FETCH · POLITE, AT SCALE</p> <h2 class="stage__title">Crawl continuously, not in batches.</h2> - <p class="stage__copy">URLs are processed as streams across a whole <a href="https://storm.apache.org/" target="_blank" rel="noopener">Storm</a> cluster — add workers, not complexity. Storm's <a href="https://storm.apache.org/releases/current/Guaranteeing-message-processing.html" target="_blank" rel="noopener">guaranteed processing</a> means no URL is silently lost, and <a href="https://www.rfc-editor.org/rfc/rfc9309" target="_blank" rel="noopener">robots.txt</a>, delays and po [...] + <p class="stage__copy">URLs are processed as streams across a whole <a href="https://storm.apache.org/" target="_blank" rel="noopener">Apache Storm®</a> cluster: add workers, not complexity. Storm's <a href="https://storm.apache.org/releases/current/Guaranteeing-message-processing.html" target="_blank" rel="noopener">guaranteed processing</a> means no URL is silently lost, and <a href="https://www.rfc-editor.org/rfc/rfc9309" target="_blank" rel="noopener">robots.txt</a>, delays a [...] <a class="slink" href="{{ site.baseurl }}/docs/latest/index.html#_configuration">CONFIGURATION <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7M9 7h8v8"/></svg></a> </div> <div class="stage reveal"> <span class="stage__node">03</span> - <p class="slabel">PARSE — REUSABLE MODULES</p> - <h2 class="stage__title">Tika, sitemaps, feeds — plug them in.</h2> - <p class="stage__copy">Apart from the core components, we provide external resources you can reuse in your project: a ParserBolt that uses <a href="https://tika.apache.org/" target="_blank" rel="noopener">Apache Tika®</a> to parse various document formats, sitemap and feed parsers, filters and much more — pluggable, mostly plain Java.</p> + <p class="slabel">PARSE · REUSABLE MODULES</p> + <h2 class="stage__title">Apache Tika®, sitemaps, feeds: plug them in.</h2> + <p class="stage__copy">Apart from the core components, we provide external resources you can reuse in your project: a ParserBolt that uses <a href="https://tika.apache.org/" target="_blank" rel="noopener">Apache Tika®</a> to parse various document formats, sitemap and feed parsers, filters and much more. Pluggable, mostly plain Java.</p> <a class="slink" href="https://github.com/apache/stormcrawler/tree/main/external/tika" target="_blank" rel="noopener">EXTERNAL MODULES <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7M9 7h8v8"/></svg></a> </div> <div class="stage" style="padding-bottom:76px;"> <span class="stage__node">04</span> - <p class="slabel">INDEX — YOUR BACKEND</p> - <h2 class="stage__title">Straight into OpenSearch, Solr or SQL.</h2> - <p class="stage__copy">Spouts and bolts for <a href="https://opensearch.org/" target="_blank" rel="noopener">OpenSearch®</a>, <a href="https://solr.apache.org/" target="_blank" rel="noopener">Apache Solr®</a> and SQL storage ship as external modules — index documents as they are fetched, whether you crawl a stream of URLs or run large-scale recursive crawls where low latency is required.</p> + <p class="slabel">INDEX · YOUR BACKEND</p> + <h2 class="stage__title">Straight into OpenSearch®, Apache Solr® or SQL.</h2> + <p class="stage__copy">Spouts and bolts for <a href="https://opensearch.org/" target="_blank" rel="noopener">OpenSearch®</a>, <a href="https://solr.apache.org/" target="_blank" rel="noopener">Apache Solr®</a> and SQL storage ship as external modules, so documents are indexed as they are fetched, whether you crawl a stream of URLs or run large-scale recursive crawls where low latency is required.</p> <a class="slink" href="https://github.com/apache/stormcrawler/tree/main/external" target="_blank" rel="noopener">STORAGE MODULES <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7M9 7h8v8"/></svg></a> </div> </div> @@ -66,7 +68,7 @@ description: Apache StormCrawler is an open source SDK for building low-latency, <pre id="archetype-cmd">$ mvn archetype:generate \ -DarchetypeGroupId=<span class="hl">org.apache.stormcrawler</span> \ -DarchetypeArtifactId=stormcrawler-archetype \ - -DarchetypeVersion=<span class="hl">3.7.0</span></pre> + -DarchetypeVersion=<span class="hl">{{ site.version }}</span></pre> <button class="copy-chip" data-copy="#archetype-cmd" hidden>copy</button> </div> <ol class="steps"> @@ -94,7 +96,7 @@ description: Apache StormCrawler is an open source SDK for building low-latency, <div class="section"> <div class="section__inner"> <h2 class="display-h2" style="text-align:center;margin-bottom:6px;">Caught in the web<span style="color:var(--accent-ink);">.</span></h2> - <p class="slabel" style="display:block;text-align:center;color:var(--text-secondary);">IN PRODUCTION SINCE 2014 — SEARCH ENGINES, ARCHIVES, RESEARCH & MONITORING</p> + <p class="slabel" style="display:block;text-align:center;color:var(--text-secondary);">IN PRODUCTION SINCE 2014 · SEARCH ENGINES, ARCHIVES, RESEARCH & MONITORING</p> <div class="logo-hang reveal"> <a href="https://pixray.com/" target="_blank" rel="noopener"><img src="{{ site.baseurl }}/img/pixray.png" alt="Pixray"></a> <a href="https://www.gov.nt.ca/" target="_blank" rel="noopener"><img src="{{ site.baseurl }}/img/gnwt.png" alt="Government of Northwest Territories" style="max-height:46px;"></a> diff --git a/js/site.js b/js/site.js index a35e926..8f43b0a 100644 --- a/js/site.js +++ b/js/site.js @@ -1,6 +1,7 @@ (function () { 'use strict'; document.documentElement.classList.remove('no-js'); + var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; // --- Nav: burger below 1060px ---------------------------------------------- var header = document.querySelector('.site-header'); @@ -62,8 +63,11 @@ } // --- Orb web (hero + closing band) --------------------------------------- - function orbWeb(svg, nodeColor, accStroke) { + // draw:true animates the web spinning itself in: spokes first, then rings, + // via stroke-dashoffset on the two compound paths (subpaths draw in order). + function orbWeb(svg, nodeColor, accStroke, draw) { if (!svg) return; + draw = draw && !reduceMotion; var c = 360, spokes = 14, rings = [58, 96, 136, 178, 222, 268, 316]; var NS = 'http://www.w3.org/2000/svg'; function ang(i) { return i * 2 * Math.PI / spokes - Math.PI / 2 + (i % 3) * 0.02; } @@ -88,16 +92,31 @@ el.setAttribute('stroke-width', w); el.setAttribute('fill', 'none'); svg.appendChild(el); + return el; } - path(radii, '1.4'); - path(spiral, '1.1'); - // dew drops + function drawIn(el, dur, delay) { + var len = el.getTotalLength(); + el.style.strokeDasharray = len + ' ' + len; + el.style.strokeDashoffset = len; + el.style.transition = 'stroke-dashoffset ' + dur + 's ease-out ' + delay + 's'; + requestAnimationFrame(function () { + requestAnimationFrame(function () { el.style.strokeDashoffset = '0'; }); + }); + } + var pRadii = path(radii, '1.4'); + var pSpiral = path(spiral, '1.1'); + if (draw) { + drawIn(pRadii, 0.9, 0); + drawIn(pSpiral, 1.25, 0.35); + } + // dew drops (fade in after the draw via .web-dew CSS) [[2, 136], [5, 222], [9, 178], [12, 268], [7, 96]].forEach(function (n) { var q = pt(n[0], n[1]), el = document.createElementNS(NS, 'circle'); el.setAttribute('cx', q[0].toFixed(1)); el.setAttribute('cy', q[1].toFixed(1)); el.setAttribute('r', '3.5'); el.setAttribute('fill', nodeColor); + if (draw) el.setAttribute('class', 'web-dew'); svg.appendChild(el); }); // one accent node @@ -110,21 +129,74 @@ acc.setAttribute('stroke', accStroke); acc.setAttribute('stroke-width', '1.5'); } + if (draw) acc.setAttribute('class', 'web-acc'); svg.appendChild(acc); - // impulse running a thread - var dot = document.createElementNS(NS, 'circle'); - dot.setAttribute('r', '4'); - dot.setAttribute('fill', 'var(--accent)'); - var mo = document.createElementNS(NS, 'animateMotion'); - mo.setAttribute('dur', '7s'); - mo.setAttribute('repeatCount', 'indefinite'); - var sp = pt(4, 346); - mo.setAttribute('path', 'M' + c + ' ' + c + ' L' + sp[0].toFixed(1) + ' ' + sp[1].toFixed(1) + ' L' + c + ' ' + c); - dot.appendChild(mo); - svg.appendChild(dot); + // impulse running a thread — skipped entirely for reduced motion + if (!reduceMotion) { + var dot = document.createElementNS(NS, 'circle'); + dot.setAttribute('r', '4'); + dot.setAttribute('fill', 'var(--accent)'); + var mo = document.createElementNS(NS, 'animateMotion'); + mo.setAttribute('dur', '7s'); + mo.setAttribute('repeatCount', 'indefinite'); + if (draw) mo.setAttribute('begin', '2.2s'); + var sp = pt(4, 346); + mo.setAttribute('path', 'M' + c + ' ' + c + ' L' + sp[0].toFixed(1) + ' ' + sp[1].toFixed(1) + ' L' + c + ' ' + c); + dot.appendChild(mo); + svg.appendChild(dot); + } + } + orbWeb(document.getElementById('webHero'), 'var(--text)', 'var(--text)', true); + // the closing band's web spins itself in when scrolled near + var webCta = document.getElementById('webCta'); + if (webCta) { + var ctaBand = webCta.closest('.cta-band'); + if (!reduceMotion && 'IntersectionObserver' in window && ctaBand) { + var ctaIo = new IntersectionObserver(function (entries) { + if (entries.some(function (e) { return e.isIntersecting; })) { + ctaIo.disconnect(); + orbWeb(webCta, '#8B8E7D', '', true); + } + }, { rootMargin: '0px 0px -15% 0px' }); + ctaIo.observe(ctaBand); + } else { + orbWeb(webCta, '#8B8E7D', '', false); + } + } + + // --- Pipeline rail: silk thread follows the scroll ------------------------ + var rail = document.querySelector('.rail'); + if (rail && !reduceMotion) { + var thread = rail.querySelector('.rail__thread'); + var crawler = rail.querySelector('.rail__crawler'); + var railNodes = rail.querySelectorAll('.stage__node'); + if (thread && crawler) { + var railTicking = false; + var railUpdate = function () { + railTicking = false; + var r = rail.getBoundingClientRect(); + var p = (window.innerHeight * 0.62 - r.top) / r.height; + p = Math.max(0, Math.min(1, p)); + var y = p * r.height; + thread.style.transform = 'scaleY(' + p + ')'; + crawler.style.transform = 'translateY(' + y.toFixed(1) + 'px)'; + crawler.style.opacity = (p > 0.005 && p < 0.995) ? '1' : '0'; + railNodes.forEach(function (n) { + var nt = n.getBoundingClientRect().top - r.top; + n.classList.toggle('is-passed', y >= nt + 10); + }); + }; + var onRailScroll = function () { + if (!railTicking) { + railTicking = true; + requestAnimationFrame(railUpdate); + } + }; + window.addEventListener('scroll', onRailScroll, { passive: true }); + window.addEventListener('resize', onRailScroll); + railUpdate(); + } } - orbWeb(document.getElementById('webHero'), 'var(--text)', 'var(--text)'); - orbWeb(document.getElementById('webCta'), '#8B8E7D', ''); // --- Smooth scroll to #quick-start --------------------------------------- document.querySelectorAll('a[href="#quick-start"]').forEach(function (a) {
