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

@clinth/vcanvas

v0.0.3

Published

A virtualized canvas web component for efficiently rendering large scrollable content using a tiled approach with automatic viewport culling and tile recycling.

Readme

x-vcanvas

A virtualized canvas web component for efficiently rendering large scrollable content using a tiled approach with automatic viewport culling and tile recycling.

Overview

The virtual canvas divides content into tiles that are only rendered when visible in the viewport. As the user scrolls, tiles are repositioned and recycled rather than destroyed and recreated, providing smooth performance even with very large content areas.

Virtualization can be applied to either axis independently, or both simultaneously:

  • Horizontal only — set logicalWidth (original behavior)
  • Vertical only — set logicalHeight
  • 2D (both axes) — set both logicalWidth and logicalHeight

When an axis is left at 0 (default), the component uses the actual viewport dimension for that axis, meaning no virtualization or scrolling occurs on that axis.

Installation

npm install @clinth/vcanvas

Usage

<x-vcanvas logicalWidth="10000"></x-vcanvas>
import '@clinth/vcanvas';

const vcanvas = document.querySelector('x-vcanvas');
vcanvas.logicalWidth = 10000;
vcanvas.drawCallback = (ctx, region, signal) => {
  ctx.fillStyle = '#3498db';
  ctx.fillRect(region.x, region.y, region.width, region.height);
};

2D virtualization

vcanvas.logicalWidth = 10000;
vcanvas.logicalHeight = 5000;
vcanvas.drawCallback = (ctx, region, signal) => {
  // region.x, region.y, region.width, region.height are all in logical coordinates
  ctx.fillStyle = '#3498db';
  ctx.fillRect(region.x, region.y, region.width, region.height);
};

Vertical-only virtualization

// logicalWidth defaults to 0, so the component uses the actual viewport width
vcanvas.logicalHeight = 5000;
vcanvas.drawCallback = (ctx, region, signal) => {
  ctx.fillRect(region.x, region.y, region.width, region.height);
};

API Reference

Properties

logicalWidth: number

The total logical width of the virtual canvas content. When set to 0 (default), the component uses the actual viewport width — no horizontal virtualization or scrollbar.

vcanvas.logicalWidth = 10000;

logicalHeight: number

The total logical height of the virtual canvas content. When set to 0 (default), the component uses the actual viewport height — no vertical virtualization or scrollbar.

vcanvas.logicalHeight = 5000;

logicalScrollLeft: number

The current horizontal logical scroll position (getter/setter). Values are automatically clamped to [0, logicalWidth - viewportWidth].

vcanvas.logicalScrollLeft = 5000;
console.log(vcanvas.logicalScrollLeft);

logicalScrollTop: number

The current vertical logical scroll position (getter/setter). Values are automatically clamped to [0, logicalHeight - viewportHeight].

vcanvas.logicalScrollTop = 2000;
console.log(vcanvas.logicalScrollTop);

containerScrollLeft: number (readonly)

The native scroll container's horizontal scroll position in pixels.

containerScrollTop: number (readonly)

The native scroll container's vertical scroll position in pixels.

viewportWidth: number (readonly)

The current viewport width in pixels. Automatically updated when the component is resized.

viewportHeight: number (readonly)

The current viewport height in pixels. Automatically updated when the component is resized.

drawCallback: DrawCallback | null

Callback invoked when a tile needs to be drawn. The region is always in logical coordinates, covering the full extent of the tile on both axes.

vcanvas.drawCallback = (ctx, region, signal) => {
  ctx.fillStyle = 'blue';
  ctx.fillRect(region.x, region.y, region.width, region.height);
};

hinting: boolean

Enables horizontal scroll-edge fade animations. Default: false.

debugTrace: boolean

Enables detailed console logging of tile operations. Default: false.

debugDraw: boolean

Enables visual debug overlay on tiles. Default: false.

Methods

setRenderingOptions(options: Partial<VCanvasRenderingOptions>): void

Updates rendering options. Any options not provided retain their current values.

vcanvas.setRenderingOptions({
  tileWidthMultiplier: 0.25,
  tileHeightMultiplier: 0.25,
  maxPoolSize: 12,
});

getRenderingOptions(): VCanvasRenderingOptions

Returns the current rendering options configuration.

invalidateAll(): void

Forces a complete redraw of all visible tiles.

theme.addEventListener('change', () => {
  vcanvas.invalidateAll();
});

invalidateRegion(x: number, y: number, width: number, height: number): void

Forces redraw of tiles intersecting the specified 2D region. More efficient than invalidateAll() for localized updates.

vcanvas.invalidateRegion(1000, 500, 200, 200);

isRegionVisible(rect: Rect): boolean

Checks if any part of a rectangular region is currently visible in the viewport. Checks both axes.

const region = { x: 1000, y: 500, width: 200, height: 200 };
if (vcanvas.isRegionVisible(region)) {
  // Region is at least partially visible
}

convertFromViewportToCanvas(x: number, y: number): Point

Converts viewport-relative coordinates to logical coordinates. Accounts for scroll position and single-tile centering on both axes.

vcanvas.addEventListener('click', (e) => {
  const logical = vcanvas.convertFromViewportToCanvas(e.offsetX, e.offsetY);
  console.log('Clicked at logical:', logical.x, logical.y);
});

convertFromCanvasToViewport(x: number, y: number): Point

Converts logical coordinates to viewport-relative coordinates.

const viewportPos = vcanvas.convertFromCanvasToViewport(markerX, markerY);
tooltip.style.left = `${viewportPos.x}px`;
tooltip.style.top  = `${viewportPos.y}px`;

positionChildRange(query: string, startX: number, endX: number, startY?: number, endY?: number): void

Positions light DOM children matching the selector at the specified logical coordinate range. The children scroll with the canvas content. startY/endY are optional and default to 0 / effectiveLogicalHeight.

// Horizontal positioning only (full height)
vcanvas.positionChildRange('#marker', 200, 400);

// Full 2D positioning
vcanvas.positionChildRange('#overlay', 1000, 1200, 500, 700);

positionChildClear(query: string): void

Removes children from the positioning system and clears their inline styles.

manualChildPositioningUpdate(): void

Forces an immediate re-layout of all positioned children.

Events

scroll-virtual

Dispatched whenever the scroll position changes on either axis.

vcanvas.addEventListener('scroll-virtual', (event) => {
  const { logicalScrollLeft, logicalScrollTop } = event.detail;
  console.log(`Scroll: ${logicalScrollLeft}, ${logicalScrollTop}`);
});

Event detail properties:

| Property | Description | |---|---| | logicalScrollLeft | Current horizontal scroll position | | logicalScrollTop | Current vertical scroll position | | containerScrollLeft | Native container horizontal scroll | | containerScrollTop | Native container vertical scroll | | viewportStartLogical | Logical x of the viewport's left edge | | viewportEndLogical | Logical x of the viewport's right edge | | viewportStartLogicalY | Logical y of the viewport's top edge | | viewportEndLogicalY | Logical y of the viewport's bottom edge | | userGesture | Whether the scroll was user-initiated |

scrollend-virtual

Dispatched when scrolling has ended (debounced).

Event detail properties:

| Property | Description | |---|---| | logicalScrollLeft | Final horizontal scroll position | | logicalScrollTop | Final vertical scroll position | | containerScrollLeft | Native container horizontal scroll | | containerScrollTop | Native container vertical scroll | | userGesture | Whether the scroll was user-initiated |

Types

VCanvasRenderingOptions

interface VCanvasRenderingOptions {
  /** Tile width as multiplier of viewport width (default: 0.5) */
  tileWidthMultiplier: number;

  /** Tile height as multiplier of viewport height (default: 0.5) */
  tileHeightMultiplier: number;

  /** Extra tiles to keep in pool beyond visible (default: 2) */
  tilesBuffer: number;

  /** Overlap between adjacent tiles as a fraction (default: 0.01) */
  overlapPercent: number;

  /** Maximum tiles in pool (default: 3; multiplied by row count in 2D mode) */
  maxPoolSize: number;

  /** Debounce delay for scroll operations in ms (default: 100) */
  scrollDebounceMs: number;
}

Rect

type Rect = {
  x: number;      // X-coordinate in logical units
  y: number;      // Y-coordinate in logical units
  width: number;  // Width in logical units
  height: number; // Height in logical units
};

DrawCallback

type DrawCallback = (
  ctx: CanvasRenderingContext2D,
  region: Rect,        // Logical extent of this tile (may include overlap)
  abortSignal: AbortSignal
) => void;

Draw Callback

The canvas context is pre-translated so that all drawing coordinates are in logical space. Both region.x/region.y and region.width/region.height reflect the 2D extent of the tile being drawn.

vcanvas.drawCallback = (ctx, region, signal) => {
  // Draw using logical coordinates — works identically for 1D or 2D tiles
  ctx.fillStyle = 'blue';
  ctx.fillRect(region.x, region.y, region.width, region.height);

  if (signal.aborted) return; // tile scrolled out of view

  // Draw content clipped to this tile's region
  for (const item of myItems) {
    if (item.x + item.width < region.x || item.x > region.x + region.width) continue;
    if (item.y + item.height < region.y || item.y > region.y + region.height) continue;
    // ... draw item
  }
};

Coordinate System

  • Logical coordinates(0, 0) is the top-left corner of the full virtual content area. All drawing and invalidateRegion/isRegionVisible calls use this system.
  • Viewport coordinates(0, 0) is the top-left of the currently visible area. Used for pointer events (e.offsetX, e.offsetY).

Use convertFromViewportToCanvas / convertFromCanvasToViewport to move between the two.

Tile Pool and Memory

In 2D mode the pool size is effectively maxPoolSize × number_of_rows, because each column of tiles is replicated across all visible rows. Tune maxPoolSize, tileWidthMultiplier, and tileHeightMultiplier together to control memory usage.

// 2D example: smaller tiles, larger pool for smooth diagonal scrolling
vcanvas.setRenderingOptions({
  tileWidthMultiplier: 0.25,
  tileHeightMultiplier: 0.25,
  maxPoolSize: 6,
});

Slotted Children

<x-vcanvas logicalWidth="10000" logicalHeight="5000">
  <div id="overlay">Content</div>
</x-vcanvas>
// Position at logical rect x:200–400, y:100–300
vcanvas.positionChildRange('#overlay', 200, 400, 100, 300);

Children maintain their logical position as the canvas is scrolled in either direction.

CSS Custom Properties

| Property | Default | Description | |----------|---------|-------------| | --scrollbar-width | auto | Scrollbar thickness (auto, thin, none) | | --scrollbar-color | auto | Scrollbar colors (<thumb-color> <track-color>) |

x-vcanvas {
  --scrollbar-width: thin;
  --scrollbar-color: #6b7280 #e5e7eb;
}

Utility Functions

import {
  viewportToCanvas,
  canvasToViewport,
  logicalToScreen,
  screenToLogical,
  logicalToScreenRect,
  screenToLogicalRect,
  clamp,
} from '@clinth/vcanvas';

Performance Tips

  1. Tune tileWidthMultiplier / tileHeightMultiplier — smaller values create more, smaller tiles which improves scroll smoothness but increases tile count and draw-callback frequency.

  2. Tune maxPoolSize — increase for smoother fast-scrolling at the cost of memory. In 2D mode the effective pool is maxPoolSize × rows.

  3. Use invalidateRegion() — for partial updates, only redraw the affected area instead of calling invalidateAll().

  4. Check visibility — use isRegionVisible() to skip work for off-screen content within a draw callback.

  5. Abort expensive operations — use the AbortSignal to cancel long draws when the tile scrolls out of view.

License

MIT