Skip to content

Commit 721cbb4

Browse files
authored
Add docs for Charming Pretext (#414)
1 parent 176e8fd commit 721cbb4

5 files changed

Lines changed: 329 additions & 1 deletion

File tree

demo/pretext/us-map-paper/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function main() {
8686
fontStyle: "normal",
8787
fontVariant: "normal",
8888
fontWeight: "normal",
89-
fontSize: 18,
89+
fontSize: 16,
9090
};
9191

9292
const layer = document.querySelector("#stage .stage__inner");

docs/.vitepress/config.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ export default defineConfig({
2121
text: "Examples",
2222
link: "https://observablehq.com/d/18b3d6f3affff5bb",
2323
},
24+
{
25+
text: "Demos",
26+
items: [
27+
{
28+
text: "Pretext",
29+
link: "https://pretext.charmingjs.org/",
30+
},
31+
],
32+
},
2433
],
2534
sidebar: [
2635
{
@@ -59,6 +68,10 @@ export default defineConfig({
5968
text: "Charming Path",
6069
link: "/path",
6170
},
71+
{
72+
text: "Charming Pretext",
73+
link: "/charming-pretext",
74+
},
6275
],
6376
},
6477
],

docs/api-index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,11 @@ Build SVG path strings for common shapes.
3131
- [_cm_.**pathRect**](/path#cm-path-rect) — closed rectangle.
3232
- [_cm_.**pathEllipse**](/path#cm-path-ellipse) — closed axis-aligned ellipse.
3333
- [_cm_.**pathPolygon**](/path#cm-path-polygon) — closed path through `[x, y]` points.
34+
35+
## [Charming Pretext](/charming-pretext)
36+
37+
Text measurement and path-bound layout on top of [Pretext](https://github.com/chenglou/pretext).
38+
39+
- [_cm_.**prepare**](/charming-pretext#cm-prepare) — prepare a string with font options for Pretext.
40+
- [_cm_.**layoutTextInPath**](/charming-pretext#cm-layout-text-in-path) — flow prepared text through a closed SVG path.
41+
- [_cm_.**clearPrepareCache**](/charming-pretext#cm-clear-prepare-cache) — clear memoized prepare and Pretext caches.

docs/charming-pretext.md

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
# Charming Pretext
2+
3+
**Charming Pretext** is a small layer on top of [Pretext](https://github.com/chenglou/pretext) for text-based data visualization and generative art. It exposes an intuitive API for **flowing text inside closed shapes** described by an SVG [`d`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) path. Pure-arithmetic measurement and line breaking keep Charming Pretext fast without DOM layout.
4+
5+
![pretext-map](/pretext/map.png)
6+
7+
## Demos
8+
9+
Live examples run at [pretext.charmingjs.org](https://pretext.charmingjs.org/). The source for those demos lives in the repo under [`demo/pretext`](https://github.com/charming-art/api/tree/main/demo/pretext).
10+
11+
## Computing layout
12+
13+
Use [_cm_.**layoutTextInPath**](#cm-layout-text-in-path) with a string and a **closed** path. It returns `texts` (fragments with positions and angles), `lines` (hachure segments, useful for debugging), and the font fields and `path` you passed in.
14+
15+
```js eval
16+
const layout = cm.layoutTextInPath({
17+
text: "Hello, Charming Pretext! I love generative art!",
18+
path: cm.pathCircle(160, 160, 150), // Builds a circle path string.
19+
fontSize: 12,
20+
fontFamily: "Inter",
21+
});
22+
```
23+
24+
You then draw `layout.texts` with Canvas or SVG (see below).
25+
26+
## Rendering with Canvas
27+
28+
Each item in `layout.texts` has `text`, `x`, `y`, and `angle` (in degrees). Apply transforms to the context before drawing each fragment.
29+
30+
```js eval code=false
31+
renderPretextWithCanvas(Object.assign({}, layout, {width: 320, height: 320}));
32+
```
33+
34+
```js
35+
const context = cm.context2d({width: 400, height: 300});
36+
context.fillStyle = "#222";
37+
context.font = `${layout.fontSize}px ${layout.fontFamily}`;
38+
context.textAlign = "center";
39+
context.textBaseline = "middle";
40+
41+
for (const t of layout.texts) {
42+
context.save();
43+
context.translate(t.x, t.y);
44+
context.rotate((t.angle * Math.PI) / 180);
45+
context.fillText(t.text, 0, 0);
46+
context.restore();
47+
}
48+
```
49+
50+
## Rendering with SVG
51+
52+
With [_cm_.**svg**](/dom#cm-svg), bind `texts` to `<text>` nodes and use `transform` for position and rotation.
53+
54+
```js eval code=false
55+
renderPretext(Object.assign({}, layout, {width: 320, height: 320}));
56+
```
57+
58+
```js
59+
const svg = cm.svg`<svg ${{
60+
width: 320,
61+
height: 320,
62+
viewBox: "0 0 320 320",
63+
}}>
64+
<text ${{
65+
data: layout.texts,
66+
text_anchor: "middle",
67+
dominant_baseline: "central",
68+
font_size: `${layout.fontSize}px`,
69+
font_family: layout.fontFamily,
70+
transform: (d) => `translate(${d.x}, ${d.y}) rotate(${d.angle})`,
71+
textContent: (d) => d.text,
72+
}}/>
73+
</svg>`;
74+
```
75+
76+
## Setting line height
77+
78+
You can set line height by passing **`lineHeight`** into `layoutTextInPath`. The default is `fontSize * 1.5`.
79+
80+
```js eval code=false
81+
renderPretext(
82+
Object.assign(
83+
{},
84+
cm.layoutTextInPath({
85+
text: "Hello, Charming Pretext! I love generative art!",
86+
path: cm.pathCircle(160, 160, 150),
87+
fontSize: 12,
88+
fontFamily: "Inter",
89+
lineHeight: 30,
90+
}),
91+
{width: 320, height: 320},
92+
),
93+
);
94+
```
95+
96+
```js
97+
const layout = cm.layoutTextInPath({
98+
//...
99+
lineHeight: 30,
100+
});
101+
```
102+
103+
## Rotating text
104+
105+
You can also change how the fill lines run by passing **`angle`** (degrees) into `layoutTextInPath`. That rotates the hachure direction and gives you more control over how the text follows the shape.
106+
107+
```js eval code=false
108+
renderPretext(
109+
Object.assign(
110+
{},
111+
cm.layoutTextInPath({
112+
text: "Hello, Charming Pretext! I love generative art!",
113+
path: cm.pathCircle(160, 160, 150),
114+
fontSize: 12,
115+
fontFamily: "Inter",
116+
angle: 25,
117+
}),
118+
{width: 320, height: 320},
119+
),
120+
);
121+
```
122+
123+
```js
124+
const layout = cm.layoutTextInPath({
125+
//...
126+
angle: 25,
127+
});
128+
```
129+
130+
## Disabling repetition
131+
132+
By default, when the text runs out, the cursor resets and layout continues. Set **`repeat`** to `false` to stop instead of cycling—useful when you have enough text to fill the shape once.
133+
134+
```js eval code=false
135+
renderPretext(
136+
Object.assign(
137+
{},
138+
cm.layoutTextInPath({
139+
text: "Hello, Charming Pretext! I love generative art!",
140+
path: cm.pathCircle(160, 160, 150),
141+
fontSize: 12,
142+
fontFamily: "Inter",
143+
repeat: false,
144+
}),
145+
{width: 320, height: 320},
146+
),
147+
);
148+
```
149+
150+
```js
151+
const layout = cm.layoutTextInPath({
152+
//...
153+
repeat: false,
154+
});
155+
```
156+
157+
## Preparing explicitly
158+
159+
By default, `cm.layoutTextInPath` prepares your string from the given font options and memoizes that work by text and font settings. As long as those stay the same, you can call it again with a new `path` (or other options) without remeasuring the string.
160+
161+
If you prefer to avoid that machinery—or you want to reuse one prepared value yourself—call `cm.prepare` explicitly and pass the result as **`prepared`** to `cm.layoutTextInPath`.
162+
163+
```js
164+
const prepared = cm.prepare(longText, {
165+
fontSize: 14,
166+
fontFamily: "Inter",
167+
});
168+
169+
const layout = cm.layoutTextInPath({
170+
text: longText,
171+
prepared,
172+
path: cm.pathCircle(200, 200, 150),
173+
});
174+
```
175+
176+
## How it works
177+
178+
First, the path is turned into polylines with [`points-on-path`](https://github.com/subairui/points-on-path). Then [`hachure-fill`](https://github.com/rough-stuff/hachure-fill) generates parallel line segments inside the shape at `lineHeight` spacing, optionally rotated by `angle`. Finally, along each segment, Pretext’s `layoutNextLine` fills the available width with text from the prepared string. If you're interested in the implementation, please read the [source code](https://github.com/charming-art/api/blob/main/src/pretext/index.js) for more information. Suggestions and feedback are welcome!
179+
180+
## _cm_.prepare(_text_, _options_) {#cm-prepare}
181+
182+
Builds a Pretext **prepared** value with the specified options.
183+
184+
- **fontSize** — default `16`.
185+
- **fontFamily** — default `"Inter"`.
186+
- **fontStyle** — default `"normal"`.
187+
- **fontVariant** — default `"normal"`.
188+
- **fontWeight** — default `"normal"`.
189+
- Any extra keys are forwarded to Pretext’s [`prepareWithSegments`](https://github.com/chenglou/pretext).
190+
191+
The return value includes Pretext’s fields plus `fontSize`, `fontFamily`, `fontStyle`, `fontVariant`, and `fontWeight` for convenience.
192+
193+
```js
194+
const prepared = cm.prepare("Measure me", {
195+
fontSize: 16,
196+
fontFamily: "Inter",
197+
});
198+
```
199+
200+
## _cm_.layoutTextInPath(_options_) {#cm-layout-text-in-path}
201+
202+
Computes text positions with the specified options.
203+
204+
- **text** — source string (required).
205+
- **path** — closed SVG path `d` (required).
206+
- **fontSize** — default `16` (same as [`prepare`](#cm-prepare)).
207+
- **fontFamily** — default `"Inter"`.
208+
- **fontStyle** — default `"normal"`.
209+
- **fontVariant** — default `"normal"`.
210+
- **fontWeight** — default `"normal"`.
211+
- **prepared** — optional Pretext prepared object from `prepare`.
212+
- **lineHeight** — spacing between lines; default `fontSize * 1.5`.
213+
- **angle** — rotation of lines in degrees; default `0`.
214+
- **repeat** — whether to loop the text to fill the shape; default `true`.
215+
216+
Returns an object with:
217+
218+
- **texts** — array of fragments.
219+
- **lines** — array of segments `[[x1, y1], [x2, y2]]`.
220+
- **path** — the input `d` string.
221+
- **fontSize** — effective font size used for the layout.
222+
- **fontFamily** — effective font family.
223+
- **fontStyle** — effective font style.
224+
- **fontVariant** — effective font variant.
225+
- **fontWeight** — effective font weight.
226+
227+
## _cm_.clearPrepareCache() {#cm-clear-prepare-cache}
228+
229+
Clears Charming’s memoized `prepare` cache and Pretext’s global `clearCache()`. Call it in long-running apps or tests if you need to free memory or reset measurement state.
230+
231+
```js
232+
cm.clearPrepareCache();
233+
```
234+
235+
```js eval code=false inspector=false
236+
function renderPretext({
237+
texts,
238+
lines,
239+
width = 400,
240+
height = 400,
241+
fontSize = 16,
242+
fontFamily = "Inter",
243+
fontStyle = "normal",
244+
fontVariant = "normal",
245+
fontWeight = "normal",
246+
path,
247+
}) {
248+
return cm.svg`<svg ${{
249+
width,
250+
height,
251+
viewBox: `0 0 ${width} ${height}`,
252+
overflow: "visible",
253+
}}>
254+
<path ${{
255+
d: path,
256+
fill: "none",
257+
stroke: "black",
258+
stroke_width: 1,
259+
}}/>
260+
<text ${{
261+
data: texts,
262+
text_anchor: "middle",
263+
dominant_baseline: "central",
264+
font_size: `${fontSize}px`,
265+
font_family: fontFamily,
266+
font_style: fontStyle,
267+
font_variant: fontVariant,
268+
font_weight: fontWeight,
269+
transform: (text) => `translate(${text.x}, ${text.y}) rotate(${text.angle})`,
270+
textContent: (text) => text.text,
271+
}}/>
272+
</svg>`;
273+
}
274+
```
275+
276+
```js eval code=false inspector=false
277+
function renderPretextWithCanvas({
278+
texts,
279+
lines,
280+
width = 400,
281+
height = 400,
282+
fontSize = 16,
283+
fontFamily = "Inter",
284+
fontStyle = "normal",
285+
fontVariant = "normal",
286+
fontWeight = "normal",
287+
path,
288+
}) {
289+
const context = cm.context2d({width, height});
290+
context.fillStyle = "#222";
291+
context.font = `${fontSize}px ${fontFamily}`;
292+
context.textAlign = "center";
293+
context.textBaseline = "middle";
294+
295+
context.stroke(new Path2D(path));
296+
297+
for (const t of texts) {
298+
context.save();
299+
context.translate(t.x, t.y);
300+
context.rotate((t.angle * Math.PI) / 180);
301+
context.fillText(t.text, 0, 0);
302+
context.restore();
303+
}
304+
305+
return context.canvas;
306+
}
307+
```

docs/public/pretext/map.png

1.29 MB
Loading

0 commit comments

Comments
 (0)