postext 1.19.2 → 1.20.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/dist/__tests__/comics/art.test.d.ts +2 -0
- package/dist/__tests__/comics/art.test.d.ts.map +1 -0
- package/dist/__tests__/comics/art.test.js +96 -0
- package/dist/__tests__/comics/art.test.js.map +1 -0
- package/dist/__tests__/comics/build.test.d.ts +2 -0
- package/dist/__tests__/comics/build.test.d.ts.map +1 -0
- package/dist/__tests__/comics/build.test.js +215 -0
- package/dist/__tests__/comics/build.test.js.map +1 -0
- package/dist/__tests__/comics/canvas.test.d.ts +2 -0
- package/dist/__tests__/comics/canvas.test.d.ts.map +1 -0
- package/dist/__tests__/comics/canvas.test.js +161 -0
- package/dist/__tests__/comics/canvas.test.js.map +1 -0
- package/dist/__tests__/comics/config.test.d.ts +2 -0
- package/dist/__tests__/comics/config.test.d.ts.map +1 -0
- package/dist/__tests__/comics/config.test.js +97 -0
- package/dist/__tests__/comics/config.test.js.map +1 -0
- package/dist/__tests__/comics/geometry.test.d.ts +2 -0
- package/dist/__tests__/comics/geometry.test.d.ts.map +1 -0
- package/dist/__tests__/comics/geometry.test.js +118 -0
- package/dist/__tests__/comics/geometry.test.js.map +1 -0
- package/dist/__tests__/comics/html.test.d.ts +2 -0
- package/dist/__tests__/comics/html.test.d.ts.map +1 -0
- package/dist/__tests__/comics/html.test.js +253 -0
- package/dist/__tests__/comics/html.test.js.map +1 -0
- package/dist/__tests__/comics/lettering.test.d.ts +2 -0
- package/dist/__tests__/comics/lettering.test.d.ts.map +1 -0
- package/dist/__tests__/comics/lettering.test.js +241 -0
- package/dist/__tests__/comics/lettering.test.js.map +1 -0
- package/dist/__tests__/comics/script.test.d.ts +2 -0
- package/dist/__tests__/comics/script.test.d.ts.map +1 -0
- package/dist/__tests__/comics/script.test.js +147 -0
- package/dist/__tests__/comics/script.test.js.map +1 -0
- package/dist/__tests__/comics/split.test.d.ts +2 -0
- package/dist/__tests__/comics/split.test.d.ts.map +1 -0
- package/dist/__tests__/comics/split.test.js +120 -0
- package/dist/__tests__/comics/split.test.js.map +1 -0
- package/dist/__tests__/comics/spread.test.d.ts +2 -0
- package/dist/__tests__/comics/spread.test.d.ts.map +1 -0
- package/dist/__tests__/comics/spread.test.js +161 -0
- package/dist/__tests__/comics/spread.test.js.map +1 -0
- package/dist/__tests__/comics/strip.test.d.ts +2 -0
- package/dist/__tests__/comics/strip.test.d.ts.map +1 -0
- package/dist/__tests__/comics/strip.test.js +231 -0
- package/dist/__tests__/comics/strip.test.js.map +1 -0
- package/dist/__tests__/exports.test.js +89 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/parse/containers.test.js +1 -1
- package/dist/__tests__/parse/containers.test.js.map +1 -1
- package/dist/canvas-backend/comic.d.ts +32 -0
- package/dist/canvas-backend/comic.d.ts.map +1 -0
- package/dist/canvas-backend/comic.js +216 -0
- package/dist/canvas-backend/comic.js.map +1 -0
- package/dist/canvas-backend/headerFooter.d.ts +4 -1
- package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.js +3 -1
- package/dist/canvas-backend/headerFooter.js.map +1 -1
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +7 -1
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/comics/art.d.ts +72 -0
- package/dist/comics/art.d.ts.map +1 -0
- package/dist/comics/art.js +154 -0
- package/dist/comics/art.js.map +1 -0
- package/dist/comics/fonts.d.ts +20 -0
- package/dist/comics/fonts.d.ts.map +1 -0
- package/dist/comics/fonts.js +42 -0
- package/dist/comics/fonts.js.map +1 -0
- package/dist/comics/geometry.d.ts +87 -0
- package/dist/comics/geometry.d.ts.map +1 -0
- package/dist/comics/geometry.js +272 -0
- package/dist/comics/geometry.js.map +1 -0
- package/dist/comics/index.d.ts +29 -0
- package/dist/comics/index.d.ts.map +1 -0
- package/dist/comics/index.js +18 -0
- package/dist/comics/index.js.map +1 -0
- package/dist/comics/layoutPage.d.ts +156 -0
- package/dist/comics/layoutPage.d.ts.map +1 -0
- package/dist/comics/layoutPage.js +697 -0
- package/dist/comics/layoutPage.js.map +1 -0
- package/dist/comics/lettering/__tests__/harness.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/harness.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/harness.test.js +273 -0
- package/dist/comics/lettering/__tests__/harness.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/letterPanel.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/letterPanel.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/letterPanel.test.js +224 -0
- package/dist/comics/lettering/__tests__/letterPanel.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/polish.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/polish.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/polish.test.js +357 -0
- package/dist/comics/lettering/__tests__/polish.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/round2.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/round2.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/round2.test.js +289 -0
- package/dist/comics/lettering/__tests__/round2.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/shapeText.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/shapeText.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/shapeText.test.js +98 -0
- package/dist/comics/lettering/__tests__/shapeText.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/shapes.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/shapes.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/shapes.test.js +137 -0
- package/dist/comics/lettering/__tests__/shapes.test.js.map +1 -0
- package/dist/comics/lettering/__tests__/stubMeasure.d.ts +5 -0
- package/dist/comics/lettering/__tests__/stubMeasure.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/stubMeasure.js +47 -0
- package/dist/comics/lettering/__tests__/stubMeasure.js.map +1 -0
- package/dist/comics/lettering/__tests__/text.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/text.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/text.test.js +86 -0
- package/dist/comics/lettering/__tests__/text.test.js.map +1 -0
- package/dist/comics/lettering/geom.d.ts +78 -0
- package/dist/comics/lettering/geom.d.ts.map +1 -0
- package/dist/comics/lettering/geom.js +376 -0
- package/dist/comics/lettering/geom.js.map +1 -0
- package/dist/comics/lettering/index.d.ts +7 -0
- package/dist/comics/lettering/index.d.ts.map +1 -0
- package/dist/comics/lettering/index.js +8 -0
- package/dist/comics/lettering/index.js.map +1 -0
- package/dist/comics/lettering/letter.d.ts +7 -0
- package/dist/comics/lettering/letter.d.ts.map +1 -0
- package/dist/comics/lettering/letter.js +397 -0
- package/dist/comics/lettering/letter.js.map +1 -0
- package/dist/comics/lettering/placement.d.ts +117 -0
- package/dist/comics/lettering/placement.d.ts.map +1 -0
- package/dist/comics/lettering/placement.js +889 -0
- package/dist/comics/lettering/placement.js.map +1 -0
- package/dist/comics/lettering/presets.d.ts +15 -0
- package/dist/comics/lettering/presets.d.ts.map +1 -0
- package/dist/comics/lettering/presets.js +61 -0
- package/dist/comics/lettering/presets.js.map +1 -0
- package/dist/comics/lettering/shape-text.d.ts +72 -0
- package/dist/comics/lettering/shape-text.d.ts.map +1 -0
- package/dist/comics/lettering/shape-text.js +550 -0
- package/dist/comics/lettering/shape-text.js.map +1 -0
- package/dist/comics/lettering/shapes.d.ts +83 -0
- package/dist/comics/lettering/shapes.d.ts.map +1 -0
- package/dist/comics/lettering/shapes.js +512 -0
- package/dist/comics/lettering/shapes.js.map +1 -0
- package/dist/comics/lettering/text.d.ts +79 -0
- package/dist/comics/lettering/text.d.ts.map +1 -0
- package/dist/comics/lettering/text.js +488 -0
- package/dist/comics/lettering/text.js.map +1 -0
- package/dist/comics/lettering/types.d.ts +270 -0
- package/dist/comics/lettering/types.d.ts.map +1 -0
- package/dist/comics/lettering/types.js +9 -0
- package/dist/comics/lettering/types.js.map +1 -0
- package/dist/comics/page.d.ts +37 -0
- package/dist/comics/page.d.ts.map +1 -0
- package/dist/comics/page.js +285 -0
- package/dist/comics/page.js.map +1 -0
- package/dist/comics/paint.d.ts +65 -0
- package/dist/comics/paint.d.ts.map +1 -0
- package/dist/comics/paint.js +219 -0
- package/dist/comics/paint.js.map +1 -0
- package/dist/comics/script.d.ts +42 -0
- package/dist/comics/script.d.ts.map +1 -0
- package/dist/comics/script.js +302 -0
- package/dist/comics/script.js.map +1 -0
- package/dist/comics/split.d.ts +135 -0
- package/dist/comics/split.d.ts.map +1 -0
- package/dist/comics/split.js +514 -0
- package/dist/comics/split.js.map +1 -0
- package/dist/comics/spread.d.ts +36 -0
- package/dist/comics/spread.d.ts.map +1 -0
- package/dist/comics/spread.js +172 -0
- package/dist/comics/spread.js.map +1 -0
- package/dist/comics/strip.d.ts +77 -0
- package/dist/comics/strip.d.ts.map +1 -0
- package/dist/comics/strip.js +89 -0
- package/dist/comics/strip.js.map +1 -0
- package/dist/comics/transform.d.ts +37 -0
- package/dist/comics/transform.d.ts.map +1 -0
- package/dist/comics/transform.js +161 -0
- package/dist/comics/transform.js.map +1 -0
- package/dist/comics/types.d.ts +143 -0
- package/dist/comics/types.d.ts.map +1 -0
- package/dist/comics/types.js +13 -0
- package/dist/comics/types.js.map +1 -0
- package/dist/comics/warnings.d.ts +14 -0
- package/dist/comics/warnings.d.ts.map +1 -0
- package/dist/comics/warnings.js +94 -0
- package/dist/comics/warnings.js.map +1 -0
- package/dist/configWarnings.d.ts.map +1 -1
- package/dist/configWarnings.js +42 -0
- package/dist/configWarnings.js.map +1 -1
- package/dist/defaults/comics.d.ts +51 -0
- package/dist/defaults/comics.d.ts.map +1 -0
- package/dist/defaults/comics.js +385 -0
- package/dist/defaults/comics.js.map +1 -0
- package/dist/defaults/index.d.ts +1 -0
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +9 -0
- package/dist/defaults/index.js.map +1 -1
- package/dist/design/layout.d.ts +13 -1
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +17 -0
- package/dist/design/layout.js.map +1 -1
- package/dist/html-backend.d.ts +3 -0
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +44 -8
- package/dist/html-backend.js.map +1 -1
- package/dist/htmlComic.d.ts +69 -0
- package/dist/htmlComic.d.ts.map +1 -0
- package/dist/htmlComic.js +179 -0
- package/dist/htmlComic.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +25 -1
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/indexMarks.d.ts.map +1 -1
- package/dist/parse/indexMarks.js +3 -0
- package/dist/parse/indexMarks.js.map +1 -1
- package/dist/parse/types.d.ts +7 -1
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +251 -9
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +4 -1
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/contentWarnings.d.ts.map +1 -1
- package/dist/pipeline/contentWarnings.js +58 -0
- package/dist/pipeline/contentWarnings.js.map +1 -1
- package/dist/pipeline/floatSlots.js +1 -1
- package/dist/pipeline/floatSlots.js.map +1 -1
- package/dist/pipeline/headerFooter.d.ts +6 -2
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +11 -3
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/pageRoles.d.ts +1 -0
- package/dist/pipeline/pageRoles.d.ts.map +1 -1
- package/dist/pipeline/pageRoles.js +3 -0
- package/dist/pipeline/pageRoles.js.map +1 -1
- package/dist/pipeline/placement.d.ts +3 -3
- package/dist/pipeline/placement.d.ts.map +1 -1
- package/dist/pipeline/placement.js +4 -4
- package/dist/pipeline/placement.js.map +1 -1
- package/dist/types.d.ts +338 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +271 -2
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/dist/worker/fonts.d.ts +2 -1
- package/dist/worker/fonts.d.ts.map +1 -1
- package/dist/worker/fonts.js +11 -1
- package/dist/worker/fonts.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lay out a comic page (`:::page`, #555–#557): its frame, the cells of its
|
|
3
|
+
* split, each panel's border, background and cropped picture, and the split
|
|
4
|
+
* lines the Sandbox drags. The lettering (balloons) is set by
|
|
5
|
+
* {@link letterPanels}.
|
|
6
|
+
*
|
|
7
|
+
* Everything is on the sheet (page px), whatever the document's writing
|
|
8
|
+
* mode or direction; the reading direction of the panels comes from
|
|
9
|
+
* `comics.readingDirection` (or the page's `direction`).
|
|
10
|
+
*/
|
|
11
|
+
import { flowRectToPage } from '../vdt';
|
|
12
|
+
import { resolvedDirection } from '../pipeline/config';
|
|
13
|
+
import { dimensionToPx } from '../units';
|
|
14
|
+
import { chineseScriptOf, directionOf, isCjkLanguage, isJapaneseLanguage, presentTag } from '../locale';
|
|
15
|
+
import { DEFAULT_LETTERING_STATIC, pickBalloonStyle, pickPanelStyle, resolvedComics } from '../defaults/comics';
|
|
16
|
+
import { comicArtCrop, comicArtPointToPage, comicArtRectToPage } from './art';
|
|
17
|
+
import { clipPolygon, comicGeometry, physicalSide, pointInPolygon, polygonBBox } from './geometry';
|
|
18
|
+
import { parseComicPoint } from './script';
|
|
19
|
+
import { letterPanelDetailed } from './lettering/letter';
|
|
20
|
+
/** The leaf a comic page is laid out on when the page is not one
|
|
21
|
+
* (`comics.viewerLeaf`): its box on the page — the print leaf scaled to
|
|
22
|
+
* `fitWidth` px wide (no taller than `fitHeight`), at the top of the
|
|
23
|
+
* trim, centred across it —, its content area, and the dpi that scale
|
|
24
|
+
* resolves every length of the comic page at. Undefined without one. */
|
|
25
|
+
export function comicViewerLeaf(comics, resolved, trim) {
|
|
26
|
+
const leaf = comics.viewerLeaf;
|
|
27
|
+
if (!leaf || !(leaf.fitWidth > 0))
|
|
28
|
+
return undefined;
|
|
29
|
+
const dpi0 = resolved.page.dpi;
|
|
30
|
+
const w0 = dimensionToPx(leaf.width, dpi0);
|
|
31
|
+
const h0 = dimensionToPx(leaf.height, dpi0);
|
|
32
|
+
if (!(w0 > 0) || !(h0 > 0))
|
|
33
|
+
return undefined;
|
|
34
|
+
const k = Math.min(leaf.fitWidth / w0, leaf.fitHeight && leaf.fitHeight > 0 ? leaf.fitHeight / h0 : Infinity);
|
|
35
|
+
const dpi = dpi0 * k;
|
|
36
|
+
const width = w0 * k;
|
|
37
|
+
const height = h0 * k;
|
|
38
|
+
const box = { x: trim.x + Math.max(0, (trim.width - width) / 2), y: trim.y, width, height };
|
|
39
|
+
const m = leaf.margins ?? {};
|
|
40
|
+
const at = (d) => (d ? dimensionToPx(d, dpi) : 0);
|
|
41
|
+
const left = at(m.left);
|
|
42
|
+
const right = at(m.right);
|
|
43
|
+
const top = at(m.top);
|
|
44
|
+
const bottom = at(m.bottom);
|
|
45
|
+
const content = { x: box.x + left, y: box.y + top, width: Math.max(0, width - left - right), height: Math.max(0, height - top - bottom) };
|
|
46
|
+
return { box, content, dpi };
|
|
47
|
+
}
|
|
48
|
+
/** A Dimension written in an attribute (`4mm`, `0.5cm`, `6pt`, `12px`, a
|
|
49
|
+
* bare number in mm), or undefined. */
|
|
50
|
+
export function parseComicDimension(value) {
|
|
51
|
+
if (value === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
const m = /^\s*([0-9]*\.?[0-9]+)\s*(mm|cm|in|pt|px)?\s*$/.exec(value);
|
|
54
|
+
if (!m)
|
|
55
|
+
return undefined;
|
|
56
|
+
return { value: Number(m[1]), unit: m[2] ?? 'mm' };
|
|
57
|
+
}
|
|
58
|
+
/** Whether a flag attribute is on: present, and not `false` / `no` / `0`. */
|
|
59
|
+
function flagOn(value) {
|
|
60
|
+
if (value === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
const t = value.trim().toLowerCase();
|
|
63
|
+
return !(t === 'false' || t === 'no' || t === '0' || t === 'none');
|
|
64
|
+
}
|
|
65
|
+
/** The reading direction of a page: its `direction` attribute, else the
|
|
66
|
+
* config's; `'auto'` is `'rtl'` in a right-to-left document (an Arabic
|
|
67
|
+
* edition mirrors its pages) and the direction the art was drawn for
|
|
68
|
+
* otherwise (`comics.artDirection`: a Japanese edition of a Western comic
|
|
69
|
+
* stays left to right, a manga stays right to left in any language). */
|
|
70
|
+
export function comicPageDirection(source, comics, resolved) {
|
|
71
|
+
const own = source.attrs.direction?.trim().toLowerCase() ?? source.attrs.dir?.trim().toLowerCase();
|
|
72
|
+
if (own === 'ltr' || own === 'rtl')
|
|
73
|
+
return own;
|
|
74
|
+
if (comics.readingDirection === 'ltr' || comics.readingDirection === 'rtl')
|
|
75
|
+
return comics.readingDirection;
|
|
76
|
+
return resolvedDirection(resolved) === 'rtl' ? 'rtl' : comics.artDirection;
|
|
77
|
+
}
|
|
78
|
+
/** A panel's `pad` (CSS-like, one to four values: top, end, bottom,
|
|
79
|
+
* start; each a Dimension or a percentage of the cell, `pad="0 12%"`) as
|
|
80
|
+
* px insets of a cell `cell` wide and high on a page read in
|
|
81
|
+
* `direction`. Undefined when unset or unreadable. */
|
|
82
|
+
export function comicPanelPadding(value, cell, direction, dpi) {
|
|
83
|
+
const parts = value?.trim().split(/[\s,]+/).filter(Boolean) ?? [];
|
|
84
|
+
if (parts.length === 0 || parts.length > 4)
|
|
85
|
+
return undefined;
|
|
86
|
+
const px = (v, along) => {
|
|
87
|
+
const pct = /^\s*(-?[0-9]*\.?[0-9]+)\s*%\s*$/.exec(v);
|
|
88
|
+
if (pct)
|
|
89
|
+
return (Number(pct[1]) / 100) * along;
|
|
90
|
+
if (/^\s*0+(?:\.0*)?\s*$/.test(v))
|
|
91
|
+
return 0;
|
|
92
|
+
const d = parseComicDimension(v);
|
|
93
|
+
return d ? dimensionToPx(d, dpi) : undefined;
|
|
94
|
+
};
|
|
95
|
+
const [t, e = t, b = t, st = e] = parts;
|
|
96
|
+
const top = px(t, cell.height);
|
|
97
|
+
const end = px(e, cell.width);
|
|
98
|
+
const bottom = px(b, cell.height);
|
|
99
|
+
const start = px(st, cell.width);
|
|
100
|
+
if (top === undefined || end === undefined || bottom === undefined || start === undefined)
|
|
101
|
+
return undefined;
|
|
102
|
+
const clampV = (v) => Math.max(0, v);
|
|
103
|
+
return direction === 'ltr'
|
|
104
|
+
? { top: clampV(top), right: clampV(end), bottom: clampV(bottom), left: clampV(start) }
|
|
105
|
+
: { top: clampV(top), right: clampV(start), bottom: clampV(bottom), left: clampV(end) };
|
|
106
|
+
}
|
|
107
|
+
/** A colour written in an attribute: `#rgb` / `#rrggbb`, or a palette id. */
|
|
108
|
+
function attrColor(value, palette) {
|
|
109
|
+
const v = value?.trim();
|
|
110
|
+
if (!v)
|
|
111
|
+
return undefined;
|
|
112
|
+
if (/^#[0-9a-f]{3}(?:[0-9a-f]{3})?(?:[0-9a-f]{2})?$/i.test(v))
|
|
113
|
+
return v;
|
|
114
|
+
if (v === 'none' || v === 'transparent')
|
|
115
|
+
return 'transparent';
|
|
116
|
+
return palette?.find((e) => e.id === v)?.value.hex;
|
|
117
|
+
}
|
|
118
|
+
/** The sides of the frame a panel bleeds through: the page's `bleed`, the
|
|
119
|
+
* panel's (`bleed`, `bleed="top start"`, `bleed=false`), else its style's. */
|
|
120
|
+
function bleedSides(page, panel, style, direction) {
|
|
121
|
+
const all = ['top', 'bottom', 'left', 'right'];
|
|
122
|
+
const own = panel?.attrs.bleed;
|
|
123
|
+
if (own !== undefined) {
|
|
124
|
+
const t = own.trim().toLowerCase();
|
|
125
|
+
if (t === '' || t === 'true' || t === 'all')
|
|
126
|
+
return all;
|
|
127
|
+
if (t === 'false' || t === 'none' || t === 'no')
|
|
128
|
+
return [];
|
|
129
|
+
return t.split(/[\s,]+/).map((s) => physicalSide(s, direction)).filter((s) => s !== undefined);
|
|
130
|
+
}
|
|
131
|
+
if (flagOn(page.attrs.bleed))
|
|
132
|
+
return all;
|
|
133
|
+
return style.bleed ? all : [];
|
|
134
|
+
}
|
|
135
|
+
/** The picture of a resource a panel names, if it is one. */
|
|
136
|
+
function pictureOf(resources, id) {
|
|
137
|
+
if (!id)
|
|
138
|
+
return undefined;
|
|
139
|
+
const r = resources.get(id);
|
|
140
|
+
if (!r)
|
|
141
|
+
return undefined;
|
|
142
|
+
if (r.kind === 'bitmap' && r.bitmap)
|
|
143
|
+
return { resource: r, kind: 'bitmap', fileId: r.bitmap.fileId, format: r.bitmap.format, width: r.bitmap.width, height: r.bitmap.height };
|
|
144
|
+
if (r.kind === 'svg' && r.svg)
|
|
145
|
+
return { resource: r, kind: 'svg', fileId: r.svg.fileId, width: r.svg.width ?? 0, height: r.svg.height ?? 0 };
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
/** `inset="x y w h"`: percentages (or fractions) of the previous panel's
|
|
149
|
+
* box. */
|
|
150
|
+
function insetBox(value, of) {
|
|
151
|
+
if (!value)
|
|
152
|
+
return undefined;
|
|
153
|
+
const parts = value.trim().split(/[\s,]+/).map((p) => Number(p.replace('%', '')));
|
|
154
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n)))
|
|
155
|
+
return undefined;
|
|
156
|
+
const fractions = !value.includes('%') && parts.every((n) => n <= 1);
|
|
157
|
+
const [x, y, w, h] = parts.map((n) => (fractions ? n : n / 100));
|
|
158
|
+
if (w <= 0 || h <= 0)
|
|
159
|
+
return undefined;
|
|
160
|
+
return { x: of.x + x * of.width, y: of.y + y * of.height, width: w * of.width, height: h * of.height };
|
|
161
|
+
}
|
|
162
|
+
/** The frame of a page: the content area on the sheet, or the trim inset
|
|
163
|
+
* by `comics.frame.margins`. */
|
|
164
|
+
export function comicPageFrame(ctx, comics = resolvedComics(ctx.resolved)) {
|
|
165
|
+
const m = comics.frame.margins;
|
|
166
|
+
if (!m)
|
|
167
|
+
return ctx.contentBox ?? flowRectToPage(ctx.page, ctx.page.contentArea);
|
|
168
|
+
const dpi = ctx.resolved.page.dpi;
|
|
169
|
+
const t = ctx.trimBox;
|
|
170
|
+
let left = dimensionToPx(m.left, dpi);
|
|
171
|
+
let right = dimensionToPx(m.right, dpi);
|
|
172
|
+
if (m.mirror && ctx.mirrorMargins)
|
|
173
|
+
[left, right] = [right, left];
|
|
174
|
+
const top = dimensionToPx(m.top, dpi);
|
|
175
|
+
const bottom = dimensionToPx(m.bottom, dpi);
|
|
176
|
+
return { x: t.x + left, y: t.y + top, width: Math.max(0, t.width - left - right), height: Math.max(0, t.height - top - bottom) };
|
|
177
|
+
}
|
|
178
|
+
/** The panel gutters (px): the page's `gutter` (one value for both, or
|
|
179
|
+
* `"rows columns"`), else the config's. */
|
|
180
|
+
function comicGutters(source, comics, dpi) {
|
|
181
|
+
const parts = source.attrs.gutter?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
182
|
+
const a = parseComicDimension(parts[0]);
|
|
183
|
+
const b = parseComicDimension(parts[1]) ?? a;
|
|
184
|
+
return {
|
|
185
|
+
rows: dimensionToPx(a ?? comics.gutter.horizontal, dpi),
|
|
186
|
+
columns: dimensionToPx(b ?? comics.gutter.vertical, dpi),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** Layout warnings of the comic pages laid out, keyed by the page they
|
|
190
|
+
* belong to (each build pass lays its pages out afresh; only the last
|
|
191
|
+
* pass's pages reach the document). */
|
|
192
|
+
const layoutWarnings = new WeakMap();
|
|
193
|
+
/** The warnings the layout of a comic page found (`comicPanelLetterbox`,
|
|
194
|
+
* `comicBalloonOverflow`). */
|
|
195
|
+
export function comicPageLayoutWarnings(comic) {
|
|
196
|
+
return layoutWarnings.get(comic) ?? [];
|
|
197
|
+
}
|
|
198
|
+
/** Set the layout warnings of a comic laid out in parts (each page of a
|
|
199
|
+
* spread gets those of its panels). Internal to the comics modules. */
|
|
200
|
+
export function setComicLayoutWarnings(comic, warnings) {
|
|
201
|
+
if (warnings.length > 0)
|
|
202
|
+
layoutWarnings.set(comic, [...warnings]);
|
|
203
|
+
else
|
|
204
|
+
layoutWarnings.delete(comic);
|
|
205
|
+
}
|
|
206
|
+
/** The document language the lettering is set in (as
|
|
207
|
+
* {@link resolvedComics} reads it). */
|
|
208
|
+
export function comicLetteringLocale(resolved) {
|
|
209
|
+
const h = resolved.bodyText.hyphenation;
|
|
210
|
+
return presentTag(resolved.locale) ?? presentTag(h.tag) ?? presentTag(h.locale) ?? 'en';
|
|
211
|
+
}
|
|
212
|
+
/** Whether balloons are set vertically: `lettering.writingMode`, `auto`
|
|
213
|
+
* meaning vertical for Japanese and Traditional Chinese (the comics of
|
|
214
|
+
* both are lettered in columns) and in a vertical document. */
|
|
215
|
+
export function comicLetteringVertical(comics, resolved, locale) {
|
|
216
|
+
const mode = comics.lettering.writingMode;
|
|
217
|
+
if (mode === 'vertical')
|
|
218
|
+
return true;
|
|
219
|
+
if (mode === 'horizontal')
|
|
220
|
+
return false;
|
|
221
|
+
return isJapaneseLanguage(locale) || chineseScriptOf(locale) === 'Hant' || resolved.layout.writingMode === 'vertical-rl';
|
|
222
|
+
}
|
|
223
|
+
/** The role's own balloon style. */
|
|
224
|
+
function roleStyleId(role) {
|
|
225
|
+
return role === 'speech' ? 'speech' : role;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* A balloon style of the config resolved to the lettering's px style
|
|
229
|
+
* (SPEC D4 → D3): the style over the book's lettering, the speaker's cast
|
|
230
|
+
* entry and the line's own `color` / `font` over both. One lettering size
|
|
231
|
+
* per book: `fontScale` is the only factor (a sound effect's `size=` scales
|
|
232
|
+
* the item, not the style).
|
|
233
|
+
*/
|
|
234
|
+
export function comicLetteringStyle(input) {
|
|
235
|
+
const { style: st, comics, cast, item, locale, vertical, dpi } = input;
|
|
236
|
+
const L = comics.lettering;
|
|
237
|
+
const em = dimensionToPx(L.fontSize, dpi);
|
|
238
|
+
const size = em * (st.fontScale > 0 ? st.fontScale : 1);
|
|
239
|
+
const cjk = isCjkLanguage(locale);
|
|
240
|
+
const rtl = directionOf(locale) === 'rtl';
|
|
241
|
+
// Leading: the book's, or what a column of CJK or a line of Arabic needs
|
|
242
|
+
// when the book leaves the default.
|
|
243
|
+
const lineHeight = L.lineHeight !== DEFAULT_LETTERING_STATIC.lineHeight ? L.lineHeight
|
|
244
|
+
: vertical || cjk ? 1.5 : rtl ? 1.45 : L.lineHeight;
|
|
245
|
+
const letterSpacing = dimensionToPx(st.letterSpacing ?? L.letterSpacing, dpi, size);
|
|
246
|
+
const dropFinalStop = L.dropFinalStop === 'auto' ? undefined : L.dropFinalStop;
|
|
247
|
+
const out = {
|
|
248
|
+
id: st.id,
|
|
249
|
+
shape: st.shape,
|
|
250
|
+
fill: cast?.fill?.hex ?? st.fill.hex,
|
|
251
|
+
stroke: st.stroke.hex,
|
|
252
|
+
strokeWidth: dimensionToPx(st.strokeWidth, dpi, size),
|
|
253
|
+
...(st.dash ? { dash: true } : {}),
|
|
254
|
+
...(st.double ? { double: true } : {}),
|
|
255
|
+
...(st.wobble ? { wobble: st.wobble } : {}),
|
|
256
|
+
roundness: st.roundness,
|
|
257
|
+
...(st.burstPoints ? { burstPoints: st.burstPoints } : {}),
|
|
258
|
+
...(st.burstDepth ? { burstDepthRatio: st.burstDepth } : {}),
|
|
259
|
+
padding: dimensionToPx(st.padding, dpi, size),
|
|
260
|
+
aspect: st.aspect,
|
|
261
|
+
tail: st.tail,
|
|
262
|
+
tailWidth: dimensionToPx(st.tailWidth, dpi, size),
|
|
263
|
+
tailReach: st.tailReach,
|
|
264
|
+
target: st.target,
|
|
265
|
+
position: st.position,
|
|
266
|
+
...(st.butt ? { butt: true } : {}),
|
|
267
|
+
fontFamily: item?.font?.trim() || cast?.fontFamily || st.fontFamily || L.fontFamily,
|
|
268
|
+
fontSizePx: size,
|
|
269
|
+
lineHeight,
|
|
270
|
+
fontWeight: (st.bold ?? L.bold) ? 700 : 400,
|
|
271
|
+
// Italics only in scripts that have them (the lettering of Arabic,
|
|
272
|
+
// Hebrew and CJK is never slanted).
|
|
273
|
+
...((st.italic ?? L.italic) && !cjk && !rtl ? { italic: true } : {}),
|
|
274
|
+
color: attrColor(item?.color, input.palette) ?? cast?.color?.hex ?? (st.color ?? L.color).hex,
|
|
275
|
+
textTransform: st.textTransform ?? L.textTransform,
|
|
276
|
+
...(letterSpacing ? { letterSpacing } : {}),
|
|
277
|
+
align: st.align,
|
|
278
|
+
emphasis: 'bold-italic',
|
|
279
|
+
writingMode: 'auto',
|
|
280
|
+
maxColumnChars: L.maxColumnChars,
|
|
281
|
+
...(dropFinalStop !== undefined ? { dropFinalStop } : {}),
|
|
282
|
+
...(L.doubleDash ? { doubleDash: true } : {}),
|
|
283
|
+
...(st.halo ? { halo: dimensionToPx(st.halo, dpi, size), haloColor: st.haloColor?.hex ?? '#ffffff' } : {}),
|
|
284
|
+
...(st.rotate ? { rotate: st.rotate } : {}),
|
|
285
|
+
};
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
/** A sound effect set in columns breaks after five characters at most
|
|
289
|
+
* (its letters are big: a long column would run out of the panel). */
|
|
290
|
+
function sfxColumns(role, style) {
|
|
291
|
+
return role === 'sfx' ? { ...style, maxColumnChars: Math.min(5, style.maxColumnChars ?? 5) } : style;
|
|
292
|
+
}
|
|
293
|
+
/** A point given in fractions of a panel's picture (of its cell, for a
|
|
294
|
+
* panel without one) on the page. */
|
|
295
|
+
function panelPoint(panel, at) {
|
|
296
|
+
if (panel.art)
|
|
297
|
+
return comicArtPointToPage(panel.art, at.x, at.y);
|
|
298
|
+
return { x: panel.bbox.x + at.x * panel.bbox.width, y: panel.bbox.y + at.y * panel.bbox.height };
|
|
299
|
+
}
|
|
300
|
+
/** The anchors of a panel's picture on the page, each with whether its
|
|
301
|
+
* mouth is in view. */
|
|
302
|
+
function panelAnchors(panel, resource) {
|
|
303
|
+
const art = panel.art;
|
|
304
|
+
if (!art || !resource?.anchors)
|
|
305
|
+
return [];
|
|
306
|
+
return resource.anchors.map((a) => {
|
|
307
|
+
const mouth = comicArtPointToPage(art, a.x, a.y);
|
|
308
|
+
return {
|
|
309
|
+
id: a.id,
|
|
310
|
+
mouth,
|
|
311
|
+
...(a.head ? { head: comicArtPointToPage(art, a.head.x, a.head.y) } : {}),
|
|
312
|
+
...(a.face ? { face: comicArtRectToPage(art, a.face) } : {}),
|
|
313
|
+
visible: pointInPolygon(panel.polygon, mouth),
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function overlaps(a, b) {
|
|
318
|
+
return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Guards over the head of every speaker, so that balloons do not land on a
|
|
322
|
+
* hat or a head of hair. Each is an avoid zone (a balloon may still cover
|
|
323
|
+
* it as a last resort; tails run to the mouth as usual):
|
|
324
|
+
* - a marked `face` grown upward by 0.4 of its height and outward by a
|
|
325
|
+
* fifth of its width (hair, a hat brim; the face itself stays a hard
|
|
326
|
+
* no-go);
|
|
327
|
+
* - with no face, a box of a few ems about the mouth (and the head point),
|
|
328
|
+
* mostly above it, sized from the mouth-to-head distance when the head
|
|
329
|
+
* is marked.
|
|
330
|
+
* {@link headZones} adds the larger head a close shot may have, as a
|
|
331
|
+
* softer preference.
|
|
332
|
+
*/
|
|
333
|
+
function headGuards(panel, resource, em) {
|
|
334
|
+
const art = panel.art;
|
|
335
|
+
if (!art || !resource?.anchors)
|
|
336
|
+
return [];
|
|
337
|
+
return resource.anchors.filter((a) => a.id !== 'sfx').map((a) => {
|
|
338
|
+
if (a.face) {
|
|
339
|
+
const f = comicArtRectToPage(art, a.face);
|
|
340
|
+
const up = 0.4 * f.height;
|
|
341
|
+
const side = 0.2 * f.width;
|
|
342
|
+
return { x: f.x - side, y: f.y - up, width: f.width + 2 * side, height: f.height + up };
|
|
343
|
+
}
|
|
344
|
+
const mouth = comicArtPointToPage(art, a.x, a.y);
|
|
345
|
+
const head = a.head ? comicArtPointToPage(art, a.head.x, a.head.y) : mouth;
|
|
346
|
+
const d = Math.hypot(head.x - mouth.x, head.y - mouth.y);
|
|
347
|
+
const size = d > 0.5 * em ? Math.max(2.4 * d, 3.3 * em) : 3.3 * em;
|
|
348
|
+
const cx = (mouth.x + head.x) / 2;
|
|
349
|
+
const half = Math.max(1.6 * em, 0.5 * size);
|
|
350
|
+
const y0 = Math.min(mouth.y, head.y) - Math.max(2.9 * em, 0.95 * size);
|
|
351
|
+
const y1 = Math.max(mouth.y, head.y) + 0.7 * em;
|
|
352
|
+
return { x: cx - half, y: y0, width: 2 * half, height: y1 - y0 };
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/** The head a speaker with no marked face may have in a closer shot (a
|
|
356
|
+
* quarter of the shorter side of the cell, above the mouth), as a region
|
|
357
|
+
* better left uncovered: a cost per area, never a fault, so that in a
|
|
358
|
+
* long shot (small figures) a balloon still comes close. */
|
|
359
|
+
function headZones(panel, resource) {
|
|
360
|
+
const art = panel.art;
|
|
361
|
+
if (!art || !resource?.anchors)
|
|
362
|
+
return [];
|
|
363
|
+
const size = 0.22 * Math.min(panel.bbox.width, panel.bbox.height);
|
|
364
|
+
return resource.anchors.filter((a) => a.id !== 'sfx' && !a.face).map((a) => {
|
|
365
|
+
const mouth = comicArtPointToPage(art, a.x, a.y);
|
|
366
|
+
return { x: mouth.x - 0.45 * size, y: mouth.y - 1.05 * size, width: 0.9 * size, height: 1.25 * size, weight: 1.5 };
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/** The body under every marked face (a figure's chest and arms, two
|
|
370
|
+
* hugging characters), as a region better left uncovered: a balloon goes
|
|
371
|
+
* over the background before it goes over the action. A cost per area,
|
|
372
|
+
* never a fault. */
|
|
373
|
+
function bodyZones(panel, resource) {
|
|
374
|
+
const art = panel.art;
|
|
375
|
+
if (!art || !resource?.anchors)
|
|
376
|
+
return [];
|
|
377
|
+
return resource.anchors.filter((a) => a.id !== 'sfx' && a.face).map((a) => {
|
|
378
|
+
const f = comicArtRectToPage(art, a.face);
|
|
379
|
+
return { x: f.x - 0.3 * f.width, y: f.y + f.height, width: 1.6 * f.width, height: 2 * f.height, weight: 1.5 };
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/** The picture's safe area as a region better left uncovered, when the art
|
|
383
|
+
* marks no face and no avoid zone (nothing else tells the lettering what
|
|
384
|
+
* matters in it). */
|
|
385
|
+
function softSafeArea(panel, resource) {
|
|
386
|
+
const art = panel.art;
|
|
387
|
+
if (!art || !resource?.safeArea)
|
|
388
|
+
return [];
|
|
389
|
+
if ((resource.avoid?.length ?? 0) > 0 || (resource.anchors ?? []).some((a) => a.face))
|
|
390
|
+
return [];
|
|
391
|
+
return [comicArtRectToPage(art, resource.safeArea)];
|
|
392
|
+
}
|
|
393
|
+
/** Text on the art with no balloon (a `none` shape) and no halo of its
|
|
394
|
+
* own gets one, light under dark letters and dark under light ones: the
|
|
395
|
+
* white outline letterers give lettering laid on a picture (#559). */
|
|
396
|
+
function withAutoHalo(style, st) {
|
|
397
|
+
if (st.shape !== 'none' || st.halo !== undefined || style.halo)
|
|
398
|
+
return style;
|
|
399
|
+
const hex = style.color.replace('#', '');
|
|
400
|
+
const v = hex.length >= 6 ? hex.slice(0, 6) : hex.split('').map((c) => c + c).join('');
|
|
401
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(v.slice(i, i + 2), 16) / 255);
|
|
402
|
+
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
403
|
+
return { ...style, halo: Math.max(0.12 * style.fontSizePx, 1), haloColor: lum < 0.5 ? '#ffffff' : '#000000' };
|
|
404
|
+
}
|
|
405
|
+
/** A box butted against the border of a framed panel (a caption at a
|
|
406
|
+
* corner) is outlined at the border's weight, so that its edges and the
|
|
407
|
+
* border read as one line. */
|
|
408
|
+
function buttedCaption(style, panel, at) {
|
|
409
|
+
const butted = style.butt && style.shape !== 'none' && (at !== undefined || (style.position !== undefined && style.position !== 'auto'));
|
|
410
|
+
if (!butted || panel.border.style === 'none' || !(panel.border.width > 0) || style.strokeWidth <= 0)
|
|
411
|
+
return style;
|
|
412
|
+
return { ...style, strokeWidth: panel.border.width };
|
|
413
|
+
}
|
|
414
|
+
/** A box grown (negative `d`) or shrunk by `d` on every side. */
|
|
415
|
+
function insetBox2(b, d) {
|
|
416
|
+
return { x: b.x + d, y: b.y + d, width: b.width - 2 * d, height: b.height - 2 * d };
|
|
417
|
+
}
|
|
418
|
+
/** The intersection of two boxes (empty boxes have no size). */
|
|
419
|
+
function intersect(a, b) {
|
|
420
|
+
const x = Math.max(a.x, b.x);
|
|
421
|
+
const y = Math.max(a.y, b.y);
|
|
422
|
+
return { x, y, width: Math.max(0, Math.min(a.x + a.width, b.x + b.width) - x), height: Math.max(0, Math.min(a.y + a.height, b.y + b.height) - y) };
|
|
423
|
+
}
|
|
424
|
+
/** The smallest box holding both. */
|
|
425
|
+
function union(a, b) {
|
|
426
|
+
const x = Math.min(a.x, b.x);
|
|
427
|
+
const y = Math.min(a.y, b.y);
|
|
428
|
+
return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The lettering of a page's panels (#559–#561): balloons, captions and
|
|
432
|
+
* sound effects, in reading order (panel order, then script order), and the
|
|
433
|
+
* `comicBalloonOverflow` warnings of the balloons that could not be placed
|
|
434
|
+
* cleanly.
|
|
435
|
+
*/
|
|
436
|
+
export function letterPanels(input) {
|
|
437
|
+
const { panels, panelSources, comics, resolved, resources, direction, sourceOffset: off } = input;
|
|
438
|
+
const dpi = resolved.page.dpi;
|
|
439
|
+
const locale = comicLetteringLocale(resolved);
|
|
440
|
+
const vertical = comicLetteringVertical(comics, resolved, locale);
|
|
441
|
+
const L = comics.lettering;
|
|
442
|
+
const insetPx = dimensionToPx(L.inset, dpi);
|
|
443
|
+
const trim = input.trim ?? input.frame;
|
|
444
|
+
const castById = new Map(comics.cast.map((c) => [c.id, c]));
|
|
445
|
+
const balloons = [];
|
|
446
|
+
const warnings = [];
|
|
447
|
+
panels.forEach((panel, pi) => {
|
|
448
|
+
const src = panelSources[pi];
|
|
449
|
+
if (!src || src.items.length === 0)
|
|
450
|
+
return;
|
|
451
|
+
const resource = panel.art ? resources.get(panel.art.resourceId) : undefined;
|
|
452
|
+
const lp = {
|
|
453
|
+
index: panel.index,
|
|
454
|
+
polygon: panel.polygon,
|
|
455
|
+
bbox: panel.bbox,
|
|
456
|
+
insetPx,
|
|
457
|
+
direction,
|
|
458
|
+
writingMode: vertical ? 'vertical' : 'horizontal',
|
|
459
|
+
locale,
|
|
460
|
+
anchors: panelAnchors(panel, resource),
|
|
461
|
+
avoid: [
|
|
462
|
+
...(panel.art ? (resource?.avoid ?? []).map((r) => comicArtRectToPage(panel.art, r)) : []),
|
|
463
|
+
...headGuards(panel, resource, dimensionToPx(L.fontSize, dpi)),
|
|
464
|
+
],
|
|
465
|
+
keepOut: (input.avoid ?? []).filter((r) => overlaps(r, insetBox2(panel.bbox, -2 * insetPx))),
|
|
466
|
+
softAvoid: [...softSafeArea(panel, resource), ...headZones(panel, resource), ...bodyZones(panel, resource)],
|
|
467
|
+
...(panel.border.style !== 'none' && panel.border.width > 0 ? { borderPx: panel.border.width } : {}),
|
|
468
|
+
neighbours: panels.filter((q) => q !== panel).map((q) => q.bbox),
|
|
469
|
+
...(input.frame ? { limit: input.frame } : {}),
|
|
470
|
+
...(trim ? { trim, sheet: input.bleedBox ? intersect(input.bleedBox, union(trim, panel.bbox)) : trim } : {}),
|
|
471
|
+
// What the panels before this one lettered: a balloon breaking out
|
|
472
|
+
// of either panel never covers the other's.
|
|
473
|
+
foreign: balloons.filter((b) => overlaps(b.bbox, panel.bbox)).map((b) => b.bbox),
|
|
474
|
+
dpi,
|
|
475
|
+
};
|
|
476
|
+
const items = src.items.map((it, k) => {
|
|
477
|
+
const cast = it.speaker ? castById.get(it.speaker) : undefined;
|
|
478
|
+
const own = pickBalloonStyle(comics, it.style?.trim());
|
|
479
|
+
const st = own ?? pickBalloonStyle(comics, it.role === 'speech' ? cast?.balloonStyle : undefined)
|
|
480
|
+
?? pickBalloonStyle(comics, roleStyleId(it.role)) ?? pickBalloonStyle(comics, 'speech');
|
|
481
|
+
const tail = it.tail;
|
|
482
|
+
return {
|
|
483
|
+
id: `${panel.index}:${k}`,
|
|
484
|
+
order: k,
|
|
485
|
+
kind: it.role === 'speech' ? 'balloon' : it.role,
|
|
486
|
+
...(it.speaker ? { speaker: it.speaker } : {}),
|
|
487
|
+
text: it.spans.length > 0 ? it.spans : it.text,
|
|
488
|
+
sourceMap: it.sourceMap.map((o) => o + off),
|
|
489
|
+
sourceStart: it.sourceStart + off,
|
|
490
|
+
sourceEnd: it.sourceEnd + off,
|
|
491
|
+
style: buttedCaption(withAutoHalo(sfxColumns(it.role, comicLetteringStyle({ style: st, comics, ...(cast ? { cast } : {}), item: it, locale, vertical, dpi, ...(resolved.colorPalette ? { palette: resolved.colorPalette } : {}) })), st), panel, it.atKeyword),
|
|
492
|
+
...(it.at ? { pin: panelPoint(panel, it.at) } : {}),
|
|
493
|
+
...(it.atKeyword ? { position: it.atKeyword } : {}),
|
|
494
|
+
...(it.to ? { tailTarget: panelPoint(panel, it.to) } : tail && tail !== 'none' && tail !== 'auto' ? { tailTarget: tail } : {}),
|
|
495
|
+
...(tail === 'none' ? { tail: 'none' } : {}),
|
|
496
|
+
...(it.join !== undefined ? { join: it.join } : {}),
|
|
497
|
+
...(it.break ? { breakBorder: true } : {}),
|
|
498
|
+
...(it.rotate !== undefined ? { rotate: it.rotate } : {}),
|
|
499
|
+
...(it.size !== undefined && it.size > 0 ? { sizeScale: it.size } : {}),
|
|
500
|
+
...(it.writingMode ? { writingMode: it.writingMode } : {}),
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
const r = letterPanelDetailed(lp, items, { joinSameSpeaker: L.joinSameSpeaker, groupBase: pi * 1000 });
|
|
504
|
+
balloons.push(...r.balloons);
|
|
505
|
+
for (const d of r.diagnostics) {
|
|
506
|
+
warnings.push({
|
|
507
|
+
kind: 'comicBalloonOverflow',
|
|
508
|
+
panel: d.panelIndex,
|
|
509
|
+
reasons: d.reasons,
|
|
510
|
+
fallbacks: d.fallbacks,
|
|
511
|
+
sourceStart: d.sourceStart,
|
|
512
|
+
sourceEnd: d.sourceEnd,
|
|
513
|
+
...(input.pageIndex !== undefined ? { pageIndex: input.pageIndex } : {}),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
return { balloons, warnings };
|
|
518
|
+
}
|
|
519
|
+
/** Lay out a comic page. */
|
|
520
|
+
export function layoutComicPage(source, given) {
|
|
521
|
+
// A host's screen page: the comic is laid out on the print leaf, scaled
|
|
522
|
+
// (its own trim, no bleed, its content area, its lengths at its scale).
|
|
523
|
+
const leaf = comicViewerLeaf(resolvedComics(given.resolved), given.resolved, given.trimBox);
|
|
524
|
+
const ctx = leaf
|
|
525
|
+
? {
|
|
526
|
+
...given,
|
|
527
|
+
resolved: { ...given.resolved, page: { ...given.resolved.page, dpi: leaf.dpi } },
|
|
528
|
+
trimBox: leaf.box,
|
|
529
|
+
bleedBox: leaf.box,
|
|
530
|
+
contentBox: leaf.content,
|
|
531
|
+
mirrorMargins: false,
|
|
532
|
+
}
|
|
533
|
+
: given;
|
|
534
|
+
const comic = layoutComicFrame(source, {
|
|
535
|
+
resolved: ctx.resolved,
|
|
536
|
+
frame: comicPageFrame(ctx),
|
|
537
|
+
bleedBox: ctx.bleedBox,
|
|
538
|
+
trimBox: ctx.trimBox,
|
|
539
|
+
resources: ctx.resources,
|
|
540
|
+
...(ctx.sourceOffset !== undefined ? { sourceOffset: ctx.sourceOffset } : {}),
|
|
541
|
+
pageIndex: ctx.page.index,
|
|
542
|
+
});
|
|
543
|
+
if (leaf)
|
|
544
|
+
comic.leaf = leaf.box;
|
|
545
|
+
return comic;
|
|
546
|
+
}
|
|
547
|
+
/** Lay out a comic (a page, a strip, a spread) in a given frame: its
|
|
548
|
+
* cells, panels, split lines and lettering, in the frame's coordinates. */
|
|
549
|
+
export function layoutComicFrame(source, ctx) {
|
|
550
|
+
const { resolved, resources, frame } = ctx;
|
|
551
|
+
const comics = resolvedComics(resolved);
|
|
552
|
+
const dpi = resolved.page.dpi;
|
|
553
|
+
const off = ctx.sourceOffset ?? 0;
|
|
554
|
+
const direction = comicPageDirection(source, comics, resolved);
|
|
555
|
+
const pageStyle = pickPanelStyle(comics, source.attrs.style?.trim());
|
|
556
|
+
const flowPanels = source.panels.filter((p) => !p.attrs.inset);
|
|
557
|
+
const gutters = comicGutters(source, comics, dpi);
|
|
558
|
+
const styleOf = (p) => (p?.attrs.style ? pickPanelStyle(comics, p.attrs.style.trim()) : pageStyle);
|
|
559
|
+
const geometry = comicGeometry({
|
|
560
|
+
tree: source.splitParse.tree,
|
|
561
|
+
frame,
|
|
562
|
+
bleedBox: ctx.bleedBox,
|
|
563
|
+
direction,
|
|
564
|
+
gutter: gutters,
|
|
565
|
+
bleed: (i) => bleedSides(source, flowPanels[i], styleOf(flowPanels[i]), direction),
|
|
566
|
+
});
|
|
567
|
+
const warnings = [];
|
|
568
|
+
const panels = [];
|
|
569
|
+
const panelSources = [];
|
|
570
|
+
const mirrorDefault = comics.mirrorArt && direction !== comics.artDirection;
|
|
571
|
+
const buildPanel = (whole, p) => {
|
|
572
|
+
const style = styleOf(p);
|
|
573
|
+
const attrs = p?.attrs ?? {};
|
|
574
|
+
const cell = padCell(whole, comicPanelPadding(attrs.pad, whole.bbox, direction, dpi));
|
|
575
|
+
const border = attrs.border?.trim();
|
|
576
|
+
const borderWidth = border === 'none' ? 0 : border ? dimensionToPx(parseComicDimension(border) ?? style.borderWidth, dpi) : dimensionToPx(style.borderWidth, dpi);
|
|
577
|
+
const bg = attrColor(attrs.bg, resolved.colorPalette) ?? style.background.hex;
|
|
578
|
+
const index = panels.length;
|
|
579
|
+
const panel = {
|
|
580
|
+
index,
|
|
581
|
+
...(attrs.id ? { id: attrs.id } : {}),
|
|
582
|
+
sourceStart: (p?.sourceStart ?? source.sourceStart) + off,
|
|
583
|
+
sourceEnd: (p?.sourceEnd ?? source.sourceStart) + off,
|
|
584
|
+
polygon: cell.polygon,
|
|
585
|
+
bbox: cell.bbox,
|
|
586
|
+
radius: cell.rect ? Math.min(dimensionToPx(style.borderRadius, dpi), cell.bbox.width / 2, cell.bbox.height / 2) : 0,
|
|
587
|
+
border: { width: borderWidth, color: style.borderColor.hex, style: borderWidth > 0 ? style.borderStyle : 'none' },
|
|
588
|
+
...(bg && bg !== 'transparent' ? { background: bg } : {}),
|
|
589
|
+
};
|
|
590
|
+
const fit = attrs.fit === 'contain' || attrs.fit === 'cover' ? attrs.fit : style.fit;
|
|
591
|
+
const focus = parseComicPoint(attrs.focus);
|
|
592
|
+
const mirrorAttr = flagOn(attrs.mirror);
|
|
593
|
+
const mirrored = mirrorAttr ?? mirrorDefault;
|
|
594
|
+
const art = pictureOf(resources, attrs.art?.trim());
|
|
595
|
+
let crop;
|
|
596
|
+
if (art) {
|
|
597
|
+
// A picture with no intrinsic size (an SVG without one) fills the
|
|
598
|
+
// cell as it is.
|
|
599
|
+
const w = art.width > 0 && art.height > 0 ? art.width : cell.bbox.width;
|
|
600
|
+
const h = art.width > 0 && art.height > 0 ? art.height : cell.bbox.height;
|
|
601
|
+
crop = comicArtCrop({ width: w, height: h, cell: cell.bbox, fit, ...(art.resource.safeArea ? { safeArea: art.resource.safeArea } : {}), ...(focus ? { focus } : {}), mirrored });
|
|
602
|
+
panel.art = artOf(art, crop, mirrored);
|
|
603
|
+
if (crop.fallback)
|
|
604
|
+
warnings.push({ kind: 'comicPanelLetterbox', resourceId: art.resource.id, panel: index, sourceStart: panel.sourceStart, sourceEnd: (p?.lineEnd ?? source.sourceStart) + off, pageIndex: ctx.pageIndex });
|
|
605
|
+
}
|
|
606
|
+
const pop = pictureOf(resources, attrs.pop?.trim());
|
|
607
|
+
if (pop) {
|
|
608
|
+
const popCrop = crop ?? comicArtCrop({ width: pop.width || cell.bbox.width, height: pop.height || cell.bbox.height, cell: cell.bbox, fit, ...(focus ? { focus } : {}), mirrored });
|
|
609
|
+
panel.pop = artOf(pop, popCrop, mirrored);
|
|
610
|
+
}
|
|
611
|
+
const alt = attrs.alt ?? art?.resource.altText;
|
|
612
|
+
if (alt)
|
|
613
|
+
panel.altText = alt;
|
|
614
|
+
return panel;
|
|
615
|
+
};
|
|
616
|
+
// Cells take the flow panels in reading order; inset panels sit over the
|
|
617
|
+
// panel before them.
|
|
618
|
+
let flowIndex = 0;
|
|
619
|
+
const pending = [...source.panels];
|
|
620
|
+
for (const cell of geometry.cells) {
|
|
621
|
+
// Inset panels that come before the next flow panel follow the panel
|
|
622
|
+
// laid out last.
|
|
623
|
+
while (pending.length > 0 && pending[0].attrs.inset)
|
|
624
|
+
layInset(pending.shift());
|
|
625
|
+
const p = flowPanels[flowIndex];
|
|
626
|
+
if (p && pending[0] === p)
|
|
627
|
+
pending.shift();
|
|
628
|
+
flowIndex++;
|
|
629
|
+
panels.push(buildPanel(cell, p));
|
|
630
|
+
panelSources.push(p);
|
|
631
|
+
}
|
|
632
|
+
while (pending.length > 0 && pending[0].attrs.inset)
|
|
633
|
+
layInset(pending.shift());
|
|
634
|
+
function layInset(p) {
|
|
635
|
+
const prev = panels[panels.length - 1];
|
|
636
|
+
const box = prev ? insetBox(p.attrs.inset, prev.bbox) : undefined;
|
|
637
|
+
if (!box)
|
|
638
|
+
return;
|
|
639
|
+
const polygon = [
|
|
640
|
+
{ x: box.x, y: box.y },
|
|
641
|
+
{ x: box.x + box.width, y: box.y },
|
|
642
|
+
{ x: box.x + box.width, y: box.y + box.height },
|
|
643
|
+
{ x: box.x, y: box.y + box.height },
|
|
644
|
+
];
|
|
645
|
+
panels.push(buildPanel({ polygon, bbox: polygonBBox(polygon), rect: true }, p));
|
|
646
|
+
panelSources.push(p);
|
|
647
|
+
}
|
|
648
|
+
const splitRange = source.attrSources.split;
|
|
649
|
+
const valueStart = (splitRange?.start ?? source.attrsEnd) + off;
|
|
650
|
+
const valueEnd = (splitRange?.end ?? source.attrsEnd) + off;
|
|
651
|
+
const splitters = geometry.lines.map((l) => ({ ...l, sourceStart: valueStart, sourceEnd: valueEnd }));
|
|
652
|
+
const comic = {
|
|
653
|
+
sourceStart: source.sourceStart + off,
|
|
654
|
+
sourceEnd: source.sourceEnd + off,
|
|
655
|
+
frame,
|
|
656
|
+
direction,
|
|
657
|
+
panels,
|
|
658
|
+
splitters,
|
|
659
|
+
balloons: [],
|
|
660
|
+
};
|
|
661
|
+
const lettered = letterPanels({ source, panels, panelSources, comics, resolved, resources, direction, sourceOffset: off, pageIndex: ctx.pageIndex, frame, bleedBox: ctx.bleedBox, ...(ctx.trimBox ? { trim: ctx.trimBox } : {}), ...(ctx.avoid ? { avoid: ctx.avoid } : {}) });
|
|
662
|
+
comic.balloons = lettered.balloons;
|
|
663
|
+
warnings.push(...lettered.warnings);
|
|
664
|
+
if (warnings.length > 0)
|
|
665
|
+
layoutWarnings.set(comic, warnings);
|
|
666
|
+
return comic;
|
|
667
|
+
}
|
|
668
|
+
/** A cell inset by its panel's `pad`. */
|
|
669
|
+
function padCell(cell, pad) {
|
|
670
|
+
if (!pad || (pad.top === 0 && pad.right === 0 && pad.bottom === 0 && pad.left === 0))
|
|
671
|
+
return cell;
|
|
672
|
+
const b = cell.bbox;
|
|
673
|
+
const x0 = b.x + pad.left;
|
|
674
|
+
const x1 = Math.max(x0, b.x + b.width - pad.right);
|
|
675
|
+
const y0 = b.y + pad.top;
|
|
676
|
+
const y1 = Math.max(y0, b.y + b.height - pad.bottom);
|
|
677
|
+
let poly = cell.polygon;
|
|
678
|
+
poly = clipPolygon(poly, { nx: -1, ny: 0, c: -x0 });
|
|
679
|
+
poly = clipPolygon(poly, { nx: 1, ny: 0, c: x1 });
|
|
680
|
+
poly = clipPolygon(poly, { nx: 0, ny: -1, c: -y0 });
|
|
681
|
+
poly = clipPolygon(poly, { nx: 0, ny: 1, c: y1 });
|
|
682
|
+
return { polygon: poly, bbox: polygonBBox(poly), rect: cell.rect };
|
|
683
|
+
}
|
|
684
|
+
function artOf(art, crop, mirrored) {
|
|
685
|
+
return {
|
|
686
|
+
resourceId: art.resource.id,
|
|
687
|
+
kind: art.kind,
|
|
688
|
+
fileId: art.fileId,
|
|
689
|
+
...(art.format ? { format: art.format } : {}),
|
|
690
|
+
...(art.resource.kind === 'svg' && art.resource.svg?.pdfFileId ? { pdfFileId: art.resource.svg.pdfFileId } : {}),
|
|
691
|
+
box: crop.box,
|
|
692
|
+
source: crop.source,
|
|
693
|
+
mirrored,
|
|
694
|
+
letterbox: crop.letterbox,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
//# sourceMappingURL=layoutPage.js.map
|