---
name: data-stream-animation
description: Use when you want "Technical, cyber, dynamic" - Shows flowing numbers, lines, or particles as data movement.
---

# Data Stream Animation

> **Category:** Premium / Futuristic  -  **Personality:** Technical, cyber, dynamic
>
> **Best use:** Security, analytics, infrastructure
>
> **Ratings:** Professional 3/5 - Casual 2/5 - Exotic 4/5 - Visual impact 4/5 - Difficulty 3/5 - Performance cost 3/5

## What it is

A composite "live data plane" effect: streaming hex/digits flow sideways through a `<canvas>` panel while glowing **packets travel along SVG paths** between pipeline nodes, and **live counters** tick in real time. It reads as a Network Operations Center / telemetry dashboard — the motion itself communicates "data is moving right now." Use it on security, analytics or infrastructure surfaces where you want a technical, cyber, alive feeling. It is deliberately *multidirectional* (lanes flow both ways, packets hop node-to-node) to distinguish it from a one-way "Matrix rain" of vertically falling glyphs.

## Dependencies / CDN

**None - vanilla JS** (HTML `<canvas>` 2D + inline `<svg>`, no libraries). The demo only loads two display fonts (optional):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Martian+Mono:wght@400;500;600;700&family=Tektur:wght@500;600;700;800&display=swap" rel="stylesheet">
```

## HTML

Three moving pieces: an SVG flow-graph (base link + animated dash + travelling packet dots + nodes), a `<canvas>` stream, and any live-counter readouts.

```html
<div class="ds-stage" id="dsStage">
  <!-- 1) SVG pipeline: each link is drawn twice (static base + animated dash) -->
  <svg class="ds-svg" viewBox="0 0 520 360" aria-label="Data pipeline">
    <defs>
      <filter id="dsGlow" x="-60%" y="-60%" width="220%" height="220%">
        <feGaussianBlur stdDeviation="2.4" result="b"/>
        <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
      </filter>
    </defs>
    <path class="ds-link" d="M70,180 L200,180"/>
    <path class="ds-flow" style="stroke:#35e3ee" d="M70,180 L200,180"/>
    <!-- one travelling packet per stream (JS positions it) -->
    <circle class="ds-pkt" r="3.6" fill="#35e3ee" filter="url(#dsGlow)" opacity="0"/>
    <g class="ds-node">
      <circle class="hit" cx="200" cy="180" r="18"/>
      <circle class="core" cx="200" cy="180" r="5" filter="url(#dsGlow)"/>
      <text class="ds-nlabel" x="200" y="212">ROUTER</text>
    </g>
  </svg>

  <!-- 2) The streaming hex lanes -->
  <div class="ds-stream"><canvas id="dsCanvas" aria-hidden="true"></canvas></div>

  <!-- 3) A live counter -->
  <div class="ds-big"><span id="dsThru">42.7</span><u>Gb/s</u></div>
</div>
```

## CSS

```css
/* the "current" flowing along each link = an animated dash gap */
.ds-link{fill:none;stroke:rgba(124,156,214,.22);stroke-width:2}
.ds-flow{fill:none;stroke-width:2.2;stroke-linecap:round;
  stroke-dasharray:2 12;animation:ds-dash 1s linear infinite}
@keyframes ds-dash{to{stroke-dashoffset:-28}}   /* negative = flows toward the node */

/* the canvas just needs a fixed box; JS handles the pixels */
.ds-stream{border:1px solid rgba(124,156,214,.16);border-radius:16px;overflow:hidden;background:#060a14}
.ds-stream canvas{display:block;width:100%;height:152px}

.ds-node .core{fill:#35e3ee}            /* glowing node hub */
.ds-big{font-family:'Martian Mono',ui-monospace,monospace;font-weight:700;
  font-size:30px;font-variant-numeric:tabular-nums}   /* tabular = digits don't jump */

@media (prefers-reduced-motion:reduce){ .ds-flow{animation:none} }
```

## JavaScript

The two motion engines (canvas hex lanes + SVG packets) plus a single `requestAnimationFrame` loop, all gated behind `prefers-reduced-motion`:

```js
(function(){
  var stage=document.getElementById('dsStage'); if(!stage) return;
  var reduce=matchMedia('(prefers-reduced-motion: reduce)').matches;

  /* --- SVG packets: walk a dot along each path with getPointAtLength --- */
  var flows=[].slice.call(stage.querySelectorAll('.ds-flow'));
  var lens=flows.map(function(p){return p.getTotalLength();});
  var P=[].slice.call(stage.querySelectorAll('.ds-pkt')).map(function(el,i){
    return {el:el, fi:i%flows.length, t:Math.random(), sp:0.16+Math.random()*0.22};
  });
  function placeDots(dt){
    P.forEach(function(p){ var L=lens[p.fi]; if(!L)return;
      p.t=(p.t+p.sp*dt)%1;
      var pt=flows[p.fi].getPointAtLength(p.t*L);
      p.el.setAttribute('cx',pt.x.toFixed(1)); p.el.setAttribute('cy',pt.y.toFixed(1));
      p.el.setAttribute('opacity',(0.2+0.8*Math.sin(Math.PI*p.t)).toFixed(2)); // fade in/out along path
    });
  }

  /* --- canvas: lanes of hex; a moving "band" lights up packets passing through --- */
  var cv=document.getElementById('dsCanvas'), ctx=cv.getContext('2d');
  var GL='0123456789ABCDEF', AC=['#35e3ee','#9d83ff','#48e0a6'];
  var CW,CH,cwid,COLS,lanes=[];
  function hash(a,b){var h=(a*73856093)^(b*19349663);h=(h^(h>>>13))*1274126177;return h>>>0;}
  function glyph(r,k){return GL.charAt(hash(r,k)%16);}            // deterministic = stable while scrolling
  function setup(){
    var DPR=Math.min(2,devicePixelRatio||1);
    CW=cv.clientWidth; CH=cv.clientHeight; cv.width=CW*DPR; cv.height=CH*DPR;
    ctx.setTransform(DPR,0,0,DPR,0,0);
    ctx.font='600 14px "Martian Mono",ui-monospace,monospace'; ctx.textBaseline='middle';
    cwid=ctx.measureText('0').width||11; COLS=Math.ceil(CW/cwid)+2;
    var lh=20, n=Math.max(3,Math.floor((CH-6)/lh)); lanes=[];
    for(var r=0;r<n;r++){ var dir=Math.random()<0.62?1:-1;
      lanes.push({y:9+r*lh+lh/2, sp:(24+Math.random()*40)*dir, off:Math.random()*CW,
        col:AC[r%3===0?(Math.random()<.5?1:2):0], band:Math.random()*CW, bsp:(0.10+Math.random()*0.12)*dir});
    }
  }
  function draw(dt){
    ctx.clearRect(0,0,CW,CH); ctx.font='600 14px "Martian Mono",ui-monospace,monospace';
    lanes.forEach(function(ln,r){
      ln.off+=ln.sp*dt; ln.band+=ln.bsp*CW*dt;
      var bc=((ln.band%CW)+CW)%CW, base=Math.floor(ln.off/cwid), frac=ln.off-base*cwid;
      for(var c=-1;c<COLS;c++){
        var k=c-base, x=c*cwid+frac, dd=Math.abs(x-bc); if(dd>CW/2)dd=CW-dd;
        var glow=Math.exp(-(dd*dd)/9800), a=0.10+0.16*((hash(r,k)%100)/100)+0.72*glow;
        if(a<0.06)continue;
        if(glow>0.72){ctx.fillStyle=ln.col;ctx.shadowColor=ln.col;ctx.shadowBlur=7;ctx.globalAlpha=Math.min(1,a);}
        else{ctx.fillStyle='#7fa6d8';ctx.shadowBlur=0;ctx.globalAlpha=a;}
        ctx.fillText(glyph(r,k),x,ln.y);
      }
    });
    ctx.globalAlpha=1; ctx.shadowBlur=0;
  }

  setup();
  if(reduce){ P.forEach(function(p,i){p.t=(i+1)/(P.length+1);}); placeDots(0); draw(0); return; } // one static frame

  var last=performance.now();
  (function loop(now){
    var dt=Math.min(0.05,(now-last)/1000); last=now;
    placeDots(dt); draw(dt);            // + tick your counters here on a throttle
    requestAnimationFrame(loop);
  })(last);

  addEventListener('resize',setup);
  if(document.fonts&&document.fonts.ready) document.fonts.ready.then(setup); // re-measure once the mono loads
})();
```

## How it works

- **Flowing links** are pure CSS: each path is painted twice — a dim static `.ds-link` and a bright `.ds-flow` with a tiny `stroke-dasharray` whose `stroke-dashoffset` animates negative, so the gaps appear to *stream* toward the node. Cheap and GPU-friendly.
- **Travelling packets** use `path.getPointAtLength(t * getTotalLength())` to read an (x,y) anywhere along the curve; advancing `t` each frame walks a dot node-to-node. `sin(pi*t)` fades it in at the start and out at the end.
- **Canvas hex lanes** never store a grid. Each character is derived on the fly from `hash(lane, tapeIndex)`, so the glyphs stay *stable* as the lane scrolls (only their screen position changes). A per-lane Gaussian "band" sweeps along and brightens whatever it overlaps — those bright runs read as data **packets** moving through the pipe. Lanes pick `dir = ±1` independently, giving the multidirectional flow.
- **Live counters** are just numbers nudged by a bounded random walk on a throttle (≈10/s) and printed with `font-variant-numeric: tabular-nums` so the layout never jitters.

## Customization

- **Charset:** swap `GL` for binary `'01'`, katakana, or base64 chars. 16 hex chars feels the most "infrastructure".
- **Flow direction/speed:** the sign of a lane's `sp`/`bsp` (and the SVG dash `to{stroke-dashoffset:-28}` value) sets direction; raise the magnitudes for a faster torrent.
- **Density:** `lh` (line height) controls lane count; smaller `font` + `cwid` packs more columns.
- **Palette:** `AC` is the accent set for the bright packet bands; the dim base is the single `#7fa6d8` fill. Keep the base low-alpha so the bright bands pop.
- **Glow band width:** the `9800` divisor is `2 * sigma^2` (sigma ≈ 70px). Lower it for tight, punchy packets; raise it for broad pulses.

## Accessibility & performance

- **Reduced motion:** check `matchMedia('(prefers-reduced-motion: reduce)')` and render exactly **one** static frame (packets parked at fixed `t`, a single canvas paint) instead of starting the rAF loop — done above.
- Decorative layers carry `aria-hidden="true"`; the SVG has a descriptive `aria-label`. Never put real, only-here information inside the stream.
- Size the canvas backing store to `devicePixelRatio` (capped at 2) so text stays crisp without exploding the fill-rate on 3x phones.
- `ctx.shadowBlur` is the expensive call — it's applied **only** to the few glyphs inside the bright band (`glow > 0.72`), not the whole field; everything else paints flat.
- One shared `requestAnimationFrame` drives every sub-animation; counters/feed updates run off throttle accumulators, not their own timers, so the page settles to a single loop the browser can pause when backgrounded.

## Gotchas

- **Re-measure after the webfont loads.** A monospace's advance width changes when `Martian Mono` swaps in; call `setup()` from `document.fonts.ready` or the columns drift. Always include a `monospace` fallback in the canvas `font`.
- **`getPointAtLength` needs the path in the live DOM** and a non-zero length — guard `if(!len) return;` (a degenerate `d` returns 0 and yields `NaN` coordinates).
- **Don't regenerate glyphs randomly each frame** — that's flicker, not flow. Derive them from a stable `hash(lane, index)` and animate position only.
- **Modulo on negative offsets:** wrap with `((v % CW) + CW) % CW`, since JS `%` keeps the sign and leftward lanes go negative.
- **Avoid `width:100vw`/margin breakouts** on the stage; keep it a contained rounded panel or you'll trigger horizontal scrollbars.
- This is GPU/CPU active work every frame — keep it to one viewport-sized canvas; don't stack several full-screen instances on one page.
