@arraypress/covers 1.3.1 → 1.5.1

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +1 -1
  3. package/package.json +3 -2
  4. package/recipes/flyers/construction.json +36 -0
  5. package/recipes/flyers/fluted-poster.json +95 -0
  6. package/recipes/flyers/grid-poster.json +91 -0
  7. package/recipes/flyers/logofolio.json +60 -0
  8. package/recipes/flyers/pixel-mosaic.json +58 -0
  9. package/recipes/flyers/series-card.json +81 -0
  10. package/schema/recipe.schema.json +768 -4
  11. package/src/index.d.ts +9 -7
  12. package/src/json-recipe.js +5 -0
  13. package/src/layer-options.js +7 -3
  14. package/src/layers/construction.d.ts +34 -0
  15. package/src/layers/construction.js +137 -0
  16. package/src/layers/guides.d.ts +38 -0
  17. package/src/layers/guides.js +212 -0
  18. package/src/layers/index.d.ts +12 -0
  19. package/src/layers/index.js +6 -0
  20. package/src/layers/isolines.d.ts +21 -0
  21. package/src/layers/isolines.js +271 -0
  22. package/src/layers/mark-draw.d.ts +21 -0
  23. package/src/layers/mark-draw.js +93 -0
  24. package/src/layers/mark-geometry.d.ts +15 -0
  25. package/src/layers/mark-geometry.js +46 -0
  26. package/src/layers/marks.d.ts +25 -0
  27. package/src/layers/marks.js +102 -0
  28. package/src/layers/mosaic.d.ts +32 -0
  29. package/src/layers/mosaic.js +155 -0
  30. package/src/layers/outline.d.ts +26 -0
  31. package/src/layers/outline.js +105 -0
  32. package/src/layers.js +7 -6
  33. package/src/type/fallback.js +1 -1
  34. package/src/type/spans.d.ts +5 -1
  35. package/src/type/spans.js +26 -3
  36. package/src/type/symbols.d.ts +1 -1
  37. package/src/type/symbols.js +20 -2
  38. package/src/type/typeset.js +6 -2
  39. package/src/version.js +1 -1
package/src/index.d.ts CHANGED
@@ -14,12 +14,12 @@ import type { RepeatLayer } from './layers/repeat.js';
14
14
  import type { TextSpan } from './type/spans.js';
15
15
  import type { GlyphControl } from './type/glyphs.js';
16
16
  import type { Cut } from './formats/cut.js';
17
- import type { ShapeLayer, SvgLayer, TableLayer, BarcodeLayer, PerforationLayer, TearLayer, TextureLayer } from './layers/index.js';
17
+ import type { ShapeLayer, SvgLayer, TableLayer, BarcodeLayer, PerforationLayer, TearLayer, TextureLayer, MosaicLayer, MarksLayer, OutlineLayer, ConstructionLayer } from './layers/index.js';
18
18
  import type { Zones, PrintInfo, FormatName } from './formats/index.js';
19
19
  import type { Motion, MotionPreset, Easing } from './animate/index.js';
20
20
 
21
21
  export type { RepeatLayer, DefsLayer, UseLayer, TextSpan, GlyphControl, Cut, Motion, MotionPreset, Easing };
22
- export type { Color, Length, Rng, LayerTransform, EffectKind, Paint, TextPath, GroupLayer, ShapeLayer, SvgLayer, TableLayer, BarcodeLayer, PerforationLayer, TearLayer, TextureLayer, Zones, PrintInfo, FormatName };
22
+ export type { Color, Length, Rng, LayerTransform, EffectKind, Paint, TextPath, GroupLayer, ShapeLayer, SvgLayer, TableLayer, BarcodeLayer, PerforationLayer, TearLayer, TextureLayer, MosaicLayer, MarksLayer, OutlineLayer, ConstructionLayer, Zones, PrintInfo, FormatName };
23
23
  export type { ShapeSpec, TableCell } from './layers/index.js';
24
24
  export type { Format } from './formats/index.js';
25
25
  export type { LintIssue } from './lint/index.js';
@@ -310,8 +310,8 @@ export interface PatternLayer extends BaseLayer {
310
310
  fade?: 'none' | 'radial' | 'edges' | 'top' | 'bottom' | 'left' | 'right';
311
311
  /** Dashed or dotted lines (or `[on, off]` lengths). */
312
312
  dash?: 'dashed' | 'dotted' | true | [Length, Length];
313
- /** Grid / lines: N equal cells (inner lines only) instead of `size`. */
314
- divisions?: number;
313
+ /** Grid / lines: N equal cells (inner lines only) instead of `size`; `[cols, rows]` for a design grid. */
314
+ divisions?: number | [number, number];
315
315
  /** Regions (fractions) the lines break around. */
316
316
  gaps?: Region[];
317
317
  region?: Region;
@@ -415,8 +415,8 @@ export interface TextLabel {
415
415
 
416
416
  export interface TextLayer extends Omit<BaseLayer, 'transform'> {
417
417
  type: 'text';
418
- /** The text; an array is one line per item. (Optional with `spans`.) */
419
- content?: string | string[];
418
+ /** The text; an array is one line per item, and an item may be a span (`{ content, bold }`: a headline whose lines change weight). (Optional with `spans`.) */
419
+ content?: string | (string | TextSpan)[];
420
420
  /** First baseline x (px or `'%'`): places the text like SVG `<text x y>`, `align` acting as the anchor. */
421
421
  x?: Length | `${number}%`;
422
422
  y?: Length | `${number}%`;
@@ -470,6 +470,8 @@ export interface TextLayer extends Omit<BaseLayer, 'transform'> {
470
470
  font?: string;
471
471
  /** Default 600. */
472
472
  weight?: number;
473
+ /** The weight of spans and lines marked `bold: true`. Default 700. */
474
+ boldWeight?: number;
473
475
  style?: 'normal' | 'italic';
474
476
  /** Default 6% of the design unit (`sqrt(width × height)`). */
475
477
  size?: Length;
@@ -718,7 +720,7 @@ export interface RuleLayer extends BaseLayer {
718
720
  }
719
721
 
720
722
  export type Layer =
721
- | GroupLayer | ShapeLayer | SvgLayer | TableLayer | BarcodeLayer | PerforationLayer | TearLayer | TextureLayer
723
+ | GroupLayer | ShapeLayer | SvgLayer | TableLayer | BarcodeLayer | PerforationLayer | TearLayer | TextureLayer | MosaicLayer | MarksLayer | OutlineLayer | ConstructionLayer
722
724
  | InkLayer | DitherLayer | ContourLayer | WaveformLayer | TilesLayer | RuleLayer
723
725
  | DiagramLayer | PaperLayer | ScribbleLayer
724
726
  | SolidLayer | LinearLayer | RadialLayer | MeshLayer | StripesLayer | ImageLayer
@@ -31,6 +31,7 @@
31
31
  * | `{ "gradientUnder": [colors, angle, size] }` | the colours a centred square of `size` covers in a linear gradient |
32
32
  * | `{ "pick": [...] }`, `{ "range": [min, max] }`, `{ "int": [min, max] }` | seeded choices |
33
33
  * | `{ "add" \| "sub" \| "mul" \| "div" \| "min" \| "max": [a, b, …] }` | arithmetic; lengths like `"54vw"` become pixels |
34
+ * | `{ "round" \| "floor" \| "ceil": x }` | to a whole number (a grid's row count from the canvas) |
34
35
  * | `{ "ramp": [from, to, steps, amount?] }` | `steps` colours from `from` toward `to` |
35
36
  * | `{ "taper": [ratio, steps] }` | `[1, ratio, ratio², …]` |
36
37
  * | `{ "length": value }` | characters in a string (or a number's digits), items in an array |
@@ -77,6 +78,7 @@ const CANVAS = ['width', 'height', 'u', 'bleed', 'safe', 'mm', 'dpi', 'trimWidth
77
78
  const OPS = /** @type {Record<string, [number, number]>} */ ({
78
79
  mix: [3, 4], readable: [1, 3], gradientUnder: [3, 3], tone: [2, 2], length: [1, 1], pick: [1, Infinity], range: [2, 2], int: [2, 2],
79
80
  add: [1, Infinity], sub: [2, Infinity], mul: [1, Infinity], div: [2, 2], min: [1, Infinity], max: [1, Infinity],
81
+ round: [1, 1], floor: [1, 1], ceil: [1, 1],
80
82
  ramp: [3, 4], taper: [2, 2], reverse: [1, 1],
81
83
  join: [1, 2], upper: [1, 1], lower: [1, 1], slice: [2, 3],
82
84
  });
@@ -390,6 +392,9 @@ function op(s, name, raw) {
390
392
  case 'div': { const [a, b] = nums(); return a / b; }
391
393
  case 'min': return Math.min(...nums());
392
394
  case 'max': return Math.max(...nums());
395
+ case 'round': return Math.round(nums()[0]);
396
+ case 'floor': return Math.floor(nums()[0]);
397
+ case 'ceil': return Math.ceil(nums()[0]);
393
398
  case 'ramp': {
394
399
  const [from, to, steps, amount = 1] = args;
395
400
  return Array.from({ length: steps }, (_, i) => mixOklab(toHex(from), toHex(to), (i / steps) * amount));
@@ -16,12 +16,12 @@ export const LAYER_OPTIONS = {
16
16
  stripes: { direction: 'vertical | horizontal | diagonal', angle: 'For diagonal.', count: 'Stripes. Default 8.', widths: 'even | random | taper | [weights]', taper: 'Ratio for widths: taper.', colors: 'Cycled.', gap: 'Length.', radius: 'Length.', region: '{ x, y, width, height } fractions.', regionRadius: 'Length.' },
17
17
  image: { x: 'Box left, px or "%" (with y, width, height: an explicit box instead of region).', y: 'Box top.', width: 'Box width.', height: 'Box height.', src: 'A local path, data: URI, URL (where allowed) or bytes ("{image}").', fit: 'cover | contain | stretch', focus: 'An anchor, attention or entropy.', region: 'Fractions.', duotone: 'true or [shadow, highlight].', grayscale: 'boolean', normalize: 'Stretch tones first.', tint: 'A colour or { color, amount, blend }.', blur: 'Length.' },
18
18
  grain: { amount: '0–1. Default 0.08.', size: 'Grain px. Default 1.', monochrome: 'Default true.', vary: '"edges": heavier toward the edges (spray paint).' },
19
- pattern: { kind: 'grid | dots | lines | rings | waves', size: 'Cell. Length.', stroke: 'Length.', color: 'Default @text.', angle: 'For lines.', center: '{ x, y } fractions.', wavelength: 'Length.', amplitude: 'Length.', fade: 'none | radial | edges | top | bottom | left | right', dash: 'dotted | dashed | [on, off] lengths.', divisions: 'Grid/lines: N equal cells (inner lines only) instead of size.', gaps: '[{ x, y, width, height }] fractions the lines break around.', region: 'Fractions.' },
19
+ pattern: { kind: 'grid | dots | lines | rings | waves', size: 'Cell. Length.', stroke: 'Length.', color: 'Default @text.', angle: 'For lines.', center: '{ x, y } fractions.', wavelength: 'Length.', amplitude: 'Length.', fade: 'none | radial | edges | top | bottom | left | right', dash: 'dotted | dashed | [on, off] lengths.', divisions: 'Grid/lines: N equal cells (inner lines only) instead of size; [cols, rows] for a design grid.', gaps: '[{ x, y, width, height }] fractions the lines break around.', region: 'Fractions.' },
20
20
  shapes: { kind: 'circle | square | pill | mixed', count: 'Default 6.', size: '[min, max] lengths.', colors: 'Cycled.', placement: 'scatter | row | column', items: '[{ kind, x, y, size, color, rotate, opacity }]', region: 'Fractions.', rotate: 'Tilt squares and pills.', radius: 'Square corner, fraction of size.', outline: 'true or a width.' },
21
21
  icon: { fill: 'A gradient, tile or preset paint for the icon (instead of color).', markup: 'Inner SVG markup on a 512 grid (a mark\'s paths); viewBox overrides the grid.', x: 'Left, px (with y: placed by its top-left corner).', y: 'Top, px.', name: 'Iconify name ("lucide:feather") or raw SVG ("{icon}").', svg: 'Raw SVG.', path: 'A path d.', viewBox: '[x, y, w, h] for path.', size: 'Length. Default "24vmin".', color: 'Default the accent on a tile, else @text.', strokeWidth: 'Stroke icons.', anchor: 'One of nine anchors.', position: '{ x, y } fractions.', offset: '{ x, y } lengths.', margin: 'Length.', tile: 'true or { size, radius, color, shadow, border }.' },
22
- text: { content: 'The text ("{title}").', font: 'Family. Default Inter.', weight: 'Default 600.', style: 'normal | italic', size: 'Length.', minSize: 'Shrink toward this before truncating.', lineHeight: 'Default 1.12.', letterSpacing: 'Em. Default -0.02.', color: 'Default @text.', align: 'left | center | right', anchor: 'Default bottom-left.', position: '{ x, y } fractions.', offset: '{ x, y } lengths.', margin: 'Length.', maxWidth: 'Length.', maxLines: 'Default 3.', ellipsis: "Default '…'.", balance: 'Default true.', transform: 'none | uppercase | lowercase', label: 'A string or { content, size, weight, color, letterSpacing, transform, gap }.', background: 'A colour or { color, radius, padding, opacity }.',
22
+ text: { content: 'The text ("{title}"); an array is a line per item, an item may be a span ({ content, bold }).', font: 'Family. Default Inter.', weight: 'Default 600.', boldWeight: 'The weight of spans and lines marked bold: true. Default 700.', style: 'normal | italic', size: 'Length.', minSize: 'Shrink toward this before truncating.', lineHeight: 'Default 1.12.', letterSpacing: 'Em. Default -0.02.', color: 'Default @text.', align: 'left | center | right', anchor: 'Default bottom-left.', position: '{ x, y } fractions.', offset: '{ x, y } lengths.', margin: 'Length.', maxWidth: 'Length.', maxLines: 'Default 3.', ellipsis: "Default '…'.", balance: 'Default true.', transform: 'none | uppercase | lowercase', label: 'A string or { content, size, weight, color, letterSpacing, transform, gap }.', background: 'A colour or { color, radius, padding, opacity }.',
23
23
  x: 'First baseline x, px or "%" (like SVG <text>; align is the text-anchor).', y: 'First baseline y.', width: 'Justify every line to this width (like textLength).', justify: 'Justify every line to maxWidth.', lengthAdjust: 'spacingAndGlyphs (default: scale) | spacing', fit: 'width (the block fills width) | lines (each line fills width) | words (a word a line, the widest filling width) | wrap (wrap at width, then fill it) | shrink', stretch: '[sx, sy] or sy: stretch about the first baseline.', axes: 'Variable-font axes: { wdth: 50, SOFT: 100, WONK: 1, opsz: 144 }.', stroke: 'Outline colour.', strokeWidth: 'Outline width.', strokes: '[{ color, width }]: keylines, widest first, under the fill.', fill: 'A colour, a gradient, or "none" (outline type).', path: 'Text on a path: { kind: circle | arc | ellipse | wave, cx, cy, r, start, end, direction } or { d }.', startOffset: 'Along the path: 0–1 or px. Default 0.5 (centred).', fitPath: 'Space the text to fill the path (a ring); a number leaves that gap.', dy: 'Shift off the path.', render: 'outline (default) | live (<text> with @font-face)', autoColors: '[light, dark] for color: "auto". Default ["#ffffff", "#0b0b0b"].', autoThreshold: 'color: "auto" takes the dark colour above this backdrop luma (0–1). Default 0.72.', lines: 'Reserved.',
24
- spans: '[{ content, font, weight, style, axes, scale, size, color, letterSpacing, dy }]: mixed runs on the same lines.', glyphs: '{ scale, scaleX, scaleY, dx, dy, rotate, color, origin, advance }: per-glyph arrays (cycled; spaces skipped).', fallback: 'A family (or list) for characters the font lacks; then fonts registered with fallback: true, then the built-in symbols.', split: 'words: a word a line.', dash: 'Dashes on the stroke: [on, off] lengths, dashed or dotted.', extrude: 'A depth, or { depth, angle, color, to, steps }: block extrusion.', maxHeight: 'Shrink the block to this height.' },
24
+ spans: '[{ content, font, weight, bold, style, axes, scale, size, color, letterSpacing, dy }]: mixed runs on the same lines.', glyphs: '{ scale, scaleX, scaleY, dx, dy, rotate, color, origin, advance }: per-glyph arrays (cycled; spaces skipped).', fallback: 'A family (or list) for characters the font lacks; then fonts registered with fallback: true, then the built-in symbols.', split: 'words: a word a line.', dash: 'Dashes on the stroke: [on, off] lengths, dashed or dotted.', extrude: 'A depth, or { depth, angle, color, to, steps }: block extrusion.', maxHeight: 'Shrink the block to this height.' },
25
25
  diagram: { icon: 'One icon (Iconify name or raw SVG), drawn as white line-art.', nodes: '[{ icon, shape: square | circle | none, size, brackets }]', layout: 'single | row | triangle | square | stack | hub', arrows: 'true | false | loop | both | cycle', shape: 'For a single icon: none (default) | square | circle.', size: 'Node size. Length.', gap: 'Space between nodes. Length.', stroke: 'Line width. Default "0.3vmin".', color: 'Line colour. Default white.', radius: 'Tile corner radius.', iconScale: 'Icon size inside a tile, 0–1. Default 0.44.', position: '{ x, y } centre, fractions.', grid: 'true or { divisions, opacity, stroke, color, pad }: a dotted grid that breaks around the drawing.' },
26
26
  paper: { count: 'Shapes in the collage. Default 4.', kind: 'mixed | rect | square | circle | blob | arch | pill', color: 'Default #faf9f5 (paper).', region: 'Where the collage sits, fractions.', items: '[{ kind, x, y, width, height, size, rotate, color }] placed in the region.', wobble: 'How rough the cut edges are, 0–1. Default 0.5.' },
27
27
  scribble: { icon: 'An icon to re-draw by hand (Iconify name or raw SVG).', graph: 'true or { nodes, dot }: a hand-drawn node graph instead.', color: 'Default #faf9f5.', stroke: 'Line width. Default "1.3vmin".', size: 'Length. Default "46vmin".', position: '{ x, y } centre, fractions.', wobble: '0–1. Default 0.5.' },
@@ -43,6 +43,10 @@ export const LAYER_OPTIONS = {
43
43
  perforation: { kind: 'holes (default) | dashes | notches', x: 'A vertical line at x.', x1: 'Start x.', y1: 'Start y.', x2: 'End x.', y2: 'End y.', size: 'Hole or notch radius.', gap: 'Spacing.', inset: 'Space before the first hole.', color: 'Default @text (or punch it with knockout).', strokeWidth: 'Dashes width.' },
44
44
  tear: { from: '[x, y] where the tear starts (edge to edge).', to: '[x, y] where it ends.', side: 'Which side keeps the sheet: above (default) | below | left | right', color: 'The sheet (a colour, a gradient, or "none" for just the edge).', sheetOpacity: 'The sheet\'s opacity.', step: 'Spacing of the jagged points.', jitter: 'How jagged.', rip: 'false, or { color, width, opacity }: the torn white edge.', fibres: 'false, or { color, width, opacity, length: [min, max], spread }.', ripColor: 'Colour of the rip and fibres. Default #fff6fb.' },
45
45
  texture: { kind: 'paper | creases | concrete | speck | wood | folds | dust', color: 'Fibre, aggregate or dust colour.', amount: 'Paper: fibre density.', frequency: 'Noise frequency (number or [x, y]).', octaves: 'Noise octaves.', depth: 'Creases: relief.', azimuth: 'Creases: light direction.', elevation: 'Creases: light height.', x: 'Folds: vertical fold positions, fractions.', y: 'Folds: horizontal fold positions, fractions.', width: 'Folds: band width.', strength: 'Folds: shading.', count: 'Dust: specks.', size: 'Dust: [min, max] radius.', region: 'Clip to { x, y, width, height } (px or "%").' },
46
+ mosaic: { markup: 'The mark\'s paths (512 grid).', x: 'Box x, px or %.', y: 'Box y.', width: 'Box width (default the canvas).', height: 'Box height.', cells: 'Cells across the mark\'s longest side, 4–48. Default 10.', gap: 'Gap between cells, a share of a cell. Default 0.12.', radius: 'Cell corner radius, a share of a cell. Default 0.', drop: 'Share of filled cells left out (seeded), 0–0.9. Default 0.12.', color: 'Default @accent.', accent: 'asterisk | dot: a small glyph in one empty cell beside the mark.', accentColor: 'Default color.' },
47
+ marks: { marks: 'The marks: paths on a 512 grid (or { markup }).', cols: 'Default from the count: 3 × 3 up to 10 × 7.', rows: 'Default from the count.', x: 'Box x, px or %.', y: 'Box y.', width: 'Box width (default the canvas).', height: 'Box height.', size: 'Share of a cell a mark\'s area takes. Default 0.5.', optical: 'false: fit each mark\'s bounds to the same box instead. Default true.', color: 'Default @text.' },
48
+ outline: { markup: 'The mark\'s paths (512 grid).', x: 'Box x, px or %.', y: 'Box y.', width: 'Box width (default the canvas).', height: 'Box height.', size: 'Share of the box\'s shorter side the whole takes. Default 1.', lines: 'Contour lines, 0–8: the outline and more round it (0: the mark solid). Default 4.', gap: 'Between lines, a share of the mark\'s longest side. Default 0.05.', direction: 'out (default) | in', stroke: 'Line width. Default "0.4vmin".', color: 'Default @text.' },
49
+ construction: { markup: 'The mark\'s paths (512 grid).', x: 'Box x, px or %.', y: 'Box y.', width: 'Box width (default the canvas).', height: 'Box height.', size: 'Share of the box\'s shorter side the mark\'s longest side takes. Default 0.6.', grid: 'Grid cells across the mark\'s longest side; 0 none. Default 8.', circles: 'The most circles read from its curves. Default 6.', lines: 'The most lines read from its straight edges. Default 6.', color: 'The guides. Default @accent.', gridColor: 'Default color.', gridOpacity: 'Default 0.3.', markColor: 'Default @text; "none" leaves the mark out.', over: 'The guides\' opacity over the mark, 0–1. Default 0.4.', stroke: 'Guide width. Default "0.15vmin".' },
46
50
  };
47
51
 
48
52
  /** @type {Record<string, string>} Options every layer takes (besides type, id and when). */
@@ -0,0 +1,34 @@
1
+ // The `construction` layer.
2
+ import type { Color, Length, BaseLayer } from '../index.js';
3
+ import type { RenderContext, Markup } from '../render/context.js';
4
+
5
+ export interface ConstructionLayer extends BaseLayer {
6
+ type: 'construction';
7
+ /** The mark's paths, on a 512 grid. */
8
+ markup: string;
9
+ /** The box the sheet fills (default the canvas); the mark is centred in it. */
10
+ x?: Length | `${number}%`; y?: Length | `${number}%`; width?: Length | `${number}%`; height?: Length | `${number}%`;
11
+ /** The share of the box's shorter side the mark's longest side takes. Default 0.6. */
12
+ size?: number;
13
+ /** Grid cells across the mark's longest side; 0 none. Default 8. */
14
+ grid?: number;
15
+ /** The most circles read from its curves. Default 6. */
16
+ circles?: number;
17
+ /** The most lines read from its straight edges. Default 6. */
18
+ lines?: number;
19
+ /** The guides. Default the palette's accent. */
20
+ color?: Color;
21
+ /** Default `color`. */
22
+ gridColor?: Color;
23
+ /** Default 0.3. */
24
+ gridOpacity?: number;
25
+ /** Default the palette's text; `'none'` leaves the mark out. */
26
+ markColor?: Color | 'none';
27
+ /** The guides' opacity over the mark, 0–1. Default 0.4. */
28
+ over?: number;
29
+ /** Guide width. Default `'0.15vmin'`. */
30
+ stroke?: Length;
31
+ }
32
+
33
+ /** Draw a mark over its own construction: grid, box, centre cross, its circles and lines. */
34
+ export function construction(layer: ConstructionLayer, ctx: RenderContext): Markup;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The `construction` layer.
3
+ *
4
+ * @module layers/construction
5
+ */
6
+
7
+ import { attrs, num } from '@logomarks/image-effects/svg';
8
+ import { toHex } from '@logomarks/image-effects/color';
9
+ import { len } from '@logomarks/image-effects/length';
10
+ import { wrap } from './util.js';
11
+ import { markGuides } from './guides.js';
12
+ import { layerBox, fitMark, markPaths } from './mark-draw.js';
13
+
14
+ /**
15
+ * `construction` — a mark drawn over its own construction, as a
16
+ * construction sheet draws it: a light dashed square grid behind (its
17
+ * cells a fraction of the mark's size, centred on it), the mark's bounding
18
+ * box, a centre cross, the circles inscribed in its box and round its
19
+ * outline, the circles its curves are drawn on (each centre marked) and
20
+ * the lines its straight edges lie on, run across the box, all dashed in
21
+ * one thin accent. The guides are read from the mark's own paths
22
+ * (`markGuides`). Where the mark covers them they show faintly over it.
23
+ *
24
+ * Options: `markup` (512 grid), `x`, `y`, `width`, `height` (the box the
25
+ * sheet fills; default the canvas), `size` (the share of the box's
26
+ * shorter side the mark's longest side takes, 0.6), `grid` (cells across
27
+ * the mark's longest side, 8; 0 none), `circles` and `lines` (the most of
28
+ * each, 6), `color` (the guides; default the accent), `gridColor`
29
+ * (default `color`), `gridOpacity` (0.3), `markColor` (default `text`;
30
+ * `'none'` leaves the mark out), `over` (the guides' opacity over the
31
+ * mark, 0.4), `stroke` (px, `'0.15vmin'`).
32
+ *
33
+ * @param {any} layer
34
+ * @param {import('../layers.js').RenderContext} ctx
35
+ * @returns {import('../layers.js').Markup}
36
+ *
37
+ * @example
38
+ * { type: 'construction', markup: mark, x: 24, y: 60, width: 352, height: 380, color: '#3b82f6', markColor: '#f4f4f2' }
39
+ */
40
+ export function construction(layer, ctx) {
41
+ const g = markGuides(layer.markup, { circles: layer.circles ?? 6, lines: layer.lines ?? 6 });
42
+ if (!g) return { defs: '', body: '' };
43
+ const area = layerBox(layer, ctx);
44
+ const fit = fitMark(g.box, area, layer.size ?? 0.6);
45
+ const { k } = fit;
46
+ const X = (/** @type {number} */ x) => x * k + fit.dx;
47
+ const Y = (/** @type {number} */ y) => y * k + fit.dy;
48
+ const color = layer.color ? toHex(layer.color) : ctx.palette.accent;
49
+ const sw = len(layer.stroke, ctx.canvas, '0.15vmin');
50
+ const dash = `${num(sw * 6)} ${num(sw * 4)}`;
51
+ const line = (/** @type {Record<string, any>} */ a) => `<path${attrs({ fill: 'none', ...a })}/>`;
52
+
53
+ // The box, its centre and the mark's longest side, canvas units.
54
+ const bx = X(g.box.x), by = Y(g.box.y), bw = g.box.width * k, bh = g.box.height * k;
55
+ const cx = bx + bw / 2, cy = by + bh / 2;
56
+ const side = Math.max(bw, bh);
57
+ const { x: ax, y: ay, width: aw, height: ah } = area;
58
+
59
+ // The grid: cells of side / grid, lines through the mark's centre.
60
+ let grid = '';
61
+ const cells = Math.round(layer.grid ?? 8);
62
+ if (cells > 0) {
63
+ const u = side / cells;
64
+ let d = '';
65
+ for (let x = cx - Math.floor((cx - ax) / u) * u; x <= ax + aw + 1e-6; x += u) d += `M${num(x)} ${num(ay)}V${num(ay + ah)}`;
66
+ for (let y = cy - Math.floor((cy - ay) / u) * u; y <= ay + ah + 1e-6; y += u) d += `M${num(ax)} ${num(y)}H${num(ax + aw)}`;
67
+ grid = line({ d, stroke: layer.gridColor ? toHex(layer.gridColor) : color, 'stroke-width': num(sw * 0.8, 3), 'stroke-dasharray': `${num(sw * 3)} ${num(sw * 3)}`, opacity: num(layer.gridOpacity ?? 0.3, 3) });
68
+ }
69
+
70
+ // Keylines: the box (solid), the centre cross and the inscribed and circumscribed circles (dashed, lighter).
71
+ let keys = line({ d: `M${num(bx)} ${num(by)}h${num(bw)}v${num(bh)}h${num(-bw)}Z`, stroke: color, 'stroke-width': num(sw, 3) });
72
+ keys += line({ d: `M${num(ax)} ${num(cy)}H${num(ax + aw)}M${num(cx)} ${num(ay)}V${num(ay + ah)}`, stroke: color, 'stroke-width': num(sw, 3), 'stroke-dasharray': dash, opacity: 0.6 });
73
+ keys += line({ d: circlePath(cx, cy, g.inscribed * k) + circlePath(cx, cy, g.circumscribed * k), stroke: color, 'stroke-width': num(sw, 3), 'stroke-dasharray': dash, opacity: 0.6 });
74
+
75
+ // The mark's own circles, each centre marked, and its edges' lines across the box.
76
+ let d = '';
77
+ let ticks = '';
78
+ const t = sw * 4;
79
+ for (const c of g.circles) {
80
+ const [x, y] = [X(c.cx), Y(c.cy)];
81
+ d += circlePath(x, y, c.r * k);
82
+ ticks += `M${num(x - t)} ${num(y)}h${num(2 * t)}M${num(x)} ${num(y - t)}v${num(2 * t)}`;
83
+ }
84
+ let lines = '';
85
+ for (const l of g.lines) {
86
+ const seg = clipLine(X(l.x), Y(l.y), Math.cos(l.angle), Math.sin(l.angle), area);
87
+ if (seg) lines += `M${num(seg[0])} ${num(seg[1])}L${num(seg[2])} ${num(seg[3])}`;
88
+ }
89
+ let guides = keys;
90
+ if (d) guides += line({ d, stroke: color, 'stroke-width': num(sw, 3), 'stroke-dasharray': dash });
91
+ if (lines) guides += line({ d: lines, stroke: color, 'stroke-width': num(sw, 3), 'stroke-dasharray': dash, opacity: 0.75 });
92
+ if (ticks) guides += line({ d: ticks, stroke: color, 'stroke-width': num(sw, 3) });
93
+
94
+ const clip = `${ctx.id}k`;
95
+ const maskId = `${ctx.id}m`;
96
+ let defs = `<clipPath id="${clip}"><rect${attrs({ x: ax, y: ay, width: aw, height: ah })}/></clipPath>`;
97
+ let body = `${grid}${guides}`;
98
+ const markColor = layer.markColor === 'none' ? null : layer.markColor ? toHex(layer.markColor) : ctx.palette.text;
99
+ if (markColor) {
100
+ const paths = markPaths(layer.markup, fit).map((p) => `<path${attrs({ d: p.d, 'fill-rule': p.evenodd ? 'evenodd' : undefined })}/>`).join('');
101
+ body += `<g fill="${markColor}">${paths}</g>`;
102
+ const over = layer.over ?? 0.4;
103
+ if (over > 0) {
104
+ defs += `<mask id="${maskId}" maskUnits="userSpaceOnUse"${attrs({ x: ax, y: ay, width: aw, height: ah })}><g fill="#fff">${paths}</g></mask>`;
105
+ body += `<g mask="url(#${maskId})" opacity="${num(over, 3)}">${guides}</g>`;
106
+ }
107
+ }
108
+ return { defs, body: wrap(layer, `<g clip-path="url(#${clip})">${body}</g>`) };
109
+ }
110
+
111
+ /**
112
+ * A circle as path data (two arcs), so circles share one path.
113
+ *
114
+ * @param {number} cx @param {number} cy @param {number} r
115
+ * @returns {string}
116
+ */
117
+ function circlePath(cx, cy, r) {
118
+ return `M${num(cx - r)} ${num(cy)}a${num(r)} ${num(r)} 0 1 0 ${num(2 * r)} 0a${num(r)} ${num(r)} 0 1 0 ${num(-2 * r)} 0Z`;
119
+ }
120
+
121
+ /**
122
+ * The part of the line through (x, y) along (ux, uy) inside a box
123
+ * (Liang–Barsky), or null when it misses.
124
+ *
125
+ * @param {number} x @param {number} y @param {number} ux @param {number} uy
126
+ * @param {{ x: number, y: number, width: number, height: number }} box
127
+ * @returns {number[] | null} `[x1, y1, x2, y2]`.
128
+ */
129
+ function clipLine(x, y, ux, uy, box) {
130
+ let t0 = -Infinity, t1 = Infinity;
131
+ for (const [p, q] of [[-ux, x - box.x], [ux, box.x + box.width - x], [-uy, y - box.y], [uy, box.y + box.height - y]]) {
132
+ if (Math.abs(p) < 1e-12) { if (q < 0) return null; continue; }
133
+ const r = q / p;
134
+ if (p < 0) t0 = Math.max(t0, r); else t1 = Math.min(t1, r);
135
+ }
136
+ return t0 < t1 ? [x + ux * t0, y + uy * t0, x + ux * t1, y + uy * t1] : null;
137
+ }
@@ -0,0 +1,38 @@
1
+ // A mark's construction, read from its paths.
2
+
3
+ /** A circle the mark's curves are drawn on. */
4
+ export interface GuideCircle {
5
+ cx: number;
6
+ cy: number;
7
+ r: number;
8
+ /** How much of it the mark draws, radians. */
9
+ sweep: number;
10
+ }
11
+
12
+ /** A line the mark's straight edges lie on. */
13
+ export interface GuideLine {
14
+ /** Radians, 0–π. */
15
+ angle: number;
16
+ /** A point on it. */
17
+ x: number;
18
+ y: number;
19
+ /** The mark's edges on it, in total. */
20
+ length: number;
21
+ }
22
+
23
+ /** A mark's construction, in its own (512 grid) units. */
24
+ export interface MarkGuides {
25
+ /** Its real bounds. */
26
+ box: { x: number; y: number; width: number; height: number };
27
+ /** The radius of the circle in its box. */
28
+ inscribed: number;
29
+ /** The radius of the circle round its outline, from the box's centre. */
30
+ circumscribed: number;
31
+ /** Its curves' circles, the most drawn first. */
32
+ circles: GuideCircle[];
33
+ /** Its straight edges' lines, the longest first. */
34
+ lines: GuideLine[];
35
+ }
36
+
37
+ /** Read a mark's construction (null when it draws nothing); at most `circles` and `lines` of each (6). */
38
+ export function markGuides(markup: string, options?: { circles?: number; lines?: number }): MarkGuides | null;
@@ -0,0 +1,212 @@
1
+ /**
2
+ * A mark's construction, read from its paths: the circles its curves are
3
+ * drawn on and the lines its straight edges lie on, for a construction
4
+ * sheet.
5
+ *
6
+ * @module layers/guides
7
+ */
8
+
9
+ import { shapes, bounds, outlinePoints, arcOf, bezierAt } from '@logomarks/core/svg';
10
+
11
+ /**
12
+ * @typedef {object} GuideCircle
13
+ * @property {number} cx @property {number} cy @property {number} r
14
+ * @property {number} sweep - How much of it the mark draws, radians (2π: all of it).
15
+ */
16
+
17
+ /**
18
+ * @typedef {object} GuideLine
19
+ * @property {number} angle - Radians, 0–π.
20
+ * @property {number} x @property {number} y - A point on it (the middle of its longest edge).
21
+ * @property {number} length - The mark's edges on it, in total.
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} MarkGuides
26
+ * @property {{ x: number, y: number, width: number, height: number }} box - Its real bounds.
27
+ * @property {number} inscribed - The radius of the circle in its box, round the box's centre.
28
+ * @property {number} circumscribed - The radius of the circle round its outline, from the box's centre.
29
+ * @property {GuideCircle[]} circles - Its curves' circles, the most drawn first.
30
+ * @property {GuideLine[]} lines - Its straight edges' lines, the longest first.
31
+ */
32
+
33
+ /** Read marks, by markup and counts. @type {Map<string, MarkGuides>} */
34
+ const cache = new Map();
35
+
36
+ /**
37
+ * Read a mark's construction. A curve counts as part of a circle when
38
+ * its points lie on the circle through its ends and middle (an arc, or a
39
+ * Bézier drawn round one); curves on the same circle are added up, and
40
+ * a circle is kept when the mark draws a good share of it (50° or more)
41
+ * and it isn't a small corner round. Straight edges on the same line are
42
+ * added up the same way, and lines along the box's own sides left out.
43
+ *
44
+ * @param {string} markup - The mark's paths (512 grid).
45
+ * @param {{ circles?: number, lines?: number }} [options] - The most of each kept (6 and 6).
46
+ * @returns {MarkGuides | null} Null when it draws nothing.
47
+ *
48
+ * @example
49
+ * markGuides('<circle cx="256" cy="256" r="200"/>').circles; // [{ cx: 256, cy: 256, r: 200, sweep: 6.28… }]
50
+ */
51
+ export function markGuides(markup, { circles = 6, lines = 6 } = {}) {
52
+ const key = `${circles}|${lines}|${markup}`;
53
+ if (cache.has(key)) return /** @type {MarkGuides} */ (cache.get(key));
54
+ const filled = shapes(String(markup ?? ''));
55
+ if (!filled.length) return null;
56
+ const b = bounds(filled);
57
+ if (!(b.w > 0 && b.h > 0)) return null;
58
+ const box = { x: b.x, y: b.y, width: b.w, height: b.h };
59
+ const size = Math.max(b.w, b.h);
60
+ const cx = b.x + b.w / 2;
61
+ const cy = b.y + b.h / 2;
62
+
63
+ /** @type {GuideCircle[]} */
64
+ const arcs = [];
65
+ /** @type {{ x1: number, y1: number, x2: number, y2: number }[]} */
66
+ const edges = [];
67
+ let far = 0;
68
+ for (const s of filled) {
69
+ for (const [x, y] of outlinePoints(s.segs)) far = Math.max(far, Math.hypot(x - cx, y - cy));
70
+ let x = 0, y = 0, sx = 0, sy = 0;
71
+ for (const seg of s.segs) {
72
+ switch (seg[0]) {
73
+ case 'M': x = sx = seg[1]; y = sy = seg[2]; break;
74
+ case 'L': edges.push({ x1: x, y1: y, x2: seg[1], y2: seg[2] }); x = seg[1]; y = seg[2]; break;
75
+ case 'C': fitCurve([[x, y], [seg[1], seg[2]], [seg[3], seg[4]], [seg[5], seg[6]]], arcs); x = seg[5]; y = seg[6]; break;
76
+ case 'Q': fitCurve([[x, y], [seg[1], seg[2]], [seg[3], seg[4]]], arcs); x = seg[3]; y = seg[4]; break;
77
+ case 'A': {
78
+ const a = arcOf(x, y, seg.slice(1));
79
+ if (a && Math.abs(a.rx - a.ry) <= 0.01 * Math.max(a.rx, a.ry)) arcs.push({ cx: a.cx, cy: a.cy, r: (a.rx + a.ry) / 2, sweep: Math.abs(a.sweep) });
80
+ x = seg[6]; y = seg[7];
81
+ break;
82
+ }
83
+ case 'Z': edges.push({ x1: x, y1: y, x2: sx, y2: sy }); x = sx; y = sy; break;
84
+ }
85
+ }
86
+ }
87
+
88
+ const guides = {
89
+ box,
90
+ inscribed: Math.min(b.w, b.h) / 2,
91
+ circumscribed: far,
92
+ circles: circleGuides(arcs, size).slice(0, Math.max(0, circles)),
93
+ lines: lineGuides(edges, box).slice(0, Math.max(0, lines)),
94
+ };
95
+ if (cache.size > 200) cache.clear();
96
+ cache.set(key, guides);
97
+ return guides;
98
+ }
99
+
100
+ /**
101
+ * The circle a curve is drawn round, added to `arcs` when its quarter
102
+ * points lie on it too (within half a unit, or 1% of its radius).
103
+ *
104
+ * @param {number[][]} P - A quadratic's 3 points or a cubic's 4.
105
+ * @param {GuideCircle[]} arcs
106
+ */
107
+ function fitCurve(P, arcs) {
108
+ const p0 = P[0];
109
+ const p1 = /** @type {number[]} */ (bezierAt(/** @type {any} */ (P), 0.5));
110
+ const p2 = P[P.length - 1];
111
+ const c = circleThrough(p0, p1, p2);
112
+ if (!c) return;
113
+ const tol = Math.max(0.5, c.r * 0.01);
114
+ for (const u of [0.25, 0.75]) {
115
+ const [x, y] = bezierAt(/** @type {any} */ (P), u);
116
+ if (Math.abs(Math.hypot(x - c.cx, y - c.cy) - c.r) > tol) return;
117
+ }
118
+ const turn = (/** @type {number[]} */ a, /** @type {number[]} */ b) => {
119
+ const ux = a[0] - c.cx, uy = a[1] - c.cy, vx = b[0] - c.cx, vy = b[1] - c.cy;
120
+ return Math.abs(Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy));
121
+ };
122
+ arcs.push({ ...c, sweep: turn(p0, p1) + turn(p1, p2) });
123
+ }
124
+
125
+ /**
126
+ * The circle through three points, or null when they're (nearly) on a line.
127
+ *
128
+ * @param {number[]} a @param {number[]} b @param {number[]} c
129
+ * @returns {{ cx: number, cy: number, r: number } | null}
130
+ */
131
+ function circleThrough(a, b, c) {
132
+ const d = 2 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1]));
133
+ if (Math.abs(d) < 1e-6) return null;
134
+ const A = a[0] ** 2 + a[1] ** 2, B = b[0] ** 2 + b[1] ** 2, C = c[0] ** 2 + c[1] ** 2;
135
+ const cx = (A * (b[1] - c[1]) + B * (c[1] - a[1]) + C * (a[1] - b[1])) / d;
136
+ const cy = (A * (c[0] - b[0]) + B * (a[0] - c[0]) + C * (b[0] - a[0])) / d;
137
+ return { cx, cy, r: Math.hypot(a[0] - cx, a[1] - cy) };
138
+ }
139
+
140
+ /**
141
+ * Arcs on the same circle added up (centres and radii within 3 units or
142
+ * 3%); kept when the mark draws 50° or more of it and its radius is 4%
143
+ * to 120% of the mark's size; the most drawn (by length) first.
144
+ *
145
+ * @param {GuideCircle[]} arcs
146
+ * @param {number} size - The mark's longest side.
147
+ * @returns {GuideCircle[]}
148
+ */
149
+ function circleGuides(arcs, size) {
150
+ /** @type {(GuideCircle & { w: number })[]} */
151
+ const groups = [];
152
+ for (const a of arcs) {
153
+ const near = Math.max(3, a.r * 0.03);
154
+ const g = groups.find((o) => Math.hypot(o.cx - a.cx, o.cy - a.cy) <= near && Math.abs(o.r - a.r) <= near);
155
+ if (!g) { groups.push({ ...a, w: a.sweep }); continue; }
156
+ // A sweep-weighted mean, so the long arcs set the circle.
157
+ const w = g.w + a.sweep;
158
+ g.cx = (g.cx * g.w + a.cx * a.sweep) / w;
159
+ g.cy = (g.cy * g.w + a.cy * a.sweep) / w;
160
+ g.r = (g.r * g.w + a.r * a.sweep) / w;
161
+ g.w = w;
162
+ g.sweep = Math.min(2 * Math.PI, g.sweep + a.sweep);
163
+ }
164
+ return groups
165
+ .filter((g) => g.sweep >= (50 * Math.PI) / 180 && g.r >= size * 0.04 && g.r <= size * 1.2)
166
+ .sort((p, q) => q.sweep * q.r - p.sweep * p.r || p.cx - q.cx || p.cy - q.cy)
167
+ .map(({ cx, cy, r, sweep }) => ({ cx, cy, r, sweep }));
168
+ }
169
+
170
+ /**
171
+ * Edges on the same line added up (within 0.75° and 1.5 units); kept
172
+ * when 8% of the mark's size or longer in total and not along a side of
173
+ * its box; the longest first.
174
+ *
175
+ * @param {{ x1: number, y1: number, x2: number, y2: number }[]} edges
176
+ * @param {{ x: number, y: number, width: number, height: number }} box
177
+ * @returns {GuideLine[]}
178
+ */
179
+ function lineGuides(edges, box) {
180
+ const size = Math.max(box.width, box.height);
181
+ /** @type {(GuideLine & { rho: number, best: number })[]} */
182
+ const groups = [];
183
+ for (const e of edges) {
184
+ const length = Math.hypot(e.x2 - e.x1, e.y2 - e.y1);
185
+ if (length < size * 0.02) continue;
186
+ let angle = Math.atan2(e.y2 - e.y1, e.x2 - e.x1);
187
+ if (angle < 0) angle += Math.PI;
188
+ if (angle >= Math.PI - 1e-9) angle -= Math.PI;
189
+ const x = (e.x1 + e.x2) / 2, y = (e.y1 + e.y2) / 2;
190
+ const rho = -Math.sin(angle) * x + Math.cos(angle) * y;
191
+ const g = groups.find((o) => {
192
+ const da = Math.abs(o.angle - angle);
193
+ const parallel = Math.min(da, Math.PI - da) < 0.013;
194
+ // Across 0 and π the line's normal flips, so its offset does too.
195
+ const apart = da < 0.013 ? Math.abs(o.rho - rho) : Math.abs(o.rho + rho);
196
+ return parallel && apart <= 1.5;
197
+ });
198
+ if (!g) { groups.push({ angle, x, y, length, rho, best: length }); continue; }
199
+ g.length += length;
200
+ if (length > g.best) { g.best = length; g.x = x; g.y = y; }
201
+ }
202
+ const onSide = (/** @type {GuideLine} */ l) => {
203
+ const flat = Math.min(l.angle, Math.PI - l.angle) < 0.013;
204
+ const upright = Math.abs(l.angle - Math.PI / 2) < 0.013;
205
+ return (flat && (Math.abs(l.y - box.y) < 1.5 || Math.abs(l.y - box.y - box.height) < 1.5))
206
+ || (upright && (Math.abs(l.x - box.x) < 1.5 || Math.abs(l.x - box.x - box.width) < 1.5));
207
+ };
208
+ return groups
209
+ .filter((g) => g.length >= size * 0.08 && !onSide(g))
210
+ .sort((p, q) => q.length - p.length || p.angle - q.angle || p.rho - q.rho)
211
+ .map(({ angle, x, y, length }) => ({ angle, x, y, length }));
212
+ }
@@ -14,5 +14,17 @@ export { tear } from './tear.js';
14
14
  export type { TearLayer } from './tear.js';
15
15
  export { texture } from './texture.js';
16
16
  export type { TextureLayer } from './texture.js';
17
+ export { mosaic, mosaicGrid } from './mosaic.js';
18
+ export type { MosaicLayer } from './mosaic.js';
19
+ export { marks, sheetGrid } from './marks.js';
20
+ export type { MarksLayer } from './marks.js';
21
+ export { markGeometry } from './mark-geometry.js';
22
+ export type { MarkGeometry } from './mark-geometry.js';
23
+ export { outline } from './outline.js';
24
+ export type { OutlineLayer } from './outline.js';
25
+ export { construction } from './construction.js';
26
+ export type { ConstructionLayer } from './construction.js';
27
+ export { markGuides } from './guides.js';
28
+ export type { MarkGuides, GuideCircle, GuideLine } from './guides.js';
17
29
  export { expandRepeat } from './repeat.js';
18
30
  export type { RepeatLayer } from './repeat.js';
@@ -12,4 +12,10 @@ export { code128 } from './code128.js';
12
12
  export { perforation } from './perforation.js';
13
13
  export { tear } from './tear.js';
14
14
  export { texture } from './texture.js';
15
+ export { mosaic, mosaicGrid } from './mosaic.js';
16
+ export { marks, sheetGrid } from './marks.js';
17
+ export { markGeometry } from './mark-geometry.js';
18
+ export { outline } from './outline.js';
19
+ export { construction } from './construction.js';
20
+ export { markGuides } from './guides.js';
15
21
  export { expandRepeat } from './repeat.js';
@@ -0,0 +1,21 @@
1
+ // Isolines of a sampled field, and a mark's signed distance field.
2
+ import type { MarkGeometry } from './mark-geometry.js';
3
+
4
+ /** A signed distance field over a mark: positive outside, negative inside, mark units. */
5
+ export interface DistanceField {
6
+ values: Float32Array;
7
+ width: number;
8
+ height: number;
9
+ /** The mark point of the first sample. */
10
+ x0: number;
11
+ y0: number;
12
+ /** Mark units between samples. */
13
+ step: number;
14
+ }
15
+
16
+ /** A mark's signed distance field over its box plus `reach`, `samples` across its longest side (500); exact near `levels`, where lines will be traced. */
17
+ export function distanceField(geo: MarkGeometry, reach: number, levels?: number[], samples?: number): DistanceField;
18
+ /** Where a field (row by row, `w` × `h`) crosses `level`: polylines of `[column, row]`, closed ones ending on their first point. */
19
+ export function isolines(f: ArrayLike<number>, w: number, h: number, level: number): number[][][];
20
+ /** A polyline simplified within `tol` and smoothed (two rounds of Chaikin, corners rounded by at most `round`), as path data. */
21
+ export function smoothLine(pts: number[][], tol?: number, round?: number): string;