@logomarks/core 0.1.3 → 0.1.5
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 +12 -0
- package/package.json +1 -1
- package/src/edit/index.d.ts +1 -1
- package/src/edit/index.js +1 -1
- package/src/edit/pixelate.d.ts +6 -0
- package/src/edit/pixelate.js +43 -6
- package/src/skill/bundle.js +26 -9
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to `@logomarks/core` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.5] — 2026-11-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `pixelCoverage(paths, grid)` measures how much of each pixel cell a mark covers from its rings, by scanline, so pixelate runs without a canvas (a server, a Worker); `pixelCells(cov, grid)` returns the filled cells themselves, for drawing them one by one (`@arraypress/covers` uses both for its pixel mosaic). `pixelPath` is unchanged.
|
|
13
|
+
|
|
14
|
+
## [0.1.4] — 2026-11-06
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- The generated `fit.mjs` fits several files each in place. Given exactly two files it used to take the second as the output and write the first one's fit into it, so the second mark was lost. A file's fit now goes to another file only with `-o out.svg`, or when the second path doesn't exist yet (`in.svg out.svg`, as before). `--help` prints the usage (it used to crash), and an unknown option, a missing path or `-o` with several inputs exits 2 with the reason.
|
|
19
|
+
|
|
8
20
|
## [0.1.3] — 2026-11-06
|
|
9
21
|
|
|
10
22
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logomarks/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Geometry, SVG reading, fitting, analysis and similarity for logomarks on a 512 grid: the kit the Logomarks set is drawn with (shapes, real booleans, strokes, tracks, iso solids, badges, letter skeletons), one SVG path parser, the 448 fit, speck and shape-tag analysis, outline signatures, and the industry-symbol parts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/edit/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export type { Adjust, Frame, Similarity, AdjustEnv } from './adjust.js';
|
|
|
3
3
|
export type { PixelGrid } from './pixelate.js';
|
|
4
4
|
export { loadShaper, shaperReady, hasCorners, reshape, pieceCount, shapeInfo, smallSizeInfo } from './reshape.js';
|
|
5
5
|
export { NO_ADJUST, isAdjusted, adjustSuffix, transformPath, adjustPaths } from './adjust.js';
|
|
6
|
-
export { SS, pixelGrid, pixelPath } from './pixelate.js';
|
|
6
|
+
export { SS, pixelGrid, pixelPath, pixelCells, pixelCoverage } from './pixelate.js';
|
package/src/edit/index.js
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { loadShaper, shaperReady, hasCorners, reshape, pieceCount, shapeInfo, smallSizeInfo } from './reshape.js';
|
|
6
6
|
export { NO_ADJUST, isAdjusted, adjustSuffix, transformPath, adjustPaths } from './adjust.js';
|
|
7
|
-
export { SS, pixelGrid, pixelPath } from './pixelate.js';
|
|
7
|
+
export { SS, pixelGrid, pixelPath, pixelCells, pixelCoverage } from './pixelate.js';
|
package/src/edit/pixelate.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { FillPath } from '../raster/scanline.js';
|
|
2
|
+
|
|
1
3
|
/** A pixel grid over a mark's box. */
|
|
2
4
|
export interface PixelGrid {
|
|
3
5
|
cols: number;
|
|
@@ -11,5 +13,9 @@ export interface PixelGrid {
|
|
|
11
13
|
export const SS: 6;
|
|
12
14
|
/** The grid for a box: 48 cells across its longest side at amount 0, 14 at amount 1, centred. */
|
|
13
15
|
export function pixelGrid(amount: number, box: { x: number; y: number; width: number; height: number }): PixelGrid;
|
|
16
|
+
/** How much of each cell the mark's rings cover (0–1, row by row), by scanline at SS × SS samples a cell. */
|
|
17
|
+
export function pixelCoverage(paths: readonly FillPath[], grid: PixelGrid): Float32Array;
|
|
18
|
+
/** The filled cells (1 each, row by row): half covered, or a fifth where a cell stands alone or bridges two. */
|
|
19
|
+
export function pixelCells(cov: ArrayLike<number>, grid: Pick<PixelGrid, 'cols' | 'rows'>): Uint8Array;
|
|
14
20
|
/** The pixelated mark as one path's d, from each cell's coverage; null when nothing is filled. */
|
|
15
21
|
export function pixelPath(cov: ArrayLike<number>, grid: PixelGrid): string | null;
|
package/src/edit/pixelate.js
CHANGED
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
* alone (a thin line or a small dot would vanish) or when it bridges two filled cells across it (a diagonal would
|
|
12
12
|
* break into steps that don't touch).
|
|
13
13
|
* - Filled cells are merged into as few rectangles as possible, so the SVG stays small and clean.
|
|
14
|
+
*
|
|
15
|
+
* Where there's no canvas to measure with (a server, a Worker), `pixelCoverage` measures the cells from the mark's
|
|
16
|
+
* rings by scanline; `pixelCells` gives the filled cells themselves, for drawing them one by one (a mosaic).
|
|
14
17
|
*/
|
|
18
|
+
import { fill } from '../raster/scanline.js';
|
|
19
|
+
|
|
15
20
|
/** Samples a cell is measured at, along each side (6 × 6 a cell). */
|
|
16
21
|
export const SS = 6;
|
|
17
22
|
|
|
@@ -32,20 +37,38 @@ export function pixelGrid(amount, box) {
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
40
|
+
* How much of each cell the mark covers, measured from its rings by scanline at `SS` × `SS` samples a cell: what a
|
|
41
|
+
* canvas would measure, without one.
|
|
42
|
+
*
|
|
43
|
+
* @param {readonly import('../raster/scanline.js').FillPath[]} paths The mark's paths (rings and fill rules).
|
|
44
|
+
* @param {import('./index.js').PixelGrid} grid The grid.
|
|
45
|
+
* @returns {Float32Array} Coverage 0–1 per cell, row by row.
|
|
46
|
+
*/
|
|
47
|
+
export function pixelCoverage(paths, { cols, rows, cell, x0, y0 }) {
|
|
48
|
+
const bmp = fill(paths, { x0, y0, scale: SS / cell, width: cols * SS, height: rows * SS });
|
|
49
|
+
const cov = new Float32Array(cols * rows);
|
|
50
|
+
for (let j = 0; j < bmp.height; j++)
|
|
51
|
+
for (let i = 0; i < bmp.width; i++)
|
|
52
|
+
if (bmp.data[j * bmp.width + i])
|
|
53
|
+
cov[Math.floor(j / SS) * cols + Math.floor(i / SS)]++;
|
|
54
|
+
for (let k = 0; k < cov.length; k++)
|
|
55
|
+
cov[k] /= SS * SS;
|
|
56
|
+
return cov;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The filled cells, from how much of each the mark covers: half or more, or a fifth or more where a cell stands alone
|
|
61
|
+
* (a thin line or a small dot would vanish) or bridges two filled cells across it (a diagonal would break).
|
|
38
62
|
*
|
|
39
63
|
* @param {ArrayLike<number>} cov Coverage 0–1 per cell, row by row.
|
|
40
64
|
* @param {import('./index.js').PixelGrid} grid The grid.
|
|
41
|
-
* @returns {
|
|
65
|
+
* @returns {Uint8Array} 1 per filled cell, row by row.
|
|
42
66
|
*/
|
|
43
|
-
export function
|
|
67
|
+
export function pixelCells(cov, { cols, rows }) {
|
|
44
68
|
const at = (m, q, r) => (q >= 0 && r >= 0 && q < cols && r < rows ? m[r * cols + q] : 0);
|
|
45
69
|
const on = new Uint8Array(cols * rows);
|
|
46
70
|
for (let i = 0; i < on.length; i++)
|
|
47
71
|
on[i] = cov[i] >= 0.5 ? 1 : 0;
|
|
48
|
-
// keep thin lines and small dots, and bridge diagonals
|
|
49
72
|
const keep = on.slice();
|
|
50
73
|
for (let r = 0; r < rows; r++)
|
|
51
74
|
for (let q = 0; q < cols; q++) {
|
|
@@ -64,6 +87,20 @@ export function pixelPath(cov, { cols, rows, cell, x0, y0 }) {
|
|
|
64
87
|
if (!near || (bridges && cov[i] >= 0.3))
|
|
65
88
|
keep[i] = 1;
|
|
66
89
|
}
|
|
90
|
+
return keep;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The pixelated mark as one path's d, from how much of each cell the mark covers: cells covered half or more are
|
|
95
|
+
* filled, cells covered a fifth or more too when they stand alone or bridge two filled cells, then merged into as few
|
|
96
|
+
* rectangles as possible. Null when nothing is filled.
|
|
97
|
+
*
|
|
98
|
+
* @param {ArrayLike<number>} cov Coverage 0–1 per cell, row by row.
|
|
99
|
+
* @param {import('./index.js').PixelGrid} grid The grid.
|
|
100
|
+
* @returns {string | null} The path data.
|
|
101
|
+
*/
|
|
102
|
+
export function pixelPath(cov, { cols, rows, cell, x0, y0 }) {
|
|
103
|
+
const keep = pixelCells(cov, { cols, rows });
|
|
67
104
|
const done = [];
|
|
68
105
|
let open = new Map();
|
|
69
106
|
for (let r = 0; r <= rows; r++) {
|
package/src/skill/bundle.js
CHANGED
|
@@ -41,8 +41,8 @@ const FIT_HEADER = `#!/usr/bin/env node
|
|
|
41
41
|
// {{url}} · CC0 · no dependencies
|
|
42
42
|
//
|
|
43
43
|
// node fit.mjs mark.svg fit in place
|
|
44
|
-
// node fit.mjs in.svg out.svg
|
|
45
|
-
// node fit.mjs marks/
|
|
44
|
+
// node fit.mjs in.svg -o out.svg fit to another file (in.svg out.svg too, while out.svg doesn't exist)
|
|
45
|
+
// node fit.mjs a.svg b.svg marks/ fit each file, and every .svg in a folder, in place
|
|
46
46
|
// node fit.mjs --check mark.svg marks/ only report; exits 1 if anything is off
|
|
47
47
|
//
|
|
48
48
|
// Reads <path d> (with fill-rule), <circle>, <ellipse>, <rect> and <polygon>; nested
|
|
@@ -58,16 +58,33 @@ import path from 'node:path';
|
|
|
58
58
|
const FIT_MAIN = `
|
|
59
59
|
// ---------- command line ----------
|
|
60
60
|
|
|
61
|
+
const USAGE = \`Usage: node fit.mjs [--check] <file.svg | folder>... [-o out.svg]
|
|
62
|
+
Fits each file in place (a folder: every .svg in it). -o writes one file's fit to another file;
|
|
63
|
+
so does a second path that doesn't exist yet (in.svg out.svg). --check only reports.\`;
|
|
64
|
+
|
|
65
|
+
/** Stops with a message and the usage. */
|
|
66
|
+
const fail = (msg) => { console.error(\`fit.mjs: \${msg}\\n\${USAGE}\`); process.exit(2); };
|
|
67
|
+
|
|
61
68
|
const args = process.argv.slice(2);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
if (args.some((a) => a === '--help' || a === '-h')) { console.log(USAGE); process.exit(0); }
|
|
70
|
+
const checking = args.includes('--check');
|
|
71
|
+
let out = null;
|
|
72
|
+
const paths = [];
|
|
73
|
+
for (let i = 0; i < args.length; i++) {
|
|
74
|
+
if (args[i] === '--check') continue;
|
|
75
|
+
if (args[i] === '-o' || args[i] === '--out') { out = args[++i] ?? fail(\`\${args[i - 1]} needs a file\`); continue; }
|
|
76
|
+
if (args[i].startsWith('-')) fail(\`unknown option \${args[i]}\`);
|
|
77
|
+
paths.push(args[i]);
|
|
78
|
+
}
|
|
79
|
+
if (!paths.length) fail('no file or folder given');
|
|
80
|
+
for (const p of paths) if (!fs.existsSync(p)) {
|
|
81
|
+
// in.svg out.svg: the second path names the file to write, as -o does
|
|
82
|
+
if (!checking && !out && paths.length === 2 && p === paths[1] && p.endsWith('.svg')) out = paths.pop();
|
|
83
|
+
else fail(\`\${p} doesn't exist\`);
|
|
67
84
|
}
|
|
85
|
+
if (out && (checking || paths.length !== 1 || fs.statSync(paths[0]).isDirectory())) fail('-o takes one .svg file to fit, without --check');
|
|
68
86
|
const files = [];
|
|
69
|
-
|
|
70
|
-
for (const a of args) {
|
|
87
|
+
for (const a of paths) {
|
|
71
88
|
if (fs.statSync(a).isDirectory()) for (const f of fs.readdirSync(a)) { if (f.endsWith('.svg')) files.push(path.join(a, f)); }
|
|
72
89
|
else files.push(a);
|
|
73
90
|
}
|