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

@underlying/svg

v1.2.1

Published

SVG path animation for @underlying/core: ride a path, draw a stroke on, or morph one shape into another, physics-first - flick, interrupt, retarget, or scrub.

Readme

SVG path animation for @underlying/core - ride a path, draw a stroke, and a resampling morph. The progress of each is a single live value, so where other libraries bake a path tween, here you can flick a marker down a path and let it settle, interrupt a stroke mid-draw, scrub a morph, or hand the same progress to scroll. No new engine: it samples the path with the native getPointAtLength/getTotalLength and drives core's animatable.

npm install @underlying/svg @underlying/core

motionPath()

Ride an element along a path. Progress t is a live Animatable, so spring it, flick it, or retarget it mid-flight - velocity is conserved across the change. autoRotate turns the element to face along the path.

import { motionPath } from '@underlying/svg'

const ride = motionPath(marker, '#track', { autoRotate: true })

ride.spring(1)               // travel to the end with a real spring
ride.flick(2.4)              // fling it down the path; it decays to a stop
ride.spring(0.4)             // retarget mid-flight, momentum kept

ride.t                       // the live 0..1 Animatable - compose it (scroll, timeline)
ride.revert()                // unbind and restore the element's transform

draw()

Draw a stroke on (0 = hidden, 1 = fully drawn). The fraction is a live Animatable too - it can overshoot, and you can interrupt it mid-draw.

import { draw } from '@underlying/svg'

const line = draw('#signature')
line.spring(1)               // draw it on
line.to(0, { duration: 400 })// erase with a timed tween, still interruptible

morph()

Turn one shape into another. Both outlines are resampled into points along their length and interpolated, so any two paths morph - you do not have to match their commands by hand. The fraction is live, so you can scrub it or grab it mid-morph.

import { morph } from '@underlying/svg'

const m = morph(blob, starPathData, { closed: true })  // target: a `d` string or an element
m.spring(1)   // morph to the star
m.spring(0)   // morph back - interruptible
m.set(0.5)    // hold it halfway

This is a resampling morph (smooth, handles arbitrary shapes). When you need corners to stay sharp, reach for morphCommands() below.

morphCommands()

A command-preserving morph that keeps corners razor-sharp. Instead of resampling both outlines into points, it parses the d commands into cubic segments, subdivides the sparser shape by arc length so anchors map to anchors (de Casteljau, so original corners stay crisp), aligns closed rings by rotation and winding (matched on centroid/scale-normalized anchors), pairs multi-piece shapes by similarity, and interpolates each anchor and control. Elliptical arcs (A) are converted to cubics. The result is real curves with sharp corners the whole way across. The fraction is live, so you can scrub it or grab it mid-morph.

import { morphCommands } from '@underlying/svg'

const m = morphCommands(icon, stopSquareData)  // target: a `d` string or an element with a `d`
m.spring(1)   // play triangle -> stop square, every corner stays sharp
m.spring(0)   // back - interruptible
m.set(0.5)    // hold it halfway

Multi-piece logos work too: subpaths pair by similarity (position + area), and a surplus piece shrinks to (or grows from) a point rather than snapping to an unrelated shape. For arbitrary blobby shapes where corners do not matter, morph() (resampling) is the simpler fallback.

The familiar one-call form

motionPath, draw, morph, and morphCommands all accept a { to } kickoff that reads like a one-call tween - but springs under the hood, and the handle is still there for the live wins.

motionPath(marker, '#track', { to: 1, autoRotate: true })
draw('#signature', { to: 1 })
morph(blob, starPathData, { to: 1, closed: true })
morphCommands(icon, stopSquareData, { to: 1 })

Composing - drive the same path from scroll or a timeline

motionPath and draw own a driver Animatable (.t / .fraction) you can hand to anything that drives a value.

import { motionPath } from '@underlying/svg'
import { createScroll } from '@underlying/scroll'

const ride = motionPath(marker, '#track', { autoRotate: true })
createScroll({ scroller }).scrub(ride.t)   // marker follows the path as you scroll

Bring your own driver

The handles are built on thin binders. If you already have an Animatable (or want to control the value yourself), bind it directly:

import { bindPath, bindDraw, samplePath } from '@underlying/svg'
import { animatable } from '@underlying/core'

const t = animatable(0)
bindPath(marker, '#track', t, { autoRotate: true }) // maps t -> transform
t.decay({ velocity: 2.4 })

samplePath('#track').at(0.5)  // low-level: { x, y, angle } at progress 0.5

resolvePathGeometry(input) is exported too - the low-level resolver that turns a selector or element into a geometry source (the same step samplePath does internally), if you want to measure or sample a path yourself.

Notes

  • Reduced motion is inherited from core: a spring/decay/to on the driver auto-degrades under prefers-reduced-motion, so the element jumps to the target with no travel.
  • Coordinate space. motionPath writes the sampled point straight to the element's transform, so the element and the path should share a coordinate space (e.g. both inside the same SVG, or the element absolutely positioned over it).
  • SSR. Sampling needs the DOM; pass an element rather than a selector on the server, or call from an effect.
  • Morph comes in two flavours. morph() resamples both outlines into points and interpolates - it handles any two shapes, but sharp corners can soften (raise samples for fidelity). morphCommands() preserves the commands and keeps corners crisp (arcs, multi-piece shapes, and wildly different scales all handled); reach for morph() when corners do not matter.

License

MIT (c) underlyi.ng