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

@fieldnotes/core

v0.8.11

Published

Vanilla TypeScript infinite canvas engine

Readme

@fieldnotes/core

A lightweight, framework-agnostic infinite canvas SDK for the web — with first-class support for embedding interactive HTML elements.

Features

  • Infinite canvas — pan, zoom, pinch-to-zoom
  • Freehand drawing — pencil tool with stroke smoothing and pressure-sensitive width
  • Sticky notes — editable text notes with customizable colors
  • Arrows — curved bezier arrows with element binding
  • Shapes — rectangles, ellipses with fill and stroke
  • Text — standalone text elements with font size and alignment
  • Images — drag & drop or programmatic placement (canvas-rendered for proper layer ordering)
  • HTML embedding — add any DOM element as a fully interactive canvas citizen
  • Layers — named layers with visibility, locking, and absolute ordering
  • Select & multi-select — click, drag box, move, resize (layer-aware)
  • Undo / redo — full history stack with configurable depth
  • State serialization — export/import JSON snapshots with automatic migration
  • Touch & tablet — Pointer Events API, pinch-to-zoom, two-finger pan, stylus pressure
  • Zero dependencies — vanilla TypeScript, no framework required
  • Tree-shakeable — ESM + CJS output

Install

npm install @fieldnotes/core

Quick Start

import {
  Viewport,
  HandTool,
  SelectTool,
  PencilTool,
  EraserTool,
  ArrowTool,
  NoteTool,
} from '@fieldnotes/core';

// Mount on any container element
const viewport = new Viewport(document.getElementById('canvas'), {
  background: { pattern: 'dots', spacing: 24 },
});

// Register tools
viewport.toolManager.register(new HandTool());
viewport.toolManager.register(new SelectTool());
viewport.toolManager.register(new PencilTool({ color: '#1a1a1a', width: 2 }));
viewport.toolManager.register(new EraserTool());
viewport.toolManager.register(new ArrowTool({ color: '#1a1a1a', width: 2 }));
viewport.toolManager.register(new NoteTool());

// Activate a tool
viewport.toolManager.setTool('select', viewport.toolContext);

// Clean up when done
viewport.destroy();

Your container element needs a defined size (width/height). The canvas fills its container.

Embedding HTML Elements

The main differentiator — embed any DOM node as a fully interactive canvas element:

const card = document.createElement('div');
card.innerHTML = '<h3>My Card</h3><button>Click me</button>';

// Buttons, inputs, links — everything works
card.querySelector('button').addEventListener('click', () => {
  console.log('Clicked inside the canvas!');
});

const elementId = viewport.addHtmlElement(card, { x: 100, y: 200 }, { w: 250, h: 150 });

HTML elements pan, zoom, and resize with the canvas. They use a two-mode interaction model:

  • Default — the element can be selected, dragged, and resized like any other element
  • Double-click — enters interact mode, making buttons, inputs, and links work
  • Escape or click outside — exits interact mode

You can also exit interact mode programmatically:

viewport.stopInteracting();

Adding Images

// Programmatic
viewport.addImage('https://example.com/photo.jpg', { x: 0, y: 0 });
viewport.addImage('/assets/map.png', { x: 0, y: 0 }, { w: 800, h: 600 });

// Drag & drop is handled automatically — drop images onto the canvas

Important: Use URLs, not base64 data URLs. Images are stored inline in the serialized state. A single base64-encoded photo can be 2-5MB, which will blow past the localStorage ~5MB quota and make JSON exports impractical. Upload images to your server or CDN and use the URL. For offline/local-first apps, store blobs in IndexedDB and reference them by URL.

Camera Control

const { camera } = viewport;

camera.pan(100, 50); // pan by offset
camera.moveTo(0, 0); // jump to position
camera.setZoom(2); // set zoom level
camera.zoomAt(1.5, { x: 400, y: 300 }); // zoom toward screen point

const world = camera.screenToWorld({ x: e.clientX, y: e.clientY });
const screen = camera.worldToScreen({ x: 0, y: 0 });

camera.onChange(() => {
  /* camera moved */
});

Element Store

Direct access to canvas elements:

const { store } = viewport;

const all = store.getAll(); // sorted by zIndex
const el = store.getById('some-id');
const strokes = store.getElementsByType('stroke');

store.update('some-id', { locked: true });
store.remove('some-id');

store.on('add', (el) => console.log('added', el));
store.on('remove', (el) => console.log('removed', el));
store.on('update', ({ previous, current }) => {
  /* ... */
});

Undo / Redo

viewport.undo();
viewport.redo();

viewport.history.canUndo; // boolean
viewport.history.canRedo; // boolean
viewport.history.onChange(() => {
  /* update UI */
});

Layers

Organize elements into named layers with visibility, lock, and ordering controls. All elements on a higher layer render above all elements on a lower layer, regardless of individual z-index.

const { layerManager } = viewport;

// Create layers
const background = layerManager.activeLayer; // "Layer 1" exists by default
layerManager.renameLayer(background.id, 'Map');
const tokens = layerManager.createLayer('Tokens');
const notes = layerManager.createLayer('Notes');

// Set active layer — new elements are created on the active layer
layerManager.setActiveLayer(tokens.id);

// Visibility and locking
layerManager.setLayerVisible(background.id, false); // hide
layerManager.setLayerLocked(background.id, true); // prevent selection/editing

// Move elements between layers
layerManager.moveElementToLayer(elementId, notes.id);

// Reorder layers
layerManager.reorderLayer(tokens.id, 5); // higher order = renders on top

// Query
layerManager.getLayers(); // sorted by order
layerManager.isLayerVisible(id);
layerManager.isLayerLocked(id);

// Listen for changes
layerManager.on('change', () => {
  /* update UI */
});

Locked layers prevent selection, erasing, and arrow binding on their elements. Hidden layers are invisible and non-interactive. The active layer cannot be hidden or locked — if you try, it automatically switches to the next available layer.

State Serialization

// Save
const json = viewport.exportJSON();
localStorage.setItem('canvas', json);

// Load
viewport.loadJSON(localStorage.getItem('canvas'));

Note: Serialized state includes all layers and element layerId assignments. States saved before layers were introduced are automatically migrated — elements are placed on a default "Layer 1".

Tool Switching

viewport.toolManager.setTool('pencil', viewport.toolContext);
viewport.toolManager.setTool('hand', viewport.toolContext);

viewport.toolManager.onChange((toolName) => {
  console.log('switched to', toolName);
});

Changing Tool Options at Runtime

All drawing tools support setOptions() for changing color, width, and other settings without re-creating the tool:

// Get a tool by name (type-safe with generics)
const pencil = viewport.toolManager.getTool<PencilTool>('pencil');
const arrow = viewport.toolManager.getTool<ArrowTool>('arrow');
const note = viewport.toolManager.getTool<NoteTool>('note');

// Change colors
pencil?.setOptions({ color: '#ff0000' });
arrow?.setOptions({ color: '#ff0000' });
note?.setOptions({ backgroundColor: '#e8f5e9' });

// Change stroke width
pencil?.setOptions({ width: 5 });
arrow?.setOptions({ width: 3 });

Stroke Smoothing

The pencil tool automatically smooths freehand strokes using Ramer-Douglas-Peucker point simplification and Catmull-Rom curve fitting. You can control the smoothing tolerance:

new PencilTool({
  smoothing: 1.5, // default — higher = smoother, lower = more detail
});

// Or at runtime
pencil?.setOptions({ smoothing: 3 });

Pressure-Sensitive Width

When using a stylus (Apple Pencil, Surface Pen), stroke width varies based on pressure automatically. The width option sets the maximum width at full pressure. Mouse input uses a default pressure of 0.5 for consistent-width strokes.

Stroke points include pressure data in the StrokePoint type:

interface StrokePoint {
  x: number;
  y: number;
  pressure: number; // 0-1
}

Custom Tools

Implement the Tool interface to create your own tools:

import type { Tool, ToolContext, PointerState } from '@fieldnotes/core';

const myTool: Tool = {
  name: 'my-tool',

  onPointerDown(state: PointerState, ctx: ToolContext) {
    const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
    // state.pressure is available for stylus input (0-1)
  },

  onPointerMove(state: PointerState, ctx: ToolContext) {
    // called during drag
  },

  onPointerUp(state: PointerState, ctx: ToolContext) {
    // finalize action
    ctx.store.add(myElement);
    ctx.requestRender();
  },

  // Optional
  onActivate(ctx) {
    ctx.setCursor?.('crosshair');
  },
  onDeactivate(ctx) {
    ctx.setCursor?.('default');
  },
  renderOverlay(canvasCtx) {
    /* draw preview on canvas */
  },
};

viewport.toolManager.register(myTool);
viewport.toolManager.setTool('my-tool', viewport.toolContext);

Configuration

Viewport Options

new Viewport(container, {
  camera: {
    minZoom: 0.1, // default: 0.1
    maxZoom: 10, // default: 10
  },
  background: {
    pattern: 'dots', // 'dots' | 'grid' | 'none' (default: 'dots')
    spacing: 24, // grid spacing in px (default: 24)
    color: '#d0d0d0', // dot/line color (default: '#d0d0d0')
  },
});

Tool Options

new PencilTool({ color: '#ff0000', width: 3, smoothing: 1.5 });
new EraserTool({ radius: 30 });
new ArrowTool({ color: '#333', width: 2 });
new NoteTool({ backgroundColor: '#fff9c4', size: { w: 200, h: 150 } });
new ImageTool({ size: { w: 400, h: 300 } });

Element Types

All elements share a base shape:

interface BaseElement {
  id: string;
  type: string;
  position: { x: number; y: number };
  zIndex: number;
  locked: boolean;
  layerId: string;
}

| Type | Key Fields | | -------- | ---------------------------------------------------------------------- | | stroke | points: StrokePoint[], color, width, opacity | | note | size, text, backgroundColor, textColor | | arrow | from, to, bend, color, width, fromBinding, toBinding | | image | size, src | | shape | size, shape (rectangle | ellipse), strokeColor, fillColor | | text | size, text, fontSize, color, textAlign | | html | size |

Built-in Interactions

| Input | Action | | -------------------- | ------------------- | | Scroll wheel | Zoom | | Middle-click drag | Pan | | Space + drag | Pan | | Two-finger pinch | Zoom | | Two-finger drag | Pan | | Delete / Backspace | Remove selected | | Ctrl+Z / Cmd+Z | Undo | | Ctrl+Shift+Z / Cmd+Y | Redo | | Double-click note | Edit text | | Double-click HTML | Enter interact mode | | Escape | Exit interact mode |

Browser Support

Works in all modern browsers supporting Pointer Events API and HTML5 Canvas.

License

MIT