@arraypress/covers 1.3.1 → 1.4.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 +18 -0
- package/README.md +1 -1
- package/package.json +2 -1
- package/recipes/flyers/grid-poster.json +91 -0
- package/recipes/flyers/logofolio.json +60 -0
- package/recipes/flyers/pixel-mosaic.json +58 -0
- package/schema/recipe.schema.json +306 -4
- package/src/index.d.ts +9 -7
- package/src/json-recipe.js +5 -0
- package/src/layer-options.js +5 -3
- package/src/layers/index.d.ts +6 -0
- package/src/layers/index.js +3 -0
- package/src/layers/mark-geometry.d.ts +15 -0
- package/src/layers/mark-geometry.js +46 -0
- package/src/layers/marks.d.ts +25 -0
- package/src/layers/marks.js +102 -0
- package/src/layers/mosaic.d.ts +32 -0
- package/src/layers/mosaic.js +155 -0
- package/src/layers.js +7 -6
- package/src/type/fallback.js +1 -1
- package/src/type/spans.d.ts +5 -1
- package/src/type/spans.js +26 -3
- package/src/type/symbols.d.ts +1 -1
- package/src/type/symbols.js +20 -2
- package/src/type/typeset.js +6 -2
- package/src/version.js +1 -1
package/src/index.d.ts
CHANGED
|
@@ -14,12 +14,12 @@ import type { RepeatLayer } from './layers/repeat.js';
|
|
|
14
14
|
import type { TextSpan } from './type/spans.js';
|
|
15
15
|
import type { GlyphControl } from './type/glyphs.js';
|
|
16
16
|
import type { Cut } from './formats/cut.js';
|
|
17
|
-
import type { ShapeLayer, SvgLayer, TableLayer, BarcodeLayer, PerforationLayer, TearLayer, TextureLayer } from './layers/index.js';
|
|
17
|
+
import type { ShapeLayer, SvgLayer, TableLayer, BarcodeLayer, PerforationLayer, TearLayer, TextureLayer, MosaicLayer, MarksLayer } from './layers/index.js';
|
|
18
18
|
import type { Zones, PrintInfo, FormatName } from './formats/index.js';
|
|
19
19
|
import type { Motion, MotionPreset, Easing } from './animate/index.js';
|
|
20
20
|
|
|
21
21
|
export type { RepeatLayer, DefsLayer, UseLayer, TextSpan, GlyphControl, Cut, Motion, MotionPreset, Easing };
|
|
22
|
-
export type { Color, Length, Rng, LayerTransform, EffectKind, Paint, TextPath, GroupLayer, ShapeLayer, SvgLayer, TableLayer, BarcodeLayer, PerforationLayer, TearLayer, TextureLayer, Zones, PrintInfo, FormatName };
|
|
22
|
+
export type { Color, Length, Rng, LayerTransform, EffectKind, Paint, TextPath, GroupLayer, ShapeLayer, SvgLayer, TableLayer, BarcodeLayer, PerforationLayer, TearLayer, TextureLayer, MosaicLayer, MarksLayer, Zones, PrintInfo, FormatName };
|
|
23
23
|
export type { ShapeSpec, TableCell } from './layers/index.js';
|
|
24
24
|
export type { Format } from './formats/index.js';
|
|
25
25
|
export type { LintIssue } from './lint/index.js';
|
|
@@ -310,8 +310,8 @@ export interface PatternLayer extends BaseLayer {
|
|
|
310
310
|
fade?: 'none' | 'radial' | 'edges' | 'top' | 'bottom' | 'left' | 'right';
|
|
311
311
|
/** Dashed or dotted lines (or `[on, off]` lengths). */
|
|
312
312
|
dash?: 'dashed' | 'dotted' | true | [Length, Length];
|
|
313
|
-
/** Grid / lines: N equal cells (inner lines only) instead of `size
|
|
314
|
-
divisions?: number;
|
|
313
|
+
/** Grid / lines: N equal cells (inner lines only) instead of `size`; `[cols, rows]` for a design grid. */
|
|
314
|
+
divisions?: number | [number, number];
|
|
315
315
|
/** Regions (fractions) the lines break around. */
|
|
316
316
|
gaps?: Region[];
|
|
317
317
|
region?: Region;
|
|
@@ -415,8 +415,8 @@ export interface TextLabel {
|
|
|
415
415
|
|
|
416
416
|
export interface TextLayer extends Omit<BaseLayer, 'transform'> {
|
|
417
417
|
type: 'text';
|
|
418
|
-
/** The text; an array is one line per item. (Optional with `spans`.) */
|
|
419
|
-
content?: string | string[];
|
|
418
|
+
/** The text; an array is one line per item, and an item may be a span (`{ content, bold }`: a headline whose lines change weight). (Optional with `spans`.) */
|
|
419
|
+
content?: string | (string | TextSpan)[];
|
|
420
420
|
/** First baseline x (px or `'%'`): places the text like SVG `<text x y>`, `align` acting as the anchor. */
|
|
421
421
|
x?: Length | `${number}%`;
|
|
422
422
|
y?: Length | `${number}%`;
|
|
@@ -470,6 +470,8 @@ export interface TextLayer extends Omit<BaseLayer, 'transform'> {
|
|
|
470
470
|
font?: string;
|
|
471
471
|
/** Default 600. */
|
|
472
472
|
weight?: number;
|
|
473
|
+
/** The weight of spans and lines marked `bold: true`. Default 700. */
|
|
474
|
+
boldWeight?: number;
|
|
473
475
|
style?: 'normal' | 'italic';
|
|
474
476
|
/** Default 6% of the design unit (`sqrt(width × height)`). */
|
|
475
477
|
size?: Length;
|
|
@@ -718,7 +720,7 @@ export interface RuleLayer extends BaseLayer {
|
|
|
718
720
|
}
|
|
719
721
|
|
|
720
722
|
export type Layer =
|
|
721
|
-
| GroupLayer | ShapeLayer | SvgLayer | TableLayer | BarcodeLayer | PerforationLayer | TearLayer | TextureLayer
|
|
723
|
+
| GroupLayer | ShapeLayer | SvgLayer | TableLayer | BarcodeLayer | PerforationLayer | TearLayer | TextureLayer | MosaicLayer | MarksLayer
|
|
722
724
|
| InkLayer | DitherLayer | ContourLayer | WaveformLayer | TilesLayer | RuleLayer
|
|
723
725
|
| DiagramLayer | PaperLayer | ScribbleLayer
|
|
724
726
|
| SolidLayer | LinearLayer | RadialLayer | MeshLayer | StripesLayer | ImageLayer
|
package/src/json-recipe.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* | `{ "gradientUnder": [colors, angle, size] }` | the colours a centred square of `size` covers in a linear gradient |
|
|
32
32
|
* | `{ "pick": [...] }`, `{ "range": [min, max] }`, `{ "int": [min, max] }` | seeded choices |
|
|
33
33
|
* | `{ "add" \| "sub" \| "mul" \| "div" \| "min" \| "max": [a, b, …] }` | arithmetic; lengths like `"54vw"` become pixels |
|
|
34
|
+
* | `{ "round" \| "floor" \| "ceil": x }` | to a whole number (a grid's row count from the canvas) |
|
|
34
35
|
* | `{ "ramp": [from, to, steps, amount?] }` | `steps` colours from `from` toward `to` |
|
|
35
36
|
* | `{ "taper": [ratio, steps] }` | `[1, ratio, ratio², …]` |
|
|
36
37
|
* | `{ "length": value }` | characters in a string (or a number's digits), items in an array |
|
|
@@ -77,6 +78,7 @@ const CANVAS = ['width', 'height', 'u', 'bleed', 'safe', 'mm', 'dpi', 'trimWidth
|
|
|
77
78
|
const OPS = /** @type {Record<string, [number, number]>} */ ({
|
|
78
79
|
mix: [3, 4], readable: [1, 3], gradientUnder: [3, 3], tone: [2, 2], length: [1, 1], pick: [1, Infinity], range: [2, 2], int: [2, 2],
|
|
79
80
|
add: [1, Infinity], sub: [2, Infinity], mul: [1, Infinity], div: [2, 2], min: [1, Infinity], max: [1, Infinity],
|
|
81
|
+
round: [1, 1], floor: [1, 1], ceil: [1, 1],
|
|
80
82
|
ramp: [3, 4], taper: [2, 2], reverse: [1, 1],
|
|
81
83
|
join: [1, 2], upper: [1, 1], lower: [1, 1], slice: [2, 3],
|
|
82
84
|
});
|
|
@@ -390,6 +392,9 @@ function op(s, name, raw) {
|
|
|
390
392
|
case 'div': { const [a, b] = nums(); return a / b; }
|
|
391
393
|
case 'min': return Math.min(...nums());
|
|
392
394
|
case 'max': return Math.max(...nums());
|
|
395
|
+
case 'round': return Math.round(nums()[0]);
|
|
396
|
+
case 'floor': return Math.floor(nums()[0]);
|
|
397
|
+
case 'ceil': return Math.ceil(nums()[0]);
|
|
393
398
|
case 'ramp': {
|
|
394
399
|
const [from, to, steps, amount = 1] = args;
|
|
395
400
|
return Array.from({ length: steps }, (_, i) => mixOklab(toHex(from), toHex(to), (i / steps) * amount));
|
package/src/layer-options.js
CHANGED
|
@@ -16,12 +16,12 @@ export const LAYER_OPTIONS = {
|
|
|
16
16
|
stripes: { direction: 'vertical | horizontal | diagonal', angle: 'For diagonal.', count: 'Stripes. Default 8.', widths: 'even | random | taper | [weights]', taper: 'Ratio for widths: taper.', colors: 'Cycled.', gap: 'Length.', radius: 'Length.', region: '{ x, y, width, height } fractions.', regionRadius: 'Length.' },
|
|
17
17
|
image: { x: 'Box left, px or "%" (with y, width, height: an explicit box instead of region).', y: 'Box top.', width: 'Box width.', height: 'Box height.', src: 'A local path, data: URI, URL (where allowed) or bytes ("{image}").', fit: 'cover | contain | stretch', focus: 'An anchor, attention or entropy.', region: 'Fractions.', duotone: 'true or [shadow, highlight].', grayscale: 'boolean', normalize: 'Stretch tones first.', tint: 'A colour or { color, amount, blend }.', blur: 'Length.' },
|
|
18
18
|
grain: { amount: '0–1. Default 0.08.', size: 'Grain px. Default 1.', monochrome: 'Default true.', vary: '"edges": heavier toward the edges (spray paint).' },
|
|
19
|
-
pattern: { kind: 'grid | dots | lines | rings | waves', size: 'Cell. Length.', stroke: 'Length.', color: 'Default @text.', angle: 'For lines.', center: '{ x, y } fractions.', wavelength: 'Length.', amplitude: 'Length.', fade: 'none | radial | edges | top | bottom | left | right', dash: 'dotted | dashed | [on, off] lengths.', divisions: 'Grid/lines: N equal cells (inner lines only) instead of size.', gaps: '[{ x, y, width, height }] fractions the lines break around.', region: 'Fractions.' },
|
|
19
|
+
pattern: { kind: 'grid | dots | lines | rings | waves', size: 'Cell. Length.', stroke: 'Length.', color: 'Default @text.', angle: 'For lines.', center: '{ x, y } fractions.', wavelength: 'Length.', amplitude: 'Length.', fade: 'none | radial | edges | top | bottom | left | right', dash: 'dotted | dashed | [on, off] lengths.', divisions: 'Grid/lines: N equal cells (inner lines only) instead of size; [cols, rows] for a design grid.', gaps: '[{ x, y, width, height }] fractions the lines break around.', region: 'Fractions.' },
|
|
20
20
|
shapes: { kind: 'circle | square | pill | mixed', count: 'Default 6.', size: '[min, max] lengths.', colors: 'Cycled.', placement: 'scatter | row | column', items: '[{ kind, x, y, size, color, rotate, opacity }]', region: 'Fractions.', rotate: 'Tilt squares and pills.', radius: 'Square corner, fraction of size.', outline: 'true or a width.' },
|
|
21
21
|
icon: { fill: 'A gradient, tile or preset paint for the icon (instead of color).', markup: 'Inner SVG markup on a 512 grid (a mark\'s paths); viewBox overrides the grid.', x: 'Left, px (with y: placed by its top-left corner).', y: 'Top, px.', name: 'Iconify name ("lucide:feather") or raw SVG ("{icon}").', svg: 'Raw SVG.', path: 'A path d.', viewBox: '[x, y, w, h] for path.', size: 'Length. Default "24vmin".', color: 'Default the accent on a tile, else @text.', strokeWidth: 'Stroke icons.', anchor: 'One of nine anchors.', position: '{ x, y } fractions.', offset: '{ x, y } lengths.', margin: 'Length.', tile: 'true or { size, radius, color, shadow, border }.' },
|
|
22
|
-
text: { content: 'The text ("{title}").', font: 'Family. Default Inter.', weight: 'Default 600.', style: 'normal | italic', size: 'Length.', minSize: 'Shrink toward this before truncating.', lineHeight: 'Default 1.12.', letterSpacing: 'Em. Default -0.02.', color: 'Default @text.', align: 'left | center | right', anchor: 'Default bottom-left.', position: '{ x, y } fractions.', offset: '{ x, y } lengths.', margin: 'Length.', maxWidth: 'Length.', maxLines: 'Default 3.', ellipsis: "Default '…'.", balance: 'Default true.', transform: 'none | uppercase | lowercase', label: 'A string or { content, size, weight, color, letterSpacing, transform, gap }.', background: 'A colour or { color, radius, padding, opacity }.',
|
|
22
|
+
text: { content: 'The text ("{title}"); an array is a line per item, an item may be a span ({ content, bold }).', font: 'Family. Default Inter.', weight: 'Default 600.', boldWeight: 'The weight of spans and lines marked bold: true. Default 700.', style: 'normal | italic', size: 'Length.', minSize: 'Shrink toward this before truncating.', lineHeight: 'Default 1.12.', letterSpacing: 'Em. Default -0.02.', color: 'Default @text.', align: 'left | center | right', anchor: 'Default bottom-left.', position: '{ x, y } fractions.', offset: '{ x, y } lengths.', margin: 'Length.', maxWidth: 'Length.', maxLines: 'Default 3.', ellipsis: "Default '…'.", balance: 'Default true.', transform: 'none | uppercase | lowercase', label: 'A string or { content, size, weight, color, letterSpacing, transform, gap }.', background: 'A colour or { color, radius, padding, opacity }.',
|
|
23
23
|
x: 'First baseline x, px or "%" (like SVG <text>; align is the text-anchor).', y: 'First baseline y.', width: 'Justify every line to this width (like textLength).', justify: 'Justify every line to maxWidth.', lengthAdjust: 'spacingAndGlyphs (default: scale) | spacing', fit: 'width (the block fills width) | lines (each line fills width) | words (a word a line, the widest filling width) | wrap (wrap at width, then fill it) | shrink', stretch: '[sx, sy] or sy: stretch about the first baseline.', axes: 'Variable-font axes: { wdth: 50, SOFT: 100, WONK: 1, opsz: 144 }.', stroke: 'Outline colour.', strokeWidth: 'Outline width.', strokes: '[{ color, width }]: keylines, widest first, under the fill.', fill: 'A colour, a gradient, or "none" (outline type).', path: 'Text on a path: { kind: circle | arc | ellipse | wave, cx, cy, r, start, end, direction } or { d }.', startOffset: 'Along the path: 0–1 or px. Default 0.5 (centred).', fitPath: 'Space the text to fill the path (a ring); a number leaves that gap.', dy: 'Shift off the path.', render: 'outline (default) | live (<text> with @font-face)', autoColors: '[light, dark] for color: "auto". Default ["#ffffff", "#0b0b0b"].', autoThreshold: 'color: "auto" takes the dark colour above this backdrop luma (0–1). Default 0.72.', lines: 'Reserved.',
|
|
24
|
-
spans: '[{ content, font, weight, style, axes, scale, size, color, letterSpacing, dy }]: mixed runs on the same lines.', glyphs: '{ scale, scaleX, scaleY, dx, dy, rotate, color, origin, advance }: per-glyph arrays (cycled; spaces skipped).', fallback: 'A family (or list) for characters the font lacks; then fonts registered with fallback: true, then the built-in symbols.', split: 'words: a word a line.', dash: 'Dashes on the stroke: [on, off] lengths, dashed or dotted.', extrude: 'A depth, or { depth, angle, color, to, steps }: block extrusion.', maxHeight: 'Shrink the block to this height.' },
|
|
24
|
+
spans: '[{ content, font, weight, bold, style, axes, scale, size, color, letterSpacing, dy }]: mixed runs on the same lines.', glyphs: '{ scale, scaleX, scaleY, dx, dy, rotate, color, origin, advance }: per-glyph arrays (cycled; spaces skipped).', fallback: 'A family (or list) for characters the font lacks; then fonts registered with fallback: true, then the built-in symbols.', split: 'words: a word a line.', dash: 'Dashes on the stroke: [on, off] lengths, dashed or dotted.', extrude: 'A depth, or { depth, angle, color, to, steps }: block extrusion.', maxHeight: 'Shrink the block to this height.' },
|
|
25
25
|
diagram: { icon: 'One icon (Iconify name or raw SVG), drawn as white line-art.', nodes: '[{ icon, shape: square | circle | none, size, brackets }]', layout: 'single | row | triangle | square | stack | hub', arrows: 'true | false | loop | both | cycle', shape: 'For a single icon: none (default) | square | circle.', size: 'Node size. Length.', gap: 'Space between nodes. Length.', stroke: 'Line width. Default "0.3vmin".', color: 'Line colour. Default white.', radius: 'Tile corner radius.', iconScale: 'Icon size inside a tile, 0–1. Default 0.44.', position: '{ x, y } centre, fractions.', grid: 'true or { divisions, opacity, stroke, color, pad }: a dotted grid that breaks around the drawing.' },
|
|
26
26
|
paper: { count: 'Shapes in the collage. Default 4.', kind: 'mixed | rect | square | circle | blob | arch | pill', color: 'Default #faf9f5 (paper).', region: 'Where the collage sits, fractions.', items: '[{ kind, x, y, width, height, size, rotate, color }] placed in the region.', wobble: 'How rough the cut edges are, 0–1. Default 0.5.' },
|
|
27
27
|
scribble: { icon: 'An icon to re-draw by hand (Iconify name or raw SVG).', graph: 'true or { nodes, dot }: a hand-drawn node graph instead.', color: 'Default #faf9f5.', stroke: 'Line width. Default "1.3vmin".', size: 'Length. Default "46vmin".', position: '{ x, y } centre, fractions.', wobble: '0–1. Default 0.5.' },
|
|
@@ -43,6 +43,8 @@ export const LAYER_OPTIONS = {
|
|
|
43
43
|
perforation: { kind: 'holes (default) | dashes | notches', x: 'A vertical line at x.', x1: 'Start x.', y1: 'Start y.', x2: 'End x.', y2: 'End y.', size: 'Hole or notch radius.', gap: 'Spacing.', inset: 'Space before the first hole.', color: 'Default @text (or punch it with knockout).', strokeWidth: 'Dashes width.' },
|
|
44
44
|
tear: { from: '[x, y] where the tear starts (edge to edge).', to: '[x, y] where it ends.', side: 'Which side keeps the sheet: above (default) | below | left | right', color: 'The sheet (a colour, a gradient, or "none" for just the edge).', sheetOpacity: 'The sheet\'s opacity.', step: 'Spacing of the jagged points.', jitter: 'How jagged.', rip: 'false, or { color, width, opacity }: the torn white edge.', fibres: 'false, or { color, width, opacity, length: [min, max], spread }.', ripColor: 'Colour of the rip and fibres. Default #fff6fb.' },
|
|
45
45
|
texture: { kind: 'paper | creases | concrete | speck | wood | folds | dust', color: 'Fibre, aggregate or dust colour.', amount: 'Paper: fibre density.', frequency: 'Noise frequency (number or [x, y]).', octaves: 'Noise octaves.', depth: 'Creases: relief.', azimuth: 'Creases: light direction.', elevation: 'Creases: light height.', x: 'Folds: vertical fold positions, fractions.', y: 'Folds: horizontal fold positions, fractions.', width: 'Folds: band width.', strength: 'Folds: shading.', count: 'Dust: specks.', size: 'Dust: [min, max] radius.', region: 'Clip to { x, y, width, height } (px or "%").' },
|
|
46
|
+
mosaic: { markup: 'The mark\'s paths (512 grid).', x: 'Box x, px or %.', y: 'Box y.', width: 'Box width (default the canvas).', height: 'Box height.', cells: 'Cells across the mark\'s longest side, 4–48. Default 10.', gap: 'Gap between cells, a share of a cell. Default 0.12.', radius: 'Cell corner radius, a share of a cell. Default 0.', drop: 'Share of filled cells left out (seeded), 0–0.9. Default 0.12.', color: 'Default @accent.', accent: 'asterisk | dot: a small glyph in one empty cell beside the mark.', accentColor: 'Default color.' },
|
|
47
|
+
marks: { marks: 'The marks: paths on a 512 grid (or { markup }).', cols: 'Default from the count: 3 × 3 up to 10 × 7.', rows: 'Default from the count.', x: 'Box x, px or %.', y: 'Box y.', width: 'Box width (default the canvas).', height: 'Box height.', size: 'Share of a cell a mark\'s area takes. Default 0.5.', optical: 'false: fit each mark\'s bounds to the same box instead. Default true.', color: 'Default @text.' },
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
/** @type {Record<string, string>} Options every layer takes (besides type, id and when). */
|
package/src/layers/index.d.ts
CHANGED
|
@@ -14,5 +14,11 @@ export { tear } from './tear.js';
|
|
|
14
14
|
export type { TearLayer } from './tear.js';
|
|
15
15
|
export { texture } from './texture.js';
|
|
16
16
|
export type { TextureLayer } from './texture.js';
|
|
17
|
+
export { mosaic, mosaicGrid } from './mosaic.js';
|
|
18
|
+
export type { MosaicLayer } from './mosaic.js';
|
|
19
|
+
export { marks, sheetGrid } from './marks.js';
|
|
20
|
+
export type { MarksLayer } from './marks.js';
|
|
21
|
+
export { markGeometry } from './mark-geometry.js';
|
|
22
|
+
export type { MarkGeometry } from './mark-geometry.js';
|
|
17
23
|
export { expandRepeat } from './repeat.js';
|
|
18
24
|
export type { RepeatLayer } from './repeat.js';
|
package/src/layers/index.js
CHANGED
|
@@ -12,4 +12,7 @@ export { code128 } from './code128.js';
|
|
|
12
12
|
export { perforation } from './perforation.js';
|
|
13
13
|
export { tear } from './tear.js';
|
|
14
14
|
export { texture } from './texture.js';
|
|
15
|
+
export { mosaic, mosaicGrid } from './mosaic.js';
|
|
16
|
+
export { marks, sheetGrid } from './marks.js';
|
|
17
|
+
export { markGeometry } from './mark-geometry.js';
|
|
15
18
|
export { expandRepeat } from './repeat.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// A mark's geometry from its markup, read with @logomarks/core.
|
|
2
|
+
import type { FillPath } from '@logomarks/core/raster';
|
|
3
|
+
|
|
4
|
+
/** A mark read from its paths. */
|
|
5
|
+
export interface MarkGeometry {
|
|
6
|
+
/** Its filled paths as rings, with their fill rules. */
|
|
7
|
+
paths: FillPath[];
|
|
8
|
+
/** Its real bounds, grid units. */
|
|
9
|
+
box: { x: number; y: number; width: number; height: number };
|
|
10
|
+
/** The share of its box it inks, 0–1. */
|
|
11
|
+
density: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Read a mark (paths on a 512 grid); null when it draws nothing. */
|
|
15
|
+
export function markGeometry(markup: string): MarkGeometry | null;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mark's geometry from its markup (paths on a 512 grid), read with
|
|
3
|
+
* `@logomarks/core`: its filled paths as rings, its real bounds, and how
|
|
4
|
+
* much of that box it inks. Shared by the `mosaic` and `marks` layers.
|
|
5
|
+
*
|
|
6
|
+
* @module layers/mark-geometry
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { shapes, rings, bounds } from '@logomarks/core/svg';
|
|
10
|
+
import { pixelGrid, pixelCoverage } from '@logomarks/core/edit';
|
|
11
|
+
|
|
12
|
+
/** Read marks, by markup (a sheet of seventy reads each once). @type {Map<string, MarkGeometry>} */
|
|
13
|
+
const cache = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} MarkGeometry
|
|
17
|
+
* @property {import('@logomarks/core/raster').FillPath[]} paths - Its filled paths as rings, with their fill rules.
|
|
18
|
+
* @property {{ x: number, y: number, width: number, height: number }} box - Its real bounds, grid units.
|
|
19
|
+
* @property {number} density - The share of its box it inks, 0–1.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read a mark.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} markup - The mark's paths (`<path d="…"/>`, on a 512 grid).
|
|
26
|
+
* @returns {MarkGeometry | null} Null when it draws nothing.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* markGeometry('<path d="M96 96h320v320H96z"/>').box; // { x: 96, y: 96, width: 320, height: 320 }
|
|
30
|
+
*/
|
|
31
|
+
export function markGeometry(markup) {
|
|
32
|
+
if (cache.has(markup)) return /** @type {MarkGeometry} */ (cache.get(markup));
|
|
33
|
+
const filled = shapes(String(markup ?? ''));
|
|
34
|
+
const paths = filled.map((s) => ({ rings: rings(s.segs), evenodd: s.evenodd })).filter((p) => p.rings.length);
|
|
35
|
+
if (!paths.length) return null;
|
|
36
|
+
const b = bounds(filled);
|
|
37
|
+
const box = { x: b.x, y: b.y, width: Math.max(b.w, 1e-6), height: Math.max(b.h, 1e-6) };
|
|
38
|
+
// Ink measured on a 48-cell grid over the box: plenty for a density.
|
|
39
|
+
const grid = pixelGrid(0, box);
|
|
40
|
+
const cov = pixelCoverage(paths, grid);
|
|
41
|
+
const density = cov.reduce((a, v) => a + v, 0) / Math.max(1, cov.length);
|
|
42
|
+
const geo = { paths, box, density };
|
|
43
|
+
if (cache.size > 500) cache.clear();
|
|
44
|
+
cache.set(markup, geo);
|
|
45
|
+
return geo;
|
|
46
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The `marks` layer.
|
|
2
|
+
import type { Color, Length, BaseLayer } from '../index.js';
|
|
3
|
+
import type { RenderContext, Markup } from '../render/context.js';
|
|
4
|
+
|
|
5
|
+
export interface MarksLayer extends BaseLayer {
|
|
6
|
+
type: 'marks';
|
|
7
|
+
/** The marks: their paths on a 512 grid (or `{ markup }`). */
|
|
8
|
+
marks: (string | { markup: string })[];
|
|
9
|
+
/** Default from the count and the box: 3 × 3 up to 10 × 7. */
|
|
10
|
+
cols?: number;
|
|
11
|
+
rows?: number;
|
|
12
|
+
/** The box the grid is centred in (default the canvas). */
|
|
13
|
+
x?: Length | `${number}%`; y?: Length | `${number}%`; width?: Length | `${number}%`; height?: Length | `${number}%`;
|
|
14
|
+
/** The share of a cell a mark's area takes. Default 0.5. */
|
|
15
|
+
size?: number;
|
|
16
|
+
/** `false`: each mark's bounds fitted to the same box, not optically sized. Default true. */
|
|
17
|
+
optical?: boolean;
|
|
18
|
+
/** Default the palette's text. */
|
|
19
|
+
color?: Color;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Draw a set of marks on an even grid, optically sized the same. */
|
|
23
|
+
export function marks(layer: MarksLayer, ctx: RenderContext): Markup;
|
|
24
|
+
/** Columns and rows for `n` marks in a box of `aspect` (3 × 3 to 10 × 7; given ones kept). */
|
|
25
|
+
export function sheetGrid(n: number, aspect: number, cols?: number, rows?: number): { cols: number; rows: number };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `marks` layer.
|
|
3
|
+
*
|
|
4
|
+
* @module layers/marks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { attrs, num } from '@logomarks/image-effects/svg';
|
|
8
|
+
import { toHex } from '@logomarks/image-effects/color';
|
|
9
|
+
import { pos, wrap } from './util.js';
|
|
10
|
+
import { shapesOnly } from './svg.js';
|
|
11
|
+
import { markGeometry } from './mark-geometry.js';
|
|
12
|
+
|
|
13
|
+
/** The smallest and largest sheet: 3 × 3 to 10 × 7. */
|
|
14
|
+
const MIN = 3;
|
|
15
|
+
const MAX_COLS = 10;
|
|
16
|
+
const MAX_ROWS = 7;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `marks` — a set of marks on an even grid of square cells, centred in its
|
|
20
|
+
* box so the margins match, each mark optically sized the same: scaled by
|
|
21
|
+
* its real bounds (not its 512 grid), to the same area, nudged up a little
|
|
22
|
+
* where it inks less of that area (an outline looks smaller than a solid
|
|
23
|
+
* of the same size), and never past its cell. The logofolio sheet.
|
|
24
|
+
*
|
|
25
|
+
* Options: `marks` (a list of markup strings, or `{ markup }`), `cols` and
|
|
26
|
+
* `rows` (default from the count and the box: 3 × 3 up to 10 × 7), `x`,
|
|
27
|
+
* `y`, `width`, `height` (the box; default the canvas), `size` (the share
|
|
28
|
+
* of a cell a mark's area takes, 0.5), `optical` (`false` fits each
|
|
29
|
+
* mark's bounds to the same box instead), `color` (default `text`).
|
|
30
|
+
*
|
|
31
|
+
* @param {any} layer
|
|
32
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
33
|
+
* @returns {import('../layers.js').Markup}
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* { type: 'marks', marks: ['<path d="…"/>', '<path d="…"/>'], x: 24, y: 120, width: 352, height: 340, color: '#f2efe8' }
|
|
37
|
+
*/
|
|
38
|
+
export function marks(layer, ctx) {
|
|
39
|
+
const list = (layer.marks ?? []).map((m) => (m && typeof m === 'object' ? m.markup : m)).filter(Boolean);
|
|
40
|
+
if (!list.length) return { defs: '', body: '' };
|
|
41
|
+
const bx = pos(layer.x ?? 0, 'x', ctx.canvas);
|
|
42
|
+
const by = pos(layer.y ?? 0, 'y', ctx.canvas);
|
|
43
|
+
const bw = pos(layer.width ?? '100%', 'x', ctx.canvas);
|
|
44
|
+
const bh = pos(layer.height ?? '100%', 'y', ctx.canvas);
|
|
45
|
+
const { cols, rows } = sheetGrid(list.length, bw / bh, layer.cols, layer.rows);
|
|
46
|
+
const cell = Math.min(bw / cols, bh / rows);
|
|
47
|
+
const ox = bx + (bw - cols * cell) / 2;
|
|
48
|
+
const oy = by + (bh - rows * cell) / 2;
|
|
49
|
+
const share = layer.size ?? 0.5;
|
|
50
|
+
const color = layer.color ? toHex(layer.color) : ctx.palette.text;
|
|
51
|
+
|
|
52
|
+
let body = '';
|
|
53
|
+
list.slice(0, cols * rows).forEach((markup, i) => {
|
|
54
|
+
const geo = markGeometry(markup);
|
|
55
|
+
if (!geo) return;
|
|
56
|
+
const { x, y, width, height } = geo.box;
|
|
57
|
+
// Optical: the same area (density-weighted, gently), capped at the cell; else the same longest side.
|
|
58
|
+
const fit = (cell * 0.86) / Math.max(width, height);
|
|
59
|
+
const k = layer.optical === false ? (cell * Math.sqrt(share)) / Math.max(width, height)
|
|
60
|
+
: Math.min(fit, (cell * Math.sqrt(share)) / (Math.sqrt(width * height) * (geo.density / 0.5) ** 0.15));
|
|
61
|
+
const cx = ox + (i % cols + 0.5) * cell;
|
|
62
|
+
const cy = oy + (Math.floor(i / cols) + 0.5) * cell;
|
|
63
|
+
body += `<svg${attrs({ x: cx - (width * k) / 2, y: cy - (height * k) / 2, width: width * k, height: height * k, viewBox: `${num(x)} ${num(y)} ${num(width)} ${num(height)}`, overflow: 'visible' })}>${shapesOnly(markup)}</svg>`;
|
|
64
|
+
});
|
|
65
|
+
return { defs: '', body: wrap(layer, `<g fill="${color}">${body}</g>`) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Columns and rows for `n` marks in a box of `aspect` (width / height):
|
|
70
|
+
* the biggest cells the box allows, less a cost for empty cells (so 25
|
|
71
|
+
* is 5 × 5, not 6 × 5 with a ragged row), 3 × 3 at the least, 10 × 7 at
|
|
72
|
+
* the most (marks past 70 are left out). Given ones are kept.
|
|
73
|
+
*
|
|
74
|
+
* @param {number} n
|
|
75
|
+
* @param {number} aspect
|
|
76
|
+
* @param {number} [cols]
|
|
77
|
+
* @param {number} [rows]
|
|
78
|
+
* @returns {{ cols: number, rows: number }}
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* sheetGrid(70, 0.8); // { cols: 10, rows: 7 }
|
|
82
|
+
* sheetGrid(25, 1.6); // { cols: 5, rows: 5 }
|
|
83
|
+
*/
|
|
84
|
+
export function sheetGrid(n, aspect, cols, rows) {
|
|
85
|
+
if (cols && rows) return { cols: Math.round(cols), rows: Math.round(rows) };
|
|
86
|
+
if (cols) return { cols: Math.round(cols), rows: Math.max(1, Math.ceil(n / cols)) };
|
|
87
|
+
if (rows) return { cols: Math.max(1, Math.ceil(n / rows)), rows: Math.round(rows) };
|
|
88
|
+
// The biggest cells, less a cost for empty ones (a ragged last row).
|
|
89
|
+
const count = Math.min(n, MAX_COLS * MAX_ROWS);
|
|
90
|
+
let best = { cols: MAX_COLS, rows: MAX_ROWS };
|
|
91
|
+
let score = -Infinity;
|
|
92
|
+
for (let c = MIN; c <= MAX_COLS; c++) {
|
|
93
|
+
const r = clamp(Math.ceil(count / c), MIN, MAX_ROWS);
|
|
94
|
+
if (c * r < count) continue;
|
|
95
|
+
const s = Math.min(aspect / c, 1 / r) * (1 - (1.5 * (c * r - count)) / (c * r));
|
|
96
|
+
if (s > score + 1e-9) [best, score] = [{ cols: c, rows: r }, s];
|
|
97
|
+
}
|
|
98
|
+
return best;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @param {number} v @param {number} lo @param {number} hi @returns {number} */
|
|
102
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The `mosaic` layer.
|
|
2
|
+
import type { Color, Length, BaseLayer } from '../index.js';
|
|
3
|
+
import type { RenderContext, Markup } from '../render/context.js';
|
|
4
|
+
import type { PixelGrid } from '@logomarks/core/edit';
|
|
5
|
+
import type { MarkGeometry } from './mark-geometry.js';
|
|
6
|
+
|
|
7
|
+
export interface MosaicLayer extends BaseLayer {
|
|
8
|
+
type: 'mosaic';
|
|
9
|
+
/** The mark's paths, on a 512 grid. */
|
|
10
|
+
markup: string;
|
|
11
|
+
/** The box the mosaic is fitted and centred in (default the canvas). */
|
|
12
|
+
x?: Length | `${number}%`; y?: Length | `${number}%`; width?: Length | `${number}%`; height?: Length | `${number}%`;
|
|
13
|
+
/** Cells across the mark's longest side, 4–48. Default 10. */
|
|
14
|
+
cells?: number;
|
|
15
|
+
/** The gap between cells, a share of a cell. Default 0.12. */
|
|
16
|
+
gap?: number;
|
|
17
|
+
/** Cell corner radius, a share of a cell. Default 0. */
|
|
18
|
+
radius?: number;
|
|
19
|
+
/** The share of filled cells left out, by the layer's seed (0–0.9). Default 0.12. */
|
|
20
|
+
drop?: number;
|
|
21
|
+
/** Default the palette's accent. */
|
|
22
|
+
color?: Color;
|
|
23
|
+
/** A small asterisk or dot in one empty cell beside the mark. */
|
|
24
|
+
accent?: 'asterisk' | 'dot' | false;
|
|
25
|
+
/** Default `color`. */
|
|
26
|
+
accentColor?: Color;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Draw a mark as a mosaic of square cells. */
|
|
30
|
+
export function mosaic(layer: MosaicLayer, ctx: RenderContext): Markup;
|
|
31
|
+
/** The mosaic's grid over a mark: `cells` across its longest side, centred on its bounds. */
|
|
32
|
+
export function mosaicGrid(geo: MarkGeometry, cells?: number): PixelGrid;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `mosaic` layer.
|
|
3
|
+
*
|
|
4
|
+
* @module layers/mosaic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { attrs, num } from '@logomarks/image-effects/svg';
|
|
8
|
+
import { toHex } from '@logomarks/image-effects/color';
|
|
9
|
+
import { pixelGrid, pixelCoverage, pixelCells } from '@logomarks/core/edit';
|
|
10
|
+
import { pos, wrap } from './util.js';
|
|
11
|
+
import { markGeometry } from './mark-geometry.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `mosaic` — a mark rastered into big square cells, each drawn on its own
|
|
15
|
+
* with a gap between, so whatever is under it shows through. The cells
|
|
16
|
+
* follow the mark's own shape (`@logomarks/core`'s pixelate: a cell is
|
|
17
|
+
* filled when the mark covers half of it, or a fifth where it would
|
|
18
|
+
* otherwise lose a thin line or a dot). `drop` leaves a share of the
|
|
19
|
+
* filled cells out, chosen by the layer's seed; `accent` puts a small
|
|
20
|
+
* asterisk or dot in one empty cell beside the mark.
|
|
21
|
+
*
|
|
22
|
+
* Options: `markup` (the mark's paths, 512 grid), `x`, `y`, `width`,
|
|
23
|
+
* `height` (the box it's fitted and centred in; default the canvas),
|
|
24
|
+
* `cells` (across the mark's longest side, 10), `gap` (a share of a cell,
|
|
25
|
+
* 0.12), `radius` (a share of a cell, 0), `drop` (0–1, 0.12), `color`
|
|
26
|
+
* (default the accent), `accent` (`'asterisk'` or `'dot'`), `accentColor`.
|
|
27
|
+
*
|
|
28
|
+
* @param {any} layer
|
|
29
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
30
|
+
* @returns {import('../layers.js').Markup}
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* { type: 'mosaic', markup: mark, x: 40, y: 60, width: 320, height: 320, cells: 11, color: '#c8ff2e', drop: 0.15, accent: 'asterisk' }
|
|
34
|
+
*/
|
|
35
|
+
export function mosaic(layer, ctx) {
|
|
36
|
+
const geo = markGeometry(layer.markup);
|
|
37
|
+
if (!geo) return { defs: '', body: '' };
|
|
38
|
+
const g = mosaicGrid(geo, layer.cells);
|
|
39
|
+
const on = pixelCells(pixelCoverage(geo.paths, g), g);
|
|
40
|
+
|
|
41
|
+
// Fit the grid into the box, centred.
|
|
42
|
+
const bx = pos(layer.x ?? 0, 'x', ctx.canvas);
|
|
43
|
+
const by = pos(layer.y ?? 0, 'y', ctx.canvas);
|
|
44
|
+
const bw = pos(layer.width ?? '100%', 'x', ctx.canvas);
|
|
45
|
+
const bh = pos(layer.height ?? '100%', 'y', ctx.canvas);
|
|
46
|
+
const k = Math.min(bw / (g.cols * g.cell), bh / (g.rows * g.cell));
|
|
47
|
+
const cell = g.cell * k;
|
|
48
|
+
const ox = bx + (bw - g.cols * cell) / 2;
|
|
49
|
+
const oy = by + (bh - g.rows * cell) / 2;
|
|
50
|
+
|
|
51
|
+
const kept = dropCells(on, layer.drop ?? 0.12, ctx.rng);
|
|
52
|
+
const gap = cell * clamp(layer.gap ?? 0.12, 0, 0.9);
|
|
53
|
+
const r = cell * clamp(layer.radius ?? 0, 0, 0.5);
|
|
54
|
+
const side = cell - gap;
|
|
55
|
+
let rects = '';
|
|
56
|
+
kept.forEach((v, i) => {
|
|
57
|
+
if (!v) return;
|
|
58
|
+
const x = ox + (i % g.cols) * cell + gap / 2;
|
|
59
|
+
const y = oy + Math.floor(i / g.cols) * cell + gap / 2;
|
|
60
|
+
rects += r ? `<rect${attrs({ x, y, width: side, height: side, rx: r })}/>` : `M${num(x)} ${num(y)}h${num(side)}v${num(side)}h${num(-side)}Z`;
|
|
61
|
+
});
|
|
62
|
+
const color = layer.color ? toHex(layer.color) : ctx.palette.accent;
|
|
63
|
+
let body = r ? `<g fill="${color}">${rects}</g>` : `<path${attrs({ d: rects, fill: color })}/>`;
|
|
64
|
+
|
|
65
|
+
if (layer.accent) {
|
|
66
|
+
const at = accentCell(on, g, ctx.rng);
|
|
67
|
+
if (at >= 0) {
|
|
68
|
+
const cx = ox + (at % g.cols + 0.5) * cell;
|
|
69
|
+
const cy = oy + (Math.floor(at / g.cols) + 0.5) * cell;
|
|
70
|
+
body += accentGlyph(layer.accent, cx, cy, side, layer.accentColor ? toHex(layer.accentColor) : color);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { defs: '', body: wrap(layer, body) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The mosaic's grid over a mark: `cells` across its longest side (4–48),
|
|
78
|
+
* centred on its bounds (pixelate's grid, at that coarseness).
|
|
79
|
+
*
|
|
80
|
+
* @param {import('./mark-geometry.js').MarkGeometry} geo
|
|
81
|
+
* @param {number} [cells=10]
|
|
82
|
+
* @returns {import('@logomarks/core/edit').PixelGrid}
|
|
83
|
+
*/
|
|
84
|
+
export function mosaicGrid(geo, cells = 10) {
|
|
85
|
+
const n = clamp(Math.round(cells), 4, 48);
|
|
86
|
+
// pixelGrid sets 48 − 34 × amount cells across.
|
|
87
|
+
return pixelGrid((48 - n) / 34, geo.box);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The filled cells less a seeded share left out (at least one is kept).
|
|
92
|
+
*
|
|
93
|
+
* @param {Uint8Array} on
|
|
94
|
+
* @param {number} drop - 0–1.
|
|
95
|
+
* @param {import('../index.js').Rng} rng
|
|
96
|
+
* @returns {Uint8Array}
|
|
97
|
+
*/
|
|
98
|
+
function dropCells(on, drop, rng) {
|
|
99
|
+
const out = on.slice();
|
|
100
|
+
const share = clamp(drop, 0, 0.9);
|
|
101
|
+
let left = 0;
|
|
102
|
+
out.forEach((v, i) => {
|
|
103
|
+
if (!v) return;
|
|
104
|
+
// One draw a filled cell, always, so the choice is stable whatever `drop` is.
|
|
105
|
+
if (rng.next() < share) out[i] = 0;
|
|
106
|
+
else left++;
|
|
107
|
+
});
|
|
108
|
+
if (!left) return on.slice();
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A seeded empty cell beside the mark (one with a filled neighbour), or -1.
|
|
114
|
+
*
|
|
115
|
+
* @param {Uint8Array} on
|
|
116
|
+
* @param {{ cols: number, rows: number }} g
|
|
117
|
+
* @param {import('../index.js').Rng} rng
|
|
118
|
+
* @returns {number}
|
|
119
|
+
*/
|
|
120
|
+
function accentCell(on, { cols, rows }, rng) {
|
|
121
|
+
const near = [];
|
|
122
|
+
for (let i = 0; i < on.length; i++) {
|
|
123
|
+
if (on[i]) continue;
|
|
124
|
+
const q = i % cols;
|
|
125
|
+
const r = Math.floor(i / cols);
|
|
126
|
+
const n = [[q - 1, r], [q + 1, r], [q, r - 1], [q, r + 1]].some(([a, b]) => a >= 0 && b >= 0 && a < cols && b < rows && on[b * cols + a]);
|
|
127
|
+
if (n) near.push(i);
|
|
128
|
+
}
|
|
129
|
+
if (!near.length) return -1;
|
|
130
|
+
return near[Math.floor(rng.next() * near.length)];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* An asterisk (three bars) or a dot, centred in a cell of `side`.
|
|
135
|
+
*
|
|
136
|
+
* @param {'asterisk' | 'dot' | string} kind
|
|
137
|
+
* @param {number} cx @param {number} cy @param {number} side
|
|
138
|
+
* @param {string} color
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
function accentGlyph(kind, cx, cy, side, color) {
|
|
142
|
+
if (kind === 'dot') return `<circle${attrs({ cx, cy, r: side * 0.24, fill: color })}/>`;
|
|
143
|
+
const L = side * 0.36;
|
|
144
|
+
const t = side * 0.085;
|
|
145
|
+
const d = [0, 60, 120].map((deg) => {
|
|
146
|
+
const a = (deg * Math.PI) / 180;
|
|
147
|
+
const [ux, uy, vx, vy] = [Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a)];
|
|
148
|
+
const pts = [[-L, -t], [L, -t], [L, t], [-L, t]].map(([u, v]) => `${num(cx + u * ux + v * vx)} ${num(cy + u * uy + v * vy)}`);
|
|
149
|
+
return `M${pts.join('L')}Z`;
|
|
150
|
+
}).join('');
|
|
151
|
+
return `<path${attrs({ d, fill: color })}/>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @param {number} v @param {number} lo @param {number} hi @returns {number} */
|
|
155
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
package/src/layers.js
CHANGED
|
@@ -29,7 +29,7 @@ import { paper, scribble } from './handmade.js';
|
|
|
29
29
|
import { ink, dither, contour, waveform, tiles, rule } from './print.js';
|
|
30
30
|
import { typeset, wantsType } from './type/index.js';
|
|
31
31
|
import { recordLayout } from './type/layout-ref.js';
|
|
32
|
-
import { shape, svg, table, barcode, perforation, tear, texture } from './layers/index.js';
|
|
32
|
+
import { shape, svg, table, barcode, perforation, tear, texture, mosaic, marks } from './layers/index.js';
|
|
33
33
|
import { pos, layerAttrs } from './layers/util.js';
|
|
34
34
|
import { paint } from './paint/index.js';
|
|
35
35
|
import { imageSize } from './lint/image-size.js';
|
|
@@ -620,10 +620,11 @@ export function pattern(layer, ctx) {
|
|
|
620
620
|
const d = [];
|
|
621
621
|
if (angle === 0) {
|
|
622
622
|
// Rows and columns: every `size` from the centre, or `divisions` equal
|
|
623
|
-
// cells (inner lines only
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
const
|
|
623
|
+
// cells (inner lines only; `[cols, rows]` for a design grid); broken
|
|
624
|
+
// wherever they cross a `gaps` region.
|
|
625
|
+
const [nx, ny] = Array.isArray(layer.divisions) ? layer.divisions.map((d) => Math.max(1, Math.round(d))) : [layer.divisions, layer.divisions];
|
|
626
|
+
const ys = ny ? Array.from({ length: ny - 1 }, (_, k) => ((k + 1) * h) / ny) : range(cy % size, h, size);
|
|
627
|
+
const xs = kind !== 'grid' ? [] : nx ? Array.from({ length: nx - 1 }, (_, k) => ((k + 1) * w) / nx) : range(cx % size, w, size);
|
|
627
628
|
const gaps = (layer.gaps ?? []).map((g) => regionPx(g, ctx.canvas));
|
|
628
629
|
d.push(gridPath(xs, ys, w, h, gaps));
|
|
629
630
|
shapes = `<path${attrs({ d: d.join(''), fill: 'none', stroke: col, 'stroke-width': stroke, ...dashAttrs })}/>`;
|
|
@@ -1037,4 +1038,4 @@ function group() {
|
|
|
1037
1038
|
throw new TypeError('A group layer is drawn by renderSvg()/renderImage(), not on its own');
|
|
1038
1039
|
}
|
|
1039
1040
|
|
|
1040
|
-
export const LAYERS = Object.freeze({ solid, linear, radial, mesh, stripes, image, grain, pattern, shapes, icon, text, frame, diagram, paper, scribble, ink, dither, contour, waveform, tiles, rule, group, shape, svg, table, barcode, perforation, tear, texture, repeat: group, defs: group, use: group });
|
|
1041
|
+
export const LAYERS = Object.freeze({ solid, linear, radial, mesh, stripes, image, grain, pattern, shapes, icon, text, frame, diagram, paper, scribble, ink, dither, contour, waveform, tiles, rule, group, shape, svg, table, barcode, perforation, tear, texture, mosaic, marks, repeat: group, defs: group, use: group });
|
package/src/type/fallback.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Glyph fallback: characters the chosen font lacks are shaped in a
|
|
3
3
|
* fallback font (the layer's `fallback`, then any font registered with
|
|
4
4
|
* `fallback: true`), and failing those drawn from the built-in symbols
|
|
5
|
-
* (✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■ ♪). Text the font covers shapes exactly as
|
|
5
|
+
* (✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■ ♪ ≠). Text the font covers shapes exactly as
|
|
6
6
|
* before (one HarfBuzz run).
|
|
7
7
|
*
|
|
8
8
|
* @module type/fallback
|
package/src/type/spans.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type { Length, Color } from '../index.js';
|
|
|
8
8
|
export interface TextSpan {
|
|
9
9
|
content: string;
|
|
10
10
|
font?: string; weight?: number; style?: 'normal' | 'italic'; axes?: Record<string, number>;
|
|
11
|
+
/** Set at the layer's `boldWeight` (default 700) when `weight` isn't given. */
|
|
12
|
+
bold?: boolean;
|
|
11
13
|
/** Size relative to the layer's (default 1). */
|
|
12
14
|
scale?: number;
|
|
13
15
|
/** Size outright. */
|
|
@@ -24,7 +26,9 @@ export interface TextSpan {
|
|
|
24
26
|
export interface Span { text: string; face: LoadedFont; scale: number; tracking: number; paint: string | undefined; dy: number }
|
|
25
27
|
|
|
26
28
|
/** Resolve a layer's spans (faces loaded, sizes relative to `size`). */
|
|
27
|
-
export function loadSpans(layer: { spans: (TextSpan | string)[]; font?: string; weight?: number; style?: string; axes?: Record<string, number>; letterSpacing?: number; transform?: string }, ctx: RenderContext, size: number): Promise<Span[]>;
|
|
29
|
+
export function loadSpans(layer: { spans: (TextSpan | string)[]; font?: string; weight?: number; boldWeight?: number; style?: string; axes?: Record<string, number>; letterSpacing?: number; transform?: string }, ctx: RenderContext, size: number): Promise<Span[]>;
|
|
30
|
+
/** A `content` array as spans, one a line, when any item is a span object; else `undefined`. */
|
|
31
|
+
export function lineSpans(content: unknown): TextSpan[] | undefined;
|
|
28
32
|
/** Spans as lines (split at `\n`, wrapped at `maxWidth`). */
|
|
29
33
|
export function spanLines(spans: Span[], size: number, maxWidth: number, fallbacks: LoadedFont[]): Span[][];
|
|
30
34
|
/** Shape a line of spans as one run. */
|
package/src/type/spans.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* style, axes, size, colour, tracking and baseline shift, set one after
|
|
4
4
|
* another on the same lines (a `\n` in a span starts a new line).
|
|
5
5
|
*
|
|
6
|
-
* `spans: [{ content, font, weight, style, axes, scale, size, color, letterSpacing, dy }]`;
|
|
6
|
+
* `spans: [{ content, font, weight, bold, style, axes, scale, size, color, letterSpacing, dy }]`;
|
|
7
7
|
* anything a span leaves out comes from the layer. `scale` sizes a span
|
|
8
|
-
* relative to the layer (`size` gives it outright)
|
|
8
|
+
* relative to the layer (`size` gives it outright); `bold: true` sets it at
|
|
9
|
+
* the layer's `boldWeight` (700). A `content` array with span objects in it
|
|
10
|
+
* is one span a line (`lineSpans`): a headline whose lines change weight.
|
|
9
11
|
*
|
|
10
12
|
* @module type/spans
|
|
11
13
|
*/
|
|
@@ -37,7 +39,8 @@ export async function loadSpans(layer, ctx, size) {
|
|
|
37
39
|
const out = [];
|
|
38
40
|
for (const given of layer.spans) {
|
|
39
41
|
const s = typeof given === 'string' ? { content: given } : given;
|
|
40
|
-
const
|
|
42
|
+
const weight = s.weight ?? (s.bold ? layer.boldWeight ?? 700 : layer.weight);
|
|
43
|
+
const face = await faceFor(ctx, { font: s.font ?? layer.font, weight, style: s.style ?? layer.style, axes: { ...layer.axes, ...s.axes } });
|
|
41
44
|
const kase = s.transform ?? layer.transform;
|
|
42
45
|
const raw = String(s.content ?? '').replace(/[\t\r ]+/g, ' ');
|
|
43
46
|
out.push({
|
|
@@ -52,6 +55,26 @@ export async function loadSpans(layer, ctx, size) {
|
|
|
52
55
|
return out;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A `content` array as spans, one a line, when any item is a span object
|
|
60
|
+
* (`{ content, bold, weight, … }`); otherwise `undefined` (plain lines stay
|
|
61
|
+
* content).
|
|
62
|
+
*
|
|
63
|
+
* @param {unknown} content - A text layer's `content`.
|
|
64
|
+
* @returns {import('./spans.js').TextSpan[] | undefined}
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* lineSpans([{ content: 'Signal', bold: true }, 'over the']);
|
|
68
|
+
* // [{ content: 'Signal\n', bold: true }, { content: 'over the' }]
|
|
69
|
+
*/
|
|
70
|
+
export function lineSpans(content) {
|
|
71
|
+
if (!Array.isArray(content) || !content.some((x) => x && typeof x === 'object')) return undefined;
|
|
72
|
+
return content.map((item, i) => {
|
|
73
|
+
const s = item && typeof item === 'object' ? item : { content: item };
|
|
74
|
+
return { ...s, content: `${s.content ?? ''}${i < content.length - 1 ? '\n' : ''}` };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
/**
|
|
56
79
|
* Spans as lines: split at `\n`, then (with a finite `maxWidth`) wrapped at
|
|
57
80
|
* spaces, greedily, at `size`.
|