BORROWDALEJS / PACKAGE REFERENCE
@borrowdale/core
Deterministic hand-drawn shapes for SVG and canvas. The stroke engine began as a TypeScript port of rough.js 4.6.6 and has no runtime dependencies. It draws from its own seeded random generator and its own trigonometry, so its output does not depend on the JavaScript engine.
npm install @borrowdale/coreimport { sketch, toSvgDocument, toSvgSketch } from '@borrowdale/core';
const box = sketch(
{ kind: 'rectangle', x: 10, y: 10, width: 120, height: 60 },
{ fill: '#e76f51', fillStyle: 'hachure' },
);
const svg = toSvgDocument({ width: 140, height: 80, body: [toSvgSketch(box)] });Same input, same drawing
A sketch is reproducible: the same shape and options produce the same strokes on every render, on
the server and in the browser. Pass seed to vary a drawing. Without it, the seed is derived from
the shape itself.
The engine never calls Math.random, and computes sine, cosine and arctangent with plain
arithmetic instead of Math.sin and friends, whose last bit can differ between JavaScript engines.
The test suite draws every reference drawing of this package in Node.js and in Chromium, Firefox
and WebKit, so in the V8, SpiderMonkey and JavaScriptCore engines, and requires identical output.
Coordinates are rounded to two decimals, which keeps the markup small.
The drawing depends on the exact version of the stroke engine, which ships inside this package.
Use the same @borrowdale/core version on the server and in the browser.
Shapes
kind |
Fields |
|---|---|
line |
x1, y1, x2, y2 |
rectangle |
x, y, width, height |
ellipse |
cx, cy, width, height |
circle |
cx, cy, diameter |
arc |
cx, cy, width, height, start, stop, closed? (radians) |
polygon |
points |
linearPath |
points (open polyline) |
curve |
points (smooth curve through the points) |
path |
d (SVG path data) |
Style
| Option | Applies to |
|---|---|
stroke, strokeWidth |
outline |
strokeLineDash (dash and gap lengths, as in SVG stroke-dasharray) |
outline; fill lines stay solid |
fill, fillStyle (hachure, solid, zigzag, cross-hatch, dashed, zigzag-line, dots) |
fill |
fillWeight, hachureAngle (default −41°; use 41 to mirror), hachureGap |
line-based fills; dots uses fillWeight as the dot size and hachureGap as the spacing, and ignores hachureAngle |
dashLength, dashGap |
dashed fill |
zigzagWidth |
zigzag-line fill |
curveTightness |
curve shape |
curveStepCount, curveFitting |
curves, ellipses, circles and arcs |
roughness, bowing |
everything |
An option set to undefined counts as not set. Anything else is checked, and a value that cannot
be drawn throws an error naming the field or option, and the value where one was given:
- Every number a shape needs must be present and finite and within ±1,000,000. Sizes must not be
negative. Numbers given as strings are rejected, and the message quotes them (
got "10"). - Every number in
pathdata must be within that range as written, and again once relative coordinates are resolved and arcs are turned into curves. A large arc's curve control points can lie outside the drawn shape, so a path can be rejected even when everything it draws is in range. pointsmust be an array of[x, y]pairs: at least 3 forpolygon, 2 forlinearPathandcurve. An arc'sstopmust be greater than itsstart, andclosed, when given, a boolean.- Unknown option names are rejected (
Unknown option: "roughnes").roughnessandbowingmust be from 0 to 10,curveTightnessandcurveFittingfrom 0 to 1, andcurveStepCountan integer from 1 to 1000.hachureAnglemay be any finite number of degrees: 400 draws as 40 does.strokeLineDashmust be a plain array of 1 to 16 lengths from 0 to 1,000,000, with no holes, adding up to at least 1. strokeWidth,fillWeight,hachureGap,dashLength,dashGapandzigzagWidthmust be numbers from 0 to 1,000,000. Left out,strokeWidthis 1,fillWeighthalf the stroke width,hachureGapfour stroke widths, anddashLength,dashGapandzigzagWidththe hachure gap.strokeandfillmust be a colour (a hex code, a name,rgb(),rgba(),hsl()orhsla()), orurl(#id)naming a pattern or gradient in the same document, such aspattern().fill, at most 100 characters. Other CSS paints are rejected: aurl()naming another file would have the page fetch it, and avar()would paint with a value the page sets. So areinherit,initial,unsetandrevert, which are not colours.currentColoris a colour: it paints with thecolorof the element the drawing is in. The renderers check that the paints of a sketch built by hand are colours orurl(#id)of up to 1,000 characters, and throwInvalid sketch: stroke must be …otherwise.
Valid input can still describe an enormous amount of work, such as a dashed fill with no dash
length or path data with a million segments. Each sketch call therefore stops after 200,000
units of drawing work and throws Drawing too complex (more than 200000 operations) instead of
hanging or running out of memory. A call given a Budget (see below) stops at that budget's limit
instead. The budget is coarse, with two known limits:
- A drawing just under the budget can still take a few seconds and produce several megabytes of
path data (for example a fine
zigzag-linefill over a 3000 × 3000 shape). Set your own time and size limits aroundsketchwhen the input is untrusted. - A filled shape taller than about 66,000 units can exceed the budget even with a wide
hachureGap, and whether it does depends on the seed. Keep filled shapes smaller, or scale the drawing down.
To bound several drawings together, such as every shape of one chart, pass them one Budget:
import { Budget, sketch } from '@borrowdale/core';
const budget = new Budget(400_000);
const bars = data.map((bar) => sketch(bar, { fill: 'teal' }, budget));Every call then charges the same budget, and the call that takes the total past maxOperations
throws. budget.spent reads the total so far. maxOperations is a whole number from 1 to
Number.MAX_SAFE_INTEGER, and budget.maxOperations reads it back. A budget is never reset. Once
a call has taken it past its limit, every later call given it throws too, so make a new one for
each drawing. A budget's limit replaces the 200,000 of a single call, so the two known limits above
grow with it: a budget in the millions lets one call run out of memory. sketch accepts only a
budget made with new Budget().
Seeds and escaping
toSeed(value) turns any string or number into the seed a drawing uses, an integer from 1 to
2^31 − 1.
Without a seed, the seed comes from stableKey(shape): the shape serialised with its keys sorted.
{ x: 0, y: 0, … } and { y: 0, x: 0, … } are therefore drawn identically.
svgElement(tag, attributes, children?) builds an SVG element string and escapes every attribute
value, throwing for a character no escape can make fit (see escapeAttribute), as toSvgSketch
and toSvgDocument do. Use it to build your own markup around Borrowdale output:
svgElement('rect', { width: 300, height: 200, fill: '#fdf8ef' });
// <rect width="300" height="200" fill="#fdf8ef"/>children, and the defs and body of toSvgDocument, are inserted as markup without escaping.
Only pass markup you built yourself, never text from users. The tag and attribute names are written
as given too, so they must be your own literals: no escaping makes an attribute such as onload
safe.
escapeAttribute(value) escapes a single string for use inside a double-quoted attribute. It
throws for a character an XML document cannot hold (a control character other than tab, line feed
and carriage return, U+FFFE, U+FFFF or an unpaired surrogate), since no escape makes it fit and a
standalone SVG holding one would not draw at all.
Portable maths
@borrowdale/core/math exports the engine's own sin and cos, and a natural logarithm log, for
numbers that must be the same in every JavaScript engine (for example points on a circle placed next
to a drawing, or positions on a logarithmic scale):
import { cos, log, sin } from '@borrowdale/core/math';
const point = [cx + r * cos(angle), cy + r * sin(angle)];
const along = (log(value) - log(low)) / (log(high) - log(low));They give the same result in every JavaScript engine. That result can differ from Math.sin,
Math.cos and Math.log in the last bit; log is within one unit in the last place of the true
value. sin and cos accept a finite number of radians within ±1,000,000, exported as MAX_ANGLE;
not far past that range the result would be wrong without any sign of it. log accepts a finite
number above 0. Each throws a RangeError for anything else.
Values in error messages
Every @borrowdale package writes a rejected value into an error message the same way: strings
quoted and escaped, a short array in brackets, and anything that could throw or run code when
converted named by its kind. Every character of a message that could break or disguise a line of a
log, such as a line break or a bidirectional override, is written as its \u escape. Messages are
plain text, not HTML: escape them before putting them into a page.
The wording of these messages is for people and can change in any release, so do not match on it in code.
The helpers that do this, and the colour checks every package applies, live in
@borrowdale/core/internal, which the packages share. It is not public API and can change in any
release.
Rendering
| Function | Output | Needs |
|---|---|---|
toSvgSketch(sketch) |
<path> markup string |
nothing (SSR-safe) |
toSvgDocument({ width, height, defs?, body }) |
complete <svg> string |
nothing |
appendSketch(parent, sketch) |
appends <path> elements to an SVG element |
a DOM |
drawSketch(context2d, sketch, paint?) |
draws into a canvas | Path2D |
toSvgDocument and the paint of drawSketch reject an option name they do not take
(Unknown option: "def"); an option set to undefined counts as not set.
A dashed outline keeps its strokeLineDash in the sketch. toSvgSketch and appendSketch write
it as stroke-dasharray, and drawSketch sets it with setLineDash.
drawSketch draws every outline with its own dash, starting at its beginning, and a solid one solid,
whatever dash the canvas had. Outlines use butt caps, miter joins and a miter limit of 4, matching
SVG defaults. A hand-built sketch's dash list with negative or non-finite entries draws solid.
The caller's canvas state is restored even if drawing throws; the error still propagates and any
pixels already drawn remain. Caller transforms, clipping, opacity and compositing still apply.
The optional paint argument to drawSketch supplies stroke and/or fill overrides as
colours, CanvasGradient or CanvasPattern objects. It replaces existing paints without
enabling a stroke or fill whose value is none. A canvas cannot paint url(#id) and ignores it,
keeping the paint it had, so pass a CanvasPattern or CanvasGradient here for such a sketch.
Canvas state is restored after each path, including when drawing throws.
The type declarations refer to DOM types (Element, CanvasRenderingContext2D), so TypeScript
projects need "dom" in compilerOptions.lib, even when they only render on the server.
Validation without drawing
assertDrawable(shape, options) checks the same shape and style rules as sketch without drawing
anything. It returns nothing and throws on invalid input. It checks structure and ranges only;
drawing still stops at its work budget.
License
MIT. The stroke engine is ported from MIT-licensed code by Preet Shihn; its trigonometry uses constants from fdlibm, and its logarithm is a port of fdlibm's; see THIRD_PARTY_NOTICES.md.