sketch-svg-kit
sketch-svg-kit
Strongly typed, deterministic hand-drawn graphics for SVG and Canvas. The API covers the Rough.js drawing primitives and fill styles, adds safe whole-SVG conversion, and works in browsers and Node.js without a runtime dependency on Rough.js.
Install
npm install sketch-svg-kit
The package publishes ESM and CommonJS entry points with matching TypeScript declarations. Node.js 22.18 or newer is supported.
Generate SVG
import { createGenerator, renderSvg } from 'sketch-svg-kit';
const sketch = createGenerator({
seed: 42,
defaults: { roughness: 1.2, stroke: '#172554' },
});
const shapes = [
sketch.rectangle({
x: 10,
y: 10,
width: 180,
height: 100,
style: {
fill: '#fde68a',
fillStyle: 'cross-hatch',
hachureGap: 5,
},
}),
sketch.line({ x1: 20, y1: 125, x2: 180, y2: 125 }),
];
const svg = renderSvg(shapes, {
width: 200,
height: 140,
title: 'Sketch card',
ariaLabel: 'A hand-drawn card and line',
});
Drawable values contain only readonly JSON-compatible drawing operations.
Rendering the same drawable always produces the same result.
Primitives
Every method accepts one named object and returns a Drawable:
sketch.line({ x1, y1, x2, y2, style });
sketch.rectangle({ x, y, width, height, style });
sketch.ellipse({ cx, cy, width, height, style });
sketch.circle({ cx, cy, diameter, style });
sketch.linearPath({ points, style });
sketch.polygon({ points, style });
sketch.arc({ cx, cy, width, height, start, stop, closed, style });
sketch.curve({ points, style });
sketch.path({ d, style });
Angles are in radians. Points are readonly [x, y] tuples. Widths, heights,
and diameters must be non-negative; every coordinate must be finite.
Fill styles are a closed TypeScript union:
type FillStyle =
| 'hachure'
| 'solid'
| 'zigzag'
| 'cross-hatch'
| 'dots'
| 'dashed'
| 'zigzag-line';
The style types expose roughness, bowing, curve fitting/tightness, randomness,
stroke and fill weights, hachure settings, line dashes, path simplification,
multi-stroke controls, vertex preservation, decimal precision, and seeded
randomness. Fill-specific settings are discriminated by fillStyle, and line
styles cannot accidentally receive fill-only settings.
Set style.strokeCount to any positive integer to control how many times each
outline is drawn (2 by default). It can also be set once in
createGenerator({ defaults: { strokeCount: 4 } }). The older
disableMultiStroke option remains supported.
SVG paths and low-level output
import { opsToPath, toPaths } from 'sketch-svg-kit';
const drawable = sketch.path({
d: 'M10 80 A45 45 0 0 1 100 80 L55 10 Z',
style: { fill: 'tomato', simplification: 0.8 },
});
const paths = toPaths(drawable); // readonly SVG path descriptions
const firstOperationSet = drawable.sets[0];
const d = firstOperationSet ? opsToPath(firstOperationSet, 2) : '';
Browser SVG DOM
import { createGenerator } from 'sketch-svg-kit';
import { renderSvgElement } from 'sketch-svg-kit/dom';
const shape = createGenerator({ seed: 7 }).circle({
cx: 50,
cy: 50,
diameter: 80,
style: { fill: 'gold', fillStyle: 'dots' },
});
document.body.append(renderSvgElement(shape, { width: 100, height: 100 }));
Importing the root package never accesses the DOM.
Animated SVG
Create a standalone animated SVG string in Node.js or the browser:
import { createGenerator, renderAnimatedSvg } from 'sketch-svg-kit';
const sketch = createGenerator({ seed: 7 });
const shape = sketch.circle({
cx: 50,
cy: 50,
diameter: 80,
style: { fill: 'gold', fillStyle: 'dots' },
});
const svg = renderAnimatedSvg(shape, { width: 100, height: 100 }, {
duration: 600,
delay: 100,
stagger: 80,
fillDuration: 200,
order: 'outline-first',
sequence: 'element',
});
For a browser element, use renderAnimatedSvgElement from
sketch-svg-kit/dom. Set order to outline-first to complete the outside
strokes before patterned or solid fills, or to fill-first for the reverse.
Set sequence to element to finish each drawable—including its fill—before
starting the next; the default document mode orders all drawable layers together.
Animation uses native SVG/CSS, needs no runtime JavaScript, honors
prefers-reduced-motion, and leaves the completed sketch visible when CSS
animation is unavailable.
Canvas
import { createGenerator } from 'sketch-svg-kit';
import { drawCanvas } from 'sketch-svg-kit/canvas';
const canvas = document.querySelector('canvas')!;
const context = canvas.getContext('2d')!;
const shape = createGenerator({ seed: 9 }).polygon({
points: [[10, 10], [90, 20], [50, 90]],
style: { fill: 'royalblue', fillStyle: 'zigzag' },
});
drawCanvas(context, shape);
drawCanvas accepts a small structural context interface, so compatible Node
Canvas implementations work without becoming package dependencies.
Convert complete SVG documents
String conversion works identically in browsers and Node.js:
import { convertSvg } from 'sketch-svg-kit/convert';
const result = convertSvg(`
<svg viewBox="0 0 100 100">
<rect x="10" y="10" width="80" height="80"
style="fill: coral; stroke: navy; stroke-width: 2"/>
<text x="50" y="55" text-anchor="middle">Hello</text>
</svg>
`, {
seed: 11,
defaults: { roughness: 1.5, fillStyle: 'hachure' },
});
console.log(result.svg);
console.table(result.warnings);
line, rect (including rounded corners), circle, ellipse, polyline,
polygon, and path are sketched. Groups, nested SVGs, transforms, viewports,
accessibility metadata, inherited presentation attributes, inline styles, and
local references are retained. Safe text, embedded raster images, gradients,
patterns, clipping, masks, and filters are preserved.
Browser conversion can resolve stylesheet rules through computed styles:
import { convertSvgElement } from 'sketch-svg-kit/dom';
const source = document.querySelector('svg')!;
const { element, warnings } = convertSvgElement(source, { seed: 11 });
source.replaceWith(element);
String conversion intentionally does not implement a CSS cascade. <style>
elements are removed with a stylesheet-not-resolved warning; use presentation
attributes, inline styles, or the DOM adapter when computed CSS matters.
Conversion security
Conversion treats input as untrusted XML:
- DTD and entity declarations are rejected.
- Scripts, animation elements,
foreignObject, and event attributes are removed. - JavaScript URLs, external URLs, external images, and external paint servers are removed.
- Images are preserved only for embedded PNG, JPEG, GIF, or WebP data URLs.
- Local fragment references such as
url(#gradient)andhref="#symbol"are allowed. - Unsupported or invalid geometry is preserved when safe and reported through a structured warning.
The warning codes are part of the public API, so applications can log or reject lossy conversions according to their own policy.
Determinism
Set a generator seed for repeatable shapes:
const a = createGenerator({ seed: 123 }).ellipse({ cx: 20, cy: 20, width: 30, height: 10 });
const b = createGenerator({ seed: 123 }).ellipse({ cx: 20, cy: 20, width: 30, height: 10 });
JSON.stringify(a) === JSON.stringify(b); // true
Without a configured seed, each new drawable receives a cryptographically
generated non-zero seed, which is stored in drawable.options.seed. A per-shape
style.seed overrides the generator seed.
Rough.js migration
| Rough.js | sketch-svg-kit |
|---|---|
rough.generator(options) |
createGenerator({ defaults, seed }) |
gen.rectangle(x, y, w, h, options) |
gen.rectangle({ x, y, width: w, height: h, style }) |
rough.svg(svg).draw(drawable) |
renderSvgElement(drawable, viewport) |
rough.canvas(canvas).draw(drawable) |
drawCanvas(context, drawable) |
generator.toPaths(drawable) |
toPaths(drawable) |
| Browser-only SVG element output | SVG strings in Node and SVG elements in browsers |
The package targets feature parity, not matching random geometry, serialized output, positional signatures, or mutable Rough.js data structures.
Development
npm run typecheck
npm test
npm run build
npm run test:package
npm run test:browser
npm run check
npm run check verifies strict types, unit/security tests, dual ESM/CommonJS
builds, declaration resolution, package metadata, and installation of the packed
tarball. Browser tests use Chromium through Playwright.
The browser suite includes a side-by-side fixture for every primitive and fill
style at test/browser/rough-comparison.html.
It renders this package beside Rough.js 4.6.6 as a visual capability reference;
the tests intentionally do not require identical randomized geometry.
License and attribution
MIT. The renderer algorithms are adapted from Rough.js under its MIT license; see THIRD_PARTY_NOTICES.md.