logomarks 0.0.0-stage → 1.0.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.
Files changed (174) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +56 -2
  4. package/bin/logomarks.js +4 -0
  5. package/data/fonts/BricolageGrotesque-SemiBold.ttf +0 -0
  6. package/data/fonts/Cagliostro-Regular.ttf +0 -0
  7. package/data/fonts/Fraunces-SemiBold.ttf +0 -0
  8. package/data/fonts/IBMPlexMono-Regular.ttf +0 -0
  9. package/data/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  10. package/data/fonts/InstrumentSans-SemiBold.ttf +0 -0
  11. package/data/fonts/InstrumentSerif-Regular.ttf +0 -0
  12. package/data/fonts/Inter-SemiBold.ttf +0 -0
  13. package/data/fonts/Limelight-Regular.ttf +0 -0
  14. package/data/fonts/OFL.txt +103 -0
  15. package/data/fonts/Shrikhand-Regular.ttf +0 -0
  16. package/data/fonts/SpaceGrotesk-SemiBold.ttf +0 -0
  17. package/data/index.bin +0 -0
  18. package/docs/api.md +31 -0
  19. package/docs/commands.md +412 -0
  20. package/docs/editing.md +61 -0
  21. package/docs/mcp.md +45 -0
  22. package/package.json +94 -4
  23. package/src/brand/brand.d.ts +23 -0
  24. package/src/brand/brand.js +29 -0
  25. package/src/brand/fonts.d.ts +61 -0
  26. package/src/brand/fonts.js +93 -0
  27. package/src/brand/mark.d.ts +53 -0
  28. package/src/brand/mark.js +151 -0
  29. package/src/checks/finding.d.ts +56 -0
  30. package/src/checks/finding.js +57 -0
  31. package/src/checks/fit.d.ts +5 -0
  32. package/src/checks/fit.js +46 -0
  33. package/src/checks/index.d.ts +33 -0
  34. package/src/checks/index.js +57 -0
  35. package/src/checks/specks.d.ts +5 -0
  36. package/src/checks/specks.js +38 -0
  37. package/src/checks/widths.d.ts +7 -0
  38. package/src/checks/widths.js +48 -0
  39. package/src/cli/args.d.ts +20 -0
  40. package/src/cli/args.js +35 -0
  41. package/src/cli/commands/check.d.ts +10 -0
  42. package/src/cli/commands/check.js +66 -0
  43. package/src/cli/commands/combine.d.ts +10 -0
  44. package/src/cli/commands/combine.js +54 -0
  45. package/src/cli/commands/cover.d.ts +10 -0
  46. package/src/cli/commands/cover.js +53 -0
  47. package/src/cli/commands/edit.d.ts +10 -0
  48. package/src/cli/commands/edit.js +70 -0
  49. package/src/cli/commands/find.d.ts +10 -0
  50. package/src/cli/commands/find.js +61 -0
  51. package/src/cli/commands/get.d.ts +10 -0
  52. package/src/cli/commands/get.js +57 -0
  53. package/src/cli/commands/glitch.d.ts +10 -0
  54. package/src/cli/commands/glitch.js +57 -0
  55. package/src/cli/commands/index.d.ts +15 -0
  56. package/src/cli/commands/index.js +24 -0
  57. package/src/cli/commands/kit.d.ts +10 -0
  58. package/src/cli/commands/kit.js +66 -0
  59. package/src/cli/commands/mcp.d.ts +9 -0
  60. package/src/cli/commands/mcp.js +36 -0
  61. package/src/cli/commands/mockup.d.ts +10 -0
  62. package/src/cli/commands/mockup.js +61 -0
  63. package/src/cli/commands/parts.d.ts +10 -0
  64. package/src/cli/commands/parts.js +44 -0
  65. package/src/cli/commands/sheet.d.ts +10 -0
  66. package/src/cli/commands/sheet.js +66 -0
  67. package/src/cli/commands/similar.d.ts +10 -0
  68. package/src/cli/commands/similar.js +111 -0
  69. package/src/cli/commands/social.d.ts +10 -0
  70. package/src/cli/commands/social.js +65 -0
  71. package/src/cli/commands/specks.d.ts +20 -0
  72. package/src/cli/commands/specks.js +103 -0
  73. package/src/cli/edited.d.ts +6 -0
  74. package/src/cli/edited.js +32 -0
  75. package/src/cli/files.d.ts +4 -0
  76. package/src/cli/files.js +27 -0
  77. package/src/cli/help.d.ts +20 -0
  78. package/src/cli/help.js +55 -0
  79. package/src/cli/index.d.ts +10 -0
  80. package/src/cli/index.js +104 -0
  81. package/src/cli/options.d.ts +30 -0
  82. package/src/cli/options.js +155 -0
  83. package/src/cli/report.d.ts +4 -0
  84. package/src/cli/report.js +28 -0
  85. package/src/cli/style.d.ts +4 -0
  86. package/src/cli/style.js +34 -0
  87. package/src/cover/index.d.ts +49 -0
  88. package/src/cover/index.js +63 -0
  89. package/src/edit/combine.d.ts +31 -0
  90. package/src/edit/combine.js +103 -0
  91. package/src/edit/doc.d.ts +34 -0
  92. package/src/edit/doc.js +96 -0
  93. package/src/edit/glitch.d.ts +48 -0
  94. package/src/edit/glitch.js +43 -0
  95. package/src/edit/index.d.ts +8 -0
  96. package/src/edit/index.js +11 -0
  97. package/src/edit/ops.d.ts +51 -0
  98. package/src/edit/ops.js +90 -0
  99. package/src/edit/preview.d.ts +8 -0
  100. package/src/edit/preview.js +51 -0
  101. package/src/find/index.d.ts +54 -0
  102. package/src/find/index.js +88 -0
  103. package/src/index.d.ts +30 -0
  104. package/src/index.js +28 -0
  105. package/src/kit/index.d.ts +26 -0
  106. package/src/kit/index.js +31 -0
  107. package/src/mcp/content.d.ts +30 -0
  108. package/src/mcp/content.js +56 -0
  109. package/src/mcp/edited.d.ts +5 -0
  110. package/src/mcp/edited.js +25 -0
  111. package/src/mcp/index.d.ts +41 -0
  112. package/src/mcp/index.js +72 -0
  113. package/src/mcp/inputs.d.ts +46 -0
  114. package/src/mcp/inputs.js +122 -0
  115. package/src/mcp/preview.d.ts +4 -0
  116. package/src/mcp/preview.js +24 -0
  117. package/src/mcp/tools/check.d.ts +15 -0
  118. package/src/mcp/tools/check.js +47 -0
  119. package/src/mcp/tools/combine.d.ts +15 -0
  120. package/src/mcp/tools/combine.js +42 -0
  121. package/src/mcp/tools/cover.d.ts +20 -0
  122. package/src/mcp/tools/cover.js +46 -0
  123. package/src/mcp/tools/edit.d.ts +21 -0
  124. package/src/mcp/tools/edit.js +73 -0
  125. package/src/mcp/tools/find.d.ts +14 -0
  126. package/src/mcp/tools/find.js +36 -0
  127. package/src/mcp/tools/get.d.ts +15 -0
  128. package/src/mcp/tools/get.js +44 -0
  129. package/src/mcp/tools/glitch.d.ts +12 -0
  130. package/src/mcp/tools/glitch.js +39 -0
  131. package/src/mcp/tools/index.d.ts +4 -0
  132. package/src/mcp/tools/index.js +23 -0
  133. package/src/mcp/tools/kit.d.ts +28 -0
  134. package/src/mcp/tools/kit.js +51 -0
  135. package/src/mcp/tools/mockup.d.ts +22 -0
  136. package/src/mcp/tools/mockup.js +51 -0
  137. package/src/mcp/tools/sheet.d.ts +15 -0
  138. package/src/mcp/tools/sheet.js +43 -0
  139. package/src/mcp/tools/similar.d.ts +13 -0
  140. package/src/mcp/tools/similar.js +42 -0
  141. package/src/mcp/tools/social.d.ts +5 -0
  142. package/src/mcp/tools/social.js +68 -0
  143. package/src/mockup/index.d.ts +52 -0
  144. package/src/mockup/index.js +152 -0
  145. package/src/raster/render.d.ts +30 -0
  146. package/src/raster/render.js +100 -0
  147. package/src/rules.d.ts +73 -0
  148. package/src/rules.js +139 -0
  149. package/src/sheet/card.d.ts +20 -0
  150. package/src/sheet/card.js +190 -0
  151. package/src/sheet/draw.d.ts +26 -0
  152. package/src/sheet/draw.js +94 -0
  153. package/src/sheet/grid.d.ts +2 -0
  154. package/src/sheet/grid.js +32 -0
  155. package/src/sheet/index.d.ts +31 -0
  156. package/src/sheet/index.js +47 -0
  157. package/src/sheet/theme.d.ts +8 -0
  158. package/src/sheet/theme.js +45 -0
  159. package/src/similar/folder.d.ts +20 -0
  160. package/src/similar/folder.js +66 -0
  161. package/src/similar/nearest.d.ts +32 -0
  162. package/src/similar/nearest.js +60 -0
  163. package/src/similar/signature.d.ts +12 -0
  164. package/src/similar/signature.js +42 -0
  165. package/src/similar/store.d.ts +67 -0
  166. package/src/similar/store.js +88 -0
  167. package/src/util/color.d.ts +2 -0
  168. package/src/util/color.js +23 -0
  169. package/src/util/files.d.ts +8 -0
  170. package/src/util/files.js +60 -0
  171. package/src/util/round.d.ts +2 -0
  172. package/src/util/round.js +14 -0
  173. package/src/util/version.d.ts +2 -0
  174. package/src/util/version.js +10 -0
@@ -0,0 +1,49 @@
1
+ import type { Mark } from '../brand/mark.js';
2
+
3
+ /** What a recipe is built from. */
4
+ export interface CoverBrand {
5
+ /** The mark's SVG. */
6
+ mark: string;
7
+ /** The brand colour. */
8
+ color: string;
9
+ /** Whether the cover has a title (the mark moves out of its way). */
10
+ titled: boolean;
11
+ }
12
+
13
+ /** A curated recipe: what it looks like, and its `@arraypress/covers` options. */
14
+ export interface CoverRecipe {
15
+ about: string;
16
+ options: (b: CoverBrand) => Record<string, unknown>;
17
+ }
18
+
19
+ /** What to draw. */
20
+ export interface CoverOptions {
21
+ mark: Mark;
22
+ /** One of `RECIPES` (`mark`). */
23
+ recipe?: string;
24
+ title?: string;
25
+ /** A small line over the title. */
26
+ label?: string;
27
+ /** The brand colour (the site's yellow). */
28
+ color?: string;
29
+ /** `og` (1200 × 630, the default), `1:1`, `16:9`, `4:5`, `3:2`, `4:3`, `2:1`, `9:16`, any `w:h`, or a number. */
30
+ aspect?: string | number;
31
+ /** Pixels wide (1200). */
32
+ width?: number;
33
+ /** Drives every random choice (the mark's slug). */
34
+ seed?: string;
35
+ }
36
+
37
+ /** A cover. */
38
+ export interface Cover {
39
+ png: Uint8Array;
40
+ /** The SVG, text as outlines. */
41
+ svg: string;
42
+ width: number;
43
+ height: number;
44
+ }
45
+
46
+ /** The curated recipes, by name. */
47
+ export const RECIPES: Record<string, CoverRecipe>;
48
+ /** A cover with the mark on it, as PNG (resvg) and SVG. Throws a TypeError for an unknown recipe. */
49
+ export function cover(o: CoverOptions): Promise<Cover>;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @module logomarks/cover
3
+ *
4
+ * Covers (blog headers, Open Graph cards, posts) with the mark on them, from
5
+ * `@arraypress/covers` recipes: a curated handful that suit a mark. The
6
+ * cover is SVG with its text as outlines; resvg draws the PNG.
7
+ */
8
+
9
+ import { renderSvg, resolvePalette } from '@arraypress/covers';
10
+ import { palette as makePalette, DEFAULT_COLOR } from '@logomarks/brand-kit';
11
+ import { render } from '../raster/render.js';
12
+
13
+ /** The mark laid over a recipe that has no icon of its own: centred, or top left when there's a title. */
14
+ const overlay = (/** @type {string} */ mark, /** @type {boolean} */ titled, /** @type {string} */ color) => ({
15
+ type: 'icon',
16
+ svg: mark,
17
+ size: titled ? '14vmin' : '20vmin',
18
+ anchor: titled ? 'top-left' : 'center',
19
+ color,
20
+ });
21
+
22
+ /**
23
+ * The curated recipes: each one's covers options from the mark and the brand.
24
+ *
25
+ * @type {Record<string, import('./index.js').CoverRecipe>}
26
+ */
27
+ export const RECIPES = {
28
+ mark: { about: 'The mark centred on the brand colour.', options: ({ mark }) => ({ recipe: 'mark', icon: mark }) },
29
+ gradient: { about: 'The mark on a gradient from the brand colour.', options: ({ mark }) => ({ recipe: 'mark', icon: mark, background: 'gradient' }) },
30
+ tile: { about: 'The mark on a raised tile over a soft tint, a glow and rings.', options: ({ mark }) => ({ recipe: 'iconTile', icon: mark }) },
31
+ stripes: { about: 'A band of tapering stripes and the mark on a disc at its edge.', options: ({ mark }) => ({ recipe: 'stripes', icon: mark }) },
32
+ aurora: { about: 'Soft sweeps of the brand’s hues, the mark over them.', options: (b) => ({ recipe: 'aurora', ...mesh(b) }) },
33
+ grid: { about: 'The same sweeps under a dotted grid.', options: (b) => ({ recipe: 'gridTitle', ...mesh(b) }) },
34
+ cutout: { about: 'A matte field, cut-paper shapes and the mark drawn by hand.', options: ({ mark }) => ({ recipe: 'cutout', icon: mark }) },
35
+ };
36
+
37
+ /**
38
+ * The mesh recipes' palette (the brand's own tints, so the sweeps stay in its hue) and the mark laid over them.
39
+ *
40
+ * @param {import('./index.js').CoverBrand} b The brand.
41
+ * @returns {object} The covers options.
42
+ */
43
+ function mesh({ mark, color, titled }) {
44
+ const p = makePalette(color);
45
+ const palette = { accent: p.primary, colors: [p.primary, p.scale['300'], p.scale['700']] };
46
+ return { palette, mesh: { hueSpan: 60 }, layers: [overlay(mark, titled, titled ? resolvePalette(palette).text : '#FFFFFF')] };
47
+ }
48
+
49
+ /**
50
+ * A cover.
51
+ *
52
+ * @param {import('./index.js').CoverOptions} o The mark, the recipe, the words and the size.
53
+ * @returns {Promise<import('./index.js').Cover>} The PNG and the SVG.
54
+ * @throws {TypeError} For a recipe that isn't one of `RECIPES`.
55
+ */
56
+ export async function cover({ mark, recipe = 'mark', title, label, color = DEFAULT_COLOR, aspect = 'og', width = 1200, seed }) {
57
+ const r = RECIPES[recipe];
58
+ if (!r) throw new TypeError(`no recipe "${recipe}": one of ${Object.keys(RECIPES).join(', ')}`);
59
+ const options = { palette: color, ...r.options({ mark: mark.svg, color, titled: Boolean(title) }) };
60
+ const svg = await renderSvg(/** @type {never} */ ({ seed: seed ?? mark.slug, aspect, width, ...(title && { title }), ...(label && { label }), ...options }));
61
+ const out = await render(svg, { png: true });
62
+ return { png: /** @type {Uint8Array} */ (out.png), svg, width: out.width, height: out.height };
63
+ }
@@ -0,0 +1,31 @@
1
+ import type { Edited } from './doc.js';
2
+
3
+ /** A way two marks combine. */
4
+ export type CombineOp = 'union' | 'subtract' | 'intersect' | 'exclude' | 'place';
5
+
6
+ /** Where the second mark goes, on the 512 canvas: scaled and turned about its centre, flipped, then moved. */
7
+ export interface Placement {
8
+ /** A factor (1). */
9
+ scale?: number;
10
+ /** Degrees, clockwise (0). */
11
+ rotate?: number;
12
+ flip?: 'horizontal' | 'vertical';
13
+ /** Units across (0). */
14
+ x?: number;
15
+ /** Units down (0). */
16
+ y?: number;
17
+ }
18
+
19
+ /** How to combine. */
20
+ export interface CombineOptions {
21
+ /** `union`, `subtract` (the second cut out of the first), `intersect`, `exclude`, or `place` (both, as they are). */
22
+ op: CombineOp;
23
+ transform?: Placement;
24
+ /** The result's name, for the checks. */
25
+ name?: string;
26
+ }
27
+
28
+ /** The ways two marks combine. */
29
+ export const COMBINE_OPS: readonly CombineOp[];
30
+ /** Two marks combined, fitted and checked; a RangeError for an unknown op, an Error when nothing is left. */
31
+ export function combineMarks(a: string, b: string, o: CombineOptions): Edited;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @module logomarks/edit/combine
3
+ *
4
+ * Two marks made one: the second placed over the first (scaled, turned,
5
+ * moved), then joined, cut, crossed or exclusive-ored with the editor's
6
+ * curve-keeping booleans, or simply placed.
7
+ */
8
+
9
+ import { insertSvg, newParts, duplicateParts, deleteParts, booleanParts, scaleParts, rotateParts, flipParts, moveParts } from '@logomarks/editor/ops';
10
+ import { parts } from '@logomarks/editor/model';
11
+ import { toDoc, finish } from './doc.js';
12
+
13
+ /** The ways two marks combine. */
14
+ export const COMBINE_OPS = /** @type {const} */ (['union', 'subtract', 'intersect', 'exclude', 'place']);
15
+
16
+ /** The ids of a document's parts. */
17
+ const idsOf = (/** @type {import('@logomarks/editor').Doc} */ doc) => parts(doc).map((p) => p.id);
18
+
19
+ /**
20
+ * Copies of some parts, in place.
21
+ *
22
+ * @param {import('@logomarks/editor').Doc} doc The document.
23
+ * @param {string[]} ids The parts.
24
+ * @returns {{ doc: import('@logomarks/editor').Doc, ids: string[] }} The document with them, and the copies' ids.
25
+ */
26
+ function copy(doc, ids) {
27
+ const next = duplicateParts(doc, ids);
28
+ return { doc: next, ids: newParts(doc, next) };
29
+ }
30
+
31
+ /**
32
+ * Every `keep` part with every `cut` part taken out of it (a part is one
33
+ * boolean operand, so each takes fresh copies of the cutters); the cutters
34
+ * are left as they were.
35
+ *
36
+ * @param {import('@logomarks/editor').Doc} doc The document.
37
+ * @param {string[]} keep The parts to cut.
38
+ * @param {string[]} cut The parts to cut with.
39
+ * @returns {{ doc: import('@logomarks/editor').Doc, ids: string[] }} The document, and what is left of `keep`.
40
+ */
41
+ function cutOut(doc, keep, cut) {
42
+ const left = [];
43
+ for (const id of keep) {
44
+ const c = copy(doc, cut);
45
+ const next = booleanParts(c.doc, 'subtract', [id, ...c.ids]);
46
+ left.push(...newParts(c.doc, next));
47
+ doc = next;
48
+ }
49
+ return { doc, ids: left };
50
+ }
51
+
52
+ /** Each boolean, from a document holding both marks' parts. @type {Record<string, (doc: import('@logomarks/editor').Doc, a: string[], b: string[]) => import('@logomarks/editor').Doc>} */
53
+ const COMBINE = {
54
+ place: (doc) => doc,
55
+ union: (doc, a, b) => booleanParts(doc, 'union', [...a, ...b]),
56
+ subtract: (doc, a, b) => deleteParts(cutOut(doc, a, b).doc, b),
57
+ // A ∩ B = A − (A − B)
58
+ intersect: (doc, a, b) => {
59
+ const a2 = copy(doc, a);
60
+ const outside = cutOut(a2.doc, a2.ids, b);
61
+ return deleteParts(cutOut(outside.doc, a, outside.ids).doc, [...b, ...outside.ids]);
62
+ },
63
+ // (A − B) ∪ (B − A), each cut with copies of the other as it was
64
+ exclude: (doc, a, b) => {
65
+ const a2 = copy(doc, a), b2 = copy(a2.doc, b);
66
+ const left = cutOut(b2.doc, a, b2.ids);
67
+ return deleteParts(cutOut(left.doc, b, a2.ids).doc, [...a2.ids, ...b2.ids]);
68
+ },
69
+ };
70
+
71
+ /**
72
+ * The second mark placed: scaled and turned about its own centre, flipped, then moved.
73
+ *
74
+ * @param {import('@logomarks/editor').Doc} doc The document.
75
+ * @param {string[]} ids Its parts.
76
+ * @param {import('./combine.js').Placement} t How.
77
+ * @returns {import('@logomarks/editor').Doc} The document.
78
+ */
79
+ function place(doc, ids, { scale = 1, rotate = 0, flip, x = 0, y = 0 } = {}) {
80
+ let d = scale === 1 ? doc : scaleParts(doc, ids, scale);
81
+ if (rotate) d = rotateParts(d, ids, rotate);
82
+ if (flip) d = flipParts(d, ids, flip);
83
+ return x || y ? moveParts(d, ids, x, y) : d;
84
+ }
85
+
86
+ /**
87
+ * Two marks combined.
88
+ *
89
+ * @param {string} a The first mark.
90
+ * @param {string} b The second, on the same 512 canvas.
91
+ * @param {import('./combine.js').CombineOptions} o The op and where the second goes.
92
+ * @returns {import('./doc.js').Edited} The SVG and the checks.
93
+ * @throws {RangeError} For an unknown op, or when nothing is left.
94
+ */
95
+ export function combineMarks(a, b, { op, transform, name }) {
96
+ const run = COMBINE[op];
97
+ if (!run) throw new RangeError(`no op "${op}": one of ${COMBINE_OPS.join(', ')}`);
98
+ const doc = toDoc(a);
99
+ toDoc(b);
100
+ const both = insertSvg(doc, b);
101
+ const bIds = newParts(doc, both);
102
+ return finish(run(place(both, bIds, transform), idsOf(doc), bIds), name);
103
+ }
@@ -0,0 +1,34 @@
1
+ import type { Doc } from '@logomarks/editor';
2
+ import type { Report } from '../checks/index.js';
3
+
4
+ /** One part of a mark, by index in drawing order. */
5
+ export interface PartInfo {
6
+ /** What an op's `parts` names it by. */
7
+ index: number;
8
+ /** `[x0, y0, x1, y1]` on the 512 canvas. */
9
+ box: [number, number, number, number];
10
+ centre: [number, number];
11
+ /** Square units, holes taken out. */
12
+ area: number;
13
+ /** How many holes it has. */
14
+ holes: number;
15
+ /** 0 the mark's colour, 1 the second tone. */
16
+ tone: number;
17
+ }
18
+
19
+ /** An edited mark: fitted and checked. */
20
+ export interface Edited {
21
+ /** 512 canvas, longest side 448, centred. */
22
+ svg: string;
23
+ /** The house checks on it. */
24
+ checks: Report;
25
+ }
26
+
27
+ /** A mark as an editable document; throws when it has no filled shapes. */
28
+ export function toDoc(svg: string): Doc;
29
+ /** Every part, by index in drawing order. */
30
+ export function listParts(doc: Doc): PartInfo[];
31
+ /** Part indices as ids (none: every part); a RangeError for one the mark doesn't have. */
32
+ export function partIds(doc: Doc, indices: readonly number[] | undefined): string[];
33
+ /** A result fitted (448 on 512, centred) and checked; throws when nothing is left. */
34
+ export function finish(doc: Doc, name?: string): Edited;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @module logomarks/edit/doc
3
+ *
4
+ * A mark as an editable document (`@logomarks/editor`'s model, no
5
+ * DOM), its parts listed by index, and a result finished the house way:
6
+ * fitted to 448 on the 512 canvas, centred, and checked (fit, specks,
7
+ * slivers, pin-holes, gaps).
8
+ */
9
+
10
+ import { parseSvg, parts as partsOf, partBounds, ring } from '@logomarks/editor/model';
11
+ import { exportSvg } from '@logomarks/editor/export';
12
+ import { fitMark } from '@logomarks/core/fit';
13
+ import { area } from '@logomarks/core/geometry';
14
+ import { check } from '../checks/index.js';
15
+ import { round } from '../util/round.js';
16
+
17
+ /**
18
+ * A mark as a document.
19
+ *
20
+ * @param {string} svg The mark.
21
+ * @returns {import('@logomarks/editor').Doc} The document.
22
+ * @throws {Error} When it has no filled shapes.
23
+ */
24
+ export function toDoc(svg) {
25
+ const doc = parseSvg(svg);
26
+ if (!partsOf(doc).length) throw new Error('the mark has no filled shapes');
27
+ return doc;
28
+ }
29
+
30
+ /**
31
+ * A contour's area.
32
+ *
33
+ * @param {import('@logomarks/editor').Contour} c The contour.
34
+ * @returns {number} Square units.
35
+ */
36
+ const contourArea = (c) => area(ring(c).map((p) => [p.x, p.y]));
37
+
38
+ /**
39
+ * Every part, by index in drawing order: what the ops' `parts` refer to.
40
+ *
41
+ * @param {import('@logomarks/editor').Doc} doc The document.
42
+ * @returns {import('./doc.js').PartInfo[]} The parts.
43
+ */
44
+ export function listParts(doc) {
45
+ return partsOf(doc).map((p, index) => {
46
+ const [x0, y0, x1, y1] = partBounds(p);
47
+ return {
48
+ index,
49
+ box: [x0, y0, x1, y1].map((v) => round(v, 1)),
50
+ centre: [round((x0 + x1) / 2, 1), round((y0 + y1) / 2, 1)],
51
+ area: round(contourArea(p.outer) - p.holes.reduce((s, h) => s + contourArea(h), 0)),
52
+ holes: p.holes.length,
53
+ tone: p.outer.tone ?? 0,
54
+ };
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Part indices as ids; none (or undefined) is every part.
60
+ *
61
+ * @param {import('@logomarks/editor').Doc} doc The document.
62
+ * @param {readonly number[] | undefined} indices The indices.
63
+ * @returns {string[]} The ids.
64
+ * @throws {RangeError} For an index the mark doesn't have.
65
+ */
66
+ export function partIds(doc, indices) {
67
+ const all = partsOf(doc);
68
+ if (!indices?.length) return all.map((p) => p.id);
69
+ return indices.map((i) => {
70
+ if (!Number.isInteger(i) || !all[i]) throw new RangeError(`no part ${i}: the mark has ${all.length} (0–${all.length - 1}); list_parts shows them`);
71
+ return all[i].id;
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Whether a document is coloured beyond `currentColor` (a second tone, or a fill).
77
+ *
78
+ * @param {import('@logomarks/editor').Doc} doc The document.
79
+ * @returns {boolean} True when it is.
80
+ */
81
+ const coloured = (doc) => doc.fill !== 'currentColor' || partsOf(doc).some((p) => p.outer.tone);
82
+
83
+ /**
84
+ * A result finished: fitted (448 on 512, centred, by marks-kit; a coloured
85
+ * one by the editor's own fit, which keeps its fills) and checked.
86
+ *
87
+ * @param {import('@logomarks/editor').Doc} doc The document.
88
+ * @param {string} [name] The mark's name, for the report.
89
+ * @returns {import('./doc.js').Edited} The SVG and the checks.
90
+ * @throws {Error} When nothing is left.
91
+ */
92
+ export function finish(doc, name = 'mark') {
93
+ if (!partsOf(doc).length) throw new Error('nothing is left of the mark');
94
+ const svg = coloured(doc) ? exportSvg(doc, { fit: true, tones: 'var' }) : fitMark(exportSvg(doc)).svg;
95
+ return { svg, checks: check(svg, { name }) };
96
+ }
@@ -0,0 +1,48 @@
1
+ import type { Edited } from './doc.js';
2
+
3
+ /** A glitch preset. */
4
+ export type GlitchPreset = 'shear' | 'slice' | 'bands' | 'stepped' | 'lane';
5
+
6
+ /** A glitch's settings. */
7
+ export interface GlitchOptions {
8
+ preset: GlitchPreset;
9
+ /** The cut's direction in degrees, clockwise from across (the mark's own slope for a shear; across the lanes for a lane change; else 0). */
10
+ angle?: number;
11
+ /** Where the first cut crosses, 0–1 across the mark (seeded). */
12
+ place?: number;
13
+ /** How far the sides end up from each other along the cut (44); a band's largest nudge. */
14
+ slide?: number;
15
+ /** The gap at each cut (22). */
16
+ gap?: number;
17
+ /** Cuts for a slice (1–3), bands for bands (3–5); seeded. */
18
+ cuts?: number;
19
+ /** Places the cuts (1): the same seed gives the same bytes. */
20
+ seed?: number;
21
+ /** The mark's name, for the checks. */
22
+ name?: string;
23
+ }
24
+
25
+ /** A glitch edition. */
26
+ export interface Glitched extends Edited {
27
+ glitch: {
28
+ preset: GlitchPreset;
29
+ seed: number;
30
+ /** The cut direction used. */
31
+ angle: number;
32
+ cuts: number;
33
+ pieces: number;
34
+ /** Placements tried before the one used. */
35
+ nudged: number;
36
+ /** Stray pieces dropped. */
37
+ removed: number;
38
+ /** A lane change's lane pitch, else null. */
39
+ pitch: number | null;
40
+ };
41
+ }
42
+
43
+ /** The presets by name, and the editor's own. */
44
+ export const PRESETS: Readonly<Record<GlitchPreset, 'shear' | 'slice' | 'bandShift' | 'stepped' | 'laneChange'>>;
45
+ /** What each preset does. */
46
+ export const PRESET_ABOUT: Record<GlitchPreset, string>;
47
+ /** A glitch edition, fitted and checked; a RangeError for an unknown preset or no clean cut. */
48
+ export function glitchMark(svg: string, o: GlitchOptions): Glitched;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @module logomarks/edit/glitch
3
+ *
4
+ * A glitch edition of a mark in one step (`@logomarks/editor`'s
5
+ * `glitch`): cut with the house gap and the pieces slid, the cuts placed by
6
+ * a seed, so the same seed gives the same bytes.
7
+ */
8
+
9
+ import { glitch, GLITCH_DEFAULTS } from '@logomarks/editor/ops';
10
+ import { toDoc, finish } from './doc.js';
11
+
12
+ /** The presets by the names the CLI and the tools use, and the editor's own. */
13
+ export const PRESETS = /** @type {const} */ ({ shear: 'shear', slice: 'slice', bands: 'bandShift', stepped: 'stepped', lane: 'laneChange' });
14
+
15
+ /** What each preset does. */
16
+ export const PRESET_ABOUT = {
17
+ shear: 'one cut along the mark’s own slope, the sides slid apart',
18
+ slice: '1–3 parallel cuts, alternate bands slid',
19
+ bands: '3–5 bands across, each nudged by a seeded amount',
20
+ stepped: 'a stair-step cut, the sides slid apart',
21
+ lane: 'one cut across a Track mark’s lanes, one side slid one lane',
22
+ };
23
+
24
+ /**
25
+ * A glitch edition.
26
+ *
27
+ * @param {string} svg The mark.
28
+ * @param {import('./glitch.js').GlitchOptions} o The preset and its settings.
29
+ * @returns {import('./glitch.js').Glitched} The SVG, the checks, and what the glitch did.
30
+ * @throws {RangeError} For an unknown preset, or when no placement cuts the mark cleanly.
31
+ */
32
+ export function glitchMark(svg, { preset, angle, place, slide, gap, cuts, seed = GLITCH_DEFAULTS.seed, name }) {
33
+ const kind = PRESETS[preset];
34
+ if (!kind) throw new RangeError(`no preset "${preset}": one of ${Object.keys(PRESETS).join(', ')}`);
35
+ // Unset settings may be undefined: vector-editor 0.2.2 and later keep its defaults for them.
36
+ const r = glitch(toDoc(svg), [], kind, { angle, position: place, offset: slide, gap, count: cuts, seed });
37
+ if (r.rejected) throw new RangeError(`no clean ${preset} for this mark at seed ${seed}: every placement left a speck or a sliver. Try another seed, angle or place.`);
38
+ if (!r.cuts.length) throw new RangeError(preset === 'lane' ? 'a lane change needs a Track mark (parallel lines with a gap); none was found' : `the ${preset} cut misses the mark: try another place or angle`);
39
+ return {
40
+ ...finish(r.doc, name),
41
+ glitch: { preset, seed, angle: Math.round(r.angle * 10) / 10, cuts: r.cuts.length, pieces: r.pieces.length, nudged: r.nudged, removed: r.removed, pitch: r.pitch },
42
+ };
43
+ }
@@ -0,0 +1,8 @@
1
+ export { glitchMark, PRESETS, PRESET_ABOUT } from './glitch.js';
2
+ export type { GlitchPreset, GlitchOptions, Glitched } from './glitch.js';
3
+ export { editMark, markParts, OPS } from './ops.js';
4
+ export type { EditOp, OpName, OpSpec, EditResult } from './ops.js';
5
+ export { combineMarks, COMBINE_OPS } from './combine.js';
6
+ export type { CombineOp, CombineOptions, Placement } from './combine.js';
7
+ export type { Edited, PartInfo } from './doc.js';
8
+ export { markPreview, partsPreview } from './preview.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @module logomarks/edit
3
+ *
4
+ * The editor's operations without the editor: glitch editions, a list of
5
+ * edits by part, and two marks combined, each fitted and checked.
6
+ */
7
+
8
+ export { glitchMark, PRESETS, PRESET_ABOUT } from './glitch.js';
9
+ export { editMark, markParts, OPS } from './ops.js';
10
+ export { combineMarks, COMBINE_OPS } from './combine.js';
11
+ export { markPreview, partsPreview } from './preview.js';
@@ -0,0 +1,51 @@
1
+ import type { Doc } from '@logomarks/editor';
2
+ import type { Edited, PartInfo } from './doc.js';
3
+
4
+ /** An op's name. */
5
+ export type OpName = 'delete' | 'move' | 'rotate' | 'scale' | 'flip' | 'round' | 'offset' | 'mirror' | 'radial' | 'union' | 'subtract' | 'color' | 'tone';
6
+
7
+ /** One op: its name, the parts it acts on, and its settings. */
8
+ export interface EditOp {
9
+ op: OpName;
10
+ /** Part indices (from `markParts` / list_parts) at the time it runs; none: every part. */
11
+ parts?: number[];
12
+ /** move: units. */
13
+ dx?: number;
14
+ dy?: number;
15
+ /** rotate: degrees, clockwise. */
16
+ angle?: number;
17
+ /** scale: a factor, or `[sx, sy]`. */
18
+ by?: number | [number, number];
19
+ /** rotate, scale, flip: the centre to turn about (the parts' own centre). */
20
+ about?: [number, number];
21
+ /** flip: `horizontal` or `vertical`; mirror: `vertical` (left matches right), `horizontal` or `both`. */
22
+ axis?: 'horizontal' | 'vertical' | 'both';
23
+ /** round: the corner radius. */
24
+ radius?: number;
25
+ /** offset: grow (positive) or shrink (negative), in units. */
26
+ distance?: number;
27
+ /** offset: how corners turn (miter). */
28
+ join?: 'miter' | 'round';
29
+ /** radial: how many copies about the centre. */
30
+ count?: number;
31
+ /** color, tone: hex. */
32
+ color?: string;
33
+ }
34
+
35
+ /** An op's description and how it runs. */
36
+ export interface OpSpec {
37
+ about: string;
38
+ run: (doc: Doc, ids: string[], op: EditOp) => Doc;
39
+ }
40
+
41
+ /** An edited mark, with its parts after. */
42
+ export interface EditResult extends Edited {
43
+ parts: PartInfo[];
44
+ }
45
+
46
+ /** Every op, by name. */
47
+ export const OPS: Record<OpName, OpSpec>;
48
+ /** A mark edited by ops in order, fitted and checked; a RangeError naming the op that failed. */
49
+ export function editMark(svg: string, ops: readonly EditOp[], o?: { name?: string }): EditResult;
50
+ /** A mark's parts, by index. */
51
+ export function markParts(svg: string): PartInfo[];
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @module logomarks/edit/ops
3
+ *
4
+ * `edit`: a short list of JSON ops run in order on a mark, each one of
5
+ * `@logomarks/editor`'s pure ops. `parts` are indices in drawing
6
+ * order as `listParts` gives them, at the time the op runs (a delete
7
+ * renumbers what follows); none means every part.
8
+ */
9
+
10
+ import {
11
+ deleteParts, moveParts, rotateParts, scaleParts, flipParts, roundParts, offsetParts,
12
+ setSymmetry, bakeSymmetry, canvasCenter, setPartAttributes, booleanParts,
13
+ } from '@logomarks/editor/ops';
14
+ import { parseColor } from '../util/color.js';
15
+ import { toDoc, partIds, listParts, finish } from './doc.js';
16
+
17
+ /**
18
+ * A colour op's hex.
19
+ *
20
+ * @param {string} value The colour.
21
+ * @returns {string} `#RRGGBB`.
22
+ * @throws {RangeError} When it isn't one.
23
+ */
24
+ function hex(value) {
25
+ const c = parseColor(value);
26
+ if (!c) throw new RangeError(`"${value}" isn't a hex colour`);
27
+ return c;
28
+ }
29
+
30
+ /** A centre as the editor takes it. */
31
+ const at = (/** @type {[number, number] | undefined} */ p) => p && { x: p[0], y: p[1] };
32
+
33
+ /**
34
+ * Each op: what it does, and how it runs on the document with its parts' ids.
35
+ *
36
+ * @type {Record<string, import('./ops.js').OpSpec>}
37
+ */
38
+ export const OPS = {
39
+ delete: { about: 'remove parts', run: (doc, ids) => deleteParts(doc, ids) },
40
+ move: { about: 'move parts by dx, dy units', run: (doc, ids, o) => moveParts(doc, ids, o.dx ?? 0, o.dy ?? 0) },
41
+ rotate: { about: 'rotate parts by angle degrees, clockwise, about their centre (or about)', run: (doc, ids, o) => rotateParts(doc, ids, o.angle ?? 0, at(o.about)) },
42
+ scale: { about: 'scale parts by a factor (or [sx, sy]) about their centre (or about)', run: (doc, ids, o) => {
43
+ const [sx, sy] = Array.isArray(o.by) ? o.by : [o.by ?? 1, o.by ?? 1];
44
+ return scaleParts(doc, ids, sx, sy, at(o.about));
45
+ } },
46
+ flip: { about: 'flip parts horizontally (left to right) or vertically', run: (doc, ids, o) => flipParts(doc, ids, o.axis ?? 'horizontal', at(o.about)) },
47
+ round: { about: 'round every corner of the parts to radius units (0 takes rounds out)', run: (doc, ids, o) => roundParts(doc, ids, o.radius ?? 0) },
48
+ offset: { about: 'grow (positive distance) or shrink (negative) the parts; join miter or round', run: (doc, ids, o) => offsetParts(doc, ids, o.distance ?? 0, { join: o.join }) },
49
+ mirror: { about: 'mirror the whole mark across the centre: vertical (left matches right), horizontal or both; overlaps are unioned', run: (doc, _ids, o) => bakeSymmetry(setSymmetry(doc, { kind: 'mirror', axis: o.axis ?? 'vertical', center: canvasCenter(doc) })) },
50
+ radial: { about: 'repeat the whole mark count times about the centre', run: (doc, _ids, o) => bakeSymmetry(setSymmetry(doc, { kind: 'radial', count: o.count ?? 4, center: canvasCenter(doc) })) },
51
+ union: { about: 'join the parts into one', run: (doc, ids) => booleanParts(doc, 'union', ids) },
52
+ subtract: { about: 'cut the other parts out of the first', run: (doc, ids) => booleanParts(doc, 'subtract', ids) },
53
+ color: { about: 'paint the whole mark in color (it is currentColor otherwise)', run: (doc, _ids, o) => ({ ...doc, fill: hex(o.color ?? '') }) },
54
+ tone: { about: 'paint the parts in a second colour: two-tone (written as var(--tone-2, color))', run: (doc, ids, o) => ({ ...setPartAttributes(doc, ids, { tone: 1 }), tones: [doc.fill, hex(o.color ?? '')] }) },
55
+ };
56
+
57
+ /** The ops that act on the whole mark, so take no `parts`. */
58
+ const WHOLE = new Set(['mirror', 'radial', 'color']);
59
+
60
+ /**
61
+ * A mark edited by a list of ops.
62
+ *
63
+ * @param {string} svg The mark.
64
+ * @param {readonly import('./ops.js').EditOp[]} ops The ops, in order.
65
+ * @param {{ name?: string }} [o] The mark's name, for the checks.
66
+ * @returns {import('./ops.js').EditResult} The SVG, the checks, and its parts after.
67
+ * @throws {RangeError} For an unknown op or a part the mark doesn't have (naming the op by its place).
68
+ */
69
+ export function editMark(svg, ops, { name } = {}) {
70
+ let doc = toDoc(svg);
71
+ ops.forEach((op, i) => {
72
+ const spec = OPS[op.op];
73
+ try {
74
+ if (!spec) throw new RangeError(`no op "${op.op}": one of ${Object.keys(OPS).join(', ')}`);
75
+ doc = spec.run(doc, WHOLE.has(op.op) ? [] : partIds(doc, op.parts), op);
76
+ } catch (e) {
77
+ throw new RangeError(`op ${i} (${op.op}): ${/** @type {Error} */ (e).message}`);
78
+ }
79
+ });
80
+ const done = finish(doc, name);
81
+ return { ...done, parts: listParts(toDoc(done.svg)) };
82
+ }
83
+
84
+ /**
85
+ * A mark's parts, by index: what an op's `parts` refer to.
86
+ *
87
+ * @param {string} svg The mark.
88
+ * @returns {import('./doc.js').PartInfo[]} The parts.
89
+ */
90
+ export const markParts = (svg) => listParts(toDoc(svg));
@@ -0,0 +1,8 @@
1
+ import type { PartInfo } from './doc.js';
2
+
3
+ /** The preview's side in pixels: 512. */
4
+ export const PREVIEW_SIZE: number;
5
+ /** The mark on white, in its own colours (two tones drawn as their colours). */
6
+ export function markPreview(svg: string): Promise<Uint8Array>;
7
+ /** The mark in grey with each part's box and index. */
8
+ export function partsPreview(svg: string, parts: readonly PartInfo[]): Promise<Uint8Array>;