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

html-overlay-node

v0.3.3

Published

Zero-dependency visual node editor — Canvas rendering + HTML/CSS widget overlay. Data-flow graphs, workflow editors, visual scripting. Undo/redo, minimap, sub-graphs, TypeScript support.

Downloads

847

Readme

html-overlay-node

npm version License: MIT

html-overlay-node is an enterprise-grade visual node editor that combines Canvas performance with the full power of HTML/CSS for node UI.

html-overlay-node는 Canvas의 고성능 렌더링과 HTML/CSS의 완전한 UI 표현력을 결합한 엔터프라이즈급 비주얼 노드 에디터입니다.

Live Demo · https://cheonghakim.github.io/HTML-overlay-node/


Why html-overlay-node?

Traditional node editors force a hard choice:

  • Pure Canvas — High performance, but extremely difficult to style or build rich interactive widgets.
  • Pure DOM — Easy to style, but performance collapses under complex edge animations or large graphs.

html-overlay-node's hybrid architecture:

  • Canvas layer handles heavy lifting: thousands of edges, animated signal flow, background grid, minimap.
  • HTML overlay handles UI: node bodies with inputs, sliders, buttons, dropdowns — all with full CSS power.

왜 html-overlay-node인가?

기존 노드 에디터는 두 가지 선택지 사이에서 타협합니다:

  • 순수 Canvas — 성능은 좋지만 CSS 스타일링 불가, 인터랙티브 위젯 구현이 매우 어렵습니다.
  • 순수 DOM — 스타일링은 쉽지만 복잡한 그래프에서 성능이 급격히 저하됩니다.

html-overlay-node의 하이브리드 아키텍처:

  • Canvas가 무거운 작업을 담당: 수천 개의 연결선, 에메랄드 신호 애니메이션, 배경 그리드, 미니맵
  • HTML 오버레이가 UI를 담당: 입력 폼, 슬라이더, 버튼 등 모든 CSS 위젯

Key Features / 주요 기능

| Feature | 기능 | |:--------|:-----| | Hybrid Canvas + HTML rendering | 하이브리드 Canvas + HTML 렌더링 | | Emerald signal flow animation | 에메랄드 신호 흐름 시각화 | | Interactive minimap (drag & click) | 드래그/클릭 가능한 인터랙티브 미니맵 | | Full undo / redo (Ctrl+Z / Ctrl+Y) | 완전한 실행 취소/재실행 | | Grid snap (G key) | 정밀 그리드 스냅 | | Node grouping (Ctrl+G) | 노드 그룹화 | | Copy / Paste / Duplicate | 복사 / 붙여넣기 / 복제 | | Fit to View (F key) | 전체 보기 맞춤 | | Bezier / Orthogonal / Straight edges | 3가지 연결선 스타일 | | Property panel (double-click node) | 속성 패널 (노드 더블클릭) | | Sub-graph editor (nested graphs) | 서브 그래프 에디터 (중첩 그래프) | | Read-only mode | 읽기 전용 모드 | | Run / Step execution modes | Run / Step 실행 모드 | | Plugin API | 플러그인 API | | TypeScript declarations | TypeScript 타입 선언 | | Zero dependencies | 제로 외부 의존성 |


Installation / 설치

npm install html-overlay-node

Quick Start / 빠른 시작

import { createGraphEditor } from "html-overlay-node";
import { registerAllNodes } from "html-overlay-node/nodes";
import "html-overlay-node/index.css";
import "html-overlay-node/src/ui/PropertyPanel.css"; // required for property panel

const editor = createGraphEditor("#editor-container", {
  theme: "dark",
  showMinimap: true,
  enablePropertyPanel: true,
});

registerAllNodes(editor.registry, editor.hooks);

Note: Both CSS files are required. index.css provides the core editor styles; PropertyPanel.css provides the property panel styles.

참고: CSS 파일 두 개 모두 필요합니다.


createGraphEditor Options / 옵션

createGraphEditor(target, options?)

| Option | Type | Default | Description | |:-------|:-----|:--------|:------------| | theme | "dark" \| "light" | system preference | Color theme | | autorun | boolean | true | Start runner automatically | | showMinimap | boolean | true | Show interactive minimap | | enablePropertyPanel | boolean | true | Enable property panel on double-click | | propertyPanelContainer | HTMLElement \| null | editor container | Mount point for property panel | | enableHelp | boolean | true | Show keyboard help overlay (?) | | helpShortcuts | Record<string,string> \| null | built-in list | Override shortcut descriptions | | setupDefaultContextMenu | boolean | true | Wire up built-in right-click menu | | setupContextMenu | function \| null | null | Custom context menu setup | | plugins | Plugin[] | [] | Plugins to install | | hooks | Hooks | auto-created | Provide your own hooks instance |


Editor API (High-Level)

The returned editor object exposes these properties:

editor.graph          // Graph data model (nodes Map, edges Map)
editor.registry       // Node type registry
editor.hooks          // Event bus (on / off / emit)
editor.controller     // High-level controller — use for all mutations
editor.runner         // Execution engine
editor.renderer       // Main CanvasRenderer
editor.htmlOverlay    // HTML overlay manager
editor.subGraphPanel  // Sub-graph split-pane editor
editor.minimap        // Minimap (null if showMinimap: false)
editor.propertyPanel  // Property panel (null if disabled)
editor.contextMenu    // Context menu
editor.iconManager    // Icon manager

editor.render()                         // Force full redraw
editor.start()                          // Start runner
editor.stop()                           // Stop runner
editor.setEdgeStyle("bezier" | "orthogonal" | "line")
editor.setExecutionMode("run" | "step") // Switch execution mode
editor.destroy()                        // Full teardown

Controller API (all mutations → undo history)

const node = editor.controller.addNode("math/Multiply", {
  title: "My Multiplier",
  x: 100, y: 100,
  state: { factor: 2 },
});

editor.controller.removeNode(node.id);

editor.controller.addEdge(fromNodeId, fromPortId, toNodeId, toPortId);

editor.controller.updateNodeState(node.id, { value: 42 });

editor.controller.updateNodeProperty(node.id, "title", "New Title");

editor.controller.fitToView();    // Fit all nodes into viewport
editor.controller.autoLayout();   // Arrange nodes in a grid

editor.controller.undo();         // Ctrl+Z
editor.controller.redo();         // Ctrl+Y

editor.controller.readOnly = true; // Block all mutations; allow pan/zoom only

Execution Modes / 실행 모드

editor.setExecutionMode("run");  // Continuous — runner ticks every animation frame
editor.setExecutionMode("step"); // Manual — click Step button or call runner.step()
  • Run mode: The runner calls onExecute on all exec-connected nodes every frame.
  • Step mode: Each click advances one exec edge, highlighting the active node and edge.

Hooks (Event Bus) / 이벤트 훅

editor.hooks.on("node:create",      (node) => { ... });
editor.hooks.on("node:click",       (node) => { ... });
editor.hooks.on("node:dblclick",    (node) => { ... });
editor.hooks.on("node:move",        (node) => { ... });
editor.hooks.on("node:resize",      (node) => { ... });
editor.hooks.on("node:updated",     (node) => { ... });
editor.hooks.on("edge:create",      (edge) => { ... });
editor.hooks.on("edge:delete",      (edge) => { ... });
editor.hooks.on("graph:serialize",  (json)  => { ... });
editor.hooks.on("graph:deserialize",(json)  => { ... });
editor.hooks.on("runner:tick",      ({ time, dt }) => { ... });
editor.hooks.on("runner:start",     () => { ... });
editor.hooks.on("runner:stop",      () => { ... });
editor.hooks.on("error",            (err)  => { ... });

Registering Custom Nodes / 커스텀 노드 등록

Pure Logic Node

editor.registry.register("math/Multiply", {
  title: "Multiply",
  color: "#2563eb",
  inputs: [
    { name: "exec", portType: "exec" },
    { name: "a", portType: "data", datatype: "number" },
    { name: "b", portType: "data", datatype: "number" },
  ],
  outputs: [
    { name: "exec", portType: "exec" },
    { name: "result", portType: "data", datatype: "number" },
  ],
  onExecute(node, { setOutput, getInput }) {
    const a = getInput("a") ?? node.state.a ?? 0;
    const b = getInput("b") ?? node.state.b ?? 0;
    setOutput("result", a * b);
    setOutput("exec", true);
  },
});

HTML Overlay Node / HTML 오버레이 노드

Use the html property to render a real HTML form inside the node body.
node.state is automatically serialized and included in undo/redo.

editor.registry.register("ui/Slider", {
  title: "Value Slider",
  size: { w: 200 },
  outputs: [
    { name: "value", portType: "data", datatype: "number" },
  ],
  onCreate(node) {
    if (node.state.value === undefined) node.state.value = 50;
  },
  html: {
    init(node, el, { body, graph }) {
      const slider = document.createElement("input");
      slider.type = "range";
      slider.min = "0";
      slider.max = "100";
      slider.value = node.state.value;
      slider.addEventListener("input", () => {
        // updateNodeState records the change in undo history
        graph.controller.updateNodeState(node.id, { value: Number(slider.value) });
      });
      body.appendChild(slider);
    },
    update(node, el, { body }) {
      const slider = body.querySelector("input");
      if (slider) slider.value = node.state.value;
    },
  },
  onExecute(node, { setOutput }) {
    setOutput("value", node.state.value);
  },
});

Property Panel Widgets / 속성 패널 위젯

노드 정의에 properties 배열을 추가하면 속성 패널이 자동으로 위젯을 렌더링합니다.
onChange로 값 변경 시 동작을 완전히 제어할 수 있습니다.

editor.registry.register("audio/Gain", {
  title: "Gain",
  inputs:  [{ name: "exec", portType: "exec" }, { name: "signal", portType: "data", datatype: "number" }],
  outputs: [{ name: "exec", portType: "exec" }, { name: "out",    portType: "data", datatype: "number" }],

  properties: [
    // Slider — real-time drag feedback, undo recorded on release
    { key: "gain",   label: "Gain",    widget: "slider", min: 0, max: 2,   step: 0.01 },
    { key: "pan",    label: "Pan",     widget: "slider", min: -1, max: 1,  step: 0.01 },

    // Number input
    { key: "delay",  label: "Delay (ms)", widget: "number", min: 0, max: 5000, step: 10 },

    // Toggle (on/off)
    { key: "mute",   label: "Mute",    widget: "toggle" },

    // Dropdown
    { key: "curve",  label: "Curve",   widget: "select",
      options: ["linear", "exponential", { label: "S-Curve", value: "scurve" }] },

    // Color picker
    { key: "color",  label: "Color",   widget: "color" },

    // Multiline text
    { key: "notes",  label: "Notes",   widget: "textarea" },

    // Custom onChange — full control over what happens
    {
      key: "threshold",
      label: "Threshold",
      widget: "slider",
      min: -60, max: 0, step: 0.5,
      onChange(node, value, { controller, graph, immediate }) {
        if (immediate) {
          // Live drag: update state directly (no undo entry)
          node.state.threshold = value;
          node.state.clipping  = value > -6;
          graph.hooks.emit("node:updated", node);
        } else {
          // Committed: record in undo history
          controller.updateNodeState(node.id, {
            threshold: value,
            clipping:  value > -6,
          });
        }
      },
    },
  ],

  onCreate(node) {
    node.state.gain      ??= 1;
    node.state.pan       ??= 0;
    node.state.mute      ??= false;
    node.state.curve     ??= "linear";
    node.state.threshold ??= -12;
    node.state.clipping  ??= false;
  },

  onExecute(node, { getInput, setOutput }) {
    if (node.state.mute) { setOutput("exec", true); return; }
    const signal = getInput("signal") ?? 0;
    setOutput("out", signal * node.state.gain);
    setOutput("exec", true);
  },
});

Supported Widget Types / 지원 위젯 타입

| Widget | Description | Options | |:-------|:------------|:--------| | "text" | 텍스트 입력 | placeholder | | "number" | 숫자 입력 | min, max, step | | "slider" | 슬라이더 (실시간 드래그) | min, max, step | | "toggle" | 토글 스위치 | — | | "select" | 드롭다운 | options: string[] \| { label, value }[] | | "radio" | 라디오 버튼 (단일 선택) | options: string[] \| { label, value }[] | | "checkbox-group" | 체크박스 그룹 (복수 선택, 배열 저장) | options: string[] \| { label, value }[] | | "color" | 컬러 피커 | — | | "textarea" | 여러 줄 텍스트 | placeholder |


Bidirectional Binding / 양방향 데이터 바인딩

속성 패널 위젯과 node.state는 완전한 양방향으로 동기화됩니다.

DOM → state (사용자 입력 → 상태 저장)

사용자가 위젯 값을 변경하면 다음 흐름으로 처리됩니다.

widget change event
  → commit(immediate)
      → onChange(node, value, ctx)   ← 사용자 정의 핸들러 (선택)
        OR controller.updateNodeState(node.id, { key: value })  ← 기본 동작 (undo 기록)
          → node.state[key] = value
          → hooks.emit("node:updated", node)

Slider live drag: immediate = true — 드래그 중엔 node.state를 직접 변경하고 node:updated를 emit합니다. Undo 히스토리에는 기록되지 않아 히스토리가 폭발하지 않습니다. 마우스를 떼는 순간 immediate = false로 한 번만 undo 항목이 기록됩니다.

state → DOM (외부 상태 변경 → 위젯 동기화)

node:updated 훅이 발생하면 패널은 DOM을 재구성하지 않고 위젯 값만 가볍게 동기화합니다. 이는 슬라이더 조작 중 focus 손실이나 스크롤 위치 초기화를 방지합니다.

hooks.emit("node:updated", node)
  → _syncWidgetValues(node)  ← DOM 재구성 없이 값만 업데이트
      for each property widget:
        _setWidgetValue(element, node.state[key], widget)

onExecute에서 최신 상태 반영

위젯으로 변경된 값은 즉시 node.state에 반영되므로 다음 onExecute 호출 시 최신 값을 사용합니다.

onExecute(node, { getInput, setOutput }) {
  // node.state.gain은 슬라이더 조작 즉시 최신값 반영
  const signal = getInput("signal") ?? 0;
  setOutput("out", signal * node.state.gain);
  setOutput("exec", true);
}

외부에서 상태 직접 변경

Runner나 다른 노드에서 node.state를 직접 변경하고 node:updated를 emit하면 속성 패널 위젯이 자동 동기화됩니다.

// 외부에서 상태 변경 후 위젯 동기화 트리거
node.state.gain = 0.5;
hooks.emit("node:updated", node);
// → 속성 패널의 gain 슬라이더가 0.5로 자동 업데이트됨

Undo 히스토리에도 기록하려면 controller.updateNodeState를 사용합니다.

controller.updateNodeState(node.id, { gain: 0.5 });
// → node.state 업데이트 + undo 스택에 기록 + node:updated emit + 패널 동기화

완전한 바인딩 흐름 요약

[사용자 위젯 조작]
       ↓
  commit(immediate)
       ↓
  onChange OR updateNodeState
       ↓
  node.state[key] 업데이트
       ↓
  node:updated 훅 emit
       ↙              ↘
패널 위젯 동기화    onExecute 다음 호출시 최신값 사용
(_syncWidgetValues)    (setOutput 반영)

Built-in Node Types / 내장 노드 타입

Register all at once:

import { registerAllNodes } from "html-overlay-node/nodes";
registerAllNodes(editor.registry, editor.hooks);

Or register selectively:

import {
  registerMathNodes,
  registerLogicNodes,
  registerValueNodes,
  registerUtilNodes,
  registerCoreNodes,
  registerSubGraphNodes,
} from "html-overlay-node/nodes";

registerMathNodes(editor.registry);    // math/Add, math/Subtract, math/Multiply, math/Divide
registerLogicNodes(editor.registry);   // logic/Branch, logic/Not, logic/And, logic/Or
registerValueNodes(editor.registry);   // value/Number, value/String, value/Boolean
registerUtilNodes(editor.registry, editor.hooks); // util/Print, util/Watch, util/Trigger, util/Delay, util/Log
registerCoreNodes(editor.registry);    // core/Note, core/HtmlNote, core/TodoNode, core/Group
registerSubGraphNodes(editor.registry); // util/SubGraph (nested graph editor)

Sub-Graph Editor / 서브 그래프 에디터

util/SubGraph nodes contain a full nested graph. Clicking the expand icon opens a split-pane editor docked inside the main editor area.

// Open the sub-graph panel programmatically
const sgNode = editor.graph.nodes.get("my-subgraph-node-id");
editor.subGraphPanel.open(sgNode, sgNode.state.subGraphData, ["Main", sgNode.title]);

// Check if the panel is open for a specific node
editor.subGraphPanel.isOpenFor("my-subgraph-node-id"); // boolean

// Close the panel (saves edited data back to the node automatically)
editor.subGraphPanel.close();

// Change dock side ("bottom" | "top" | "left" | "right")
editor.subGraphPanel.setDock("right");

Sub-graph execution is automatic:

  • When the panel is open: executes live in the visible sub-editor and animates edges.
  • When the panel is closed: executes headlessly from the node's saved state.subGraphData.

Graph Serialization / 그래프 직렬화

// Save
const json = editor.graph.toJSON();
localStorage.setItem("graph", JSON.stringify(json));

// Load
const json = JSON.parse(localStorage.getItem("graph"));
editor.graph.clear();
editor.graph.fromJSON(json);
editor.render();

Graph JSON format:

{
  "version": 2,
  "meta": { "name": "My Graph", "description": "", "author": "" },
  "nodes": [ { "id": "n1", "type": "math/Add", "x": 100, "y": 100, "state": { "a": 10 } } ],
  "edges": [ { "id": "e1", "fromNode": "n1", "fromPort": "po-exec", "toNode": "n2", "toPort": "pi-exec" } ]
}

Plugin API / 플러그인 API

const myPlugin = {
  name: "my-plugin",
  install({ graph, registry, hooks, runner, controller, contextMenu }, options) {
    // Register custom nodes
    registry.register("my/Node", { ... });

    // Listen to events
    hooks.on("node:create", (node) => {
      console.log("Node created:", node.type);
    });

    // Add context menu items
    contextMenu.addItem({
      label: "Export selection",
      action: () => { /* ... */ },
    });
  },
};

const editor = createGraphEditor("#editor", {
  plugins: [{ ...myPlugin, options: { apiKey: "..." } }],
});

Custom Context Menu / 커스텀 컨텍스트 메뉴

import { setupDefaultContextMenu } from "html-overlay-node/defaults";

const editor = createGraphEditor("#editor", {
  setupDefaultContextMenu: false, // disable built-in menu
  setupContextMenu(menu, { controller, graph, hooks }) {
    setupDefaultContextMenu(menu, { controller, graph, hooks }); // include defaults
    menu.addItem({ label: "My Action", action: () => { ... } });
  },
});

Read-Only Mode / 읽기 전용 모드

// Block all mutations; only pan and zoom remain active
editor.controller.readOnly = true;

// Re-enable editing
editor.controller.readOnly = false;

Edge Labels / 연결선 레이블

Edges rendered in orthogonal style show a small label at the midpoint. Port names are used as labels automatically. To customize, set name on the port definition:

{ name: "result", portType: "data", datatype: "number" }

Keyboard Shortcuts / 단축키

Selection & Edit / 선택 및 편집

| Action | Shortcut | 동작 | |:-------|:---------|:-----| | Box select | Ctrl + Drag | 박스 선택 | | Select all | Ctrl + A | 전체 선택 | | Undo / Redo | Ctrl + Z / Ctrl + Y | 실행 취소 / 재실행 | | Delete | Delete / Backspace | 삭제 | | Copy / Paste | Ctrl + C / Ctrl + V | 복사 / 붙여넣기 | | Duplicate | Ctrl + D | 복제 | | Group | Ctrl + G | 그룹화 | | Ungroup | Ctrl + Shift + G | 그룹 해제 |

View / 뷰

| Action | Shortcut | 동작 | |:-------|:---------|:-----| | Zoom in / out | Ctrl + (+/-) or Wheel | 확대 / 축소 | | Fit to view | F | 전체 보기 맞춤 | | Grid snap toggle | G | 그리드 스냅 토글 | | Align horizontal | A | 수평 정렬 | | Align vertical | Shift + A | 수직 정렬 | | Pan | Middle-button drag | 화면 이동 | | Help | ? | 단축키 도움말 |


TypeScript / 타입스크립트

Type declarations are included in the package:

import { createGraphEditor, GraphEditor, GraphEditorOptions, NodeDefinition } from "html-overlay-node";

const editor: GraphEditor = createGraphEditor("#editor", {
  theme: "dark",
} satisfies GraphEditorOptions);

Development / 개발

npm install
npm run dev          # Start dev server (localhost:5173)
npm test             # Run tests
npm run test:coverage # Coverage report
npm run build        # Production library build
BUILD_DEMO=1 npm run build # Demo build (GitHub Pages)

License / 라이선스

MIT © cheonghakim