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

@vkcha/svg-core

v0.1.2

Published

A lightweight SVG rendering core library in TypeScript for the web: scene graph, viewport culling, zoom/pan, event handling

Downloads

254

Readme

@vkcha/svg-core

Lightweight SVG scene rendering core for the web (TypeScript):

  • Pan / zoom on an <svg> (wheel + pointer drag)
  • Scene graph of many “nodes” backed by <g> elements
  • Viewport culling (removes offscreen nodes from DOM for performance)
  • Hit-testing + node events (click, “double click”, right click)
  • SVG fragment utilities (sanitize/measure/parse fragments)

Live demo: https://vkcha.com


Install

npm i @vkcha/svg-core

Quick start (Vanilla TS/JS)

Create an <svg> that fills its container:

<div id="root" style="height: 100vh">
  <svg id="canvas" style="width: 100%; height: 100%" xmlns="http://www.w3.org/2000/svg"></svg>
</div>

Then initialize the core and draw a few nodes:

import { SvgCore, Node } from "@vkcha/svg-core";

const svg = document.querySelector("#canvas") as SVGSVGElement;

const core = new SvgCore(svg, {
  panZoom: {
    wheelMode: "pan", // or "zoom"
    zoomRequiresCtrlKey: true, // macOS pinch usually sets ctrlKey=true
  },
  culling: { enabled: true, overscanPx: 30 },
});

core.setNodes([
  new Node({
    id: "hello",
    x: 0,
    y: 0,
    fragment: `
      <rect width="160" height="60" rx="10" fill="#111827"/>
      <text x="16" y="38" font-size="18" fill="white">Hello</text>
    `,
    onClick: (n) => console.log("clicked", n.id),
  }),
  new Node({
    id: "world",
    x: 220,
    y: 120,
    fragment: `<circle r="40" cx="40" cy="40" fill="#60a5fa"/>`,
    onRightClick: (n) => console.log("right click", n.id),
  }),
]);

// Optional: observe state changes (event-driven, no polling)
const unsub = core.onPanZoomChange((s) => console.log("pan/zoom", s));

// Cleanup
// unsub(); core.destroy();

Quick start (React)

import { useEffect, useRef } from "react";
import { SvgCore, Node } from "@vkcha/svg-core";

export function SvgScene() {
  const svgRef = useRef<SVGSVGElement | null>(null);
  const coreRef = useRef<SvgCore | null>(null);

  useEffect(() => {
    if (!svgRef.current) return;
    const core = new SvgCore(svgRef.current, {
      panZoom: { wheelMode: "pan", zoomRequiresCtrlKey: true },
      culling: { enabled: true, overscanPx: 30 },
    });
    coreRef.current = core;

    core.setNodes([
      new Node({
        id: "a",
        x: 0,
        y: 0,
        fragment: `<rect width="120" height="60" rx="10" fill="#10b981"/>`,
      }),
    ]);

    return () => {
      core.destroy();
      coreRef.current = null;
    };
  }, []);

  return <svg ref={svgRef} style={{ width: "100%", height: "100%" }} />;
}

Core concepts

SvgCore(svg, options?)

SvgCore owns:

  • an internal PanZoomCanvas (creates a world <g> and applies a matrix(...) transform)
  • a dedicated nodes layer (<g data-layer="nodes">) inside world
  • culling + hit-testing + interaction wiring on the root <svg>

Useful properties:

  • core.svg: the root SVGSVGElement
  • core.world: a <g> for “world space” content
  • core.state: { zoom, panX, panY }
  • core.panZoomOptions: merged pan/zoom options (min/max, wheel mode, etc.)

Pan/zoom can be configured on init via new SvgCore(svg, { panZoom: ... }) and any time later via core.configurePanZoom(...).

Defaults (what you get with new SvgCore(svg))

Pan/zoom state defaults

  • state.zoom = 1
  • state.panX = 0
  • state.panY = 0

Pan/zoom option defaults (PanZoomOptions)

  • wheelMode: "pan"
  • zoomRequiresCtrlKey: false
  • panRequiresSpaceKey: false
  • minZoom: 0.2
  • maxZoom: 8
  • zoomSpeed: 1
  • invertZoom: false
  • invertPan: false

Culling defaults

  • enabled: true
  • overscanPx: 30

Interaction defaults

  • double-click time window: 300ms
  • click suppression after drag threshold: 5px

SvgCore API (concise reference)

Props

  • core.svg: SVGSVGElement — the SVG root you passed in.
  • core.world: SVGGElement — the world layer (<g data-layer="world">) transformed by pan/zoom.
  • core.state: { zoom: number; panX: number; panY: number } — current pan/zoom state (panX/panY are screen px).
  • core.panZoomOptions: Readonly<PanZoomOptions> — current pan/zoom options (min/max zoom, wheel mode, etc.).

Scene

  • core.setNodes(nodes: Node[]) — replace the full scene. Also (re)builds internal id index + bounds.
  • core.redraw(ids?: string[]) — re-render:
    • no args: redraw all nodes
    • ids: redraw only those nodes; still re-applies culling for the full scene
  • core.remove(ids?: string[]) — remove nodes by id; if ids omitted/empty, clears the whole scene.

Pan/zoom

  • core.setState(partialState) — set { zoom?, panX?, panY? } directly.
  • core.setZoom(nextZoom, anchor?) — set zoom while keeping an anchor point stable in screen space.
  • core.zoomBy(factor, anchor?) — multiply current zoom by a factor.
  • core.resetView() — reset to { zoom: 1, panX: 0, panY: 0 }.
  • core.configurePanZoom(partialOptions) — update pan/zoom behavior at runtime.

Example: update pan/zoom after init:

core.configurePanZoom({ wheelMode: "zoom", zoomRequiresCtrlKey: false, minZoom: 0.5, maxZoom: 12 });

Culling

  • core.setCullingEnabled(enabled) — enable/disable culling.
  • core.setCullingOverscanPx(px) — set overscan margin in screen px. Higher values keep nodes “visible” a bit before/after they enter/leave the viewport (fewer pop-ins, more DOM).
  • core.onCullingStatsChange(fn) — subscribe to { visible, hidden, total } updates (event-driven).

Example: tune overscan:

core.setCullingEnabled(true);
core.setCullingOverscanPx(80);

Picking / coordinates

  • core.clientToCanvas(clientX, clientY) — convert screen px to world coords.
  • core.hitTestVisibleNodeAtClient(clientX, clientY) — returns topmost visible node at that point (or null).

Events

  • core.onPanZoomChange(fn) — subscribe to pan/zoom updates (event-driven).
  • Note: onPanZoomChange does not fire immediately on subscribe; read core.state for the current value.

Lifecycle

  • core.destroy() — removes event listeners / observers and clears internal subscriptions. Call on teardown.

Node

A Node is a lightweight wrapper around a lazily-created <g> element:

  • id (required, should be unique)
  • fragment (SVG markup without an outer <svg>)
  • x, y (world coordinates)
  • optional width, height (world units). If omitted, bounds are derived from the fragment’s measured bbox.
  • optional callbacks: onClick, onDoubleClick, onRightClick

Node API (concise reference)

new Node({
  id: "node-1", // required, non-empty string
  fragment: "<rect .../>", // SVG markup (no outer <svg>)
  x: 100,
  y: 40, // optional (defaults to 0,0)
  width: 240,
  height: 160, // optional; if omitted the core derives bounds from fragment metrics
  onClick: (n) => console.log("click", n.id),
  onDoubleClick: (n) => console.log("double", n.id),
  onRightClick: (n) => console.log("right", n.id),
});

Node defaults

  • x / y: default to 0
  • width / height: default to unset (null)
    • when unset, the core derives size from measureFragmentMetrics(fragment) (bbox + stroke padding)
    • if fragment is empty/invalid or measurement fails, the core falls back to 240×160
  • event callbacks: default to undefined

What if id is missing?

  • new Node({ ... }) will throw if id is not a non-empty string.

What if multiple nodes share the same id?

  • core.setNodes(nodes) will console.warn(...) about duplicates.
  • Internally, the core stores an id -> index map; the last node with that id wins for id-based operations like redraw(["id"]) / remove(["id"]) / hit-test lookup.
  • You should treat ids as unique keys.

SVG fragments: sanitization & sizing

This package includes fragment helpers:

  • sanitizeFragment(markup) removes unsafe content and normalizes markup
  • measureFragmentMetrics(markup) measures fragment bbox via getBBox() (requires DOM)
  • parseFragmentElements(markup) parses markup into SVG Element[]

Security note: fragments are sanitized:

  • removes <script> and <foreignObject>
  • strips on* event handler attributes

If you accept SVG from users, you should still apply your own security policy (CSP, allowlists, server-side validation, etc.).


Culling (performance)

Culling means: nodes outside the current viewport are treated as not visible and are removed from the DOM (only the visible subset is attached to the nodes layer). This can drastically improve performance for large scenes.

Use:

  • onCullingStatsChange(({ visible, hidden, total }) => ...)
  • culling.overscanPx / core.setCullingOverscanPx(px) to keep a margin outside the viewport before hiding nodes

Interaction model

The core wires SVG events on the root <svg> and maps them to nodes:

  • Click: delayed slightly to detect a second click
  • “Double click”: implemented by timing two clicks (does not use native dblclick). If a second click happens inside the time window, the pending single-click is cancelled and onDoubleClick fires.
  • Right click: uses contextmenu and calls preventDefault()

Hit-testing only considers the currently visible (unculled) nodes, and returns the topmost hit node based on render order.