reladraw 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/render.js CHANGED
@@ -19,7 +19,7 @@ export const DARK_THEME = {
19
19
  mutedText: '#8b8b8b',
20
20
  link: '#5c5c7c',
21
21
  // Both sampled off the reference's machine glyphs. Note that the reference
22
- // gives each icon its own hue — the drive is grey, the laptop periwinkle, the
22
+ // gives each icon its own hue — the drive is gray, the laptop periwinkle, the
23
23
  // workstation violet — which is a drawing tool's per-shape default and not a
24
24
  // system. One pair for the whole set is the deliberate difference: an icon
25
25
  // should read as part of the diagram's palette, not as clip art dropped in.
@@ -64,11 +64,11 @@ export function render(layout, options = {}) {
64
64
  width: Math.ceil(ink.maxX) - Math.floor(ink.minX),
65
65
  height: Math.ceil(ink.maxY) - Math.floor(ink.minY),
66
66
  };
67
- const arrowColours = new Set(layout.links.map((link) => colourOf(link.appearance, theme.link)));
67
+ const arrowColors = new Set(layout.links.map((link) => lineOf(link.appearance, theme.link)));
68
68
  return [
69
69
  `<svg xmlns="http://www.w3.org/2000/svg" width="${canvas.width}" height="${canvas.height}" viewBox="${canvas.x} ${canvas.y} ${canvas.width} ${canvas.height}" font-family=${quote(measurer.fontFamily)} font-size="${fontSize}px">`,
70
70
  ' <defs>',
71
- ...[...arrowColours].map((colour) => arrowMarker(colour)),
71
+ ...[...arrowColors].map((color) => arrowMarker(color)),
72
72
  ' </defs>',
73
73
  ` <rect x="${canvas.x}" y="${canvas.y}" width="${canvas.width}" height="${canvas.height}" fill="${theme.background}"/>`,
74
74
  ...body,
@@ -85,7 +85,7 @@ function drawNode(node, theme, measurer, fontSize) {
85
85
  const textHeight = measurer.lineHeight(size);
86
86
  if (node.kind === 'note') {
87
87
  return sized(textBlock(node.lines, node.x, node.y, node.width, textHeight, size, {
88
- colour: colourOf(node.appearance, theme.text),
88
+ color: textOf(node.appearance, theme.text),
89
89
  align: 'start',
90
90
  }), size, fontSize);
91
91
  }
@@ -94,12 +94,12 @@ function drawNode(node, theme, measurer, fontSize) {
94
94
  const glyphSide = ICON_LINES * textHeight;
95
95
  if (shape.body !== undefined) {
96
96
  // No outline, no fill, no padding — the node is the picture. The label, if
97
- // there is one, sits under it and centred.
97
+ // there is one, sits under it and centered.
98
98
  const drawn = [drawIcon(shape.body, node.x + (node.width - glyphSide) / 2, node.y, glyphSide, theme)];
99
99
  if (node.lines.some((line) => line.length > 0)) {
100
100
  drawn.push(sized(textBlock(node.lines, node.x, node.y + glyphSide + ICON_GAP, node.width, textHeight, size, {
101
- colour: colourOf(node.appearance, theme.text),
102
- subColour: subtextOf(node.appearance, theme),
101
+ color: textOf(node.appearance, theme.text),
102
+ subColor: subtextOf(node.appearance, theme),
103
103
  align: 'middle',
104
104
  }), size, fontSize));
105
105
  }
@@ -108,25 +108,28 @@ function drawNode(node, theme, measurer, fontSize) {
108
108
  const parts = [];
109
109
  const face = faceOf(node);
110
110
  const container = node.children.length > 0;
111
- const stroke = colourOf(node.appearance, container ? theme.containerStroke : theme.boxStroke);
111
+ const border = borderOf(node.appearance, container ? theme.containerStroke : theme.boxStroke);
112
112
  const fill = fillOf(node.appearance, container ? theme.containerFill : theme.boxFill);
113
- const subColour = subtextOf(node.appearance, theme);
113
+ // A box is the one kind with two inkable parts, which is why its text needs
114
+ // a word of its own — `border:` cannot stand in for it.
115
+ const text = textOf(node.appearance, theme.text);
116
+ const subColor = subtextOf(node.appearance, theme);
114
117
  // Deck copies sit behind the front face, furthest back drawn first.
115
118
  for (let depth = node.deckLabels.length; depth >= 1; depth -= 1) {
116
119
  const x = face.x - depth * DECK_STEP;
117
120
  const y = face.y - depth * DECK_STEP;
118
- parts.push(` <path d="${outlinePath(shape.outline, x, y, face.width, face.height)}" fill="${theme.containerFill}" stroke="${stroke}"/>`);
121
+ parts.push(` <path d="${outlinePath(shape.outline, x, y, face.width, face.height)}" fill="${theme.containerFill}" stroke="${border}"/>`);
119
122
  const label = node.deckLabels[depth - 1];
120
123
  if (label !== undefined) {
121
124
  parts.push(sized(textBlock([label], x + PAD, y + PAD, face.width - PAD * 2, textHeight, size, {
122
- colour: theme.text,
125
+ color: text,
123
126
  align: 'start',
124
127
  }), size, fontSize));
125
128
  }
126
129
  }
127
- parts.push(` <path d="${outlinePath(shape.outline, face.x, face.y, face.width, face.height)}" fill="${fill}" stroke="${stroke}"/>`);
130
+ parts.push(` <path d="${outlinePath(shape.outline, face.x, face.y, face.width, face.height)}" fill="${fill}" stroke="${border}"/>`);
128
131
  for (const extra of outlineDetail(shape.outline, face.x, face.y, face.width, face.height)) {
129
- parts.push(` <path d="${extra}" fill="none" stroke="${stroke}"/>`);
132
+ parts.push(` <path d="${extra}" fill="none" stroke="${border}"/>`);
130
133
  }
131
134
  // The icon takes a column on the right and the label lays out in what is
132
135
  // left, which is the room the resolver already reserved for exactly this.
@@ -135,24 +138,27 @@ function drawNode(node, theme, measurer, fontSize) {
135
138
  const hasLabel = node.lines.some((line) => line.length > 0);
136
139
  const iconRoom = icon === undefined ? 0 : iconSide + (hasLabel ? ICON_GAP : 0);
137
140
  if (!container) {
138
- // A leaf centres its label in the box, both ways — in the room beside the
139
- // icon rather than the whole box, so the two sit side by side.
141
+ // A leaf centers its label in the box, both ways — in the room beside the
142
+ // icon rather than the whole box, so the two sit side by side. Centered
143
+ // across is only the default: a label of several lines may say `align`, and
144
+ // there is genuine slack between lines of unequal length to range them in.
145
+ const leafAlign = labelStyleFor(node.label, node.line, 'middle').align;
140
146
  const top = face.y + (face.height - node.lines.length * textHeight) / 2;
141
147
  parts.push(sized(textBlock(node.lines, face.x, top, face.width - iconRoom, textHeight, size, {
142
- colour: theme.text,
143
- subColour,
144
- align: 'middle',
148
+ color: text,
149
+ subColor,
150
+ align: leafAlign,
145
151
  }), size, fontSize));
146
152
  }
147
153
  else {
148
154
  // The label and the icon share a band at one end of the box, and the
149
155
  // resolver has already given the contents the other end. A heading is
150
- // ranged left at the top; a caption is centred at the bottom.
156
+ // ranged left at the top; a caption is centered at the bottom.
151
157
  const band = Math.max(node.lines.length * textHeight, iconSide);
152
158
  const bandTop = labelStyle.at === 'top' ? face.y + PAD : face.y + face.height - PAD - band;
153
159
  parts.push(sized(textBlock(node.lines, face.x + PAD, bandTop, face.width - PAD * 2 - iconRoom, textHeight, size, {
154
- colour: theme.text,
155
- subColour,
160
+ color: text,
161
+ subColor,
156
162
  align: labelStyle.align,
157
163
  }), size, fontSize));
158
164
  for (const child of node.children) {
@@ -161,7 +167,7 @@ function drawNode(node, theme, measurer, fontSize) {
161
167
  }
162
168
  if (icon !== undefined) {
163
169
  // A container's icon rides in the label's band, at whichever end that is; a
164
- // leaf's label is centred, so the icon centres with it. Both follow the
170
+ // leaf's label is centered, so the icon centers with it. Both follow the
165
171
  // label rather than being placed by a rule of their own, which is what
166
172
  // keeps an icon reading as part of the title block and not as a sticker.
167
173
  const left = face.x + face.width - PAD - iconSide;
@@ -226,12 +232,12 @@ function outlineDetail(shape, x, y, w, h) {
226
232
  /** One icon, scaled from its own grid onto a square of `side` at `x, y`. */
227
233
  function drawIcon(icon, x, y, side, theme) {
228
234
  const scale = side / icon.grid;
229
- const colour = (tone) => tone === 'ink' ? theme.iconInk : tone === 'shade' ? theme.iconShade : theme.background;
235
+ const color = (tone) => tone === 'ink' ? theme.iconInk : tone === 'shade' ? theme.iconShade : theme.background;
230
236
  const paths = icon.paths.map((path) => {
231
- const fill = path.fill === undefined ? 'none' : colour(path.fill);
237
+ const fill = path.fill === undefined ? 'none' : color(path.fill);
232
238
  const stroke = path.stroke === undefined
233
239
  ? ''
234
- : ` stroke="${colour(path.stroke)}" stroke-width="${ICON_STROKE}" stroke-linejoin="round"`;
240
+ : ` stroke="${color(path.stroke)}" stroke-width="${ICON_STROKE}" stroke-linejoin="round"`;
235
241
  return ` <path d="${path.d}" fill="${fill}"${stroke}/>`;
236
242
  });
237
243
  return [
@@ -243,12 +249,12 @@ function drawIcon(icon, x, y, side, theme) {
243
249
  // --- links -------------------------------------------------------------------
244
250
  function drawLink(link, ends, corridor, theme, measurer, fontSize) {
245
251
  const { start, end } = ends;
246
- const colour = colourOf(link.appearance, theme.link);
247
- const markerEnd = ` marker-end="url(#${markerId(colour)})"`;
248
- const markerStart = link.both ? ` marker-start="url(#${markerId(colour)}-back)"` : '';
252
+ const color = lineOf(link.appearance, theme.link);
253
+ const markerEnd = ` marker-end="url(#${markerId(color)})"`;
254
+ const markerStart = link.both ? ` marker-start="url(#${markerId(color)}-back)"` : '';
249
255
  // A named side is a statement about how the line should leave or arrive, so
250
256
  // it is drawn as a curve that actually does leave and arrive that way. With
251
- // neither side named there is nothing to honour and the line stays straight.
257
+ // neither side named there is nothing to honor and the line stays straight.
252
258
  const curved = start.side !== undefined || end.side !== undefined;
253
259
  const parts = [];
254
260
  // What the line actually covers, so the canvas can be sized to hold it. A
@@ -260,7 +266,7 @@ function drawLink(link, ends, corridor, theme, measurer, fontSize) {
260
266
  if (corridor) {
261
267
  const path = corridorPath(start, end, corridor);
262
268
  ink = union(ink, path.ink);
263
- parts.push(` <path d="${path.d}" fill="none" stroke="${colour}" stroke-width="${LINE_WIDTH}"${markerEnd}${markerStart}/>`);
269
+ parts.push(` <path d="${path.d}" fill="none" stroke="${color}" stroke-width="${LINE_WIDTH}"${markerEnd}${markerStart}/>`);
264
270
  // The label goes on the straight run rather than at the midpoint of the
265
271
  // whole path, so it sits in the gap the author asked the line to travel.
266
272
  midX = path.mid.x;
@@ -277,7 +283,7 @@ function drawLink(link, ends, corridor, theme, measurer, fontSize) {
277
283
  const by = (ends.bow?.y ?? 0) * lift;
278
284
  const c1 = { x: start.x + start.tx * reach + bx, y: start.y + start.ty * reach + by };
279
285
  const c2 = { x: end.x + end.tx * reach + bx, y: end.y + end.ty * reach + by };
280
- parts.push(` <path d="M ${round(start.x)} ${round(start.y)} C ${round(c1.x)} ${round(c1.y)}, ${round(c2.x)} ${round(c2.y)}, ${round(end.x)} ${round(end.y)}" fill="none" stroke="${colour}" stroke-width="${LINE_WIDTH}"${markerEnd}${markerStart}/>`);
286
+ parts.push(` <path d="M ${round(start.x)} ${round(start.y)} C ${round(c1.x)} ${round(c1.y)}, ${round(c2.x)} ${round(c2.y)}, ${round(end.x)} ${round(end.y)}" fill="none" stroke="${color}" stroke-width="${LINE_WIDTH}"${markerEnd}${markerStart}/>`);
281
287
  ink = union(ink, cubicExtent(start, c1, c2, end));
282
288
  // The point halfway along a cubic, which is where the label belongs.
283
289
  midX = (start.x + 3 * c1.x + 3 * c2.x + end.x) / 8;
@@ -298,19 +304,19 @@ function drawLink(link, ends, corridor, theme, measurer, fontSize) {
298
304
  x: end.x - run.x + ends.bow.x * lift,
299
305
  y: end.y - run.y + ends.bow.y * lift,
300
306
  };
301
- parts.push(` <path d="M ${round(start.x)} ${round(start.y)} C ${round(c1.x)} ${round(c1.y)}, ${round(c2.x)} ${round(c2.y)}, ${round(end.x)} ${round(end.y)}" fill="none" stroke="${colour}" stroke-width="${LINE_WIDTH}"${markerEnd}${markerStart}/>`);
307
+ parts.push(` <path d="M ${round(start.x)} ${round(start.y)} C ${round(c1.x)} ${round(c1.y)}, ${round(c2.x)} ${round(c2.y)}, ${round(end.x)} ${round(end.y)}" fill="none" stroke="${color}" stroke-width="${LINE_WIDTH}"${markerEnd}${markerStart}/>`);
302
308
  ink = union(ink, cubicExtent(start, c1, c2, end));
303
309
  midX = (start.x + 3 * c1.x + 3 * c2.x + end.x) / 8;
304
310
  midY = (start.y + 3 * c1.y + 3 * c2.y + end.y) / 8;
305
311
  }
306
312
  else {
307
- parts.push(` <line x1="${round(start.x)}" y1="${round(start.y)}" x2="${round(end.x)}" y2="${round(end.y)}" stroke="${colour}" stroke-width="${LINE_WIDTH}"${markerEnd}${markerStart}/>`);
313
+ parts.push(` <line x1="${round(start.x)}" y1="${round(start.y)}" x2="${round(end.x)}" y2="${round(end.y)}" stroke="${color}" stroke-width="${LINE_WIDTH}"${markerEnd}${markerStart}/>`);
308
314
  midX = (start.x + end.x) / 2;
309
315
  midY = (start.y + end.y) / 2;
310
316
  }
311
317
  if (link.label !== undefined) {
312
318
  // A link label breaks on ` / ` exactly as a box label does, so a two-line
313
- // caption on an arrow needs no vocabulary of its own. The block is centred
319
+ // caption on an arrow needs no vocabulary of its own. The block is centered
314
320
  // on the midpoint, which keeps a one-line label where it has always been.
315
321
  const size = fontSizeFor('link', link.appearance, fontSize, link.line);
316
322
  const textHeight = measurer.lineHeight(size);
@@ -328,9 +334,9 @@ function drawLink(link, ends, corridor, theme, measurer, fontSize) {
328
334
  maxY: top + height,
329
335
  });
330
336
  parts.push(sized(textBlock(lines, midX - width / 2, top, width, textHeight, size, {
331
- // A coloured link carries its meaning into its label; an uncoloured
337
+ // A colored link carries its meaning into its label; an uncolored
332
338
  // one leaves the words to read as ordinary text.
333
- colour: colourOf(link.appearance, theme.text),
339
+ color: textOf(link.appearance, lineOf(link.appearance, theme.text)),
334
340
  align: 'middle',
335
341
  }), size, fontSize));
336
342
  }
@@ -399,17 +405,17 @@ function cubicExtent(p0, c1, c2, p3) {
399
405
  const [minY, maxY] = span(p0.y, c1.y, c2.y, p3.y);
400
406
  return { minX, minY, maxX, maxY };
401
407
  }
402
- /** Walk out from the centre of a box toward a point, stopping at the border. */
408
+ /** Walk out from the center of a box toward a point, stopping at the border. */
403
409
  function edgePoint(box, toward) {
404
- const centre = centreOf(box);
405
- const dx = toward.x - centre.x;
406
- const dy = toward.y - centre.y;
410
+ const center = centerOf(box);
411
+ const dx = toward.x - center.x;
412
+ const dy = toward.y - center.y;
407
413
  if (dx === 0 && dy === 0)
408
- return centre;
414
+ return center;
409
415
  const scaleX = dx === 0 ? Infinity : box.width / 2 / Math.abs(dx);
410
416
  const scaleY = dy === 0 ? Infinity : box.height / 2 / Math.abs(dy);
411
417
  const scale = Math.min(scaleX, scaleY);
412
- return { x: centre.x + dx * scale, y: centre.y + dy * scale };
418
+ return { x: center.x + dx * scale, y: center.y + dy * scale };
413
419
  }
414
420
  // --- where a link meets a box -------------------------------------------------
415
421
  const SIDES = ['top', 'bottom', 'left', 'right'];
@@ -417,7 +423,7 @@ const SIDES = ['top', 'bottom', 'left', 'right'];
417
423
  * Work out where every link meets every box.
418
424
  *
419
425
  * An author names a *side* — `to: top` — and never a point on it. Alone on a
420
- * side a link lands at its centre; sharing the side with others, the points
426
+ * side a link lands at its center; sharing the side with others, the points
421
427
  * spread so they do not sit on top of each other. Which one goes where is
422
428
  * derived from where the far ends actually are, never chosen: of two links
423
429
  * arriving at one top edge, the one coming from further left arrives further
@@ -442,7 +448,7 @@ function planEndpoints(links, measurer, fontSize) {
442
448
  link,
443
449
  which: 'start',
444
450
  side: fromSide,
445
- toward: centreOf(faceOf(link.to)),
451
+ toward: centerOf(faceOf(link.to)),
446
452
  rank: rankIn(bundles.get(link), link, link.from, fromSide),
447
453
  });
448
454
  }
@@ -451,7 +457,7 @@ function planEndpoints(links, measurer, fontSize) {
451
457
  link,
452
458
  which: 'end',
453
459
  side: toSide,
454
- toward: centreOf(faceOf(link.from)),
460
+ toward: centerOf(faceOf(link.from)),
455
461
  rank: rankIn(bundles.get(link), link, link.to, toSide),
456
462
  });
457
463
  }
@@ -500,9 +506,9 @@ function planEndpoints(links, measurer, fontSize) {
500
506
  continue;
501
507
  }
502
508
  // With neither end named this is the straight line it always was, each end
503
- // aiming at the other box's centre.
504
- const start = partial.start ?? free(fromFace, partial.end ?? centreOf(toFace));
505
- const end = partial.end ?? free(toFace, partial.start ?? centreOf(fromFace));
509
+ // aiming at the other box's center.
510
+ const start = partial.start ?? free(fromFace, partial.end ?? centerOf(toFace));
511
+ const end = partial.end ?? free(toFace, partial.start ?? centerOf(fromFace));
506
512
  ends.set(link, { start, end, bow: bows.get(link) });
507
513
  }
508
514
  return ends;
@@ -553,8 +559,8 @@ function planBundles(links, measurer, fontSize) {
553
559
  const [first, second] = group.ends;
554
560
  const t0 = tangentOf(first.side);
555
561
  const t1 = tangentOf(second.side);
556
- const from = sideCentre(first);
557
- const to = sideCentre(second);
562
+ const from = sideCenter(first);
563
+ const to = sideCenter(second);
558
564
  const run = { x: to.x - from.x, y: to.y - from.y };
559
565
  // Nesting is a matter of which side of the line each end steps toward. Step
560
566
  // both ends to the same side of the run and the whole line translates;
@@ -587,7 +593,7 @@ function planBundles(links, measurer, fontSize) {
587
593
  * The sideways offset each link takes when several run between the same two
588
594
  * boxes and none of them names a side.
589
595
  *
590
- * An unnamed end has no side to spread along: it aims at the far box's centre
596
+ * An unnamed end has no side to spread along: it aims at the far box's center
591
597
  * and attaches wherever that ray crosses the border, so every link in such a
592
598
  * group produces the *same* ray and they are drawn on top of one another —
593
599
  * one visible line, every label stacked on one point. `planEndpoints` cannot
@@ -643,10 +649,10 @@ function planSpreads(links, measurer, fontSize) {
643
649
  for (const group of groups.values()) {
644
650
  if (group.links.length < 2)
645
651
  continue;
646
- const from = centreOf(faceOf(group.first));
652
+ const from = centerOf(faceOf(group.first));
647
653
  const sample = group.links[0];
648
654
  const other = sample.from === group.first ? sample.to : sample.from;
649
- const to = centreOf(faceOf(other));
655
+ const to = centerOf(faceOf(other));
650
656
  const dx = to.x - from.x;
651
657
  const dy = to.y - from.y;
652
658
  const length = Math.hypot(dx, dy) || 1;
@@ -750,10 +756,10 @@ function bowBundles(bundles, achieved) {
750
756
  * `aligned`, which adds rather than subtracts.
751
757
  */
752
758
  function laneStep(lanes, drift, measurer, fontSize) {
753
- const labelled = lanes.filter((link) => link.label !== undefined);
754
- if (labelled.length < 2)
759
+ const labeled = lanes.filter((link) => link.label !== undefined);
760
+ if (labeled.length < 2)
755
761
  return 0;
756
- const need = (axis) => Math.max(...labelled.map((link) => labelExtent(link.label, link.appearance, axis, measurer, fontSize, link.line)));
762
+ const need = (axis) => Math.max(...labeled.map((link) => labelExtent(link.label, link.appearance, axis, measurer, fontSize, link.line)));
757
763
  const along = (axis, reach) => reach === 0 ? Infinity : need(axis) / Math.abs(reach);
758
764
  return Math.min(along('x', drift.x), along('y', drift.y));
759
765
  }
@@ -774,7 +780,7 @@ function tangentOf(side) {
774
780
  return side === 'top' || side === 'bottom' ? { x: 1, y: 0 } : { x: 0, y: 1 };
775
781
  }
776
782
  /** The midpoint of one side of a box. */
777
- function sideCentre(end) {
783
+ function sideCenter(end) {
778
784
  const face = faceOf(end.node);
779
785
  const along = end.side === 'top' || end.side === 'bottom' ? face.width : face.height;
780
786
  const origin = end.side === 'top' || end.side === 'bottom' ? face.x : face.y;
@@ -811,19 +817,19 @@ function anchorOn(face, side, at) {
811
817
  /** An end with no side named: leave from the border, pointing at the far end. */
812
818
  function free(face, toward) {
813
819
  const point = edgePoint(face, toward);
814
- const centre = centreOf(face);
815
- const dx = point.x - centre.x;
816
- const dy = point.y - centre.y;
820
+ const center = centerOf(face);
821
+ const dx = point.x - center.x;
822
+ const dy = point.y - center.y;
817
823
  const length = Math.hypot(dx, dy) || 1;
818
824
  return { x: point.x, y: point.y, tx: dx / length, ty: dy / length };
819
825
  }
820
826
  /**
821
827
  * Walk from a point inside a box along a direction, stopping at the border.
822
828
  *
823
- * `edgePoint` walks from the centre, which is the only place a single line
829
+ * `edgePoint` walks from the center, which is the only place a single line
824
830
  * passes through. A fanned-out group's lines are parallel to that one and
825
831
  * beside it, so each needs the border crossing of its own line rather than of
826
- * the centre's — which is what keeps the group parallel instead of splayed.
832
+ * the center's — which is what keeps the group parallel instead of splayed.
827
833
  */
828
834
  function exitAlong(box, from, dir) {
829
835
  // A shift wider than the box leaves the origin outside it; clamping back in
@@ -849,8 +855,8 @@ function exitAlong(box, from, dir) {
849
855
  * of the two edges meeting there. Neither is a case in the code.
850
856
  */
851
857
  function parallelEnds(from, to, offset) {
852
- const a = centreOf(from);
853
- const b = centreOf(to);
858
+ const a = centerOf(from);
859
+ const b = centerOf(to);
854
860
  const dx = b.x - a.x;
855
861
  const dy = b.y - a.y;
856
862
  const length = Math.hypot(dx, dy) || 1;
@@ -987,7 +993,7 @@ function planCorridors(links, ends, measurer, fontSize) {
987
993
  return plans;
988
994
  }
989
995
  /**
990
- * An end whose side the author did not name aims at the far box's centre, which
996
+ * An end whose side the author did not name aims at the far box's center, which
991
997
  * is the wrong thing to aim at once the line has been told to go somewhere else
992
998
  * on the way. Point those ends at the corridor instead.
993
999
  */
@@ -1008,7 +1014,7 @@ function aimFreeEnds(links, ends, corridors) {
1008
1014
  }
1009
1015
  /**
1010
1016
  * How much room a link's label takes across the corridor — its depth in a
1011
- * horizontal channel, its width in a vertical one. Zero for an unlabelled link,
1017
+ * horizontal channel, its width in a vertical one. Zero for an unlabeled link,
1012
1018
  * which needs no more than the arrow spacing.
1013
1019
  *
1014
1020
  * `labelExtent` measures the knockout along whichever axis it is handed, and the
@@ -1073,19 +1079,19 @@ function sideAttr(link, key) {
1073
1079
  }
1074
1080
  return value;
1075
1081
  }
1076
- function arrowMarker(colour) {
1077
- const id = markerId(colour);
1082
+ function arrowMarker(color) {
1083
+ const id = markerId(color);
1078
1084
  return [
1079
1085
  ` <marker id="${id}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="${ARROW_MARKER_WIDTH}" markerHeight="${ARROW_MARKER_WIDTH}" orient="auto-start-reverse">`,
1080
- ` <path d="M 0 0 L 10 5 L 0 10 z" fill="${colour}"/>`,
1086
+ ` <path d="M 0 0 L 10 5 L 0 10 z" fill="${color}"/>`,
1081
1087
  ' </marker>',
1082
1088
  ` <marker id="${id}-back" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="${ARROW_MARKER_WIDTH}" markerHeight="${ARROW_MARKER_WIDTH}" orient="auto-start-reverse">`,
1083
- ` <path d="M 0 0 L 10 5 L 0 10 z" fill="${colour}"/>`,
1089
+ ` <path d="M 0 0 L 10 5 L 0 10 z" fill="${color}"/>`,
1084
1090
  ' </marker>',
1085
1091
  ].join('\n');
1086
1092
  }
1087
- function markerId(colour) {
1088
- return `arrow-${colour.replace(/[^a-zA-Z0-9]/g, '')}`;
1093
+ function markerId(color) {
1094
+ return `arrow-${color.replace(/[^a-zA-Z0-9]/g, '')}`;
1089
1095
  }
1090
1096
  /** The rectangle actually drawn. Differs from the node box only for a deck. */
1091
1097
  function faceOf(node) {
@@ -1096,7 +1102,7 @@ function faceOf(node) {
1096
1102
  height: node.height - node.inset,
1097
1103
  };
1098
1104
  }
1099
- function centreOf(box) {
1105
+ function centerOf(box) {
1100
1106
  return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
1101
1107
  }
1102
1108
  /**
@@ -1118,25 +1124,35 @@ function textBlock(lines, x, top, width, lineHeight, fontSize, style) {
1118
1124
  const baseline = top + index * lineHeight + lineHeight / 2 + fontSize * 0.35;
1119
1125
  // A label's first line is its name; anything after it is a qualifier, and
1120
1126
  // `subtext:` is how a box says that qualifier should read as secondary.
1121
- const colour = index === 0 ? style.colour : style.subColour ?? style.colour;
1122
- return ` <text x="${round(anchorX)}" y="${round(baseline)}" fill="${colour}" text-anchor="${style.align}">${escapeXml(line)}</text>`;
1127
+ const color = index === 0 ? style.color : style.subColor ?? style.color;
1128
+ return ` <text x="${round(anchorX)}" y="${round(baseline)}" fill="${color}" text-anchor="${style.align}">${escapeXml(line)}</text>`;
1123
1129
  })
1124
1130
  .filter((element) => element.length > 0)
1125
1131
  .join('\n');
1126
1132
  }
1127
1133
  /**
1128
- * A colour is written as the viewer will receive it — `#14532d`, or any CSS
1129
- * colour. The renderer keeps no list of colour words of its own, so a diagram
1134
+ * A color is written as the viewer will receive it — `#14532d`, or any CSS
1135
+ * color. The renderer keeps no list of color words of its own, so a diagram
1130
1136
  * is never limited to the ones somebody remembered to add here.
1137
+ *
1138
+ * Each names the part it colors, so each reads exactly one key. The word these
1139
+ * replaced, `stroke:`, named no part and meant a different one on every kind,
1140
+ * which is why a box's text could not be colored at all until `text:`.
1131
1141
  */
1132
- function colourOf(appearance, fallback) {
1133
- return appearance['stroke'] ?? fallback;
1142
+ function borderOf(appearance, fallback) {
1143
+ return appearance['border'] ?? fallback;
1144
+ }
1145
+ function lineOf(appearance, fallback) {
1146
+ return appearance['line'] ?? fallback;
1147
+ }
1148
+ function textOf(appearance, fallback) {
1149
+ return appearance['text'] ?? fallback;
1134
1150
  }
1135
1151
  /**
1136
- * The colour for every label line after the first, or undefined when the box
1152
+ * The color for every label line after the first, or undefined when the box
1137
1153
  * said nothing and all its lines should read alike. `muted` is the one reserved
1138
1154
  * word: it defers to the theme, so a label's qualifier stays readable when the
1139
- * theme changes. Anything else is a colour, same as `stroke` and `fill` take.
1155
+ * theme changes. Anything else is a color, same as `text` and `fill` take.
1140
1156
  */
1141
1157
  function subtextOf(appearance, theme) {
1142
1158
  const named = appearance['subtext'];