@thi.ng/hiccup-svg
Advanced tools
Comparing version 5.0.37 to 5.0.38
# Change Log | ||
- **Last updated**: 2023-12-09T19:12:03Z | ||
- **Last updated**: 2023-12-11T10:07:09Z | ||
- **Generator**: [thi.ng/monopub](https://thi.ng/monopub) | ||
@@ -5,0 +5,0 @@ |
import { fattribs, ff } from "./format.js"; | ||
export const circle = (p, r, attribs, ...body) => [ | ||
"circle", | ||
fattribs({ | ||
...attribs, | ||
cx: ff(p[0]), | ||
cy: ff(p[1]), | ||
r: ff(r), | ||
}), | ||
...body, | ||
const circle = (p, r, attribs, ...body) => [ | ||
"circle", | ||
fattribs({ | ||
...attribs, | ||
cx: ff(p[0]), | ||
cy: ff(p[1]), | ||
r: ff(r) | ||
}), | ||
...body | ||
]; | ||
export { | ||
circle | ||
}; |
326
convert.js
@@ -16,173 +16,185 @@ import { implementsFunction } from "@thi.ng/checks/implements-function"; | ||
const ATTRIB_ALIASES = { | ||
alpha: "opacity", | ||
dash: "stroke-dasharray", | ||
dashOffset: "stroke-dashoffset", | ||
lineCap: "stroke-linecap", | ||
lineJoin: "stroke-linejoin", | ||
miterLimit: "stroke-miterlimit", | ||
weight: "stroke-width", | ||
alpha: "opacity", | ||
dash: "stroke-dasharray", | ||
dashOffset: "stroke-dashoffset", | ||
lineCap: "stroke-linecap", | ||
lineJoin: "stroke-linejoin", | ||
miterLimit: "stroke-miterlimit", | ||
weight: "stroke-width" | ||
}; | ||
const TEXT_ALIGN = { | ||
left: "start", | ||
right: "end", | ||
center: "middle", | ||
start: "start", | ||
end: "end", | ||
left: "start", | ||
right: "end", | ||
center: "middle", | ||
start: "start", | ||
end: "end" | ||
}; | ||
const BASE_LINE = { | ||
top: "text-top", | ||
bottom: "text-bottom", | ||
top: "text-top", | ||
bottom: "text-bottom" | ||
}; | ||
const precisionStack = []; | ||
/** | ||
* Takes a normalized hiccup tree of [`thi.ng/geom`](https://thi.ng/geom) and/or | ||
* [thi.ng/hdom-canvas](https://thi.ng/hdom-canvas) shape definitions and | ||
* recursively converts it into an hiccup flavor which is compatible for direct | ||
* SVG serialization. This conversion also involves translation, stringification | ||
* & reorg of various attributes. The function returns new tree. The original | ||
* remains untouched, as will any unrecognized tree/shape nodes. | ||
* | ||
* @remarks | ||
* The `__prec` control attribute can be used (on a per-shape basis) to control | ||
* the formatting used for various floating point values (except color | ||
* conversions). See {@link setPrecision}. Child shapes (of a group) inherit the | ||
* precision setting of their parent. | ||
* | ||
* To control the formatting precision for colors, use [the relevant function in | ||
* the thi.ng/color | ||
* package](https://docs.thi.ng/umbrella/color/functions/setPrecision.html). | ||
* | ||
* @param tree - shape tree | ||
*/ | ||
export const convertTree = (tree) => { | ||
if (tree == null) | ||
return null; | ||
if (implementsFunction(tree, "toHiccup")) { | ||
return convertTree(tree.toHiccup()); | ||
} | ||
const type = tree[0]; | ||
if (isArray(type)) { | ||
return tree.map(convertTree); | ||
} | ||
let attribs = convertAttribs(tree[1]); | ||
if (attribs.__prec) { | ||
precisionStack.push(PRECISION); | ||
setPrecision(attribs.__prec); | ||
} | ||
let result; | ||
switch (tree[0]) { | ||
case "svg": | ||
case "defs": | ||
case "a": | ||
case "g": | ||
result = [type, fattribs(attribs)]; | ||
for (let i = 2, n = tree.length; i < n; i++) { | ||
const c = convertTree(tree[i]); | ||
c != null && result.push(c); | ||
} | ||
break; | ||
case "linearGradient": | ||
result = linearGradient(attribs.id, attribs.from, attribs.to, tree[2], { | ||
gradientUnits: attribs.gradientUnits || "userSpaceOnUse", | ||
...(attribs.gradientTransform | ||
? { gradientTransform: attribs.gradientTransform } | ||
: null), | ||
}); | ||
break; | ||
case "radialGradient": | ||
result = radialGradient(attribs.id, attribs.from, attribs.to, attribs.r1, attribs.r2, tree[2], { | ||
gradientUnits: attribs.gradientUnits || "userSpaceOnUse", | ||
...(attribs.gradientTransform | ||
? { gradientTransform: attribs.gradientTransform } | ||
: null), | ||
}); | ||
break; | ||
case "circle": | ||
result = circle(tree[2], tree[3], attribs, ...tree.slice(4)); | ||
break; | ||
case "ellipse": | ||
result = ellipse(tree[2], tree[3][0], tree[3][1], attribs, ...tree.slice(4)); | ||
break; | ||
case "rect": { | ||
const r = tree[5] || 0; | ||
result = roundedRect(tree[2], tree[3], tree[4], r, r, attribs, ...tree.slice(6)); | ||
break; | ||
const convertTree = (tree) => { | ||
if (tree == null) | ||
return null; | ||
if (implementsFunction(tree, "toHiccup")) { | ||
return convertTree(tree.toHiccup()); | ||
} | ||
const type = tree[0]; | ||
if (isArray(type)) { | ||
return tree.map(convertTree); | ||
} | ||
let attribs = convertAttribs(tree[1]); | ||
if (attribs.__prec) { | ||
precisionStack.push(PRECISION); | ||
setPrecision(attribs.__prec); | ||
} | ||
let result; | ||
switch (tree[0]) { | ||
case "svg": | ||
case "defs": | ||
case "a": | ||
case "g": | ||
result = [type, fattribs(attribs)]; | ||
for (let i = 2, n = tree.length; i < n; i++) { | ||
const c = convertTree(tree[i]); | ||
c != null && result.push(c); | ||
} | ||
break; | ||
case "linearGradient": | ||
result = linearGradient( | ||
attribs.id, | ||
attribs.from, | ||
attribs.to, | ||
tree[2], | ||
{ | ||
gradientUnits: attribs.gradientUnits || "userSpaceOnUse", | ||
...attribs.gradientTransform ? { gradientTransform: attribs.gradientTransform } : null | ||
} | ||
case "line": | ||
result = line(tree[2], tree[3], attribs, ...tree.slice(4)); | ||
break; | ||
case "hline": | ||
result = hline(tree[2], attribs); | ||
break; | ||
case "vline": | ||
result = vline(tree[2], attribs); | ||
break; | ||
case "polyline": | ||
result = polyline(tree[2], attribs, ...tree.slice(3)); | ||
break; | ||
case "polygon": | ||
result = polygon(tree[2], attribs, ...tree.slice(3)); | ||
break; | ||
case "path": | ||
result = path(tree[2], attribs, ...tree.slice(3)); | ||
break; | ||
case "text": | ||
result = text(tree[2], tree[3], attribs, ...tree.slice(4)); | ||
break; | ||
case "img": | ||
result = image(tree[3], tree[2].src, attribs, ...tree.slice(4)); | ||
break; | ||
case "points": | ||
result = points(tree[2], attribs.shape, attribs.size, attribs, ...tree.slice(3)); | ||
break; | ||
case "packedPoints": | ||
result = packedPoints(tree[2], attribs.shape, attribs.size, attribs, ...tree.slice(3)); | ||
break; | ||
default: | ||
result = tree; | ||
); | ||
break; | ||
case "radialGradient": | ||
result = radialGradient( | ||
attribs.id, | ||
attribs.from, | ||
attribs.to, | ||
attribs.r1, | ||
attribs.r2, | ||
tree[2], | ||
{ | ||
gradientUnits: attribs.gradientUnits || "userSpaceOnUse", | ||
...attribs.gradientTransform ? { gradientTransform: attribs.gradientTransform } : null | ||
} | ||
); | ||
break; | ||
case "circle": | ||
result = circle(tree[2], tree[3], attribs, ...tree.slice(4)); | ||
break; | ||
case "ellipse": | ||
result = ellipse( | ||
tree[2], | ||
tree[3][0], | ||
tree[3][1], | ||
attribs, | ||
...tree.slice(4) | ||
); | ||
break; | ||
case "rect": { | ||
const r = tree[5] || 0; | ||
result = roundedRect( | ||
tree[2], | ||
tree[3], | ||
tree[4], | ||
r, | ||
r, | ||
attribs, | ||
...tree.slice(6) | ||
); | ||
break; | ||
} | ||
if (attribs.__prec) { | ||
setPrecision(precisionStack.pop()); | ||
} | ||
return result; | ||
case "line": | ||
result = line(tree[2], tree[3], attribs, ...tree.slice(4)); | ||
break; | ||
case "hline": | ||
result = hline(tree[2], attribs); | ||
break; | ||
case "vline": | ||
result = vline(tree[2], attribs); | ||
break; | ||
case "polyline": | ||
result = polyline(tree[2], attribs, ...tree.slice(3)); | ||
break; | ||
case "polygon": | ||
result = polygon(tree[2], attribs, ...tree.slice(3)); | ||
break; | ||
case "path": | ||
result = path(tree[2], attribs, ...tree.slice(3)); | ||
break; | ||
case "text": | ||
result = text(tree[2], tree[3], attribs, ...tree.slice(4)); | ||
break; | ||
case "img": | ||
result = image(tree[3], tree[2].src, attribs, ...tree.slice(4)); | ||
break; | ||
case "points": | ||
result = points( | ||
tree[2], | ||
attribs.shape, | ||
attribs.size, | ||
attribs, | ||
...tree.slice(3) | ||
); | ||
break; | ||
case "packedPoints": | ||
result = packedPoints( | ||
tree[2], | ||
attribs.shape, | ||
attribs.size, | ||
attribs, | ||
...tree.slice(3) | ||
); | ||
break; | ||
default: | ||
result = tree; | ||
} | ||
if (attribs.__prec) { | ||
setPrecision(precisionStack.pop()); | ||
} | ||
return result; | ||
}; | ||
const convertAttribs = (attribs) => { | ||
const res = {}; | ||
if (!attribs) | ||
return res; | ||
// convertTransforms(res, attribs); | ||
for (let id in attribs) { | ||
const v = attribs[id]; | ||
const aid = ATTRIB_ALIASES[id]; | ||
if (aid) { | ||
res[aid] = v; | ||
} | ||
else { | ||
convertAttrib(res, id, v); | ||
} | ||
const res = {}; | ||
if (!attribs) | ||
return res; | ||
for (let id in attribs) { | ||
const v = attribs[id]; | ||
const aid = ATTRIB_ALIASES[id]; | ||
if (aid) { | ||
res[aid] = v; | ||
} else { | ||
convertAttrib(res, id, v); | ||
} | ||
return res; | ||
} | ||
return res; | ||
}; | ||
const convertAttrib = (res, id, v) => { | ||
switch (id) { | ||
case "font": { | ||
const i = v.indexOf(" "); | ||
res["font-size"] = v.substring(0, i); | ||
res["font-family"] = v.substring(i + 1); | ||
break; | ||
} | ||
case "align": | ||
res["text-anchor"] = TEXT_ALIGN[v]; | ||
break; | ||
case "baseline": | ||
res["dominant-baseline"] = BASE_LINE[v] || v; | ||
break; | ||
// case "filter": | ||
// TODO needs to be translated into <filter> def first | ||
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter | ||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/filter | ||
// break; | ||
default: | ||
res[id] = v; | ||
switch (id) { | ||
case "font": { | ||
const i = v.indexOf(" "); | ||
res["font-size"] = v.substring(0, i); | ||
res["font-family"] = v.substring(i + 1); | ||
break; | ||
} | ||
case "align": | ||
res["text-anchor"] = TEXT_ALIGN[v]; | ||
break; | ||
case "baseline": | ||
res["dominant-baseline"] = BASE_LINE[v] || v; | ||
break; | ||
default: | ||
res[id] = v; | ||
} | ||
}; | ||
export { | ||
convertTree | ||
}; |
@@ -1,1 +0,4 @@ | ||
export const defs = (...defs) => ["defs", {}, ...defs]; | ||
const defs = (...defs2) => ["defs", {}, ...defs2]; | ||
export { | ||
defs | ||
}; |
import { fattribs, ff } from "./format.js"; | ||
export const ellipse = (p, rx, ry, attribs, ...body) => [ | ||
"ellipse", | ||
fattribs({ | ||
...attribs, | ||
cx: ff(p[0]), | ||
cy: ff(p[1]), | ||
rx: ff(rx), | ||
ry: ff(ry), | ||
}), | ||
...body, | ||
const ellipse = (p, rx, ry, attribs, ...body) => [ | ||
"ellipse", | ||
fattribs({ | ||
...attribs, | ||
cx: ff(p[0]), | ||
cy: ff(p[1]), | ||
rx: ff(rx), | ||
ry: ff(ry) | ||
}), | ||
...body | ||
]; | ||
export { | ||
ellipse | ||
}; |
217
format.js
import { isArrayLike } from "@thi.ng/checks/is-arraylike"; | ||
import { isString } from "@thi.ng/checks/is-string"; | ||
import { css } from "@thi.ng/color/css/css"; | ||
export let PRECISION = 3; | ||
/** | ||
* Sets the number of fractional digits used for formatting various floating | ||
* point values in the serialized SVG. The current value can be read via | ||
* {@link PRECISION}. | ||
* | ||
* @param n | ||
*/ | ||
export const setPrecision = (n) => (PRECISION = n); | ||
/** @internal */ | ||
export const ff = (x) => x === (x | 0) ? String(x) : x.toFixed(PRECISION); | ||
/** @internal */ | ||
export const fpoint = (p) => ff(p[0]) + "," + ff(p[1]); | ||
/** @internal */ | ||
export const fpoints = (pts, sep = " ") => pts ? pts.map(fpoint).join(sep) : ""; | ||
let PRECISION = 3; | ||
const setPrecision = (n) => PRECISION = n; | ||
const ff = (x) => x === (x | 0) ? String(x) : x.toFixed(PRECISION); | ||
const fpoint = (p) => ff(p[0]) + "," + ff(p[1]); | ||
const fpoints = (pts, sep = " ") => pts ? pts.map(fpoint).join(sep) : ""; | ||
const DEFAULT_NUMERIC_IDS = [ | ||
"font-size", | ||
"opacity", | ||
"stroke-width", | ||
"stroke-miterlimit", | ||
"font-size", | ||
"opacity", | ||
"stroke-width", | ||
"stroke-miterlimit" | ||
]; | ||
/** | ||
* Takes an attributes object and a number of attrib IDs whose values should be | ||
* formatted using {@link ff}. Mutates and returns `attribs` object. | ||
* | ||
* @param attribs - | ||
* @param ids - | ||
* | ||
* @internal | ||
*/ | ||
const numericAttribs = (attribs, ids) => { | ||
let v; | ||
for (let id of DEFAULT_NUMERIC_IDS.concat(ids)) { | ||
typeof (v = attribs[id]) === "number" && (attribs[id] = ff(v)); | ||
} | ||
return attribs; | ||
let v; | ||
for (let id of DEFAULT_NUMERIC_IDS.concat(ids)) { | ||
typeof (v = attribs[id]) === "number" && (attribs[id] = ff(v)); | ||
} | ||
return attribs; | ||
}; | ||
/** | ||
* Takes an attributes object and converts any `fill`, `stroke` or | ||
* transformation attributes, i.e. `transform`, `rotate`, `scale`, `translate`. | ||
* | ||
* @remarks | ||
* If the element has a `transform` attrib, conversion of the other attribs will | ||
* be skipped, else the values are assumed to be either strings or: | ||
* | ||
* - `transform`: 6-element numeric array (mat23) | ||
* - `translate`: 2-element array | ||
* - `rotate`: number (angle in radians) | ||
* - `scale`: number (uniform scale) or 2-elem array | ||
* | ||
* If no `transform` is given, the resulting transformation order will always be | ||
* TRS. Any string values given will be used as-is and therefore need to be | ||
* complete, e.g. `{ rotate: "rotate(60)" }` | ||
* | ||
* For color related attribs (`fill`, `stroke`), if given value is array-like, a | ||
* number or an | ||
* [`IColor`](https://docs.thi.ng/umbrella/color/interfaces/IColor.html) | ||
* instance, it will be converted into a CSS color string using | ||
* [`css()`](https://docs.thi.ng/umbrella/color/functions/css.html). | ||
* | ||
* String color attribs prefixed with `$` are replaced with `url(#...)` refs | ||
* (used for referencing gradients). | ||
* | ||
* Additional attribute names given (via rest args) will be formatted as numeric | ||
* values (using configured precision, see {@link setPrecision}). Formatting is | ||
* done via {@link numericAttribs}. | ||
* | ||
* Returns updated attribs or `undefined` if `attribs` itself is null-ish. | ||
* | ||
* @param attribs - attributes object | ||
* @param numericIDs - numeric attribute names | ||
* | ||
* @internal | ||
*/ | ||
export const fattribs = (attribs, ...numericIDs) => { | ||
if (!attribs) | ||
return; | ||
const res = ftransforms(attribs); | ||
let v; | ||
(v = attribs.fill) && (res.fill = fcolor(v)); | ||
(v = attribs.stroke) && (res.stroke = fcolor(v)); | ||
return numericAttribs(attribs, numericIDs); | ||
const fattribs = (attribs, ...numericIDs) => { | ||
if (!attribs) | ||
return; | ||
const res = ftransforms(attribs); | ||
let v; | ||
(v = attribs.fill) && (res.fill = fcolor(v)); | ||
(v = attribs.stroke) && (res.stroke = fcolor(v)); | ||
return numericAttribs(attribs, numericIDs); | ||
}; | ||
/** | ||
* Converts any transformation related attribs. | ||
* | ||
* {@link fattribs} | ||
* | ||
* @param attribs - attributes object | ||
* | ||
* @internal | ||
*/ | ||
const ftransforms = (attribs) => { | ||
let v; | ||
if ((v = attribs.transform) || | ||
attribs.translate || | ||
attribs.scale || | ||
attribs.rotate) { | ||
if (v) { | ||
attribs.transform = !isString(v) | ||
? `matrix(${[...v].map(ff).join(" ")})` | ||
: v; | ||
delete attribs.translate; | ||
delete attribs.rotate; | ||
delete attribs.scale; | ||
} | ||
else { | ||
attribs.transform = buildTransform(attribs); | ||
} | ||
let v; | ||
if ((v = attribs.transform) || attribs.translate || attribs.scale || attribs.rotate) { | ||
if (v) { | ||
attribs.transform = !isString(v) ? `matrix(${[...v].map(ff).join(" ")})` : v; | ||
delete attribs.translate; | ||
delete attribs.rotate; | ||
delete attribs.scale; | ||
} else { | ||
attribs.transform = buildTransform(attribs); | ||
} | ||
return attribs; | ||
} | ||
return attribs; | ||
}; | ||
/** | ||
* @internal | ||
*/ | ||
const buildTransform = (attribs) => { | ||
const tx = []; | ||
let v; | ||
if ((v = attribs.translate)) { | ||
tx.push(isString(v) ? v : `translate(${ff(v[0])} ${ff(v[1])})`); | ||
delete attribs.translate; | ||
} | ||
if ((v = attribs.rotate)) { | ||
tx.push(isString(v) ? v : `rotate(${ff((v * 180) / Math.PI)})`); | ||
delete attribs.rotate; | ||
} | ||
if ((v = attribs.scale)) { | ||
tx.push(isString(v) | ||
? v | ||
: isArrayLike(v) | ||
? `scale(${ff(v[0])} ${ff(v[1])})` | ||
: `scale(${ff(v)})`); | ||
delete attribs.scale; | ||
} | ||
return tx.join(" "); | ||
const tx = []; | ||
let v; | ||
if (v = attribs.translate) { | ||
tx.push(isString(v) ? v : `translate(${ff(v[0])} ${ff(v[1])})`); | ||
delete attribs.translate; | ||
} | ||
if (v = attribs.rotate) { | ||
tx.push(isString(v) ? v : `rotate(${ff(v * 180 / Math.PI)})`); | ||
delete attribs.rotate; | ||
} | ||
if (v = attribs.scale) { | ||
tx.push( | ||
isString(v) ? v : isArrayLike(v) ? `scale(${ff(v[0])} ${ff(v[1])})` : `scale(${ff(v)})` | ||
); | ||
delete attribs.scale; | ||
} | ||
return tx.join(" "); | ||
}; | ||
/** | ||
* Attempts to convert a single color attrib value. If `col` is prefixed with | ||
* `$`, the value will be converted into a `url(#...)` reference. If not a | ||
* string already, it will be converted into a CSS color string using | ||
* [`css()`](https://docs.thi.ng/umbrella/color/functions/css.html) | ||
* | ||
* {@link fattribs} | ||
* | ||
* @param col - color value | ||
* | ||
* @internal | ||
*/ | ||
export const fcolor = (col) => isString(col) | ||
? col[0] === "$" | ||
? `url(#${col.substring(1)})` | ||
: col | ||
: css(col); | ||
/** @internal */ | ||
export const withoutKeys = (src, keys) => { | ||
const dest = {}; | ||
for (let k in src) { | ||
src.hasOwnProperty(k) && !keys.has(k) && (dest[k] = src[k]); | ||
} | ||
return dest; | ||
const fcolor = (col) => isString(col) ? col[0] === "$" ? `url(#${col.substring(1)})` : col : css(col); | ||
const withoutKeys = (src, keys) => { | ||
const dest = {}; | ||
for (let k in src) { | ||
src.hasOwnProperty(k) && !keys.has(k) && (dest[k] = src[k]); | ||
} | ||
return dest; | ||
}; | ||
export { | ||
PRECISION, | ||
fattribs, | ||
fcolor, | ||
ff, | ||
fpoint, | ||
fpoints, | ||
setPrecision, | ||
withoutKeys | ||
}; |
import { fattribs, fcolor, ff } from "./format.js"; | ||
const RE_ALPHA_COLOR = /(rgb|hsl)a\(([a-z0-9.-]+),([0-9.%]+),([0-9.%]+),([0-9.]+)\)/; | ||
const gradient = (type, attribs, stops) => [ | ||
type, | ||
fattribs(attribs), | ||
...stops.map(gradientStop), | ||
type, | ||
fattribs(attribs), | ||
...stops.map(gradientStop) | ||
]; | ||
const gradientStop = ([offset, col]) => { | ||
col = fcolor(col); | ||
// use stop-opacity attrib for safari compatibility | ||
// https://stackoverflow.com/a/26220870/294515 | ||
let opacity; | ||
const parts = RE_ALPHA_COLOR.exec(col); | ||
if (parts) { | ||
col = `${parts[1]}(${parts[2]},${parts[3]},${parts[4]})`; | ||
opacity = parts[5]; | ||
} | ||
return ["stop", { offset, "stop-color": col, "stop-opacity": opacity }]; | ||
col = fcolor(col); | ||
let opacity; | ||
const parts = RE_ALPHA_COLOR.exec(col); | ||
if (parts) { | ||
col = `${parts[1]}(${parts[2]},${parts[3]},${parts[4]})`; | ||
opacity = parts[5]; | ||
} | ||
return ["stop", { offset, "stop-color": col, "stop-opacity": opacity }]; | ||
}; | ||
export const linearGradient = (id, from, to, stops, attribs) => gradient("linearGradient", { | ||
const linearGradient = (id, from, to, stops, attribs) => gradient( | ||
"linearGradient", | ||
{ | ||
...attribs, | ||
@@ -26,5 +26,9 @@ id, | ||
x2: ff(to[0]), | ||
y2: ff(to[1]), | ||
}, stops); | ||
export const radialGradient = (id, from, to, fr, r, stops, attribs) => gradient("radialGradient", { | ||
y2: ff(to[1]) | ||
}, | ||
stops | ||
); | ||
const radialGradient = (id, from, to, fr, r, stops, attribs) => gradient( | ||
"radialGradient", | ||
{ | ||
...attribs, | ||
@@ -37,3 +41,9 @@ id, | ||
fr: ff(fr), | ||
r: ff(r), | ||
}, stops); | ||
r: ff(r) | ||
}, | ||
stops | ||
); | ||
export { | ||
linearGradient, | ||
radialGradient | ||
}; |
11
group.js
import { fattribs } from "./format.js"; | ||
export const group = (attribs, ...body) => [ | ||
"g", | ||
fattribs({ ...attribs }), | ||
...body, | ||
const group = (attribs, ...body) => [ | ||
"g", | ||
fattribs({ ...attribs }), | ||
...body | ||
]; | ||
export { | ||
group | ||
}; |
23
image.js
import { fattribs, ff } from "./format.js"; | ||
export const image = (pos, url, attribs, ...body) => [ | ||
"image", | ||
fattribs({ | ||
...attribs, | ||
// TODO replace w/ SVG2 `href` once Safari supports it | ||
"xlink:href": url, | ||
x: ff(pos[0]), | ||
y: ff(pos[1]), | ||
}), | ||
...body, | ||
const image = (pos, url, attribs, ...body) => [ | ||
"image", | ||
fattribs({ | ||
...attribs, | ||
// TODO replace w/ SVG2 `href` once Safari supports it | ||
"xlink:href": url, | ||
x: ff(pos[0]), | ||
y: ff(pos[1]) | ||
}), | ||
...body | ||
]; | ||
export { | ||
image | ||
}; |
29
line.js
import { fattribs, ff } from "./format.js"; | ||
export const line = (a, b, attribs, ...body) => [ | ||
"line", | ||
fattribs({ | ||
...attribs, | ||
x1: ff(a[0]), | ||
y1: ff(a[1]), | ||
x2: ff(b[0]), | ||
y2: ff(b[1]), | ||
}), | ||
...body, | ||
const line = (a, b, attribs, ...body) => [ | ||
"line", | ||
fattribs({ | ||
...attribs, | ||
x1: ff(a[0]), | ||
y1: ff(a[1]), | ||
x2: ff(b[0]), | ||
y2: ff(b[1]) | ||
}), | ||
...body | ||
]; | ||
export const hline = (y, attribs) => line([-1e6, y], [1e6, y], attribs); | ||
export const vline = (x, attribs) => line([x, -1e6], [x, 1e6], attribs); | ||
const hline = (y, attribs) => line([-1e6, y], [1e6, y], attribs); | ||
const vline = (x, attribs) => line([x, -1e6], [x, 1e6], attribs); | ||
export { | ||
hline, | ||
line, | ||
vline | ||
}; |
{ | ||
"name": "@thi.ng/hiccup-svg", | ||
"version": "5.0.37", | ||
"version": "5.0.38", | ||
"description": "SVG element functions for @thi.ng/hiccup & related tooling", | ||
@@ -27,3 +27,5 @@ "type": "module", | ||
"scripts": { | ||
"build": "yarn clean && tsc --declaration", | ||
"build": "yarn build:esbuild && yarn build:decl", | ||
"build:decl": "tsc --declaration --emitDeclarationOnly", | ||
"build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts", | ||
"clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc", | ||
@@ -37,8 +39,9 @@ "doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts", | ||
"dependencies": { | ||
"@thi.ng/checks": "^3.4.11", | ||
"@thi.ng/color": "^5.6.3", | ||
"@thi.ng/prefixes": "^2.2.7" | ||
"@thi.ng/checks": "^3.4.12", | ||
"@thi.ng/color": "^5.6.4", | ||
"@thi.ng/prefixes": "^2.2.8" | ||
}, | ||
"devDependencies": { | ||
"@microsoft/api-extractor": "^7.38.3", | ||
"esbuild": "^0.19.8", | ||
"rimraf": "^5.0.5", | ||
@@ -138,3 +141,3 @@ "tools": "^0.0.1", | ||
}, | ||
"gitHead": "25f2ac8ff795a432a930119661b364d4d93b59a0\n" | ||
"gitHead": "5e7bafedfc3d53bc131469a28de31dd8e5b4a3ff\n" | ||
} |
77
path.js
import { fattribs, ff, fpoint, fpoints } from "./format.js"; | ||
const DEG = 180 / Math.PI; | ||
export const path = (segments, attribs, ...body) => { | ||
let res = []; | ||
for (let seg of segments) { | ||
res.push(seg[0]); | ||
switch (seg[0].toLowerCase()) { | ||
case "a": | ||
res.push([ | ||
// rx | ||
ff(seg[1]), | ||
// ry | ||
ff(seg[2]), | ||
// x-axis (theta) | ||
ff(seg[3] * DEG), | ||
// xl | ||
seg[4] ? 1 : 0, | ||
// clockwise | ||
seg[5] ? 1 : 0, | ||
// target xy | ||
ff(seg[6][0]), | ||
ff(seg[6][1]), | ||
].join(",")); | ||
break; | ||
case "h": | ||
case "v": | ||
res.push(ff(seg[1])); | ||
break; | ||
case "m": | ||
case "l": | ||
res.push(fpoint(seg[1])); | ||
break; | ||
case "z": | ||
break; | ||
default: | ||
res.push(fpoints(seg.slice(1), ",")); | ||
} | ||
const path = (segments, attribs, ...body) => { | ||
let res = []; | ||
for (let seg of segments) { | ||
res.push(seg[0]); | ||
switch (seg[0].toLowerCase()) { | ||
case "a": | ||
res.push( | ||
[ | ||
// rx | ||
ff(seg[1]), | ||
// ry | ||
ff(seg[2]), | ||
// x-axis (theta) | ||
ff(seg[3] * DEG), | ||
// xl | ||
seg[4] ? 1 : 0, | ||
// clockwise | ||
seg[5] ? 1 : 0, | ||
// target xy | ||
ff(seg[6][0]), | ||
ff(seg[6][1]) | ||
].join(",") | ||
); | ||
break; | ||
case "h": | ||
case "v": | ||
res.push(ff(seg[1])); | ||
break; | ||
case "m": | ||
case "l": | ||
res.push(fpoint(seg[1])); | ||
break; | ||
case "z": | ||
break; | ||
default: | ||
res.push(fpoints(seg.slice(1), ",")); | ||
} | ||
return ["path", fattribs({ ...attribs, d: res.join("") }), ...body]; | ||
} | ||
return ["path", fattribs({ ...attribs, d: res.join("") }), ...body]; | ||
}; | ||
export { | ||
path | ||
}; |
143
points.js
import { fattribs, ff, withoutKeys } from "./format.js"; | ||
/** | ||
* Shape instancing group. | ||
* | ||
* @remarks | ||
* The `shape` arg can be an SVG shape `#id` defined elsewhere in the | ||
* document or set to `circle` or `rect` (default). | ||
* | ||
* The `size` arg is only used for the latter two shape types and | ||
* defines the radius or width respectively. | ||
* | ||
* @param pts - points | ||
* @param shape - shape type | ||
* @param size - point size/radius | ||
* @param attribs - attributes | ||
*/ | ||
export const points = (pts, shape, size = 1, attribs, ...body) => { | ||
const group = [ | ||
"g", | ||
fattribs(withoutKeys(attribs, new Set(["shape", "size"]))), | ||
...body, | ||
]; | ||
const href = buildSymbol(group, shape, size); | ||
for (let p of pts) { | ||
// TODO replace w/ SVG2 `href` once Safari supports it | ||
group.push(["use", { "xlink:href": href, x: ff(p[0]), y: ff(p[1]) }]); | ||
} | ||
return group; | ||
const points = (pts, shape, size = 1, attribs, ...body) => { | ||
const group = [ | ||
"g", | ||
fattribs(withoutKeys(attribs, /* @__PURE__ */ new Set(["shape", "size"]))), | ||
...body | ||
]; | ||
const href = buildSymbol(group, shape, size); | ||
for (let p of pts) { | ||
group.push(["use", { "xlink:href": href, x: ff(p[0]), y: ff(p[1]) }]); | ||
} | ||
return group; | ||
}; | ||
/** | ||
* Similar to {@link points}, but takes points from a single large flat | ||
* buffer of coordinates with arbitrary striding. | ||
* | ||
* @remarks | ||
* In addition to `shape` and `size`, the following attribs can be used | ||
* to define the index range and strides: | ||
* | ||
* - `start` - start index (default: 0) | ||
* - `num` - number of points (default: buffer length/2) | ||
* - `cstride` - component stride (default: 1) | ||
* - `estride` - element stride (default: 2) | ||
* | ||
* @param pts - flat point buffer | ||
* @param shape - shape type | ||
* @param size - point size/radius | ||
* @param attribs - other attributes | ||
*/ | ||
export const packedPoints = (pts, shape, size = 1, attribs, ...body) => { | ||
attribs = { | ||
start: 0, | ||
cstride: 1, | ||
estride: 2, | ||
...attribs, | ||
}; | ||
const { start, cstride, estride } = attribs; | ||
let num = attribs && attribs.num != null | ||
? attribs.num | ||
: ((pts.length - start) / estride) | 0; | ||
const group = [ | ||
"g", | ||
fattribs(withoutKeys(attribs, new Set(["start", "cstride", "estride", "shape", "size", "num"]))), | ||
...body, | ||
]; | ||
const href = buildSymbol(group, shape, size); | ||
for (let i = start; num-- > 0; i += estride) { | ||
// TODO replace w/ SVG2 `href` once Safari supports it | ||
group.push([ | ||
"use", | ||
{ "xlink:href": href, x: ff(pts[i]), y: ff(pts[i + cstride]) }, | ||
]); | ||
} | ||
return group; | ||
const packedPoints = (pts, shape, size = 1, attribs, ...body) => { | ||
attribs = { | ||
start: 0, | ||
cstride: 1, | ||
estride: 2, | ||
...attribs | ||
}; | ||
const { start, cstride, estride } = attribs; | ||
let num = attribs && attribs.num != null ? attribs.num : (pts.length - start) / estride | 0; | ||
const group = [ | ||
"g", | ||
fattribs( | ||
withoutKeys( | ||
attribs, | ||
/* @__PURE__ */ new Set(["start", "cstride", "estride", "shape", "size", "num"]) | ||
) | ||
), | ||
...body | ||
]; | ||
const href = buildSymbol(group, shape, size); | ||
for (let i = start; num-- > 0; i += estride) { | ||
group.push([ | ||
"use", | ||
{ "xlink:href": href, x: ff(pts[i]), y: ff(pts[i + cstride]) } | ||
]); | ||
} | ||
return group; | ||
}; | ||
const buildSymbol = (group, shape, size) => { | ||
let href; | ||
if (!shape || shape[0] !== "#") { | ||
href = "_" + ((Math.random() * 1e6) | 0).toString(36); | ||
group.push(["g", { opacity: 0 }, buildShape(shape, href, size)]); | ||
href = "#" + href; | ||
} | ||
else { | ||
href = shape; | ||
} | ||
return href; | ||
let href; | ||
if (!shape || shape[0] !== "#") { | ||
href = "_" + (Math.random() * 1e6 | 0).toString(36); | ||
group.push(["g", { opacity: 0 }, buildShape(shape, href, size)]); | ||
href = "#" + href; | ||
} else { | ||
href = shape; | ||
} | ||
return href; | ||
}; | ||
const buildShape = (shape, id, r) => { | ||
const rf = ff(r); | ||
if (shape === "circle") { | ||
return ["circle", { id, cx: 0, cy: 0, r: rf }]; | ||
} | ||
const rf2 = ff(-r / 2); | ||
return ["rect", { id, x: rf2, y: rf2, width: rf, height: rf }]; | ||
const rf = ff(r); | ||
if (shape === "circle") { | ||
return ["circle", { id, cx: 0, cy: 0, r: rf }]; | ||
} | ||
const rf2 = ff(-r / 2); | ||
return ["rect", { id, x: rf2, y: rf2, width: rf, height: rf }]; | ||
}; | ||
export { | ||
packedPoints, | ||
points | ||
}; |
import { fattribs, fpoints } from "./format.js"; | ||
export const polygon = (pts, attribs, ...body) => [ | ||
"polygon", | ||
fattribs({ | ||
...attribs, | ||
points: fpoints(pts), | ||
}), | ||
...body, | ||
const polygon = (pts, attribs, ...body) => [ | ||
"polygon", | ||
fattribs({ | ||
...attribs, | ||
points: fpoints(pts) | ||
}), | ||
...body | ||
]; | ||
export { | ||
polygon | ||
}; |
import { fattribs, fpoints } from "./format.js"; | ||
export const polyline = (pts, attribs, ...body) => [ | ||
"polyline", | ||
fattribs({ | ||
fill: "none", | ||
points: fpoints(pts), | ||
...attribs, | ||
}), | ||
...body, | ||
const polyline = (pts, attribs, ...body) => [ | ||
"polyline", | ||
fattribs({ | ||
fill: "none", | ||
points: fpoints(pts), | ||
...attribs | ||
}), | ||
...body | ||
]; | ||
export { | ||
polyline | ||
}; |
@@ -168,3 +168,3 @@ <!-- This file is generated - DO NOT EDIT! --> | ||
Package sizes (brotli'd, pre-treeshake): ESM: 2.36 KB | ||
Package sizes (brotli'd, pre-treeshake): ESM: 2.37 KB | ||
@@ -171,0 +171,0 @@ ## Dependencies |
32
rect.js
import { fattribs, ff } from "./format.js"; | ||
export const rect = (p, width, height, attribs, ...body) => roundedRect(p, width, height, 0, 0, attribs, ...body); | ||
export const roundedRect = (p, width, height, rx, ry, attribs, ...body) => { | ||
attribs = fattribs({ | ||
...attribs, | ||
x: ff(p[0]), | ||
y: ff(p[1]), | ||
width: ff(width), | ||
height: ff(height), | ||
}); | ||
if (rx > 0 || ry > 0) { | ||
attribs.rx = ff(rx); | ||
attribs.ry = ff(ry); | ||
} | ||
return ["rect", attribs, ...body]; | ||
const rect = (p, width, height, attribs, ...body) => roundedRect(p, width, height, 0, 0, attribs, ...body); | ||
const roundedRect = (p, width, height, rx, ry, attribs, ...body) => { | ||
attribs = fattribs({ | ||
...attribs, | ||
x: ff(p[0]), | ||
y: ff(p[1]), | ||
width: ff(width), | ||
height: ff(height) | ||
}); | ||
if (rx > 0 || ry > 0) { | ||
attribs.rx = ff(rx); | ||
attribs.ry = ff(ry); | ||
} | ||
return ["rect", attribs, ...body]; | ||
}; | ||
export { | ||
rect, | ||
roundedRect | ||
}; |
46
svg.js
import { XML_SVG, XML_XLINK } from "@thi.ng/prefixes/xml"; | ||
import { convertTree } from "./convert.js"; | ||
import { fattribs } from "./format.js"; | ||
/** | ||
* Defines an <svg> root element with default XML namespaces. By default | ||
* currently still defaults to SVG version to 1.1 to support Safari and other | ||
* legacy tooling. | ||
* | ||
* @remarks | ||
* If the `__convert` boolean attrib is enabled, all body elements will be | ||
* automatically converted using {@link convertTree}. The `__convert` attrib | ||
* will be removed afterward and is NOT going to be serialized in the final | ||
* output. | ||
* | ||
* @param attribs - attributes object | ||
* @param body - shape primitives | ||
*/ | ||
export const svg = (attribs, ...body) => { | ||
attribs = fattribs({ | ||
version: "1.1", | ||
xmlns: XML_SVG, | ||
"xmlns:xlink": XML_XLINK, | ||
...attribs, | ||
}, "width", "height", "stroke-width"); | ||
if (attribs.__convert) { | ||
delete attribs.__convert; | ||
body = body.map(convertTree); | ||
} | ||
return ["svg", attribs, ...body]; | ||
const svg = (attribs, ...body) => { | ||
attribs = fattribs( | ||
{ | ||
version: "1.1", | ||
xmlns: XML_SVG, | ||
"xmlns:xlink": XML_XLINK, | ||
...attribs | ||
}, | ||
"width", | ||
"height", | ||
"stroke-width" | ||
); | ||
if (attribs.__convert) { | ||
delete attribs.__convert; | ||
body = body.map(convertTree); | ||
} | ||
return ["svg", attribs, ...body]; | ||
}; | ||
export { | ||
svg | ||
}; |
21
text.js
import { fattribs, ff } from "./format.js"; | ||
export const text = (p, body, attribs, ...xs) => [ | ||
"text", | ||
fattribs({ | ||
...attribs, | ||
x: ff(p[0]), | ||
y: ff(p[1]), | ||
}), | ||
body, | ||
...xs, | ||
const text = (p, body, attribs, ...xs) => [ | ||
"text", | ||
fattribs({ | ||
...attribs, | ||
x: ff(p[0]), | ||
y: ff(p[1]) | ||
}), | ||
body, | ||
...xs | ||
]; | ||
export { | ||
text | ||
}; |
66822
6
817
Updated@thi.ng/checks@^3.4.12
Updated@thi.ng/color@^5.6.4
Updated@thi.ng/prefixes@^2.2.8