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

xpict-web

v0.1.0

Published

Xpict Web is the browser-based version of Xpict: the generation of standardized images from declarative models using Canvas.

Readme

Xpict Web

Xpict Web is the browser version of Xpict, built on top of the native Canvas APIs (OffscreenCanvas / HTMLCanvasElement, createImageBitmap and FontFace) designed to create templates for generating standardized images with data—which can be static or dynamic. It features support for conditional rendering, runtime-generated position axes, and dynamic text.

📦 Installation

Installation is straightforward; just use your preferred package manager. Here is an example using NPM:

npm i xpict-web

🚀 Usage

Template

The foundation for creating images in Xpict is the Template. This is where you define your image layers and encapsulate all assembly logic. There are two ways to define an initial Template.

Using the config property (width, height, and fill):

import xpict from "xpict-web"

type RenderData = {} // Your data structure used in layer logic

const template = xpict.template<RenderData>({
    config: {
        width: 1280,
        height: 720,
        fill: "#020817" // optional
    },
    layers: [...]
})

Using an image as a background:

import xpict from "xpict-web"

type RenderData = {} // Your data structure used in layer logic

const template = xpict.template<RenderData>({
    image: "/assets/background.png" // URL, Blob, ArrayBuffer, ImageBitmap, HTMLImageElement or canvas,
    layers: [...]
})

Layers

Layers are another fundamental component of Xpict. They act as building blocks—sometimes functional, sometimes just wrappers—that organize code declaratively and provide instructions to the Canvas API on how to perform operations within our canvas context.

All axes (x, y) across all layer types support dynamic values:

x: ({ data, index /* index is useful if you are inside a `RepeatLayer` loop */ }) => index * 110,
y: ({ data, index /* index is useful if you are inside a `RepeatLayer` loop */ }) => index * 50,

Every layer also supports conditional rendering via the when property. If not defined, the layer renders by default.

when: ({ data, index }) => false // must return a boolean

TextLayer

To insert text into the canvas, use TextLayer:

import xpict from "xpict-web"

const myFont = xpict.utils.font({
    name: "curse-casual",
    source: "/fonts/font.ttf" // URL or binary data, loaded via FontFace,
    color: "#ffffff",
})

type RenderData = {} 

const template = xpict.template<RenderData>({
    config: {
        width: 1280,
        height: 720,
        fill: "#020817"
    },
    layers: [
        xpict.text({
            x: 640,
            y: 360,
            font: myFont({
                size: 60,
                color: "#ffffff" // optional, as it was previously defined in font config
            }),
            text: "Hello World",
            align: "center", // optional
            baseline: "middle", // optional
            stroke: {
                width: 4,
                fill: "#000000"
            } // optional
        })
    ]
})

Using dynamic text:

text: ({ data, index }) => data.myText

ImageLayer

To insert images into the canvas, use ImageLayer:

import xpict from "xpict-web"

const template = xpict.template<RenderData>({
    config: {
        width: 1280,
        height: 720,
        fill: "#020817"
    },
    layers: [
        xpict.image({
            src: "path/to/image.png",
            x: 0,
            y: 0,
        }),
    ]
})

Using a dynamic image:

src: ({ data, index }) => data.myImagePath

RectangleLayer

To insert rectangles into the canvas, use RectangleLayer:

xpict.rectangle({
    x: 0,
    y: 0,
    fill: "#ffffff",
    width: 150,
    height: 50,
    borderRadius: 5 // optional
})

CircleLayer

To insert circles into the canvas, use CircleLayer:

xpict.circle({
    x: 0,
    y: 0,
    fill: "#ffffff",
    radius: 50,
})

LineLayer

To insert lines into the canvas, use LineLayer:

xpict.line({
    from: { x: 0, y: 0 },
    to: { x: 0, y: 50 },
    color: "#ffffff",
    width: 1
})

GroupLayer

If you want to group layers that share a common context, you can use GroupLayer to keep your code organized and apply an offset if desired:

xpict.group({
    x: 50,
    y: 50,
    layers: [...]
})

RepeatLayer

If you need to handle arrays to generate layers, use RepeatLayer to organize your code and loop logic:

xpict.repeat({
    each: ({ data }) => data.images,
    x: ({ index }) => index * 50,
    y: ({ index }) => index * 50,
    layer: (image, { index, length }) => xpict.image({
        x: 0,
        y: 0,
        src: image
    })
})

Image Transformations and Effects

If you want to manipulate an image or apply effects, you can do so using the transform property of ImageLayer, or via flipX, flipY, and rotate.

Using transform:

xpict.image({
    x: 0,
    y: 0,
    src: "...",
    transform: [
        xpict.effects.blur(2.5),
        ({ canvas, ctx, data, index }) => {
            // manipulate the `ctx` directly here to apply custom logic
        }
    ]
})

🧪 Tests

Tests are written with Vitest and live in __tests__. The library maintains 100% coverage (statements, branches, functions and lines), enforced by a threshold in vitest.config.mts.

npm run test            # run the test suite once
npm run test:watch      # watch mode
npm run test:coverage   # run with coverage report (also written to ./coverage)

🤝 Contributing

Want to contribute? Follow these steps:

  1. Fork the repository.
  2. Create a new branch (git checkout -b feature-new).
  3. Commit your changes (git commit -m 'Add new feature').
  4. Push to the branch (git push origin feature-new).
  5. Open a Pull Request.

📝 License

This project is licensed under the MIT License.


🌐 Differences from the Node version

  • No @napi-rs/canvas: everything uses browser APIs (also works inside Web Workers via OffscreenCanvas).
  • Template.render() returns an HTMLCanvasElement (or OffscreenCanvas in workers). Use template.renderToBlob(data, "image/png") to get a Blob.
  • imagePath became image and filePath (fonts) became source; image sources can be URLs, Blobs, binary data, ImageBitmaps, images or canvases. URLs are fetched, so cross-origin images need CORS headers.
  • Fonts are loaded with FontFace; render() automatically waits for them (xpict.utils.fontsReady() is also exposed).
  • The blur and grayscale effects rely on ctx.filter, which Safari does not support; use boxBlur there.