npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@milesmfe/text-pipes

v4.3.1

Published

Animated SVG text pipe effect — generate path data from any font, render responsive animations with drain/restore/scrub controls

Readme

Text Pipes

Animated SVG text effect. Parses any .ttf/.otf font, traces each glyph, extends it with a randomised "pipe" path, and animates the strokes draining away from (or returning to) the letterforms.

Generation and rendering are split into two independent modules so you can run the heavy font-parsing step on a server and ship only the lightweight renderer to the browser. A root entry point re-exports both for convenience.

Install

npm install @milesmfe/text-pipes

Quick start

1. Generate path data

The generator accepts raw font bytes (Buffer in Node, ArrayBuffer in the browser) and returns a plain JSON-serialisable object.

Node

import { readFileSync } from "fs";
import { buildSVGData } from "@milesmfe/text-pipes/generator";

const font = readFileSync("./fonts/Inter.ttf");
const data = buildSVGData(font, "hello world");

Browser

import { buildSVGData } from "@milesmfe/text-pipes/generator";

const res  = await fetch("/fonts/Inter.ttf");
const font = await res.arrayBuffer();
const data = buildSVGData(font, "hello world");

2. Render and animate

The renderer creates an SVG that scales responsively to fill its container via viewBox. Just give the container a size and the text scales to fit.

<div id="tp" style="width: 600px; max-width: 100%"></div>

<script type="module">
  import { TextPipes } from "@milesmfe/text-pipes/renderer";

  const tp = new TextPipes(document.getElementById("tp"), data, {
    color: "#fff",
  });

  // animate strokes away from the text, then bring them back
  await tp.drain();
  await tp.restore();

  // clean up when done
  tp.destroy();
</script>

Both drain() and restore() return a Promise that resolves when the animation completes, so you can chain animations or run logic after them.

3. Alignment

Control how the text sits within its container using the align option. The SVG's preserveAspectRatio is set accordingly.

// left-aligned (e.g. for a heading above body text)
new TextPipes(el, data, { align: "left" });

// right-aligned
new TextPipes(el, data, { align: "right" });

// centered (default)
new TextPipes(el, data, { align: "center" });

4. Scroll-driven scrubbing

setProgress maps a 0-1 value directly to the animation position with transitions disabled, making it ideal for scroll-linked effects.

window.addEventListener("scroll", () => {
  const t = window.scrollY / (document.body.scrollHeight - window.innerHeight);
  tp.setProgress(t);
});

5. One-import browser usage

If you don't need the server/client split, the root entry point re-exports everything:

import { buildSVGData, TextPipes } from "@milesmfe/text-pipes";

const font = await fetch("/fonts/Inter.ttf").then(r => r.arrayBuffer());
const data = buildSVGData(font, "hello");
const tp   = new TextPipes(document.getElementById("tp"), data);

6. Static HTML (no bundler)

A self-contained IIFE build is included for use via CDN or a local <script> tag. It bundles all dependencies and exposes a textPipes global.

<div id="tp" style="width: 600px; max-width: 100%"></div>

<script src="https://cdn.jsdelivr.net/npm/@milesmfe/text-pipes/dist/text-pipes.iife.js"></script>
<script>
  const { buildSVGData, TextPipes } = window.textPipes;

  fetch("/fonts/Inter.ttf")
    .then(r => r.arrayBuffer())
    .then(font => {
      const data = buildSVGData(font, "hello");
      const tp = new TextPipes(document.getElementById("tp"), data, {
        color: "#fff",
      });
      tp.drain();
    });
</script>

Architecture

text-pipes/generator          text-pipes/renderer
┌──────────────────┐          ┌──────────────────┐
│  Font buffer      │          │  SVGData JSON     │
│  + text string    │─ JSON ──▶│  + DOM container  │
│                   │          │                   │
│  → SVGData JSON   │          │  → Live SVG       │
│    (serialisable) │          │    + drain/restore │
└──────────────────┘          └──────────────────┘
  Runs anywhere (Node/Browser)   Browser only

The generator depends on opentype.js and svg-path-properties. The renderer has zero runtime dependencies.

API

text-pipes/generator

buildSVGData(fontBuffer, text, fontSize?, options?)

Parses a font, traces the glyphs for text, appends a randomised pipe segment to each subpath, and returns an SVGData object.

| Param | Type | Default | Description | |---|---|---|---| | fontBuffer | ArrayBuffer \| Buffer | | Raw .ttf or .otf font data | | text | string | | The text to render | | fontSize | number | 150 | Font size in px (controls path detail, not display size) | | options | GeneratorOptions | {} | See below |

GeneratorOptions

| Option | Type | Default | Description | |---|---|---|---| | rng | () => number | Math.random | Random number generator. Supply a seeded RNG for deterministic output (important for SSR) | | pipeMinLength | number | 150 | Minimum length of the first pipe segment | | pipeMaxLength | number | 350 | Maximum length of the first pipe segment | | pipeExtension | number | 1500 | Length of the second (long) pipe segment | | speed | number | 1600 | Base speed divisor for computing animation duration | | stagger | number | 0.03 | Delay increment between successive subpaths (seconds) |

Returns: SVGData

interface SVGData {
  fillD: string;         // combined fill path for the solid text shape
  pathData: PathDatum[]; // per-subpath data (path d, lengths, timing)
  viewBox: string;       // SVG viewBox computed from the font's bounding box
  width: number;         // natural width of the text (px)
  height: number;        // natural height of the text (px)
  totalWidth: number;    // advance width of the text (px)
  fontSize: number;      // the fontSize that was used
  textOffsetX: number;   // legacy horizontal centering offset
  textOffsetY: number;   // legacy vertical centering offset
}

interface PathDatum {
  fullD: string;        // character subpath + pipe extension
  letterLength: number; // length of the character portion
  totalLength: number;  // length of the full path
  duration: number;     // computed animation duration (seconds)
  delay: number;        // computed stagger delay (seconds)
}

The returned object is plain JSON — you can JSON.stringify it, store it in a database, embed it in a <script> tag, or return it from an API endpoint.


text-pipes/renderer

new TextPipes(container, data, options?)

Creates a responsive SVG inside container. The SVG uses viewBox to scale proportionally — the container only needs a width and the text scales to fit. No position: relative or explicit height required.

| Param | Type | Description | |---|---|---| | container | HTMLElement | The element that will hold the SVG | | data | SVGData | The object returned by buildSVGData | | options | RendererOptions | See below |

RendererOptions

| Option | Type | Default | Description | |---|---|---|---| | color | string | "#111" | CSS color for strokes and fill | | strokeWidth | number | 2 | Stroke width in px | | fadeSpeedFactor | number | 10 | How aggressively the fill fades during scrubbing (1 = slow, 20 = instant) | | drainSpeed | number | 1 | Speed multiplier for drain() transitions | | restoreSpeed | number | 1 | Speed multiplier for restore() transitions | | easing | string | "linear" | CSS easing function for stroke transitions | | align | "left" \| "center" \| "right" | "center" | Horizontal alignment within the container |

Methods

| Method | Returns | Description | |---|---|---| | drain() | Promise<void> | Animate every stroke away from its character. Resolves when the animation completes | | restore() | Promise<void> | Animate all strokes back to idle. Resolves when the animation completes and the fill is visible again | | setProgress(t) | void | Scrub to an exact position. 0 = idle, 1 = fully drained. Transitions are disabled so updates are instant | | destroy() | void | Remove the SVG and release all references |

Properties

| Property | Type | Description | |---|---|---| | state | "idle" \| "draining" \| "returning" | Current animation state (read-only) |

Recipes

Chaining animations

await tp.drain();
console.log(tp.state); // "draining" (visually drained, strokes are away)

await tp.restore();
console.log(tp.state); // "idle" (text is fully visible again)

Deterministic output for SSR

Pass a seeded RNG to get identical output across runs. This avoids hydration mismatches when generating on the server and rendering on the client.

import seedrandom from "seedrandom";
import { buildSVGData } from "@milesmfe/text-pipes/generator";

const data = buildSVGData(font, "hello", 150, {
  rng: seedrandom("my-seed"),
});

Pre-generate and cache

Since SVGData is plain JSON, you can generate once at build time and cache indefinitely.

// build step or API route
import { readFileSync, writeFileSync } from "fs";
import { buildSVGData } from "@milesmfe/text-pipes/generator";

const font = readFileSync("./fonts/Inter.ttf");
const data = buildSVGData(font, "hello");
writeFileSync("./data/hello.json", JSON.stringify(data));
<!-- client — only the renderer is loaded, no font parsing deps -->
<script type="module">
  import { TextPipes } from "@milesmfe/text-pipes/renderer";

  const data = await fetch("/data/hello.json").then(r => r.json());
  const tp = new TextPipes(document.getElementById("tp"), data);
  tp.drain();
</script>

Custom easing

const tp = new TextPipes(container, data, {
  easing: "cubic-bezier(0.4, 0, 0.2, 1)",
  drainSpeed: 0.6,
});

Responsive sizing

The SVG scales to fit its container's width, maintaining aspect ratio. Just set a width on the container — no height needed.

<!-- fills available width, height adjusts automatically -->
<div id="tp" style="width: 100%"></div>

<!-- constrained width -->
<div id="tp" style="width: 400px; max-width: 100%"></div>

React

import { useEffect, useRef } from "react";
import { TextPipes } from "@milesmfe/text-pipes/renderer";

function Pipes({ data }) {
  const ref = useRef(null);

  useEffect(() => {
    const tp = new TextPipes(ref.current, data, { color: "#fff" });
    tp.drain();
    return () => tp.destroy();
  }, [data]);

  return <div ref={ref} style={{ width: "100%", maxWidth: 600 }} />;
}

Svelte

<script>
  import { onMount } from "svelte";

  let { data } = $props();
  let container = $state<HTMLElement>();

  onMount(() => {
    const { TextPipes } = await import("@milesmfe/text-pipes/renderer");
    const tp = new TextPipes(container, data, { color: "#111" });
    tp.drain();
    return () => tp.destroy();
  });
</script>

<div bind:this={container} style="width: 100%; max-width: 600px;"></div>

TypeScript

Full type declarations ship with the package. All interfaces are importable from any entry point:

import type { SVGData, GeneratorOptions } from "@milesmfe/text-pipes/generator";
import type { RendererOptions }           from "@milesmfe/text-pipes/renderer";

// or from the root
import type { SVGData, PathDatum, GeneratorOptions, RendererOptions } from "@milesmfe/text-pipes";

Module formats

All entry points ship as ESM (.mjs) and CommonJS (.cjs) with sourcemaps and declaration maps. A self-contained IIFE build (dist/text-pipes.iife.js) is also included for CDN / <script> tag usage.

| File | Format | Dependencies | Use case | |---|---|---|---| | dist/index.mjs | ESM | External | Bundler (Vite, Webpack, etc.) | | dist/index.cjs | CJS | External | Node require() | | dist/generator.mjs / .cjs | ESM / CJS | External | Server-only generation | | dist/renderer.mjs / .cjs | ESM / CJS | None | Client-only rendering | | dist/text-pipes.iife.js | IIFE | Bundled | <script> tag, no build step |

License

MIT