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/core

v1.1.0

Published

Headless core of PenKit: stroke/session model, bounds, geometry, host adapter contract.

Readme

@penkit/core

PenKit의 headless domain model과 geometry utility 패키지다.

DOM input, rendering, host editor dependency 없이 안정적인 contract만 필요할 때 사용한다. 대부분의 브라우저 앱은 @penkit/sdk에서 시작하면 된다. adapter, custom engine pipeline, persistence layer, replay UI, geometry feature를 만들 때는 @penkit/core를 직접 사용한다.

npm install @penkit/core

LLM Agent Quick Start

새 host project에 이 패키지를 설치했다면 다음 순서를 지킨다.

  1. @penkit/core를 pure하게 유지한다. DOM, React, canvas, SVG, host editor API를 core 성격의 코드에 넣지 않는다.
  2. committed ink는 PenItemProps로 모델링한다. item은 strokes, style, aggregate bounds, generic transform을 가진다.
  3. host boundary는 PenHostAdapter로 둔다. adapter는 중립 PenCommitPayload/PenItemProps를 받아 host renderer나 command system으로 변환한다.
  4. browser input이 필요하면 @penkit/dom-input 또는 @penkit/sdk를 설치한다.
  5. ready runtime이 필요하면 @penkit/engine 또는 @penkit/sdk를 설치한다.

주요 모듈

Domain Types

host data shape이나 adapter signature를 정의할 때 import한다.

import type {
  Bounds,
  InkStyle,
  PenCommitPayload,
  PenHostAdapter,
  PenItemProps,
  PenStroke,
  RawPoint,
  Transform,
} from "@penkit/core";

중요 contract:

  • RawPoint: host canvas 좌표계로 정규화된 pointer sample.
  • PenStroke: raw sample, centerline path, pressure outline path, closed flag, bounds를 가진 stroke.
  • PenItemProps: 하나 이상의 stroke를 담는 persisted host item.
  • PenCommitPayload: adapter가 host item id/transform을 부여하기 전의 중립 commit payload.
  • PenHostAdapter: renderer/editor integration seam.

Pointer Buttons

stylus side button이나 eraser-end metadata를 tool override로 매핑할 때 사용한다.

import { PenButton, hasButton } from "@penkit/core";

if (hasButton(point, PenButton.BARREL)) {
  // 이 stroke 동안 eraser/lasso/pointer 같은 도구로 전환한다.
}

Geometry and Stroke Paths

renderer, hit test, precomputed path pipeline에서 사용한다.

import {
  buildCenterlinePath,
  buildPressureOutline,
  computeBounds,
  mergeBounds,
  stabilizeStrokePoints,
} from "@penkit/core";

const pathPoints = stabilizeStrokePoints(rawPoints, {
  smoothing: 0.25,
  simplifyTolerance: 0.8,
});

const stroke = {
  id: "stroke-1",
  rawPoints,
  centerPathD: buildCenterlinePath(pathPoints),
  outlinePathD: buildPressureOutline(pathPoints, { baseWidth: 6 }),
  closed: false,
  bounds: computeBounds(pathPoints),
};

Lasso Geometry

lassoSelect는 polygon으로 candidate item을 선택한다. candidate bounds는 빠른 필터로 유지하고, candidate가 stroke paths를 함께 넘기면 최종 hit-test는 넓은 빈 bounds가 아니라 실제 path와 선택 보정값 hitTolerance를 기준으로 판정한다. 일반 lasso는 mode: "intersect", item geometry가 polygon 안에 완전히 들어와야 할 때는 mode: "contain"을 사용한다.

import { lassoSelect } from "@penkit/core";

const selected = lassoSelect(lassoPolygon, items, {
  mode: "intersect",
});

StrokeSession

StrokeSession은 연속 stroke를 하나의 중립 commit payload로 묶는다. core는 시간을 읽지 않고 호출자가 시간을 넘긴다.

import { StrokeSession } from "@penkit/core";

const session = new StrokeSession({ commitDebounceMs: 600 }, style);
session.addStroke(strokeA, 1000);
session.addStroke(strokeB, 1400);

if (session.shouldCommit(2100)) {
  const payload = session.commit();
  adapter.createPenItem(payload);
}

Document Snapshots

JSON save/load에는 snapshot을 사용한다. createPenDocumentSnapshot은 item을 deep clone하고, assertPenDocumentSnapshot은 import 전에 optional brush, outline, stabilization metadata까지 runtime shape을 검증한다. Migration policy는 docs/snapshot-migration-policy.md에 둔다.

import {
  assertPenDocumentSnapshot,
  createPenDocumentSnapshot,
} from "@penkit/core";

const snapshot = createPenDocumentSnapshot(items, {
  metadata: { documentId: "demo" },
});

const loaded: unknown = JSON.parse(serialized);
assertPenDocumentSnapshot(loaded);
for (const item of loaded.items) adapter.render(item);

Ink Replay

InkReplayController는 headless다. render-ready frame을 반환하고, host가 committed items와 active preview를 그린다.

import { InkReplayController } from "@penkit/core";

const replay = new InkReplayController(items);
const frame = replay.seek(250);

renderCommittedItems(frame.items);
if (frame.preview) {
  adapter.renderPreview?.([frame.preview.stroke], frame.preview.style);
}

Adapter Contract

renderer나 host editor adapter는 PenHostAdapter를 구현한다.

import type { PenCommitPayload, PenHostAdapter, PenItemProps } from "@penkit/core";

type HostNode = unknown;

class HostAdapter implements PenHostAdapter<HostNode> {
  readonly name = "host";

  createPenItem(payload: PenCommitPayload): HostNode {
    return host.commands.createPenNode(payload);
  }

  render(item: PenItemProps): void {
    host.renderer.renderPenItem(item);
  }

  remove(itemId: string): void {
    host.commands.removeNode(itemId);
  }
}

built-in 또는 custom BrushRenderHints를 실제로 그리는 adapter는 brushCapabilities를 선언한다. host는 preset을 선택하기 전에 renderer 지원 여부를 확인할 수 있다.

import { BUILTIN_BRUSH_PRESETS, supportsBrushRenderHints } from "@penkit/core";

supportsBrushRenderHints(adapter.brushCapabilities, BUILTIN_BRUSH_PRESETS.pencil.renderHints);

다음 책임은 host에 남긴다.

  • browser/client 좌표를 document 좌표로 변환.
  • z-index, selection handle, editor command, undo policy, persistence storage.
  • SVG path, canvas drawing, WebGL buffer, native editor node 같은 renderer-specific 결정.

하지 말 것

  • DOM event handling을 @penkit/core에 넣지 않는다. @penkit/dom-input을 사용한다.
  • core가 adapter를 import하게 만들지 않는다. adapter가 core에 의존해야 한다.
  • browser PointerEvent 객체를 저장하지 않는다. RawPoint[]를 저장한다.
  • engine이 받은 뒤의 RawPoint.x/y를 client coordinate라고 가정하지 않는다. host가 먼저 canvas/document 좌표로 변환해야 한다.