@richapps/pen-tool
v0.1.1
Published
A TypeScript library for creating Figma-style pen tool with Bezier curves and SVG path manipulation
Maintainers
Readme
Pen Tool Library
A TypeScript library providing a Figma-style pen tool for creating and editing SVG paths with Bezier curves.
🎨 Live Demo
Try the interactive Angular demo with all features!
Features
- ✅ Auto-Import: Automatically import and edit existing SVG paths
- ✅ View Mode: Display paths without any interactive control elements
- ✅ Drawing Mode: Click to add straight points, click-and-drag to create curves
- ✅ Edit Mode: Move anchor points, adjust Bezier handles, add/delete points
- ✅ Smart Point Addition: Hover preview indicator shows where new points will be added (configurable distance)
- ✅ Perfect Curve Subdivision: Adding points preserves exact curve shape using De Casteljau's algorithm
- ✅ Bezier Curves: Full cubic Bezier support with three handle mirroring modes
- Mirrored: Both angle and length stay synchronized
- Angle-locked: Same angle, independent lengths
- Independent: Complete handle independence
- ✅ Dual Renderers: Choose between SVG or Canvas 2D rendering
- SVG Renderer: DOM-based, resolution-independent, easier debugging
- Canvas Renderer: Faster performance, lightweight, better for animations
- ✅ Keyboard Modifiers: Shift for angle snapping, Alt for independent handles, Enter/Escape for path operations
- ✅ Interactive UI: Real-time visual feedback with handles, preview lines, hover indicators, and selection
Installation
npm install @richapps/pen-toolDevelopment
# Start development server
npm run dev
# Build library
npm run build
# Type checking
npm run type-checkQuick Start
Simplest Example - Auto-Import Existing Paths
import { PathManager, EditMode, SvgPathRenderer } from '@richapps/pen-tool';
const svg = document.getElementById('canvas'); // Contains existing <path> elements
const pathManager = new PathManager();
// That's it! Existing paths are automatically imported and editable
const renderer = new SvgPathRenderer(svg, pathManager);
const editMode = new EditMode(svg, pathManager, renderer);Manual Import
// Import from SVG path data string
const path = pathManager.importFromSVG('M 0 0 L 100 100 C 120 80, 150 80, 170 100', {
stroke: '#ff0000',
strokeWidth: 3,
fill: 'none'
});
// Import all paths from an SVG element
const paths = pathManager.importFromSVGElement(svgElement);Creating New Paths with Pen Tool
SVG Renderer
import { PathManager, PenTool, SvgPathRenderer } from '@richapps/pen-tool';
// Setup
const svg = document.getElementById('canvas');
const pathManager = new PathManager();
const renderer = new SvgPathRenderer(svg, pathManager);
// Create pen tool
const penTool = new PenTool(pathManager, {}, {
onPathModified: (path) => renderer.update(pathManager)
});
// Handle mouse events
svg.addEventListener('mousedown', (e) => {
const pos = { x: e.offsetX, y: e.offsetY };
penTool.onMouseDown(pos);
});
svg.addEventListener('mousemove', (e) => {
const pos = { x: e.offsetX, y: e.offsetY };
penTool.onMouseMove(pos);
});
svg.addEventListener('mouseup', (e) => {
const pos = { x: e.offsetX, y: e.offsetY };
penTool.onMouseUp(pos);
});Canvas Renderer
import { PathManager, PenTool, CanvasPathRenderer } from '@richapps/pen-tool';
// Setup
const canvas = document.getElementById('canvas');
const pathManager = new PathManager();
const renderer = new CanvasPathRenderer(canvas, pathManager);
// Create pen tool (same as SVG)
const penTool = new PenTool(pathManager, {}, {
onPathModified: (path) => renderer.update(pathManager)
});
// Handle mouse events (same as SVG)
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const pos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
penTool.onMouseDown(pos);
});
// ... etcExamples
Run the development server and open:
- examples/auto-import-demo.html - ⭐ Simplest example - Auto-import existing SVG paths
- examples/index.html - SVG Renderer with pen tool
- examples/canvas.html - Canvas Renderer demo
- examples/import.html - Advanced import features
- examples/angular/ - Angular 21 example with signals
See examples/README.md for detailed documentation of all examples.
Choosing a Renderer
Use SVG Renderer when:
- You need resolution-independent graphics
- DOM inspection/debugging is important
- Working with simpler paths (< 100 elements)
- Need easy hit testing and interactivity
- Exporting to SVG format
Use Canvas Renderer when:
- Performance is critical (many paths/points)
- Creating animations or real-time effects
- Need lightweight rendering
- Working in a game or animation context
- Don't need DOM access to path elements
Both renderers implement the same IPathRenderer interface, so switching is seamless!
Architecture
Core Modules
- types.ts - TypeScript interfaces and enums
- path.ts - Path manipulation, SVG generation, Bezier calculations (including De Casteljau subdivision)
- handles.ts - Handle mirroring and control point management
Tools
- penTool.ts - Drawing mode with click/drag interactions
- editMode.ts - Edit mode for modifying existing paths with hover preview
Renderers
- IPathRenderer.ts - Renderer interface for pluggable rendering backends
- pathRenderer.ts - SVG renderer implementation (SvgPathRenderer)
- canvasPathRenderer.ts - Canvas 2D renderer implementation (CanvasPathRenderer)
API Overview
PathManager
const pathManager = new PathManager();
// Create new paths
const path = pathManager.createPath();
pathManager.addAnchorPoint(path, { x: 100, y: 100 });
// Export to SVG
const svgPath = pathManager.toSVGPath(path);
// Import from SVG path data
const importedPath = pathManager.importFromSVG('M 0 0 L 100 100 C 120 80, 150 80, 170 100', {
stroke: '#ff0000',
strokeWidth: 3,
fill: 'none'
});
// Import all paths from an SVG element
const importedPaths = pathManager.importFromSVGElement(svgElement);Supported SVG Commands:
- M/m (moveto), L/l (lineto), H/h (horizontal), V/v (vertical)
- C/c (cubic Bezier), S/s (smooth cubic Bezier)
- Z/z (close path)
PenTool
const penTool = new PenTool(pathManager, options, callbacks);
penTool.onMouseDown(position);
penTool.onMouseMove(position);
penTool.onMouseUp(position);EditMode
const editMode = new EditMode(pathManager, callbacks, options);
// Configure hover distance (default: 5px)
editMode.setHoverDistance(10);
// Mouse interactions
editMode.onMouseDown(position);
editMode.onMouseMove(position); // Shows hover preview near paths
editMode.onDoubleClick(position); // Add point to path
// Callbacks
{
onPathModified: (path) => { /* path was modified */ },
onSelectionChange: (points) => { /* selection changed */ },
onHoverPreview: (point, path) => {
// point is null when not hovering near a path
// Shows preview indicator for adding points
renderer.renderHoverPreviewPoint(point);
}
}Renderers
// SVG Renderer - Auto-imports existing paths by default
const svgRenderer = new SvgPathRenderer(svgElement, pathManager);
// Disable auto-import if needed
const svgRenderer = new SvgPathRenderer(svgElement, pathManager, {
autoImport: false
});
svgRenderer.update(pathManager);
svgRenderer.renderPreviewLine(fromPoint, toPoint);
// View-only mode (no interactive elements)
svgRenderer.renderViewOnly(pathManager);
// Canvas Renderer - Can import from data attribute
const canvasRenderer = new CanvasPathRenderer(canvasElement, pathManager);
canvasRenderer.update(pathManager);
canvasRenderer.renderPreviewLine(fromPoint, toPoint);
canvasRenderer.renderViewOnly(pathManager);
// Both implement IPathRenderer interface
// Backward compatibility: PathRenderer is an alias for SvgPathRenderer
const renderer = new PathRenderer(svgElement, pathManager);Keyboard Shortcuts
- Shift - Constrain angles to 45° increments
- Enter - Close current path
- Escape - Finish current path
- Delete/Backspace - Remove selected points (edit mode)
License
MIT
