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.
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +56 -2
- package/bin/logomarks.js +4 -0
- package/data/fonts/BricolageGrotesque-SemiBold.ttf +0 -0
- package/data/fonts/Cagliostro-Regular.ttf +0 -0
- package/data/fonts/Fraunces-SemiBold.ttf +0 -0
- package/data/fonts/IBMPlexMono-Regular.ttf +0 -0
- package/data/fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/data/fonts/InstrumentSans-SemiBold.ttf +0 -0
- package/data/fonts/InstrumentSerif-Regular.ttf +0 -0
- package/data/fonts/Inter-SemiBold.ttf +0 -0
- package/data/fonts/Limelight-Regular.ttf +0 -0
- package/data/fonts/OFL.txt +103 -0
- package/data/fonts/Shrikhand-Regular.ttf +0 -0
- package/data/fonts/SpaceGrotesk-SemiBold.ttf +0 -0
- package/data/index.bin +0 -0
- package/docs/api.md +31 -0
- package/docs/commands.md +412 -0
- package/docs/editing.md +61 -0
- package/docs/mcp.md +45 -0
- package/package.json +94 -4
- package/src/brand/brand.d.ts +23 -0
- package/src/brand/brand.js +29 -0
- package/src/brand/fonts.d.ts +61 -0
- package/src/brand/fonts.js +93 -0
- package/src/brand/mark.d.ts +53 -0
- package/src/brand/mark.js +151 -0
- package/src/checks/finding.d.ts +56 -0
- package/src/checks/finding.js +57 -0
- package/src/checks/fit.d.ts +5 -0
- package/src/checks/fit.js +46 -0
- package/src/checks/index.d.ts +33 -0
- package/src/checks/index.js +57 -0
- package/src/checks/specks.d.ts +5 -0
- package/src/checks/specks.js +38 -0
- package/src/checks/widths.d.ts +7 -0
- package/src/checks/widths.js +48 -0
- package/src/cli/args.d.ts +20 -0
- package/src/cli/args.js +35 -0
- package/src/cli/commands/check.d.ts +10 -0
- package/src/cli/commands/check.js +66 -0
- package/src/cli/commands/combine.d.ts +10 -0
- package/src/cli/commands/combine.js +54 -0
- package/src/cli/commands/cover.d.ts +10 -0
- package/src/cli/commands/cover.js +53 -0
- package/src/cli/commands/edit.d.ts +10 -0
- package/src/cli/commands/edit.js +70 -0
- package/src/cli/commands/find.d.ts +10 -0
- package/src/cli/commands/find.js +61 -0
- package/src/cli/commands/get.d.ts +10 -0
- package/src/cli/commands/get.js +57 -0
- package/src/cli/commands/glitch.d.ts +10 -0
- package/src/cli/commands/glitch.js +57 -0
- package/src/cli/commands/index.d.ts +15 -0
- package/src/cli/commands/index.js +24 -0
- package/src/cli/commands/kit.d.ts +10 -0
- package/src/cli/commands/kit.js +66 -0
- package/src/cli/commands/mcp.d.ts +9 -0
- package/src/cli/commands/mcp.js +36 -0
- package/src/cli/commands/mockup.d.ts +10 -0
- package/src/cli/commands/mockup.js +61 -0
- package/src/cli/commands/parts.d.ts +10 -0
- package/src/cli/commands/parts.js +44 -0
- package/src/cli/commands/sheet.d.ts +10 -0
- package/src/cli/commands/sheet.js +66 -0
- package/src/cli/commands/similar.d.ts +10 -0
- package/src/cli/commands/similar.js +111 -0
- package/src/cli/commands/social.d.ts +10 -0
- package/src/cli/commands/social.js +65 -0
- package/src/cli/commands/specks.d.ts +20 -0
- package/src/cli/commands/specks.js +103 -0
- package/src/cli/edited.d.ts +6 -0
- package/src/cli/edited.js +32 -0
- package/src/cli/files.d.ts +4 -0
- package/src/cli/files.js +27 -0
- package/src/cli/help.d.ts +20 -0
- package/src/cli/help.js +55 -0
- package/src/cli/index.d.ts +10 -0
- package/src/cli/index.js +104 -0
- package/src/cli/options.d.ts +30 -0
- package/src/cli/options.js +155 -0
- package/src/cli/report.d.ts +4 -0
- package/src/cli/report.js +28 -0
- package/src/cli/style.d.ts +4 -0
- package/src/cli/style.js +34 -0
- package/src/cover/index.d.ts +49 -0
- package/src/cover/index.js +63 -0
- package/src/edit/combine.d.ts +31 -0
- package/src/edit/combine.js +103 -0
- package/src/edit/doc.d.ts +34 -0
- package/src/edit/doc.js +96 -0
- package/src/edit/glitch.d.ts +48 -0
- package/src/edit/glitch.js +43 -0
- package/src/edit/index.d.ts +8 -0
- package/src/edit/index.js +11 -0
- package/src/edit/ops.d.ts +51 -0
- package/src/edit/ops.js +90 -0
- package/src/edit/preview.d.ts +8 -0
- package/src/edit/preview.js +51 -0
- package/src/find/index.d.ts +54 -0
- package/src/find/index.js +88 -0
- package/src/index.d.ts +30 -0
- package/src/index.js +28 -0
- package/src/kit/index.d.ts +26 -0
- package/src/kit/index.js +31 -0
- package/src/mcp/content.d.ts +30 -0
- package/src/mcp/content.js +56 -0
- package/src/mcp/edited.d.ts +5 -0
- package/src/mcp/edited.js +25 -0
- package/src/mcp/index.d.ts +41 -0
- package/src/mcp/index.js +72 -0
- package/src/mcp/inputs.d.ts +46 -0
- package/src/mcp/inputs.js +122 -0
- package/src/mcp/preview.d.ts +4 -0
- package/src/mcp/preview.js +24 -0
- package/src/mcp/tools/check.d.ts +15 -0
- package/src/mcp/tools/check.js +47 -0
- package/src/mcp/tools/combine.d.ts +15 -0
- package/src/mcp/tools/combine.js +42 -0
- package/src/mcp/tools/cover.d.ts +20 -0
- package/src/mcp/tools/cover.js +46 -0
- package/src/mcp/tools/edit.d.ts +21 -0
- package/src/mcp/tools/edit.js +73 -0
- package/src/mcp/tools/find.d.ts +14 -0
- package/src/mcp/tools/find.js +36 -0
- package/src/mcp/tools/get.d.ts +15 -0
- package/src/mcp/tools/get.js +44 -0
- package/src/mcp/tools/glitch.d.ts +12 -0
- package/src/mcp/tools/glitch.js +39 -0
- package/src/mcp/tools/index.d.ts +4 -0
- package/src/mcp/tools/index.js +23 -0
- package/src/mcp/tools/kit.d.ts +28 -0
- package/src/mcp/tools/kit.js +51 -0
- package/src/mcp/tools/mockup.d.ts +22 -0
- package/src/mcp/tools/mockup.js +51 -0
- package/src/mcp/tools/sheet.d.ts +15 -0
- package/src/mcp/tools/sheet.js +43 -0
- package/src/mcp/tools/similar.d.ts +13 -0
- package/src/mcp/tools/similar.js +42 -0
- package/src/mcp/tools/social.d.ts +5 -0
- package/src/mcp/tools/social.js +68 -0
- package/src/mockup/index.d.ts +52 -0
- package/src/mockup/index.js +152 -0
- package/src/raster/render.d.ts +30 -0
- package/src/raster/render.js +100 -0
- package/src/rules.d.ts +73 -0
- package/src/rules.js +139 -0
- package/src/sheet/card.d.ts +20 -0
- package/src/sheet/card.js +190 -0
- package/src/sheet/draw.d.ts +26 -0
- package/src/sheet/draw.js +94 -0
- package/src/sheet/grid.d.ts +2 -0
- package/src/sheet/grid.js +32 -0
- package/src/sheet/index.d.ts +31 -0
- package/src/sheet/index.js +47 -0
- package/src/sheet/theme.d.ts +8 -0
- package/src/sheet/theme.js +45 -0
- package/src/similar/folder.d.ts +20 -0
- package/src/similar/folder.js +66 -0
- package/src/similar/nearest.d.ts +32 -0
- package/src/similar/nearest.js +60 -0
- package/src/similar/signature.d.ts +12 -0
- package/src/similar/signature.js +42 -0
- package/src/similar/store.d.ts +67 -0
- package/src/similar/store.js +88 -0
- package/src/util/color.d.ts +2 -0
- package/src/util/color.js +23 -0
- package/src/util/files.d.ts +8 -0
- package/src/util/files.js +60 -0
- package/src/util/round.d.ts +2 -0
- package/src/util/round.js +14 -0
- package/src/util/version.d.ts +2 -0
- package/src/util/version.js +10 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/raster/render
|
|
3
|
+
*
|
|
4
|
+
* SVG to pixels with resvg compiled to WebAssembly: the same engine (and
|
|
5
|
+
* version) as the internal tools' `@resvg/resvg-js`, with no native
|
|
6
|
+
* binaries. The module is initialised once, on first use; text uses the
|
|
7
|
+
* bundled typefaces (IBM Plex Mono unless an SVG names another).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { initWasm, Resvg } from '@resvg/resvg-wasm';
|
|
13
|
+
import { fontFiles, LABEL_FONT } from '../brand/fonts.js';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
/** @type {Promise<void> | null} */
|
|
18
|
+
let ready = null;
|
|
19
|
+
/** @type {Uint8Array[] | null} */
|
|
20
|
+
let fonts = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Loads the WebAssembly module (once).
|
|
24
|
+
*
|
|
25
|
+
* @returns {Promise<void>} Resolves when rendering is ready.
|
|
26
|
+
*/
|
|
27
|
+
export function initRenderer() {
|
|
28
|
+
ready ??= initWasm(fs.readFileSync(require.resolve('@resvg/resvg-wasm/index_bg.wasm')));
|
|
29
|
+
return ready;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The bundled typefaces' bytes (read once).
|
|
34
|
+
*
|
|
35
|
+
* @returns {Uint8Array[]} One per file.
|
|
36
|
+
*/
|
|
37
|
+
const bundledFonts = () => (fonts ??= fontFiles().map((u) => new Uint8Array(fs.readFileSync(u))));
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Loads the WebAssembly module and reads the typefaces now, so the first
|
|
41
|
+
* render of a long-lived process (the MCP server) is as quick as the rest.
|
|
42
|
+
*
|
|
43
|
+
* @returns {Promise<void>} Resolves when both are ready.
|
|
44
|
+
*/
|
|
45
|
+
export async function warmRenderer() {
|
|
46
|
+
bundledFonts();
|
|
47
|
+
await initRenderer();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* resvg's font options: the bundled typefaces, or none.
|
|
52
|
+
*
|
|
53
|
+
* @param {boolean} text Whether the SVG has text.
|
|
54
|
+
* @returns {import('@resvg/resvg-wasm').ResvgRenderOptions['font']} The options.
|
|
55
|
+
*/
|
|
56
|
+
function fontOptions(text) {
|
|
57
|
+
if (!text) return { loadSystemFonts: false };
|
|
58
|
+
return { fontBuffers: bundledFonts(), defaultFontFamily: LABEL_FONT, monospaceFamily: LABEL_FONT };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Renders an SVG.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} svg The SVG's text.
|
|
65
|
+
* @param {import('./render.js').RenderOptions} [o] Size, text and output.
|
|
66
|
+
* @returns {Promise<import('./render.js').Rendered>} Its pixels (and PNG bytes, when asked).
|
|
67
|
+
*/
|
|
68
|
+
export async function render(svg, { width, text = false, png = false } = {}) {
|
|
69
|
+
await initRenderer();
|
|
70
|
+
const r = new Resvg(svg, { ...(width && { fitTo: { mode: 'width', value: width } }), font: fontOptions(text) });
|
|
71
|
+
const img = r.render();
|
|
72
|
+
const out = { width: img.width, height: img.height, pixels: img.pixels, ...(png && { png: img.asPng() }) };
|
|
73
|
+
img.free();
|
|
74
|
+
r.free();
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* An SVG drawn to a PNG: the rasteriser `@logomarks/brand-kit` is handed, so
|
|
80
|
+
* the kit needs no sharp. Its text is already outlines.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} svg The SVG.
|
|
83
|
+
* @param {{ width: number }} size The PNG's width (the height follows the SVG's own shape).
|
|
84
|
+
* @returns {Promise<Uint8Array>} The PNG.
|
|
85
|
+
*/
|
|
86
|
+
export const rasterize = async (svg, { width }) => /** @type {Uint8Array} */ ((await render(svg, { width, png: true })).png);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A mark's alpha channel at a size, filled black (as the tools do: `currentColor` → `#000`).
|
|
90
|
+
*
|
|
91
|
+
* @param {string} svg The mark.
|
|
92
|
+
* @param {number} size The width in pixels.
|
|
93
|
+
* @returns {Promise<Float32Array>} Coverage 0–1, row by row.
|
|
94
|
+
*/
|
|
95
|
+
export async function coverage(svg, size) {
|
|
96
|
+
const { pixels, width, height } = await render(svg.replace(/fill="currentColor"/g, 'fill="#000"'), { width: size });
|
|
97
|
+
const a = new Float32Array(width * height);
|
|
98
|
+
for (let i = 0; i < a.length; i++) a[i] = pixels[i * 4 + 3] / 255;
|
|
99
|
+
return a;
|
|
100
|
+
}
|
package/src/rules.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** The canvas side: `viewBox="0 0 512 512"`. */
|
|
2
|
+
export const CANVAS: 512;
|
|
3
|
+
/** The mark's longest side, measured on the real outline. */
|
|
4
|
+
export const FIT: 448;
|
|
5
|
+
/** The margin a fitted mark leaves on its long side (32). */
|
|
6
|
+
export const MARGIN: number;
|
|
7
|
+
/** Where a fitted mark's box is centred (256). */
|
|
8
|
+
export const CENTRE: number;
|
|
9
|
+
/** How far size, centre and canvas edges may drift (0.5). */
|
|
10
|
+
export const FIT_TOLERANCE: number;
|
|
11
|
+
/** Decimal places in coordinates (1). */
|
|
12
|
+
export const DECIMALS: number;
|
|
13
|
+
/** Markup a finished mark must not contain. */
|
|
14
|
+
export const FORBIDDEN_MARKUP: RegExp;
|
|
15
|
+
|
|
16
|
+
/** Speck, sliver and pin-hole limits (square units, units). */
|
|
17
|
+
export const SPECKS: Readonly<{
|
|
18
|
+
/** A piece under this area is a speck (60). */
|
|
19
|
+
piece: number;
|
|
20
|
+
/** A hole under this area is a pin-hole (30). */
|
|
21
|
+
hole: number;
|
|
22
|
+
/** Under this area a part is two outlines touching, left out (0.5). */
|
|
23
|
+
tiny: number;
|
|
24
|
+
/** Mean width under this is an invisible hairline, left out (0.12). */
|
|
25
|
+
hairline: number;
|
|
26
|
+
/** A small piece thinner than this is a sliver rather than a speck (3). */
|
|
27
|
+
sliver: number;
|
|
28
|
+
/** This many small parts of one area are a pattern, left out (6). */
|
|
29
|
+
repeat: number;
|
|
30
|
+
/** How alike two areas must be to repeat: vector and raster reads. */
|
|
31
|
+
repeatTolerance: Readonly<{ vector: number; raster: number }>;
|
|
32
|
+
/** Pixels a unit for the raster read (8). */
|
|
33
|
+
rasterScale: number;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
/** Live marks whose small parts were reviewed and kept, with the reason. */
|
|
37
|
+
export const REVIEWED: Readonly<Record<string, string>>;
|
|
38
|
+
|
|
39
|
+
/** Gaps, and what counts as an even run (units). */
|
|
40
|
+
export const GAPS: Readonly<{
|
|
41
|
+
/** The house gap (22). */
|
|
42
|
+
house: number;
|
|
43
|
+
/** The bold family's range (20–24). */
|
|
44
|
+
range: [number, number];
|
|
45
|
+
/** Gaps further apart than this are different gaps (3). */
|
|
46
|
+
same: number;
|
|
47
|
+
/** How much a run may widen along its length (2). */
|
|
48
|
+
tolerance: number;
|
|
49
|
+
/** How much most of it may vary (1). */
|
|
50
|
+
spread: number;
|
|
51
|
+
/** How many times its width a run is long (3). */
|
|
52
|
+
run: number;
|
|
53
|
+
/** The shortest run (16). */
|
|
54
|
+
shortest: number;
|
|
55
|
+
/** Wider runs are space, not cuts (32). */
|
|
56
|
+
widest: number;
|
|
57
|
+
/** Narrower runs are hairlines (1). */
|
|
58
|
+
narrowest: number;
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
/** Nothing narrower than this, ink or gap (10). */
|
|
62
|
+
export const MIN_FEATURE: number;
|
|
63
|
+
/** Pixels a unit for the width read (2). */
|
|
64
|
+
export const WIDTH_SCALE: number;
|
|
65
|
+
/** How far a width read can be off (0.5). */
|
|
66
|
+
export const WIDTH_PRECISION: number;
|
|
67
|
+
/** The chip sizes a mark must read at. */
|
|
68
|
+
export const CHIPS: readonly [32, 16];
|
|
69
|
+
|
|
70
|
+
/** Outline near-duplicates: raster size, and the too-close and near-copy scores. */
|
|
71
|
+
export const SIMILAR: Readonly<{ size: number; tooClose: number; nearCopy: number }>;
|
|
72
|
+
/** Knocked-out letters: render size, crop size, and the too-close and near-copy scores. */
|
|
73
|
+
export const LETTERS: Readonly<{ render: number; size: number; tooClose: number; nearCopy: number }>;
|
package/src/rules.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/rules
|
|
3
|
+
*
|
|
4
|
+
* The house rules every Logomark is drawn to, in one place. Numbers are units
|
|
5
|
+
* of the 512 canvas unless named otherwise. They come from the skill
|
|
6
|
+
* (`SKILL.md`: "The standard" and the "Review checklist") and from the
|
|
7
|
+
* internal pipeline (`fit.mjs`, `speckcheck.mjs`, `dupcheck.mjs`,
|
|
8
|
+
* `holecheck.mjs`); each constant names its source.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The canvas: `viewBox="0 0 512 512"` (SKILL.md, the standard). */
|
|
12
|
+
export const CANVAS = 512;
|
|
13
|
+
|
|
14
|
+
/** The mark's longest side, measured on the real outline (SKILL.md; fit.mjs `FIT`). */
|
|
15
|
+
export const FIT = 448;
|
|
16
|
+
|
|
17
|
+
/** The margin a fitted mark leaves on its long side: 32. */
|
|
18
|
+
export const MARGIN = (CANVAS - FIT) / 2;
|
|
19
|
+
|
|
20
|
+
/** Where a fitted mark's box is centred, on both axes. */
|
|
21
|
+
export const CENTRE = CANVAS / 2;
|
|
22
|
+
|
|
23
|
+
/** How far size, centre and canvas edges may drift before a fit fails (fit.mjs `TOL`). */
|
|
24
|
+
export const FIT_TOLERANCE = 0.5;
|
|
25
|
+
|
|
26
|
+
/** Coordinates are written to one decimal place (SKILL.md, the standard). */
|
|
27
|
+
export const DECIMALS = 1;
|
|
28
|
+
|
|
29
|
+
/** Markup a finished mark must not contain: flatten to filled paths (fit.mjs `check()`). */
|
|
30
|
+
export const FORBIDDEN_MARKUP = /<(g|style|mask|clipPath|use|text|image)\b|transform=|stroke=/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stray specks, slivers and pin-holes a boolean left behind (speckcheck.mjs).
|
|
34
|
+
* Small parts are reported unless they are tiny (two outlines touching at a
|
|
35
|
+
* point), hairline (a zero-width sliver along an edge, invisible) or repeated
|
|
36
|
+
* (a dot field, a pattern).
|
|
37
|
+
*/
|
|
38
|
+
export const SPECKS = {
|
|
39
|
+
/** A piece under this area (square units) is a speck. */
|
|
40
|
+
piece: 60,
|
|
41
|
+
/** A hole under this area is a pin-hole. */
|
|
42
|
+
hole: 30,
|
|
43
|
+
/** Under this area a part is only two outlines touching: left out. */
|
|
44
|
+
tiny: 0.5,
|
|
45
|
+
/** Mean width (2·area/perimeter) under this is a hairline, invisible below ~4000 px: left out. */
|
|
46
|
+
hairline: 0.12,
|
|
47
|
+
/** A small piece thinner than this (mean width) is reported as a sliver rather than a speck. */
|
|
48
|
+
sliver: 3,
|
|
49
|
+
/** This many small parts of one area (within the tolerance below) are a pattern, not specks. */
|
|
50
|
+
repeat: 6,
|
|
51
|
+
/** How alike two areas must be to count as repeats, as a fraction: vector read, raster read. */
|
|
52
|
+
repeatTolerance: { vector: 0.02, raster: 0.05 },
|
|
53
|
+
/** Pixels a unit for the raster read, used when the vector read fails. */
|
|
54
|
+
rasterScale: 8,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Live marks looked at large (2026-11-05) and kept: their small parts are the
|
|
59
|
+
* design, or can't be seen (speckcheck.mjs `REVIEWED`). A file with one of
|
|
60
|
+
* these names gets its speck findings as notes, with the reason.
|
|
61
|
+
*/
|
|
62
|
+
export const REVIEWED = {
|
|
63
|
+
'dot-sphere': 'a dot field clipped by its ring; the rim fragments are the clip',
|
|
64
|
+
'dot-pyramid': 'dots shrinking with distance',
|
|
65
|
+
halftone: 'halftone rows, the apex dot the smallest',
|
|
66
|
+
'line-triangle': 'stacked bars ending in a small apex triangle',
|
|
67
|
+
'spiral-delta': 'xor of rotated triangles; the small shards are the spiral',
|
|
68
|
+
spirograph: 'the cells where the curve crosses itself',
|
|
69
|
+
'lens-rosette': 'xor of twelve lenses; the lacy centre is where their tips cross',
|
|
70
|
+
'notched-diamond': 'a 0.14-wide sliver along the cut (a separate path in the file), invisible below ~4000 px',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gaps: the cuts between parts. One width per mark; the set's gap is 22, the
|
|
75
|
+
* bold family's range 20–24 (SKILL.md, "Bold negative space" and the review
|
|
76
|
+
* checklist: "One gap width and one stroke weight (or a clear 1:2 ratio)").
|
|
77
|
+
* Gaps and strokes are measured on a raster as runs of even width (see
|
|
78
|
+
* `raster/channels.js`); the numbers below say what counts as a run.
|
|
79
|
+
*/
|
|
80
|
+
export const GAPS = {
|
|
81
|
+
/** The house gap. */
|
|
82
|
+
house: 22,
|
|
83
|
+
/** The bold family's range. */
|
|
84
|
+
range: /** @type {[number, number]} */ ([20, 24]),
|
|
85
|
+
/** Gaps further apart than this (units) are different gaps. */
|
|
86
|
+
same: 3,
|
|
87
|
+
/** A run widens by at most this along its length (units): a cut or a stroke, not a tapering point. */
|
|
88
|
+
tolerance: 2,
|
|
89
|
+
/** And most of it (10th to 90th percentile) by at most this. */
|
|
90
|
+
spread: 1,
|
|
91
|
+
/** A run is at least this many times as long as it is wide… */
|
|
92
|
+
run: 3,
|
|
93
|
+
/** …and at least this long (units). */
|
|
94
|
+
shortest: 16,
|
|
95
|
+
/** Runs wider than this are open space or counters, not cuts (the house gap fits ~20–30 after fitting). */
|
|
96
|
+
widest: 32,
|
|
97
|
+
/** Runs narrower than this are hairlines (the speck check's business), not features. */
|
|
98
|
+
narrowest: 1,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Nothing narrower than this, ink or gap: under it a feature closes up at 32 px (SKILL.md review checklist). */
|
|
102
|
+
export const MIN_FEATURE = 10;
|
|
103
|
+
|
|
104
|
+
/** Pixels a unit for the width read (gaps and strokes). */
|
|
105
|
+
export const WIDTH_SCALE = 2;
|
|
106
|
+
|
|
107
|
+
/** How far a width read at that scale can be off (units): a feature is only too thin below `MIN_FEATURE` less this. */
|
|
108
|
+
export const WIDTH_PRECISION = 0.5;
|
|
109
|
+
|
|
110
|
+
/** The small sizes a mark must read at: 32 px and 16 px chips (SKILL.md workflow and "Bold negative space"). */
|
|
111
|
+
export const CHIPS = /** @type {const} */ ([32, 16]);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Near-duplicates by outline (dupcheck.mjs): each mark rastered at 48 px,
|
|
115
|
+
* blurred 3×3, and compared by overlap (IoU) in all 8 rotations and flips.
|
|
116
|
+
*/
|
|
117
|
+
export const SIMILAR = {
|
|
118
|
+
/** The raster's side in pixels. */
|
|
119
|
+
size: 48,
|
|
120
|
+
/** A score at or above this is too close: look at the two side by side. */
|
|
121
|
+
tooClose: 0.72,
|
|
122
|
+
/** At or above this it is a near-copy (Dot Stack vs Rack: 0.99); 1.00 is usually a rotation. */
|
|
123
|
+
nearCopy: 0.9,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Knocked-out letters (holecheck.mjs): badges all score high on their shared
|
|
128
|
+
* outline, so the letter (the hole) is compared alone, cropped to its box.
|
|
129
|
+
*/
|
|
130
|
+
export const LETTERS = {
|
|
131
|
+
/** The render's side in pixels. */
|
|
132
|
+
render: 128,
|
|
133
|
+
/** The cropped letter's side in pixels. */
|
|
134
|
+
size: 40,
|
|
135
|
+
/** Flag at or above this (a heavy A scores ~0.75 against any heavy A). */
|
|
136
|
+
tooClose: 0.7,
|
|
137
|
+
/** A real copy scores this or more. */
|
|
138
|
+
nearCopy: 0.85,
|
|
139
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Report } from '../checks/index.js';
|
|
2
|
+
import type { Match } from '../similar/nearest.js';
|
|
3
|
+
import type { MarkIndex } from '../similar/store.js';
|
|
4
|
+
|
|
5
|
+
/** What a card shows. */
|
|
6
|
+
export interface CardInput {
|
|
7
|
+
/** The mark's name, shown as the card's title. */
|
|
8
|
+
name: string;
|
|
9
|
+
/** The mark. */
|
|
10
|
+
svg: string;
|
|
11
|
+
/** Its check. */
|
|
12
|
+
report: Report;
|
|
13
|
+
/** Its nearest live marks (three are shown). */
|
|
14
|
+
matches: Match[];
|
|
15
|
+
/** The live marks, for drawing the neighbours. */
|
|
16
|
+
index: MarkIndex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** One review card as SVG, starting `y` pixels down the page, and its height. */
|
|
20
|
+
export function card(input: CardInput, y: number): Promise<{ svg: string; height: number }>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/sheet/card
|
|
3
|
+
*
|
|
4
|
+
* One review card: the mark large on dark and on light (the light one with
|
|
5
|
+
* its findings boxed and numbered), 32 px and 16 px chips on both grounds
|
|
6
|
+
* (magnified, and at actual size), its three nearest live marks with
|
|
7
|
+
* scores, and the check results as badges with their messages.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { render } from '../raster/render.js';
|
|
11
|
+
import { CHIPS } from '../rules.js';
|
|
12
|
+
import { COLORS, CARD, ZOOM } from './theme.js';
|
|
13
|
+
import { text, panel, mark, pixels } from './draw.js';
|
|
14
|
+
|
|
15
|
+
/** Characters' advance in the 13 px monospace, for sizing badges. */
|
|
16
|
+
const CHAR = 7.8;
|
|
17
|
+
|
|
18
|
+
/** How many finding messages a card lists under its badges. */
|
|
19
|
+
const MESSAGES = 6;
|
|
20
|
+
|
|
21
|
+
/** A severity's colour and symbol. */
|
|
22
|
+
const SEVERITY = {
|
|
23
|
+
error: { color: COLORS.error, mark: '×' },
|
|
24
|
+
warning: { color: COLORS.warning, mark: '!' },
|
|
25
|
+
info: { color: COLORS.info, mark: 'i' },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A mark at a small size on a ground, as PNG bytes.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} svg The mark.
|
|
32
|
+
* @param {number} size The size in pixels.
|
|
33
|
+
* @param {string} ground The ground colour.
|
|
34
|
+
* @param {string} ink The mark's colour.
|
|
35
|
+
* @returns {Promise<Uint8Array>} The PNG.
|
|
36
|
+
*/
|
|
37
|
+
async function chip(svg, size, ground, ink) {
|
|
38
|
+
const doc = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" fill="${ground}"/>${mark(svg, 0, 0, size, ink)}</svg>`;
|
|
39
|
+
return /** @type {Uint8Array} */ ((await render(doc, { png: true })).png);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The chips column: each size on dark and light, magnified, with the actual size beside.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} svg The mark.
|
|
46
|
+
* @param {number} x Left.
|
|
47
|
+
* @param {number} y Top.
|
|
48
|
+
* @returns {Promise<string>} The SVG.
|
|
49
|
+
*/
|
|
50
|
+
async function chips(svg, x, y) {
|
|
51
|
+
let out = '';
|
|
52
|
+
let top = y;
|
|
53
|
+
for (const size of CHIPS) {
|
|
54
|
+
const zoom = ZOOM[size], big = size * zoom;
|
|
55
|
+
out += text(x, top + 13, `${size} px ×${zoom}`, { size: 12, color: COLORS.muted });
|
|
56
|
+
const [dark, light] = await Promise.all([chip(svg, size, COLORS.dark, COLORS.darkInk), chip(svg, size, COLORS.light, COLORS.lightInk)]);
|
|
57
|
+
out += pixels(dark, x, top + 22, big) + pixels(light, x + big + 8, top + 22, big);
|
|
58
|
+
const ax = x + 2 * big + 16;
|
|
59
|
+
out += pixels(dark, ax, top + 22, size) + pixels(light, ax, top + 22 + size + 6, size);
|
|
60
|
+
top += 22 + big + 26;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Where a finding is, as a box (a point becomes a small box round it).
|
|
67
|
+
*
|
|
68
|
+
* @param {import('../checks/finding.js').Finding} f The finding.
|
|
69
|
+
* @returns {import('@logomarks/core/geometry').Box} Its box on the canvas.
|
|
70
|
+
*/
|
|
71
|
+
function where(f) {
|
|
72
|
+
if (f.box) return f.box;
|
|
73
|
+
const [px, py] = f.point ?? [256, 256];
|
|
74
|
+
return [px - 6, py - 6, px + 6, py + 6];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The finding boxes over the light view, numbered as the badges are.
|
|
79
|
+
*
|
|
80
|
+
* @param {readonly import('../checks/finding.js').Finding[]} located The findings with a location.
|
|
81
|
+
* @param {number} x The view's left.
|
|
82
|
+
* @param {number} y Its top.
|
|
83
|
+
* @returns {string} The SVG.
|
|
84
|
+
*/
|
|
85
|
+
function overlay(located, x, y) {
|
|
86
|
+
const k = CARD.large / 512;
|
|
87
|
+
return located.map((f, i) => {
|
|
88
|
+
const { color } = SEVERITY[f.severity];
|
|
89
|
+
const [x0, y0, x1, y1] = where(f);
|
|
90
|
+
const pad = 3, bx = x + x0 * k - pad, by = y + y0 * k - pad, bw = (x1 - x0) * k + 2 * pad, bh = (y1 - y0) * k + 2 * pad;
|
|
91
|
+
const tag = Math.max(x + 2, Math.min(bx, x + CARD.large - 20));
|
|
92
|
+
return `<rect x="${bx}" y="${by}" width="${bw}" height="${bh}" rx="3" fill="none" stroke="${color}" stroke-width="2"/>`
|
|
93
|
+
+ `<rect x="${tag}" y="${Math.max(y + 2, by - 18)}" width="18" height="16" rx="3" fill="${color}"/>`
|
|
94
|
+
+ text(tag + 9, Math.max(y + 2, by - 18) + 12, String(i + 1), { size: 11, color: '#000', weight: 600, anchor: 'middle' });
|
|
95
|
+
}).join('');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The nearest live marks as tiles with names and scores.
|
|
100
|
+
*
|
|
101
|
+
* @param {readonly import('../similar/nearest.js').Match[]} matches The matches.
|
|
102
|
+
* @param {import('../similar/store.js').MarkIndex} index The live marks.
|
|
103
|
+
* @param {number} x Left.
|
|
104
|
+
* @param {number} y Top.
|
|
105
|
+
* @returns {string} The SVG.
|
|
106
|
+
*/
|
|
107
|
+
function neighbours(matches, index, x, y) {
|
|
108
|
+
const s = CARD.neighbour;
|
|
109
|
+
let out = text(x, y + 13, 'nearest live marks', { size: 12, color: COLORS.muted });
|
|
110
|
+
matches.forEach((m, i) => {
|
|
111
|
+
const live = index.marks.find((l) => l.slug === m.slug);
|
|
112
|
+
const tx = x + i * (s + 12), hot = m.verdict !== 'ok';
|
|
113
|
+
const color = m.verdict === 'near-copy' ? COLORS.error : m.verdict === 'too-close' ? COLORS.warning : COLORS.muted;
|
|
114
|
+
out += panel(tx, y + 22, s, s, COLORS.dark, { stroke: hot ? color : COLORS.line });
|
|
115
|
+
if (live) out += mark(live.svg, tx + 14, y + 36, s - 28, COLORS.darkInk);
|
|
116
|
+
out += text(tx, y + s + 40, m.name.length > 17 ? `${m.name.slice(0, 16)}…` : m.name, { size: 12 });
|
|
117
|
+
out += text(tx, y + s + 57, `${m.score.toFixed(2)}${hot ? ` ${m.verdict}` : ''}`, { size: 12, weight: 600, color });
|
|
118
|
+
});
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Badges for the findings, wrapped to the card's width.
|
|
124
|
+
*
|
|
125
|
+
* @param {readonly import('../checks/finding.js').Finding[]} findings The findings (located ones first, numbered).
|
|
126
|
+
* @param {number} numbered How many carry a number.
|
|
127
|
+
* @param {number} y Top.
|
|
128
|
+
* @returns {{ svg: string, height: number }} The SVG and the height it takes.
|
|
129
|
+
*/
|
|
130
|
+
function badges(findings, numbered, y) {
|
|
131
|
+
const left = CARD.pad, right = CARD.width - CARD.pad;
|
|
132
|
+
if (!findings.length) {
|
|
133
|
+
const label = 'passes every check';
|
|
134
|
+
return { svg: panel(left, y, label.length * CHAR + 20, CARD.badge, 'none', { r: 14, stroke: COLORS.pass }) + text(left + 10, y + 18, label, { color: COLORS.pass, weight: 600 }), height: CARD.badge };
|
|
135
|
+
}
|
|
136
|
+
let svg = '', x = left, top = y;
|
|
137
|
+
findings.forEach((f, i) => {
|
|
138
|
+
const { color, mark: sym } = SEVERITY[f.severity];
|
|
139
|
+
const label = `${i < numbered ? `${i + 1} ` : ''}${sym} ${f.id}${f.value !== undefined ? ` ${f.value}` : ''}`;
|
|
140
|
+
const w = label.length * CHAR + 20;
|
|
141
|
+
if (x + w > right) { x = left; top += CARD.badge + 8; }
|
|
142
|
+
svg += panel(x, top, w, CARD.badge, 'none', { r: 14, stroke: color }) + text(x + 10, top + 18, label, { color, weight: 600 });
|
|
143
|
+
x += w + 8;
|
|
144
|
+
});
|
|
145
|
+
return { svg, height: top - y + CARD.badge };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One review card.
|
|
150
|
+
*
|
|
151
|
+
* @param {import('./card.js').CardInput} input The mark, its report and its neighbours.
|
|
152
|
+
* @param {number} y Where the card starts on the page.
|
|
153
|
+
* @returns {Promise<{ svg: string, height: number }>} The card and its height.
|
|
154
|
+
*/
|
|
155
|
+
export async function card({ name, svg, report, matches, index }, y) {
|
|
156
|
+
const { pad, gap, head, large } = CARD;
|
|
157
|
+
const located = report.findings.filter((f) => f.box || f.point);
|
|
158
|
+
const ordered = [...located, ...report.findings.filter((f) => !f.box && !f.point)];
|
|
159
|
+
const top = y + head;
|
|
160
|
+
let out = '';
|
|
161
|
+
// header: name, size and counts, verdict
|
|
162
|
+
const verdict = report.errors ? ['FAIL', COLORS.error] : report.warnings ? ['PASS · WARNINGS', COLORS.warning] : ['PASS', COLORS.pass];
|
|
163
|
+
const size = report.size ? `${report.size.w} × ${report.size.h}` : 'empty';
|
|
164
|
+
const counts = `${report.errors} error${report.errors === 1 ? '' : 's'} · ${report.warnings} warning${report.warnings === 1 ? '' : 's'}`;
|
|
165
|
+
const closest = matches[0] ? ` · nearest ${matches[0].score.toFixed(2)}` : '';
|
|
166
|
+
out += text(pad, y + 36, name, { size: 22, weight: 600 });
|
|
167
|
+
out += text(pad + name.length * 13.2 + 18, y + 35, `${size} · ${counts}${closest}`, { size: 13, color: COLORS.muted });
|
|
168
|
+
const vw = verdict[0].length * CHAR + 24;
|
|
169
|
+
out += panel(CARD.width - pad - vw, y + 16, vw, 28, verdict[1], { r: 14 }) + text(CARD.width - pad - vw / 2, y + 35, verdict[0], { color: '#000', weight: 600, anchor: 'middle' });
|
|
170
|
+
// large views: dark, then light with the findings boxed
|
|
171
|
+
out += panel(pad, top, large, large, COLORS.dark) + mark(svg, pad + 30, top + 30, large - 60, COLORS.darkInk);
|
|
172
|
+
const lx = pad + large + gap;
|
|
173
|
+
out += panel(lx, top, large, large, COLORS.light) + mark(svg, lx, top, large, COLORS.lightInk) + overlay(located, lx, top);
|
|
174
|
+
// small sizes, then neighbours
|
|
175
|
+
const cx = lx + large + gap;
|
|
176
|
+
out += await chips(svg, cx, top);
|
|
177
|
+
out += neighbours(matches, index, cx + CARD.chips + gap, top);
|
|
178
|
+
// badges and the first messages
|
|
179
|
+
const b = badges(ordered, located.length, top + large + gap);
|
|
180
|
+
out += b.svg;
|
|
181
|
+
let line = top + large + gap + b.height + 22;
|
|
182
|
+
ordered.slice(0, MESSAGES).forEach((f, i) => {
|
|
183
|
+
const msg = `${i < located.length ? `${i + 1}. ` : '· '}${f.message}`;
|
|
184
|
+
out += text(pad, line, msg.length > 165 ? `${msg.slice(0, 164)}…` : msg, { size: 12, color: COLORS.muted });
|
|
185
|
+
line += 18;
|
|
186
|
+
});
|
|
187
|
+
if (ordered.length > MESSAGES) { out += text(pad, line, `… and ${ordered.length - MESSAGES} more (run check --json for all)`, { size: 12, color: COLORS.muted }); line += 18; }
|
|
188
|
+
const height = line - y + pad - 10;
|
|
189
|
+
return { svg: panel(0, y, CARD.width, height, COLORS.card, { r: 14 }) + out, height };
|
|
190
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** How a line of text looks. */
|
|
2
|
+
export interface TextStyle {
|
|
3
|
+
/** Pixels (13). */
|
|
4
|
+
size?: number;
|
|
5
|
+
color?: string;
|
|
6
|
+
/** The face's weight: 400 or 600 for the labels (400). */
|
|
7
|
+
weight?: number;
|
|
8
|
+
anchor?: 'start' | 'middle' | 'end';
|
|
9
|
+
/** A bundled face's family, with its `weight` (IBM Plex Mono). */
|
|
10
|
+
font?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Text made safe for XML. */
|
|
14
|
+
export function esc(s: string): string;
|
|
15
|
+
/** A line of text at a baseline. */
|
|
16
|
+
export function text(x: number, y: number, s: string, o?: TextStyle): string;
|
|
17
|
+
/** A rounded rectangle, with an optional outline. */
|
|
18
|
+
export function panel(x: number, y: number, w: number, h: number, fill: string, o?: { r?: number; stroke?: string }): string;
|
|
19
|
+
/** A mark drawn in a square of side `size`, in one colour. */
|
|
20
|
+
export function mark(svg: string, x: number, y: number, size: number, color: string): string;
|
|
21
|
+
/** A PNG placed at a size, scaled by nearest neighbour so its pixels show. */
|
|
22
|
+
export function pixels(png: Uint8Array, x: number, y: number, size: number): string;
|
|
23
|
+
/** An SVG (with a `viewBox`) placed in a box, scaled to fit and centred. */
|
|
24
|
+
export function nest(svg: string, x: number, y: number, w: number, h: number): string;
|
|
25
|
+
/** A PNG placed in a box, smoothly scaled. */
|
|
26
|
+
export function image(png: Uint8Array, x: number, y: number, w: number, h: number): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/sheet/draw
|
|
3
|
+
*
|
|
4
|
+
* Small SVG builders the review card is made of: text, panels, a mark placed
|
|
5
|
+
* in a box, a raster chip.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { body, viewBox } from '@logomarks/core/svg';
|
|
9
|
+
import { COLORS, FONT } from './theme.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Text made safe for XML.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} s The text.
|
|
15
|
+
* @returns {string} It, escaped.
|
|
16
|
+
*/
|
|
17
|
+
export const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A line of text.
|
|
21
|
+
*
|
|
22
|
+
* @param {number} x Where it starts (or its middle, centred).
|
|
23
|
+
* @param {number} y Its baseline.
|
|
24
|
+
* @param {string} s The text.
|
|
25
|
+
* @param {import('./draw.js').TextStyle} [o] How it looks.
|
|
26
|
+
* @returns {string} The SVG.
|
|
27
|
+
*/
|
|
28
|
+
export const text = (x, y, s, { size = 13, color = COLORS.text, weight = 400, anchor = 'start', font } = {}) =>
|
|
29
|
+
`<text x="${x}" y="${y}" font-family="${font ?? FONT}" font-weight="${weight}" font-size="${size}" fill="${color}" text-anchor="${anchor}">${esc(s)}</text>`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A rounded rectangle.
|
|
33
|
+
*
|
|
34
|
+
* @param {number} x Left.
|
|
35
|
+
* @param {number} y Top.
|
|
36
|
+
* @param {number} w Width.
|
|
37
|
+
* @param {number} h Height.
|
|
38
|
+
* @param {string} fill Its colour.
|
|
39
|
+
* @param {object} [o] Corners and outline.
|
|
40
|
+
* @param {number} [o.r=10] Corner radius.
|
|
41
|
+
* @param {string} [o.stroke] Outline colour.
|
|
42
|
+
* @returns {string} The SVG.
|
|
43
|
+
*/
|
|
44
|
+
export const panel = (x, y, w, h, fill, { r = 10, stroke } = {}) =>
|
|
45
|
+
`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${r}" fill="${fill}"${stroke ? ` stroke="${stroke}" stroke-width="1.5"` : ''}/>`;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A mark drawn in a square, its 512 canvas filling it.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} svg The mark.
|
|
51
|
+
* @param {number} x Left.
|
|
52
|
+
* @param {number} y Top.
|
|
53
|
+
* @param {number} size The square's side.
|
|
54
|
+
* @param {string} color The ink.
|
|
55
|
+
* @returns {string} The SVG.
|
|
56
|
+
*/
|
|
57
|
+
export const mark = (svg, x, y, size, color) =>
|
|
58
|
+
`<svg x="${x}" y="${y}" width="${size}" height="${size}" viewBox="0 0 512 512" fill="${color}">${body(svg).replace(/fill="currentColor"/g, `fill="${color}"`)}</svg>`;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A raster image placed at a size, scaled by nearest neighbour so its pixels show.
|
|
62
|
+
*
|
|
63
|
+
* @param {Uint8Array} png The image.
|
|
64
|
+
* @param {number} x Left.
|
|
65
|
+
* @param {number} y Top.
|
|
66
|
+
* @param {number} size Its side on the sheet.
|
|
67
|
+
* @returns {string} The SVG.
|
|
68
|
+
*/
|
|
69
|
+
export const pixels = (png, x, y, size) =>
|
|
70
|
+
`<image x="${x}" y="${y}" width="${size}" height="${size}" image-rendering="optimizeSpeed" href="data:image/png;base64,${Buffer.from(png).toString('base64')}"/>`;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* An SVG placed in a box, scaled to fit and centred.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} svg The SVG (with a `viewBox`).
|
|
76
|
+
* @param {number} x Left.
|
|
77
|
+
* @param {number} y Top.
|
|
78
|
+
* @param {number} w Width.
|
|
79
|
+
* @param {number} h Height.
|
|
80
|
+
* @returns {string} The SVG.
|
|
81
|
+
*/
|
|
82
|
+
export const nest = (svg, x, y, w, h) => `<svg x="${x}" y="${y}" width="${w}" height="${h}" viewBox="${viewBox(svg)?.join(' ')}">${body(svg)}</svg>`;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A raster image placed in a box, smoothly scaled.
|
|
86
|
+
*
|
|
87
|
+
* @param {Uint8Array} png The image.
|
|
88
|
+
* @param {number} x Left.
|
|
89
|
+
* @param {number} y Top.
|
|
90
|
+
* @param {number} w Width.
|
|
91
|
+
* @param {number} h Height.
|
|
92
|
+
* @returns {string} The SVG.
|
|
93
|
+
*/
|
|
94
|
+
export const image = (png, x, y, w, h) => `<image x="${x}" y="${y}" width="${w}" height="${h}" href="data:image/png;base64,${Buffer.from(png).toString('base64')}"/>`;
|