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,28 @@
1
+ import type { Tool } from '../index.js';
2
+ import type { StyleId } from '@logomarks/brand-kit';
3
+
4
+ /** What `build_brand_kit` and `build_social_pack` share: the brand, and the social pack's choices. */
5
+ export interface BrandToolInput {
6
+ mark: string;
7
+ name?: string;
8
+ color?: string;
9
+ font?: string;
10
+ tagline?: string;
11
+ url?: string;
12
+ handle?: string;
13
+ /** Image ids or platform names. */
14
+ platforms?: string[];
15
+ styles?: StyleId[];
16
+ animated?: boolean;
17
+ /** The folder to write. */
18
+ out?: string;
19
+ }
20
+
21
+ /** `build_brand_kit`'s arguments. */
22
+ export interface KitInput extends BrandToolInput {
23
+ /** Include the social pack (true). */
24
+ social?: boolean;
25
+ }
26
+
27
+ /** `build_brand_kit`: the brand kit written to a folder, and its brand sheet as an image. */
28
+ export const tool: Tool<KitInput>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/kit
3
+ *
4
+ * `build_brand_kit`: the brand kit (`@logomarks/brand-kit`) written to a folder,
5
+ * with its brand sheet to look at.
6
+ */
7
+
8
+ import path from 'node:path';
9
+ import { z } from 'zod';
10
+ import { kit } from '../../kit/index.js';
11
+ import { slugify } from '../../brand/mark.js';
12
+ import { writeFiles } from '../../util/files.js';
13
+ import { schema, readMark, readColor, resolvePath } from '../inputs.js';
14
+ import { result, text, image } from '../content.js';
15
+
16
+ /** @type {import('../index.js').Tool<import('./kit.js').KitInput>} */
17
+ export const tool = {
18
+ name: 'build_brand_kit',
19
+ title: 'Build a brand kit',
20
+ description: [
21
+ 'Build a brand kit on a chosen mark and write it to a folder; use it once someone has picked a mark and wants it set up as a brand.',
22
+ 'It writes mark/ (SVG on light, dark and colour, and one that follows dark mode; PNG 64 to 2048), favicon/ (.ico, favicon.svg, apple-touch-icon.png, any and maskable icons, site.webmanifest, head.html),',
23
+ 'lockup/ (horizontal and stacked on light, dark and colour, the name as outlines), colors/ (palette.css, a Tailwind theme and design tokens: a 50–950 OKLCH scale, text that clears WCAG AA),',
24
+ 'social/ (the social pack, as build_social_pack makes it), brand-sheet.png and .svg, brand.json and README.md.',
25
+ 'Returns the folder’s absolute path, how many files, brand.json (colours with their contrast, the font, the files) and the brand sheet as an image: look at it before handing the kit over.',
26
+ ].join(' '),
27
+ inputSchema: {
28
+ mark: schema.mark,
29
+ name: schema.name,
30
+ color: schema.color,
31
+ font: schema.font,
32
+ tagline: schema.tagline,
33
+ url: schema.url,
34
+ handle: schema.handle,
35
+ styles: schema.styles,
36
+ platforms: schema.platforms,
37
+ animated: schema.animated,
38
+ social: z.boolean().optional().describe('Include the social pack (social/), as build_social_pack makes it. Default true.'),
39
+ out: z.string().optional().describe('The folder to write (made if needed; files in it are overwritten). Default "<name>-kit" in the working folder.'),
40
+ },
41
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
42
+ async run({ mark: ref, color, font, out, ...o }, { cwd }) {
43
+ const mark = readMark(ref, cwd);
44
+ const name = o.name?.trim() || mark.name;
45
+ const dir = resolvePath(cwd, out ?? `${slugify(name) || 'brand'}-kit`);
46
+ const k = await kit({ ...o, mark, name, color: readColor(color), font: /** @type {import('../../brand/fonts.js').FontId | undefined} */ (font) });
47
+ const count = writeFiles(dir, k.files);
48
+ const { files, ...brand } = k.brand;
49
+ return result(text({ dir, sheet: path.join(dir, 'brand-sheet.png'), count, ...brand, files }), image(/** @type {Uint8Array} */ (k.files.get('brand-sheet.png'))));
50
+ },
51
+ };
@@ -0,0 +1,22 @@
1
+ import type { Tool } from '../index.js';
2
+
3
+ /** `render_mockup`'s arguments. */
4
+ export interface MockupInput {
5
+ mark: string;
6
+ /** One of `OBJECTS`. */
7
+ object: string;
8
+ name?: string;
9
+ color?: string;
10
+ font?: string;
11
+ layout?: string;
12
+ surface?: string;
13
+ view?: 'front' | 'angle';
14
+ base?: string;
15
+ size?: number;
16
+ seed?: string;
17
+ /** Also write it here (.png or .svg). */
18
+ out?: string;
19
+ }
20
+
21
+ /** `render_mockup`: the brand on an object, as an image. */
22
+ export const tool: Tool<MockupInput>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/mockup
3
+ *
4
+ * `render_mockup`: the brand on a 2D object, as an image; written to a file when
5
+ * asked.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import { mockup, OBJECTS, DEFAULT_SIZE } from '../../mockup/index.js';
10
+ import { writeFile } from '../../util/files.js';
11
+ import { schema, readMark, readColor, resolvePath } from '../inputs.js';
12
+ import { result, text, image } from '../content.js';
13
+ import { preview, PREVIEW } from '../preview.js';
14
+
15
+
16
+ /** Each object with its layouts, for the description. */
17
+ const objects = Object.entries(OBJECTS).map(([k, o]) => (o.layouts ? `${k} (${o.layouts.join('/')})` : k)).join(', ');
18
+
19
+ /** @type {import('../index.js').Tool<import('./mockup.js').MockupInput>} */
20
+ export const tool = {
21
+ name: 'render_mockup',
22
+ title: 'Render a brand mockup',
23
+ description: [
24
+ 'Put a brand (a mark, its name and colour) on a 2D object and return the image; use it to show how a mark looks in use.',
25
+ 'Garments, mugs and bags carry the mark; full-bleed prints carry the lockup on the brand colour.',
26
+ `Objects, with their layouts (the first is the default): ${objects}.`,
27
+ `Returns a preview up to ${PREVIEW} px; pass out to write the full-size file (.png, or .svg for the layered SVG).`,
28
+ ].join(' '),
29
+ inputSchema: {
30
+ mark: schema.mark,
31
+ object: z.enum(/** @type {[string, ...string[]]} */ (Object.keys(OBJECTS))).describe(`The object: ${Object.keys(OBJECTS).join(', ')}.`),
32
+ name: schema.name,
33
+ color: schema.color,
34
+ font: schema.font,
35
+ layout: z.string().optional().describe("One of the object's layouts (listed in the description). Default: its first."),
36
+ surface: z.string().optional().describe('What it lies on: oak, concrete, linen, paper, light, dark, or a hex colour. Default: the object’s own.'),
37
+ view: z.enum(['front', 'angle']).optional().describe("A screen's or a sign's view. Default front."),
38
+ base: z.string().optional().describe("The object's own colour: a garment, mug or bag (white, black, navy, or a hex colour), a box's board, a device's finish. Default: the object's own."),
39
+ size: z.number().int().min(200).max(4000).optional().describe(`The written file's longest side in pixels. Default ${DEFAULT_SIZE}.`),
40
+ seed: schema.seed,
41
+ out: z.string().optional().describe('Also write the full-size file here: .png, or .svg for the layered SVG. Relative to the working folder; an existing file is replaced.'),
42
+ },
43
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
44
+ async run({ mark: ref, object, name, color, font, layout, surface, view, base, size = DEFAULT_SIZE, seed, out }, { cwd }) {
45
+ const mark = readMark(ref, cwd);
46
+ const file = out && resolvePath(cwd, out);
47
+ const m = await mockup({ mark, object, name: name?.trim() || undefined, color: readColor(color), font: /** @type {import('../../brand/fonts.js').FontId | undefined} */ (font), layout, surface, view, base, size: file ? size : PREVIEW, seed });
48
+ if (file) writeFile(file, file.toLowerCase().endsWith('.svg') ? m.svg : m.png);
49
+ return result(text({ object, ...(file && { file, width: m.width, height: m.height }) }), image(await preview(m)));
50
+ },
51
+ };
@@ -0,0 +1,15 @@
1
+ import type { Tool } from '../index.js';
2
+
3
+ /** `review_sheet`'s arguments: `svg` text or `paths`. */
4
+ export interface SheetInput {
5
+ svg?: string;
6
+ svgName?: string;
7
+ paths?: string[];
8
+ strict?: boolean;
9
+ ignore?: string[];
10
+ /** Also write the PNG here. */
11
+ out?: string;
12
+ }
13
+
14
+ /** `review_sheet`: review cards as an image, and each mark's summary. */
15
+ export const tool: Tool<SheetInput>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/sheet
3
+ *
4
+ * `review_sheet`: review cards for a set of marks, as an image to look at.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import { sheet } from '../../sheet/index.js';
9
+ import { writeFile } from '../../util/files.js';
10
+ import { schema, readSvgs, resolvePath } from '../inputs.js';
11
+ import { result, text, image } from '../content.js';
12
+
13
+ /** @type {import('../index.js').Tool<import('./sheet.js').SheetInput>} */
14
+ export const tool = {
15
+ name: 'review_sheet',
16
+ title: 'Review sheet',
17
+ description: [
18
+ 'Draw review cards for one or more marks as a PNG and return it to look at; use it as the last look before handing marks over.',
19
+ 'Each card: the mark large on dark and light with its findings boxed and numbered, at 32 and 16 px, and beside its three nearest live marks (amber: too close, compare by eye; red: a near-copy, cut it).',
20
+ 'Look at every card before handing marks over: the same trick as an existing mark is cut even when it scores low.',
21
+ 'Also returns each mark\'s check summary and nearest marks as JSON.',
22
+ ].join(' '),
23
+ inputSchema: {
24
+ svg: schema.svg,
25
+ svgName: schema.svgName,
26
+ paths: schema.paths,
27
+ strict: schema.strict,
28
+ ignore: schema.ignore,
29
+ out: z.string().optional().describe('Also write the PNG here. Relative to the working folder; an existing file is replaced.'),
30
+ },
31
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
32
+ async run({ svg, svgName, paths, strict = false, ignore = [], out }, { cwd }) {
33
+ const s = await sheet(readSvgs({ svg, svgName, paths }, cwd), { strict, ignore });
34
+ const file = out && resolvePath(cwd, out);
35
+ if (file) writeFile(file, s.png);
36
+ const marks = s.marks.map(({ name, report, matches }) => ({
37
+ name, ok: report.ok, errors: report.errors, warnings: report.warnings,
38
+ findings: report.findings.map((f) => f.id),
39
+ nearest: matches.map((m) => ({ slug: m.slug, score: m.score, verdict: m.verdict })),
40
+ }));
41
+ return result(text({ ...(file && { file }), marks }), image(s.png));
42
+ },
43
+ };
@@ -0,0 +1,13 @@
1
+ import type { Tool } from '../index.js';
2
+
3
+ /** `similar_marks`'s arguments: `svg` text or `paths`. */
4
+ export interface SimilarInput {
5
+ svg?: string;
6
+ svgName?: string;
7
+ paths?: string[];
8
+ top?: number;
9
+ letters?: boolean;
10
+ }
11
+
12
+ /** `similar_marks`: the nearest live marks, with scores and verdicts. */
13
+ export const tool: Tool<SimilarInput>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/similar
3
+ *
4
+ * `similar_marks`: the nearest live marks to one SVG or a folder of them.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import { similar } from '../../similar/nearest.js';
9
+ import { loadIndex } from '../../similar/store.js';
10
+ import { SIMILAR, LETTERS } from '../../rules.js';
11
+ import { schema, readSvgs } from '../inputs.js';
12
+ import { result, text } from '../content.js';
13
+
14
+ /** @type {import('../index.js').Tool<import('./similar.js').SimilarInput>} */
15
+ export const tool = {
16
+ name: 'similar_marks',
17
+ title: 'Find the nearest live marks',
18
+ description: [
19
+ 'Find the live Logomarks nearest to a new mark, by outline (rotations and mirrors included); use it before handing over a mark you drew or edited.',
20
+ `A score of ${SIMILAR.tooClose}+ is too close (compare the two by eye: solid shapes often share an outline); ${SIMILAR.nearCopy}+ is a near-copy and fails.`,
21
+ `letters=true compares a badge's knocked-out letter instead (${LETTERS.tooClose} / ${LETTERS.nearCopy}).`,
22
+ 'Returns { ok, thresholds, marks: [{ name, ok, tooClose, matches: [{ slug, name, score, verdict }] }] }. ok is false when any mark is a near-copy.',
23
+ ].join(' '),
24
+ inputSchema: {
25
+ svg: schema.svg,
26
+ svgName: schema.svgName,
27
+ paths: schema.paths,
28
+ top: z.number().int().min(1).max(20).optional().describe('How many nearest marks per mark. Default 5.'),
29
+ letters: z.boolean().optional().describe("Compare a badge's knocked-out letter (its holes) instead of its outline. Default false."),
30
+ },
31
+ annotations: { readOnlyHint: true, openWorldHint: false },
32
+ async run({ svg, svgName, paths, top = 5, letters = false }, { cwd }) {
33
+ const index = loadIndex();
34
+ const marks = [];
35
+ for (const { name, file, svg: source } of readSvgs({ svg, svgName, paths }, cwd)) {
36
+ const matches = await similar(source, { top, letters, index });
37
+ marks.push({ name, ...(file && { file }), ok: !matches.some((m) => m.verdict === 'near-copy'), tooClose: matches.some((m) => m.verdict !== 'ok'), matches });
38
+ }
39
+ const t = letters ? LETTERS : SIMILAR;
40
+ return result(text({ ok: marks.every((m) => m.ok), thresholds: { tooClose: t.tooClose, nearCopy: t.nearCopy }, live: index.marks.length, marks }));
41
+ },
42
+ };
@@ -0,0 +1,5 @@
1
+ import type { Tool } from '../index.js';
2
+ import type { BrandToolInput } from './kit.js';
3
+
4
+ /** `build_social_pack`: every platform image in each style, written to a folder, with a preview of each style. */
5
+ export const tool: Tool<BrandToolInput>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @module logomarks/mcp/tools/social
3
+ *
4
+ * `build_social_pack`: every platform image in each style (`@logomarks/brand-kit`),
5
+ * written to a folder, with one image per style to look at.
6
+ */
7
+
8
+ import path from 'node:path';
9
+ import { pngSize } from '@logomarks/brand-kit';
10
+ import { social } from '../../kit/index.js';
11
+ import { slugify } from '../../brand/mark.js';
12
+ import { writeFiles } from '../../util/files.js';
13
+ import { schema, readMark, readColor, resolvePath } from '../inputs.js';
14
+ import { result, text, image } from '../content.js';
15
+ import { preview, PREVIEW } from '../preview.js';
16
+
17
+ /**
18
+ * The first image of each style, as a preview to look at.
19
+ *
20
+ * @param {Map<string, string | Uint8Array>} files The pack.
21
+ * @returns {Promise<Uint8Array[]>} One PNG per style.
22
+ */
23
+ async function samples(files) {
24
+ const firsts = new Map();
25
+ for (const p of files.keys()) {
26
+ const [style, file] = p.split('/');
27
+ if (file?.endsWith('.png') && !firsts.has(style) && style !== 'email') firsts.set(style, p);
28
+ }
29
+ return Promise.all([...firsts.values()].map((p) => {
30
+ const png = /** @type {Uint8Array} */ (files.get(p));
31
+ return preview({ png, svg: /** @type {string} */ (files.get(p.replace(/\.png$/, '.svg'))), ...pngSize(png) });
32
+ }));
33
+ }
34
+
35
+ /** @type {import('../index.js').Tool<import('./kit.js').BrandToolInput>} */
36
+ export const tool = {
37
+ name: 'build_social_pack',
38
+ title: 'Build a social pack',
39
+ description: [
40
+ 'Draw the brand for every social platform at its current size, the logo kept inside each safe zone, and write it to a folder; use it for profile images, banners and posts.',
41
+ 'It covers Instagram (post, portrait, story, highlight), LinkedIn, X, YouTube (avatar, banner, thumbnail), Facebook, TikTok, Twitch, Discord, Threads and Bluesky,',
42
+ 'each in minimal, bold and pattern styles as <style>/<id>.png and .svg (animated SVGs too with animated), plus an email signature, the Open Graph card and a README of sizes and sources.',
43
+ `Returns the folder’s absolute path and its files, and the first image of each style (a preview up to ${PREVIEW} px).`,
44
+ ].join(' '),
45
+ inputSchema: {
46
+ mark: schema.mark,
47
+ name: schema.name,
48
+ color: schema.color,
49
+ font: schema.font,
50
+ tagline: schema.tagline,
51
+ url: schema.url,
52
+ handle: schema.handle,
53
+ platforms: schema.platforms,
54
+ styles: schema.styles,
55
+ animated: schema.animated,
56
+ out: schema.out.describe('The folder to write (made if needed). Default "<name>-social" in the working folder.'),
57
+ },
58
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
59
+ async run({ mark: ref, color, font, out, ...o }, { cwd }) {
60
+ const mark = readMark(ref, cwd);
61
+ const name = o.name?.trim() || mark.name;
62
+ const dir = resolvePath(cwd, out ?? `${slugify(name) || 'brand'}-social`);
63
+ const pack = await social({ ...o, mark, name, color: readColor(color), font: /** @type {import('../../brand/fonts.js').FontId | undefined} */ (font) });
64
+ writeFiles(dir, pack.files);
65
+ const files = [...pack.files.keys()];
66
+ return result(text({ dir, readme: path.join(dir, 'README.md'), count: files.length, files }), ...(await samples(pack.files)).map(image));
67
+ },
68
+ };
@@ -0,0 +1,52 @@
1
+ import type { Mark } from '../brand/mark.js';
2
+ import type { Brand } from '@logomarks/brand-kit';
3
+ import type { FontId } from '../brand/fonts.js';
4
+
5
+ /** One object: its mockup function, its layouts (the first is the default), the option `base` sets, and its art. */
6
+ export interface ObjectSpec {
7
+ make: (options: never) => { svg: string; width: number; height: number };
8
+ layouts?: string[];
9
+ base?: string;
10
+ /** Whether that option takes hex only (garment colour names are converted). */
11
+ hex?: boolean;
12
+ options: (b: Brand, base?: string) => Promise<Record<string, unknown>>;
13
+ }
14
+
15
+ /** What to mock up. */
16
+ export interface MockupOptions {
17
+ mark: Mark;
18
+ /** The brand's name (the mark's). */
19
+ name?: string;
20
+ /** The brand colour (the site's yellow). */
21
+ color?: string;
22
+ font?: FontId;
23
+ /** One of `OBJECTS`. */
24
+ object: string;
25
+ /** The object's layout (each has its own; the first is the default). */
26
+ layout?: string;
27
+ /** What it lies on: `concrete`, `oak`, `linen`, `paper`, `light`, `dark` or hex. */
28
+ surface?: string;
29
+ /** A screen's or a sign's view: `front` or `angle`. */
30
+ view?: string;
31
+ /** The object's own colour: a garment, mug or bottle (`white`, `black`, `navy`, hex …), a bag's paper, a box's board, a device's finish. */
32
+ base?: string;
33
+ /** The output's side in pixels (1600). */
34
+ size?: number;
35
+ seed?: string;
36
+ }
37
+
38
+ /** A mockup. */
39
+ export interface Mockup {
40
+ png: Uint8Array;
41
+ /** The layered SVG it was drawn from. */
42
+ svg: string;
43
+ width: number;
44
+ height: number;
45
+ }
46
+
47
+ /** The output's side when none is given: 1600. */
48
+ export const DEFAULT_SIZE: number;
49
+ /** The 2D objects, by name. */
50
+ export const OBJECTS: Record<string, ObjectSpec>;
51
+ /** The brand on an object, as PNG (resvg) and SVG. Throws a TypeError for an object or option the mockups don't take. */
52
+ export function mockup(o: MockupOptions): Promise<Mockup>;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @module logomarks/mockup
3
+ *
4
+ * The brand on a 2D object, with `@logomarks/mockups` (its layered SVG,
5
+ * rasterised by resvg here). What goes on the object depends on it: a
6
+ * garment, mug or bag carries the mark alone; anything printed edge to edge
7
+ * (a card, a poster, a sleeve, a screen, a billboard) carries the brand in
8
+ * one of `@logomarks/brand-kit`'s styles, drawn to its proportions. The hand layouts and the 3D scenes are
9
+ * left out, and so is the bus shelter (one of its blurs stops resvg 2.6).
10
+ */
11
+
12
+ import * as mockups from '@logomarks/mockups';
13
+ import { resolveBrand, artwork, markSvg, markTile } from '@logomarks/brand-kit';
14
+ import { contrastRatio, readableText, WCAG } from '@arraypress/color-utils';
15
+ import { brandInput } from '../brand/brand.js';
16
+ import { parseColor } from '../util/color.js';
17
+ import { nest } from '../sheet/draw.js';
18
+ import { render } from '../raster/render.js';
19
+
20
+ /** The output's side in pixels when none is given. */
21
+ export const DEFAULT_SIZE = 1600;
22
+
23
+ /**
24
+ * A colour name the garments know (`navy`), or hex, as hex: for the objects that take hex only.
25
+ *
26
+ * @param {string} base The colour.
27
+ * @returns {string} Its hex (the input when it's neither).
28
+ */
29
+ const hexOf = (base) => /** @type {Record<string, string>} */ (mockups.GARMENT_COLORS)[base] ?? parseColor(base) ?? base;
30
+
31
+ /**
32
+ * The colour a mark is printed in on an object: the brand's when it reads, else the brand's ink or paper.
33
+ *
34
+ * @param {import('@logomarks/brand-kit').Brand} b The brand.
35
+ * @param {string | undefined} base The object's colour (a name from the mockups, or hex; white when unset).
36
+ * @returns {string} The ink.
37
+ */
38
+ function inkOn(b, base) {
39
+ const ground = (base && parseColor(hexOf(base))) || '#FFFFFF';
40
+ const p = b.palette;
41
+ return contrastRatio(p.primary, ground) >= WCAG.LARGE ? p.primary : readableText(ground, p.paper, p.ink);
42
+ }
43
+
44
+ /**
45
+ * The mark alone, printed in a colour.
46
+ *
47
+ * @param {import('@logomarks/brand-kit').Brand} b The brand.
48
+ * @param {string} color The ink.
49
+ * @returns {string} The SVG.
50
+ */
51
+ const print = (b, color) => markSvg(b.mark, color);
52
+
53
+ /**
54
+ * Branded artwork for a print, sized to it (`@logomarks/brand-kit`'s styles).
55
+ *
56
+ * @param {import('@logomarks/brand-kit').Brand} b The brand.
57
+ * @param {number} width Pixels.
58
+ * @param {number} height Pixels.
59
+ * @param {Partial<import('@logomarks/brand-kit').ArtworkOptions>} [o] The style (bold) and the kind (by shape; `logo`: the mark alone).
60
+ * @returns {Promise<string>} The SVG.
61
+ */
62
+ const art = (b, width, height, o = {}) => artwork(b, { width, height, ...o });
63
+
64
+ /**
65
+ * The lockup in the brand's colours for paper, with no clear space.
66
+ *
67
+ * @param {import('@logomarks/brand-kit').Brand} b The brand.
68
+ * @returns {import('@logomarks/brand-kit').Lockup} The lockup.
69
+ */
70
+ const paperLockup = (b) => {
71
+ const g = b.grounds['on-light'];
72
+ return b.lockup('horizontal', { markColor: g.mark, textColor: g.text, clear: 0 });
73
+ };
74
+
75
+ /**
76
+ * An A4 letter: the lockup top left, a band of the brand colour at the foot.
77
+ *
78
+ * @param {import('@logomarks/brand-kit').Brand} b The brand.
79
+ * @returns {string} The SVG, 1240 × 1754.
80
+ */
81
+ function letter(b) {
82
+ const l = paperLockup(b);
83
+ const w = 1240, h = 1754, logoH = 64;
84
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><rect width="${w}" height="${h}" fill="${b.palette.paper}"/>${nest(l.svg, 110, 110, (logoH * l.width) / l.height, logoH)}<rect y="${h - 18}" width="${w}" height="18" fill="${b.palette.primary}"/></svg>`;
85
+ }
86
+
87
+ /**
88
+ * A landing page: a bar with the lockup, then the brand colour with the lockup large.
89
+ *
90
+ * @param {import('@logomarks/brand-kit').Brand} b The brand.
91
+ * @returns {Promise<string>} The SVG, 1440 × 900.
92
+ */
93
+ async function page(b) {
94
+ const l = paperLockup(b);
95
+ const hero = await art(b, 1440, 780);
96
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="900" viewBox="0 0 1440 900"><rect width="1440" height="900" fill="${b.palette.paper}"/>${nest(l.svg, 64, 40, (40 * l.width) / l.height, 40)}${nest(hero, 0, 120, 1440, 780)}</svg>`;
97
+ }
98
+
99
+ /**
100
+ * The 2D objects: each one's mockup function, which option takes `base` (the object's own colour), and its art.
101
+ *
102
+ * @type {Record<string, import('./index.js').ObjectSpec>}
103
+ */
104
+ export const OBJECTS = {
105
+ card: { make: mockups.card, layouts: ['pair', 'flat', 'stack', 'fan'], options: async (b) => ({ art: await art(b, 1700, 1100, { kind: 'logo' }), back: await art(b, 1700, 1100, { style: 'minimal' }) }) },
106
+ letterhead: { make: mockups.letterhead, layouts: ['set', 'flat', 'envelope'], options: async (b) => ({ art: letter(b), logo: print(b, b.palette.primary), card: await art(b, 1700, 1100, { kind: 'logo' }) }) },
107
+ ticket: { make: mockups.ticket, layouts: ['pair'], options: async (b) => ({ art: await art(b, 1397, 508) }) },
108
+ lanyard: { make: mockups.lanyard, options: async (b) => ({ art: await art(b, 1050, 1480), name: b.name }) },
109
+ sticker: { make: mockups.sticker, layouts: ['laptop', 'bottle', 'sheet'], base: 'color', hex: true, options: async (b) => ({ art: print(b, b.palette.primary), shape: 'contour' }) },
110
+ tote: { make: mockups.tote, base: 'color', options: async (b, base) => ({ art: print(b, inkOn(b, base)) }) },
111
+ tee: { make: mockups.tee, base: 'color', options: async (b, base) => ({ art: print(b, inkOn(b, base)) }) },
112
+ hoodie: { make: mockups.hoodie, base: 'color', options: async (b, base) => ({ art: print(b, inkOn(b, base)) }) },
113
+ cap: { make: mockups.cap, base: 'color', options: async (b, base) => ({ art: print(b, inkOn(b, base)) }) },
114
+ mug: { make: mockups.mug, layouts: ['single', 'pair'], base: 'color', hex: true, options: async (b, base) => ({ art: print(b, inkOn(b, base)) }) },
115
+ bag: { make: mockups.bag, base: 'paper', options: async (b, base) => ({ art: print(b, inkOn(b, base === 'kraft' || !base ? '#C8A27A' : base)) }) },
116
+ box: { make: mockups.box, base: 'board', options: async (b) => ({ art: await art(b, 1600, 1600, { style: 'minimal' }) }) },
117
+ poster: { make: mockups.poster, options: async (b) => ({ art: await art(b, 1414, 2000) }) },
118
+ vinyl: { make: mockups.vinyl, options: async (b) => ({ art: await art(b, 1600, 1600) }) },
119
+ cd: { make: mockups.cd, options: async (b) => ({ art: await art(b, 1600, 1600) }) },
120
+ cassette: { make: mockups.cassette, options: async (b) => ({ art: await art(b, 1600, 1600, { kind: 'logo' }), title: b.name }) },
121
+ phone: { make: mockups.phone, base: 'finish', options: async (b) => ({ art: print(b, b.palette.onPrimary), screen: 'home', name: b.name, iconBackground: b.palette.primary, wallpaper: b.palette.scale['900'] }) },
122
+ laptop: { make: mockups.laptop, base: 'finish', options: async (b) => ({ art: await page(b), domain: `${b.slug}.com`, title: b.name, icon: markTile(b.mark, { size: 128, color: b.palette.onPrimary, ground: b.palette.primary, pad: 0.08, radius: 0.22 }) }) },
123
+ billboard: { make: mockups.billboard, options: async (b) => ({ art: await art(b, 1500, 500) }) },
124
+ shopfront: { make: mockups.shopfront, options: async (b) => ({ art: b.mark.svg, name: b.name, color: b.palette.primary, ink: b.palette.onPrimary }) },
125
+ };
126
+
127
+ /**
128
+ * A mockup.
129
+ *
130
+ * @param {import('./index.js').MockupOptions} o The mark, the brand, the object and how to show it.
131
+ * @returns {Promise<import('./index.js').Mockup>} The PNG and the SVG it was drawn from.
132
+ * @throws {TypeError} For an object or option the mockups don't take.
133
+ */
134
+ export async function mockup({ mark, name, color, font, object, layout, surface, view, base, size = DEFAULT_SIZE, seed }) {
135
+ const spec = OBJECTS[object];
136
+ if (!spec) throw new TypeError(`no object "${object}": one of ${Object.keys(OBJECTS).join(', ')}`);
137
+ if (layout && !spec.layouts?.includes(layout)) throw new TypeError(spec.layouts ? `${object} layouts: ${spec.layouts.join(', ')}` : `${object} has one layout`);
138
+ if (base && !spec.base) throw new TypeError(`${object} has no base colour`);
139
+ const brand = await resolveBrand(brandInput({ mark, name, color, font }));
140
+ const options = {
141
+ ...(await spec.options(brand, base)),
142
+ size,
143
+ ...((layout ?? spec.layouts?.[0]) && { layout: layout ?? spec.layouts?.[0] }),
144
+ ...(surface && { surface }),
145
+ ...(view && { view }),
146
+ ...(seed && { seed }),
147
+ ...(base && spec.base && { [spec.base]: spec.hex ? hexOf(base) : base }),
148
+ };
149
+ const m = spec.make(/** @type {never} */ (options));
150
+ const png = /** @type {Uint8Array} */ ((await render(m.svg, { png: true, width: size })).png);
151
+ return { png, svg: m.svg, width: m.width, height: m.height };
152
+ }
@@ -0,0 +1,30 @@
1
+ /** How to render. */
2
+ export interface RenderOptions {
3
+ /** Scale to this width in pixels (the SVG's own size otherwise). */
4
+ width?: number;
5
+ /** Load the bundled typefaces, for SVGs with text. */
6
+ text?: boolean;
7
+ /** Encode a PNG too. */
8
+ png?: boolean;
9
+ }
10
+
11
+ /** A rendered image. */
12
+ export interface Rendered {
13
+ width: number;
14
+ height: number;
15
+ /** RGBA, row by row. */
16
+ pixels: Uint8Array;
17
+ /** The image as PNG bytes, when asked for. */
18
+ png?: Uint8Array;
19
+ }
20
+
21
+ /** Loads the WebAssembly renderer (once); every render awaits it. */
22
+ export function initRenderer(): Promise<void>;
23
+ /** Loads the renderer and reads the typefaces up front (for long-lived processes). */
24
+ export function warmRenderer(): Promise<void>;
25
+ /** Renders an SVG with resvg (WebAssembly). */
26
+ export function render(svg: string, o?: RenderOptions): Promise<Rendered>;
27
+ /** An SVG drawn to a PNG at a width: the rasteriser handed to `@logomarks/brand-kit`. */
28
+ export function rasterize(svg: string, size: { width: number; height?: number }): Promise<Uint8Array>;
29
+ /** A mark's coverage (alpha, 0–1) at `size` pixels wide, `currentColor` filled black. */
30
+ export function coverage(svg: string, size: number): Promise<Float32Array>;