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

@penkit/engine

v1.1.0

Published

PenKit runtime: the headless editor that owns active tool, input routing, item store, selection, and live preview. The thing a host mounts.

Readme

@penkit/engine

Headless PenKit runtime이다. active tool, pointer routing, item state, selection, history, document import/export, stroke grouping, live preview adapter call을 관리한다.

npm install @penkit/engine @penkit/core

대부분의 브라우저 앱은 @penkit/sdk로 시작하면 된다. 이미 input pipeline이 있거나, non-DOM runtime/test harness가 있거나, host app이 정규화된 RawPoint sample을 직접 feed하려면 @penkit/engine을 직접 사용한다.

LLM Agent Quick Start

  1. @penkit/corePenHostAdapter를 구현하거나 import한다.
  2. new PenEngine({ adapter })를 만든다.
  3. pointerDown, pointerMove, pointerUp으로 RawPoint sample을 넣거나 engine.pointerHandlers@penkit/dom-input에 넘긴다.
  4. host UI update가 필요하면 engine event를 subscribe한다.
  5. DOM mounting을 한 번에 끝내고 싶다면 @penkit/sdk를 사용한다.
import { PenEngine } from "@penkit/engine";
import type { PenHostAdapter, PenItemProps, RawPoint } from "@penkit/core";

const adapter: PenHostAdapter = {
  name: "host",
  createPenItem(payload) {
    return payload;
  },
  render(item: PenItemProps) {
    hostRenderer.render(item);
  },
  remove(itemId: string) {
    hostRenderer.remove(itemId);
  },
};

const engine = new PenEngine({ adapter });
engine.setTool("pen");

주요 모듈

PenEngine

PenEngine은 host가 mount하는 runtime이다. 여전히 headless이며 DOM, SVG, React, host editor import가 없다.

주요 설정:

const engine = new PenEngine({
  adapter,
  tool: "pen",
  style: {
    color: "#2563eb",
    baseWidth: 5,
    opacity: 1,
    cap: "round",
    join: "round",
  },
  strokeStabilization: { smoothing: 0.25, simplifyTolerance: 0.8 },
  barrelButtonTool: "eraser",
  eraserButtonTool: "eraser",
  transformPoint(point) {
    return {
      ...point,
      x: (point.x - viewportLeft) / zoom + pan.x,
      y: (point.y - viewportTop) / zoom + pan.y,
    };
  },
});

주요 method/property:

  • setTool(kind), tool, availableTools
  • setStyle(patch), style, setStrokeStabilization(options)
  • pointerDown(point), pointerMove(points), pointerUp(point)
  • pointerHandlers
  • items, selection, selectionBounds()
  • selectAll(), clearSelection(), deleteSelection(), duplicateSelection(), transformSelection(), updateSelectionStyle(), updateSelectionBrush()
  • undo(), redo(), canUndo, canRedo
  • exportDocument(), importDocument(snapshot)
  • strokeGrouping, hasPendingStrokeGroup, flushStrokeGroup()

Built-In Tools

createDefaultTools()는 기본 runtime tool을 등록한다.

  • pen
  • highlighter
  • eraser
  • lasso
  • pointer
  • select

createNoteToolset()은 note-app 스타일 tool을 추가한다.

  • fountain, ballpoint, marker, pencil
  • precision-eraser
  • line, rectangle, ellipse, triangle, arrow
  • move
  • recognizer가 주입되면 smart shape tools

engine은 @penkit/ai를 import하지 않는다. host가 recognizer를 주입한다.

Custom Tools

새 기능은 ToolStrategy를 구현하고 ToolContext를 통해서만 상태를 바꾼다.

import type { ToolContext, ToolStrategy } from "@penkit/engine";
import type { RawPoint } from "@penkit/core";

class EyedropperTool implements ToolStrategy {
  readonly kind = "eyedropper";

  onStart(point: RawPoint, ctx: ToolContext): void {
    const hit = ctx.hitTest(point);
    if (hit) {
      ctx.setStyle({
        color: hit.style.color,
        baseWidth: hit.style.baseWidth,
        opacity: hit.style.opacity,
      });
    }
  }

  onMove(): void {}
  onEnd(): void {}
}

engine.registerTool(new EyedropperTool());
engine.setTool("eyedropper");

tool은 commitStroke, removeItem, setSelection, moveSelection, haptic, setPreview, beginHistory 같은 ToolContext method만 사용한다.

class SnapTool implements ToolStrategy {
  readonly kind = "snap";

  onStart(_point: RawPoint, ctx: ToolContext): void {
    ctx.haptic({ kind: "impact", style: "medium", source: "snap" });
  }

  onMove(): void {}
  onEnd(): void {}
}

Shape Assist and Scribble Erase

smart shape snapping은 opt-in이다. @penkit/airecognizeShape 같은 ShapeRecognizer를 넘긴다.

import { recognizeShape } from "@penkit/ai";
import { SmartShapeTool } from "@penkit/engine";

engine.registerTool(new SmartShapeTool(recognizeShape));
engine.setTool("smart-shape");

ScribbleEraseInkTool은 scribble-to-erase의 guarded PoC다. false positive를 처리할 host UX 없이 일반 ink를 대체하지 않는다.

SmartShapeTool과 hold-to-shape가 켜진 HoldShapeInkTool은 인식된 freehand stroke를 clean shape로 snap할 때 ctx.haptic({ kind: "impact", style: "medium", source: "smart-shape" })를 호출한다. haptic bridge가 없는 host에서는 안전하게 no-op이다.

Stroke Grouping

기본은 pen-up 하나가 item 하나를 만든다. note app에서 빠른 여러 stroke를 하나의 PenItemProps로 묶고 싶다면 grouping을 켠다.

const engine = new PenEngine({
  adapter,
  strokeGrouping: { commitDebounceMs: 600 },
});

engine.flushStrokeGroup();

commitStroke(..., { group: true })를 호출하는 tool만 grouping session에 들어간다.

Selection, Editing, History

engine은 selected item id를 저장하고 selectionchange를 emit한다. handle과 overlay는 host UI 책임이다.

engine.on("selectionchange", (ids) => drawSelectionOverlay(ids));
engine.selectAll();
engine.updateSelectionStyle({ color: "#ef4444", baseWidth: 8 });
engine.updateSelectionBrush(calibratedBrushPreset);
engine.transformSelection({ scale: 1.1, rotateDeg: 15 });
engine.undo();
engine.redo();

updateSelectionStyle({ baseWidth })는 저장된 raw point로 pressure outline을 다시 만든다. 선택된 item에 built-in 또는 보정된 BrushPreset의 pressure curve까지 적용해야 할 때는 updateSelectionBrush(brush)를 사용한다.

새 stroke에는 engine.setBrush(calibratedBrushPreset)로 넘긴 active BrushPreset outline이 적용된다. built-in ink tool이 같은 brush kind를 commit하더라도 active preset의 pressureCurve 같은 보정값을 우선한다.

Document Save and Load

exportDocument()@penkit/core의 versioned snapshot을 반환한다. importDocument(snapshot)는 검증 후 engine document를 교체한다.

const snapshot = engine.exportDocument({
  metadata: { source: "host-doc-123" },
});

localStorage.setItem("ink", JSON.stringify(snapshot));
engine.importDocument(JSON.parse(localStorage.getItem("ink") ?? "{}"));

Events

engine.on(event, listener)로 subscribe한다. 반환 함수는 unsubscribe다.

const offCommit = engine.on("commit", (item) => {
  analytics.track("pen_commit", { id: item.id, strokes: item.strokes.length });
});

offCommit();

하지 말 것

  • browser PointerEvent 객체를 직접 넘기지 않는다. RawPoint로 정규화한다.
  • custom ToolStrategy 안에 DOM/renderer 코드를 넣지 않는다.
  • built-in hit test를 full vector/rotated-shape hit test라고 가정하지 않는다. 기본은 item bounds 기반이다.
  • engine이 OCR, handwriting recognition, production LLM을 수행한다고 말하지 않는다.