rough-markup
Version:
A markup-style wrapper for `rough.js`.
166 lines (161 loc) • 5.22 kB
JavaScript
import htm from 'htm';
import rough from 'roughjs';
/**
* Virtual node structure.
*
* @alpha
*/
class VNode {
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}
/**
* The SVG tag that renders the given string template to virtual nodes.
*
* @public
*/
const svg = htm.bind((type, props, ...children) => {
return new VNode(type, props, children);
});
/**
* A map between a type and the drawing function.
*/
const drawMap = {
line: (g, node) => {
const { x1, y1, x2, y2 } = node.props;
return g.line(toNumber(x1), toNumber(y1), toNumber(x2), toNumber(y2), node.props);
},
rect: (g, node) => {
const { x, y, width, height } = node.props;
return g.rectangle(toNumber(x), toNumber(y), toNumber(width), toNumber(height), node.props);
},
ellipse: (g, node) => {
const { cx, cy, rx, ry, width, height } = node.props;
return g.ellipse(toNumber(cx), toNumber(cy), typeof width !== "undefined" ? toNumber(width) : toNumber(rx) * 2, typeof height !== "undefined" ? toNumber(height) : toNumber(ry) * 2, node.props);
},
circle: (g, node) => {
const { x, y, r } = node.props;
return g.circle(toNumber(x), toNumber(y), toNumber(r) * 2, node.props);
},
linearPath: (g, node) => {
const points = Array.isArray(node.props.points)
? node.props.points
: `${node.props.points}`
.split(" ")
.map((p) => p.split(",").map(toNumber));
return g.linearPath(points, node.props);
},
polygon: (g, node) => {
const vertices = Array.isArray(node.props.vertices)
? node.props.vertices
: `${node.props.vertices}`
.split(" ")
.map((p) => p.split(",").map(toNumber));
return g.polygon(vertices, node.props);
},
arc: (g, node) => {
const { x, y, width, height, start, stop, closed } = node.props;
return g.arc(Number(x), Number(y), Number(width), Number(height), Number(start), Number(stop), Boolean(closed), node.props);
},
curve: (g, node) => {
const points = Array.isArray(node.props.points)
? node.props.points
: `${node.props.points}`
.split(" ")
.map((p) => p.split(",").map(toNumber));
return g.curve(points, node.props);
},
path: (g, node) => {
return g.path(node.props.d, node.props);
},
g: (g, node) => {
const subDrawables = renderToDrawables(node.children);
const translate = node.props.transform.match(/translate\(\s*(\s*-?\d+)\s*,\s*(-?\d+)\s*\)/);
if (translate) {
const delta = translate.slice(1);
subDrawables.forEach((d) => d.sets.forEach((s) => s.ops.forEach((o) => {
o.data = o.data.map((da, i) => da + parseInt(delta[i % 2]));
})));
}
return subDrawables;
},
};
/**
* Convert numbers or throw error.
*
* @param raw - the raw type.
* @returns number
*/
function toNumber(raw) {
const n = parseInt(raw, 10);
if (isNaN(n)) {
throw new Error("number is expected but got NaN");
}
return n;
}
/**
* Convert the virtual nodes into drawables.
*
* @param nodes - the virtual nodes.
* @returns rough drawables.
* @public
*/
function renderToDrawables(nodes, config) {
const g = rough.generator(config);
return nodes.reduce((drawables, node) => {
const draw = drawMap[node.type];
if (draw) {
const subDrawables = draw(g, node);
if (Array.isArray(subDrawables)) {
drawables.push(...subDrawables);
}
else {
drawables.push(subDrawables);
}
}
else {
drawables.push(...renderToDrawables(node.children));
}
return drawables;
}, []);
}
/**
* Render the virtuanl nodes and mounted to given element.
*
* @param node - the virtual nodes.
* @param el - the element (or css selector string) to draw on.
* @public
*/
function render(node, el, config) {
const drawables = renderToDrawables(Array.isArray(node) ? node : [node], config?.rough);
const element = typeof el === "string" ? document.querySelector(el) : el;
if (element instanceof HTMLCanvasElement) {
return renderToCanvas(drawables, element, config);
}
if (element instanceof SVGSVGElement) {
return renderToSvg(drawables, element, config);
}
throw new Error("unexpected element to render");
}
function renderToSvg(drawables, el, config) {
if (config?.clear) {
el.childNodes.forEach((c) => c.remove());
}
const rc = rough.svg(el, config?.rough);
drawables.forEach((drawable) => {
el.appendChild(rc.draw(drawable));
});
}
function renderToCanvas(drawables, el, config) {
if (config?.clear) {
el.getContext("2d")?.clearRect(0, 0, el.width, el.height);
}
const rc = rough.canvas(el, config?.rough);
drawables.forEach((drawable) => {
rc.draw(drawable);
});
}
export { VNode, render, renderToDrawables, svg };