Skip to content

Charming Path ​

Charming Path builds SVG d path strings from numbers, handy for <path>, getPointAtLength, or APIs such as layoutTextInPath that expect a path description. Shapes are axis-aligned in user space (no rotation in the path itself). cm.pathContext records paths as command arrays for cm.path.

js
(() => {
  const d = cm.pathCircle(100, 100, 80);
  return cm.svg`<svg ${{width: 200, height: 200}}>
    <path ${{fill: "none", stroke: "black", stroke_width: 2, d}} />
  </svg>`;
})();
js
const d = cm.pathCircle(100, 100, 80);

cm.svg`<svg ${{width: 200, height: 200}}>
  <path ${{fill: "none", stroke: "black", stroke_width: 2, d}} />
</svg>`;

cm.pathLine(x1, y1, x2, y2) ​

Returns an open path: M to (x1, y1), L to (x2, y2).

js
(() => {
  const d = cm.pathLine(10, 90, 90, 10);
  return cm.svg`<svg ${{width: 100, height: 100, viewBox: "0 0 100 100"}}>
    <path ${{fill: "none", stroke: "steelblue", stroke_width: 2, d}} />
  </svg>`;
})();
js
const d = cm.pathLine(10, 90, 90, 10);

cm.pathCircle(cx, cy, r) ​

Returns a closed circle centered at (cx, cy) with radius r, using two arc segments.

js
(() => {
  const d = cm.pathCircle(50, 50, 40);
  return cm.svg`<svg ${{width: 100, height: 100, viewBox: "0 0 100 100"}}>
    <path ${{fill: "none", stroke: "teal", stroke_width: 2, d}} />
  </svg>`;
})();
js
const d = cm.pathCircle(100, 100, 50);

cm.pathRect(x, y, width, height) ​

Returns a closed rectangle with top-left (x, y) and size width × height.

js
(() => {
  const d = cm.pathRect(15, 25, 70, 50);
  return cm.svg`<svg ${{width: 100, height: 100, viewBox: "0 0 100 100"}}>
    <path ${{fill: "none", stroke: "purple", stroke_width: 2, d}} />
  </svg>`;
})();
js
const d = cm.pathRect(0, 0, 100, 50);

cm.pathEllipse(cx, cy, rx, ry) ​

Returns a closed axis-aligned ellipse centered at (cx, cy) with radii rx and ry, using two elliptical arcs.

js
(() => {
  const d = cm.pathEllipse(50, 50, 42, 28);
  return cm.svg`<svg ${{width: 100, height: 100, viewBox: "0 0 100 100"}}>
    <path ${{fill: "none", stroke: "coral", stroke_width: 2, d}} />
  </svg>`;
})();
js
const d = cm.pathEllipse(100, 100, 60, 40);

cm.pathPolygon(points) ​

Returns a closed path through points, an array of [x, y] pairs. The first point is moved to with M; later points use L; the path ends with Z.

js
(() => {
  const d = cm.pathPolygon([
    [50, 10],
    [90, 90],
    [10, 90],
  ]);
  return cm.svg`<svg ${{width: 100, height: 100, viewBox: "0 0 100 100"}}>
    <path ${{fill: "none", stroke: "darkgreen", stroke_width: 2, d}} />
  </svg>`;
})();
js
const d = cm.pathPolygon([
  [0, 0],
  [100, 0],
  [50, 80],
]);

cm.pathContext() ​

Returns a d3-path-compatible context that records path commands as arrays instead of a string. Call the CanvasPathMethods on it (moveTo, lineTo, quadraticCurveTo, bezierCurveTo, arc, arcTo, rect, closePath), then context.toArray() to get the commands. Pass them as the d of cm.path, so a path from d3-shape or d3-geo can be drawn without parsing a string.

js
const context = cm.pathContext();
context.moveTo(0, 0);
context.lineTo(10, 0);
context.lineTo(10, 10);
context.closePath();
context.toArray(); // [["M", 0, 0], ["L", 10, 0], ["L", 10, 10], ["Z"]]

For example, with d3-geo:

js
const path = d3.geoPath(d3.geoOrthographic());

root.append(cm.path, {
  d: () => {
    const context = cm.pathContext();
    path.context(context)(d3.geoCircle()());
    return context.toArray();
  },
  stroke: "black",
});