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

@happyvertical/animation

v0.5.0

Published

GSAP-native animated components — expressive SVG characters for GSAP landing pages

Readme

@happyvertical/animation

GSAP-native animated components: expressive SVG characters (Happy — the => smiley: expressions, lip-synced speech), the "Happy Vertical" living-wordmark lockup, a floating chat widget with pluggable conversation backends, a consent-aware control registry, a spotlight overlay for guided tours, and scroll stories (pinned scene sequences with a chapter rail and deep links).

Framework-agnostic: gsap (+ gsap/MorphSVGPlugin, gsap/ScrollTrigger) are peer dependencies resolved from the host page, injected styles live under @layer hv.animation so plain host CSS always wins, and conversation backends are pluggable (ChatBackend) — smrt-service backends ship separately in @happyvertical/smrt-animations.

import { createHappy, createHappyChat } from '@happyvertical/animation';

const happy = createHappy({ target: '#happy', size: 180 });
happy.express('heart');

createHappyChat({ target: document.body }); // mock backend, no server needed

Portable photo-cutout rigs

Photo cutouts are a separate, GSAP-free API. A rig is pure JSON with numeric polygon clips and asset ids; it never includes image URLs, SVG/CSS markup, or callbacks. Exported bundles carry sanitized PNG bytes, which lets an editor save a portable configuration without retaining an expiring object URL.

import { mountPhotoCutout, validatePhotoCutoutRig } from '@happyvertical/animation';

validatePhotoCutoutRig(rig);
const character = mountPhotoCutout(rig, {
  target: '#character',
  resolveAsset: async (assetId, { signal }) => localAssets.get(assetId, signal),
});
character.setMouthOpen(0.35); // host-driven real audio state only

Setup workflows can validate an untrusted silhouette independently of a full rig: validatePhotoCutoutOutline(points, { width, height, minPoints? }) returns safe plain points and rejects duplicate vertices, zero-area, self-intersecting, non-finite, and out-of-bounds geometry. For a lower-face segment, clipPhotoCutoutOutlineBelowLine(points, { width, height, y, minPoints? }) clips at a horizontal mouth line, preserving eligible contour vertices and adding only line intersections. It rejects a result that cannot remain one valid polygon; callers must not replace it with a rectangle.

jaw.maxOpenOffset?: { x, y } adds bounded canvas-coordinate translation at full mouth-open and composes with the existing maxOpenDegrees rotation. A split-head rig can use maxOpenDegrees: 0 with a positive y offset. Leave enough transparent lower canvas/image-frame padding for that translation. The jaw may share the head silhouette boundary, which preserves a real lower face contour. Every jaw segment is still checked across concave head geometry, so a segment that leaves the head is rejected.

Hosts may apply a bounded visual pose with character.setExpression({ headTiltDegrees, jawTiltDegrees }). Both numeric values clamp to -20 through 20 degrees. Head tilt carries the head, jaw, mouth, and accessory layers around the canvas centre; body layers remain anchored. Jaw tilt applies only while the mouth is open. This is host-driven presentation, not inferred emotion or lip-sync, and reduced-motion mounts keep the neutral pose. For example, a speaking pose can combine the rig's jaw.maxOpenDegrees with character.setExpression({ headTiltDegrees: 4, jawTiltDegrees: 7 }); omitted fields return to zero. Non-finite values throw.

Version 1 accepts static PNG, JPEG, and WebP source assets up to 10 MiB each; import is bounded to 32 MiB encoded data and 16 million decoded pixels in aggregate. It permits at most 16 layers and 256 points per layer. Head and jaw layers are required; an optional mouth-interior renders behind the jaw. Omit it when an opened split should reveal the host background transparently. The jaw clip is removed from the head using an even-odd numeric clip and rotates around its declared pivot. destroy() aborts pending asset work and revokes all object URLs. Mounts retain the fallback and hide every mouth interior, including a solid fill, until every required image texture has sanitized successfully.

A mouth-interior layer may instead use pure solid-color data:

{ kind: 'solid', id: 'mouth', role: 'mouth-interior',
  color: { r: 75, g: 31, b: 36 }, clip: mouthPolygon, opacity: 1 }

Only mouth interiors accept kind: 'solid'; its RGB channels are integers from 0 through 255, with no assetId, CSS, or additional fields. It renders as a clipped fill without allocating texture pixels. Existing image layers keep their assetId shape and aspect-preserving placement; bundle assets contain only references required by image layers. This additive form is part of the unreleased v1 photo-cutout schema.

Image layers optionally carry imageFrame: { x, y, width, height }, a numeric viewport using the same xMidYMid meet placement as the default canvas. This lets crops translate compact or shared textures without upsampling their pixels. Coordinates may be negative; each magnitude is bounded to 16 million, width/height are positive, and their product is at most 16 million. Unknown frame fields and non-finite values are rejected; solid layers have no frame.

The codec inspects bounded PNG/JPEG/WebP headers and the remaining aggregate pixel budget before calling the decoder, then verifies decoded dimensions. APNG, animated WebP, conflicting WebP canvas/payload dimensions and unsupported JPEG frame forms fail closed. Both source and sanitized PNG byte totals must fit the aggregate budget. Runtime loading uses this same boundary, resolves and sanitizes each distinct asset ID once, and displays textures only when the whole set succeeds. Failed loads retain the text fallback and release URLs.

Docs, demos, and source: https://github.com/happyvertical/animation

External voice conversations

A host supplying its own realtime audio can use chat.setInputHandler(text => { voice.sendText(text); return true; }) to consume composer and programmatic input. Return false, or remove the handler with setInputHandler(), to use the existing chat backend. A consumed input does not independently add history or invoke the backend: feed the transport's transcript events back through chat.receiveTurn({ id, role, content, final }).

receiveTurn upserts one visible, plain-text bubble and history entry per id. Partial updates replace its content; a final update emits the existing send/reply callback and event once. Late duplicates are ignored. It does not synthesize audio, evaluate inline navigation cues, or call a backend. clear() releases external ids; destroy() removes the input handler and makes late updates fail explicitly. The host owns transport cancellation and reconnect/history policy.

Use chat.happy.setSpeaking(true) while external audio plays and setSpeaking(false) when playback ends or is interrupted. This animates a continuous mouth using the host's GSAP instance, never invokes speech synthesis, and is an activity indicator rather than phoneme alignment. Expression changes preserve the talking mouth. Scripted say() timelines remain unchanged; stopping external playback does not stop a normal scripted sentence. Always stop external playback activity before starting a scripted tour.