@litfamily/litopencode 1.0.31 → 1.0.32
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 +24 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/activation-probe.js +1 -0
- package/dist/activation-prompt-utils.d.ts +1 -1
- package/dist/activation-reentry.d.ts +20 -0
- package/dist/activation-reentry.js +110 -1
- package/dist/activation-routing.d.ts +2 -1
- package/dist/activation-routing.js +100 -11
- package/dist/activation-workflow-prompts.d.ts +4 -0
- package/dist/activation-workflow-prompts.js +30 -0
- package/dist/activation.d.ts +1 -1
- package/dist/activation.js +44 -5
- package/dist/adhd-next.d.ts +51 -0
- package/dist/adhd-next.js +150 -0
- package/dist/cli/managed-skill-assets.d.ts +1 -1
- package/dist/cli/managed-skill-assets.js +7 -1
- package/dist/cli/native-skill-integrity.js +1 -1
- package/dist/cli.js +7 -1
- package/dist/commands.d.ts +28 -1
- package/dist/commands.js +48 -1
- package/dist/features.d.ts +80 -1
- package/dist/features.js +105 -1
- package/dist/ledger.js +12 -1
- package/dist/secret-shapes.js +5 -5
- package/dist/skills.d.ts +22 -1
- package/dist/skills.js +36 -0
- package/dist/start-work-typed.d.ts +34 -0
- package/dist/start-work-typed.js +156 -0
- package/dist/state.d.ts +2 -0
- package/dist/state.js +2 -0
- package/dist/tools.js +22 -2
- package/dist/tui/mission-band.d.ts +20 -1
- package/dist/tui/mission-band.js +73 -16
- package/dist/tui/mission-looks.d.ts +38 -0
- package/dist/tui/mission-looks.js +73 -0
- package/dist/tui/mission-model.d.ts +2 -0
- package/dist/tui/mission-model.js +3 -1
- package/dist/tui/mission-rainbow.d.ts +33 -0
- package/dist/tui/mission-rainbow.js +90 -0
- package/dist/tui/mission-source.d.ts +48 -0
- package/dist/tui/mission-source.js +184 -2
- package/dist/tui/mission-text.d.ts +2 -0
- package/dist/tui/mission-text.js +9 -1
- package/dist/tui/mission-threads.d.ts +50 -0
- package/dist/tui/mission-threads.js +119 -0
- package/dist/tui.js +84 -6
- package/dist/uiux-visual-catalog.js +8 -1
- package/dist/v2/session-hooks.js +38 -5
- package/dist/workflow-stack.d.ts +53 -0
- package/dist/workflow-stack.js +196 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/skills/lit-schematic.webp +0 -0
- package/docs/reference-Ko-KR.md +30 -2
- package/docs/reference.md +34 -2
- package/package.json +1 -1
- package/skills/frontend-ui-ux/SKILL.md +3 -2
- package/skills/frontend-ui-ux/kit/components.css +414 -0
- package/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/skills/frontend-ui-ux/references/brand-and-imagery.md +2 -2
- package/skills/frontend-ui-ux/references/complete-contract.md +2 -1
- package/skills/frontend-ui-ux/references/craft-floor.md +6 -2
- package/skills/frontend-ui-ux/references/default-editorial-pixel.json +3 -0
- package/skills/frontend-ui-ux/references/house-styles.md +236 -0
- package/skills/frontend-ui-ux/references/motion-guide.md +2 -2
- package/skills/frontend-ui-ux/references/production.md +3 -1
- package/skills/frontend-ui-ux/references/slop-register.md +4 -4
- package/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/skills/frontend-ui-ux/scripts/probe-lock.mjs +235 -0
- package/skills/frontend-ui-ux/scripts/probe-page.mjs +34 -12
- package/skills/frontend-ui-ux/scripts/probe-static.mjs +16 -3
- package/skills/frontend-ui-ux/scripts/probe-thresholds.json +8 -2
- package/skills/frontend-ui-ux/scripts/probe.mjs +35 -7
- package/skills/lit-adhd/SKILL.md +208 -0
- package/skills/lit-adhd/references/brief-format.md +43 -0
- package/skills/lit-adhd/references/card-examples.md +82 -0
- package/skills/lit-adhd/references/status-taxonomy.md +29 -0
- package/skills/lit-adhd/scripts/rewind.mjs +835 -0
- package/skills/lit-diagram-drawer/SKILL.md +4 -3
- package/skills/lit-diagram-drawer/references/style-guide.md +20 -11
- package/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
- package/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
- package/skills/lit-diagram-drawer/references/type-tree.md +3 -3
- package/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
- package/skills/lit-diagram-drawer/references/verifier-guide.md +1 -0
- package/skills/lit-diagram-drawer/scripts/craft-thresholds.json +1 -2
- package/skills/lit-diagram-drawer/scripts/visual-quality.mjs +1 -2
- package/skills/lit-docx/SKILL.md +2 -1
- package/skills/lit-handoff/SKILL.md +2 -0
- package/skills/lit-hijack/SKILL.md +422 -0
- package/skills/lit-hijack/references/capture-and-provenance.md +87 -0
- package/skills/lit-hijack/references/clean-room-build.md +111 -0
- package/skills/lit-hijack/references/distil-and-licence.md +100 -0
- package/skills/lit-hijack/references/gap-and-port-design.md +126 -0
- package/skills/lit-hijack/references/parity-and-report.md +110 -0
- package/skills/lit-hijack/scripts/hijack.mjs +993 -0
- package/skills/lit-plan/SKILL.md +2 -0
- package/skills/lit-pptx/SKILL.md +27 -11
- package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/SOURCE.md +17 -0
- package/skills/lit-pptx/references/density-and-fill.md +9 -3
- package/skills/lit-pptx/references/direction-step.md +21 -6
- package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +3 -6
- package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +1 -5
- package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +0 -4
- package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +2 -2
- package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +4 -7
- package/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
- package/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/skills/lit-pptx/references/layout-families.md +65 -47
- package/skills/lit-pptx/references/title-treatments.md +83 -134
- package/skills/lit-pptx/references/tonalities/atlas.md +3 -3
- package/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
- package/skills/lit-pptx/references/tonalities/chalk.md +7 -6
- package/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
- package/skills/lit-pptx/references/tonalities/gazette.md +1 -1
- package/skills/lit-pptx/references/tonalities/ledger.md +1 -1
- package/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
- package/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
- package/skills/lit-pptx/references/tonalities/night.md +1 -1
- package/skills/lit-pptx/references/tonalities/paper.md +2 -2
- package/skills/lit-pptx/references/tonalities/riso.md +137 -0
- package/skills/lit-pptx/references/tonalities/signal.md +3 -3
- package/skills/lit-pptx/references/tonalities/studio.md +7 -6
- package/skills/lit-pptx/run.mjs +2 -1
- package/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/skills/lit-pptx/scripts/craft-thresholds.json +4 -8
- package/skills/lit-pptx/scripts/craft_extras.py +56 -3
- package/skills/lit-pptx/scripts/deck_output.py +75 -90
- package/skills/lit-pptx/scripts/embed_fonts.py +13 -1
- package/skills/lit-pptx/scripts/layout_inventory.py +12 -5
- package/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
- package/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/skills/lit-pptx/scripts/lib/render-pack.js +373 -566
- package/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
- package/skills/lit-pptx/scripts/qa_deck.py +10 -11
- package/skills/lit-pptx/scripts/render_pages.py +118 -0
- package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
- package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
- package/skills/lit-recap/SKILL.md +4 -0
- package/skills/lit-schematic/SKILL.md +118 -0
- package/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
- package/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
- package/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
- package/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
- package/skills/lit-schematic/references/cutaway-outputs.md +70 -0
- package/skills/lit-schematic/references/isometric-craft.md +101 -0
- package/skills/lit-schematic/references/journal-figures.md +72 -0
- package/skills/lit-schematic/references/kit-api.md +134 -0
- package/skills/lit-schematic/references/pfd-and-layout.md +53 -0
- package/skills/lit-schematic/references/pid-drafting.md +103 -0
- package/skills/lit-schematic/references/pid-symbols.md +163 -0
- package/skills/lit-schematic/references/verification.md +100 -0
- package/skills/lit-schematic/scripts/doctor.mjs +57 -0
- package/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
- package/skills/lit-schematic/scripts/lib/fonts.mjs +25 -0
- package/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
- package/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
- package/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
- package/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
- package/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
- package/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
- package/skills/lit-schematic/scripts/lib/png.mjs +132 -0
- package/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
- package/skills/lit-schematic/scripts/lib/route.mjs +218 -0
- package/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
- package/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
- package/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
- package/skills/lit-schematic/scripts/motion.mjs +129 -0
- package/skills/lit-schematic/scripts/qa.mjs +92 -0
- package/skills/lit-schematic/scripts/render.mjs +233 -0
- package/skills/lit-schematic/scripts/scaffold.mjs +60 -0
- package/skills/lit-scientific-visualization/SKILL.md +27 -4
- package/skills/lit-typographic-motion/SKILL.md +19 -17
- package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +9 -0
- package/skills/lit-typographic-motion/engine/accents.mjs +6 -0
- package/skills/lit-typographic-motion/engine/assets.mjs +282 -0
- package/skills/lit-typographic-motion/engine/blender/runner.py +228 -0
- package/skills/lit-typographic-motion/engine/blender.mjs +126 -0
- package/skills/lit-typographic-motion/engine/board.mjs +138 -0
- package/skills/lit-typographic-motion/engine/cover/assets.mjs +11 -8
- package/skills/lit-typographic-motion/engine/cover/capture.mjs +6 -6
- package/skills/lit-typographic-motion/engine/cover/render.mjs +20 -7
- package/skills/lit-typographic-motion/engine/cover/treatment.mjs +30 -3
- package/skills/lit-typographic-motion/engine/fill.mjs +54 -0
- package/skills/lit-typographic-motion/engine/gallery.mjs +118 -0
- package/skills/lit-typographic-motion/engine/house.mjs +42 -0
- package/skills/lit-typographic-motion/engine/look.mjs +65 -8
- package/skills/lit-typographic-motion/engine/loudness.mjs +101 -0
- package/skills/lit-typographic-motion/engine/scaffold.mjs +219 -0
- package/skills/lit-typographic-motion/engine/score.mjs +594 -0
- package/skills/lit-typographic-motion/engine/sound.mjs +9 -57
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +23 -9
- package/skills/lit-typographic-motion/engine/stage/clock.js +8 -2
- package/skills/lit-typographic-motion/engine/stage/cover-init.js +12 -1
- package/skills/lit-typographic-motion/engine/stage/frame-worker.mjs +15 -7
- package/skills/lit-typographic-motion/engine/stage/gate.mjs +48 -4
- package/skills/lit-typographic-motion/engine/stage/qa-page.js +16 -5
- package/skills/lit-typographic-motion/engine/stage/qa.mjs +148 -7
- package/skills/lit-typographic-motion/engine/stage/render.mjs +69 -7
- package/skills/lit-typographic-motion/engine/stage/scan.mjs +20 -1
- package/skills/lit-typographic-motion/engine/stage/stage-kit.js +85 -4
- package/skills/lit-typographic-motion/engine/stills.mjs +1 -1
- package/skills/lit-typographic-motion/engine/styles.mjs +221 -0
- package/skills/lit-typographic-motion/engine/text.mjs +1 -1
- package/skills/lit-typographic-motion/engine/treatment.mjs +190 -6
- package/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/skills/lit-typographic-motion/gate.mjs +18 -2
- package/skills/lit-typographic-motion/probe.mjs +13 -1
- package/skills/lit-typographic-motion/recipes/hero-3d/expected.json +64 -0
- package/skills/lit-typographic-motion/recipes/hero-3d/index.html +132 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +2 -0
- package/skills/lit-typographic-motion/recipes/painted-light/expected.json +2 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +2 -0
- package/skills/lit-typographic-motion/references/cover.md +56 -13
- package/skills/lit-typographic-motion/references/craft-loop.md +20 -7
- package/skills/lit-typographic-motion/references/depth.md +112 -0
- package/skills/lit-typographic-motion/references/runtime.md +21 -6
- package/skills/lit-typographic-motion/references/sound.md +76 -0
- package/skills/lit-typographic-motion/references/stage.md +50 -7
- package/skills/lit-typographic-motion/references/studio.md +162 -0
- package/skills/lit-typographic-motion/references/style-bibles.md +18 -0
- package/skills/lit-typographic-motion/references/treatment.md +108 -9
- package/skills/lit-typographic-motion/render.mjs +60 -3
- package/skills/lit-typographic-motion/styles/paper.json +185 -0
- package/skills/lit-typographic-motion/styles/swiss-signal.json +188 -0
- package/skills/lit-typographic-motion/styles/terminalcore.json +182 -0
- package/skills/lit-typographic-motion/styles/tidal.json +187 -0
- package/skills/litresearch/SKILL.md +2 -0
- package/skills/managed-skill-manifest.json +517 -88
- package/skills/readme-studio/SKILL.md +1 -1
- package/skills/review-work/SKILL.md +2 -0
- package/skills/start-work/SKILL.md +8 -0
- package/skills/workflow-loop/SKILL.md +4 -0
- package/tools/check-pack-payload.mjs +4 -2
- package/tools/payload-substance-parity.json +4 -3
- package/tools/qa-cover-recipes.mjs +4 -3
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const path = require("path");
|
|
16
16
|
const G = require("./grid-resolver");
|
|
17
|
+
const SG = require("./slide-grid");
|
|
17
18
|
const { looksLikeKpi } = require("./layout-resolver");
|
|
18
19
|
|
|
19
20
|
// Helpers shared with the legacy renderer (hex, cleanMd, tables, charts, images, placements).
|
|
@@ -35,9 +36,9 @@ function packStyle(pack) {
|
|
|
35
36
|
pack, P, sizes: pack.tok.sizes, tok: pack.tok, grid: pack.grid, faces: pack.faces,
|
|
36
37
|
deco: new Set(pack.decoration || []),
|
|
37
38
|
// Heavier faces set wider: the advance estimate grows with the weight so frames hold their lines.
|
|
38
|
-
wide(role = "body") { return G.faceWidth(pack.faces[role]); },
|
|
39
|
-
ems(text, role) { return G.textEms(text) * this.wide(role); },
|
|
40
|
-
lines(text, w, size, role) { return G.lineCount(text, w / this.wide(role), size); },
|
|
39
|
+
wide(role = "body", text) { return G.faceWidth(pack.faces[role], text); },
|
|
40
|
+
ems(text, role) { return G.textEms(text) * this.wide(role, text); },
|
|
41
|
+
lines(text, w, size, role) { return G.lineCount(text, w / this.wide(role, text), size); },
|
|
41
42
|
colour: (role) => shared().hex(P[role] || P.ink),
|
|
42
43
|
// Tracking is a percentage of the size; Hangul body text is never tracked.
|
|
43
44
|
tracking(face, size) {
|
|
@@ -75,7 +76,7 @@ function balanced(S, text, w, size, role, room = Infinity) {
|
|
|
75
76
|
if (/\*\*/u.test(s)) return s;
|
|
76
77
|
const lines = S.lines(s, w, size, role);
|
|
77
78
|
if (lines < 2) return s;
|
|
78
|
-
const per = w / S.wide(role) / size;
|
|
79
|
+
const per = w / S.wide(role, s) / size;
|
|
79
80
|
const set = G.balanceLines(s, lines, per);
|
|
80
81
|
// A set ending on one word gets a line more when the frame holds it and that ends on two.
|
|
81
82
|
const widow = (t) => t.includes("\n") && !/ /u.test(t.slice(t.lastIndexOf("\n") + 1));
|
|
@@ -111,7 +112,7 @@ function packText(slide, S, text, box, o = {}) {
|
|
|
111
112
|
text = String(text == null ? "" : text).replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/gu, (_, label, url) => { links.push([label, url]); return label; });
|
|
112
113
|
if (o.balance) text = balanced(S, text, box.w, size, faceRole, Math.floor(box.h / (size * (o.lineSpacing || 1.15)) + 0.01));
|
|
113
114
|
else if (!o.title && faceRole !== "numeral") {
|
|
114
|
-
text = G.keepLines(text, box.w / S.wide(faceRole) / size, Math.floor(box.h / (size * (o.lineSpacing || G.leading(plainText(text)))) + 0.01));
|
|
115
|
+
text = G.keepLines(text, box.w / S.wide(faceRole, plainText(text)) / size, Math.floor(box.h / (size * (o.lineSpacing || G.leading(plainText(text)))) + 0.01));
|
|
115
116
|
}
|
|
116
117
|
const tracking = o.tracking != null ? o.tracking : S.tracking(faceRole, size);
|
|
117
118
|
const base = { fontFace: face.font, fontSize: size, color: o.colour || S.colour("ink"), bold: Boolean(face.bold || o.bold),
|
|
@@ -136,11 +137,15 @@ function packText(slide, S, text, box, o = {}) {
|
|
|
136
137
|
|
|
137
138
|
function packRect(slide, S, box, o = {}) {
|
|
138
139
|
const radius = o.radius || 0;
|
|
140
|
+
// In a pack with hard edges a block of the field (a band, a plate, a rail) carries the border too.
|
|
141
|
+
if (S.pack.shadow && o.fill === "field" && !o.line && box.w >= 24 && box.h >= 24) o = { ...o, line: S.pack["edge-colour"] || "line", lineWidth: S.pack["edge-weight"] };
|
|
139
142
|
slide.addShape(radius ? "roundRect" : "rect", {
|
|
140
143
|
x: IN(box.x), y: IN(box.y), w: IN(box.w), h: IN(box.h),
|
|
141
144
|
...(radius ? { rectRadius: IN(radius) } : {}),
|
|
142
145
|
fill: o.fill ? { color: S.colour(o.fill), transparency: o.alpha || 0 } : { type: "none" },
|
|
143
146
|
line: o.line ? { color: S.colour(o.line), width: o.lineWidth || 0.75 } : { type: "none" },
|
|
147
|
+
// A hard offset shadow: no blur, fully opaque, down and to the right.
|
|
148
|
+
...(o.shadow && S.pack.shadow ? { shadow: { type: "outer", blur: 0, offset: S.pack.shadow.offset, angle: 45, color: S.colour(S.pack.shadow.colour).replace("#", ""), opacity: 1 } } : {}),
|
|
144
149
|
...(o.name ? { objectName: o.name } : {}),
|
|
145
150
|
});
|
|
146
151
|
}
|
|
@@ -148,13 +153,23 @@ function packRect(slide, S, box, o = {}) {
|
|
|
148
153
|
/** A surface (card, box, panel) in the pack's edge treatment and radius. */
|
|
149
154
|
function surfaceRect(slide, S, box, fill = "surface") {
|
|
150
155
|
const edge = S.pack.edge;
|
|
156
|
+
// A shadowed surface is filled, so its shadow never shows through it.
|
|
157
|
+
const shadow = S.pack.shadow || null;
|
|
151
158
|
packRect(slide, S, box, {
|
|
152
|
-
fill: edge === "border" ? null : fill,
|
|
153
|
-
line: edge === "fill" ? null : "line",
|
|
159
|
+
fill: edge === "border" && !shadow ? null : edge === "border" ? "ground" : fill,
|
|
160
|
+
line: edge === "fill" ? null : S.pack["edge-colour"] || "line",
|
|
161
|
+
lineWidth: S.pack["edge-weight"],
|
|
154
162
|
radius: S.pack.radius,
|
|
163
|
+
shadow,
|
|
155
164
|
});
|
|
156
165
|
}
|
|
157
166
|
|
|
167
|
+
/** A pack with a hard shadow draws every box with its border and shadow (filled, `fill` when given). */
|
|
168
|
+
function hardEdge(S, fill) {
|
|
169
|
+
if (!S.pack.shadow) return {};
|
|
170
|
+
return { line: S.pack["edge-colour"] || "line", lineWidth: S.pack["edge-weight"], shadow: S.pack.shadow, ...(fill ? { fill } : {}) };
|
|
171
|
+
}
|
|
172
|
+
|
|
158
173
|
// ── Content atoms ───────────────────────────────────────────────────────────
|
|
159
174
|
|
|
160
175
|
/** Body paragraphs of a list, in reading order. */
|
|
@@ -264,8 +279,8 @@ function measureTable(item, w, S, state) {
|
|
|
264
279
|
const minRow = S.tok.rowMin * (state.anchor ? 1.25 : 1);
|
|
265
280
|
// Rows are measured at the face's natural line (about 1.45 of the size with its gap), which is
|
|
266
281
|
// what office renderers lay a cell out at; a smaller estimate lets rows grow past the frame.
|
|
267
|
-
const rowH = (cells, size) => Math.max(minRow, ...cells.map((t, i) => G.textHeight(plainText(t), (widths[i] || w) - cell * 2, size, 1.45) + cell * 2));
|
|
268
|
-
const heights = [rowH(headers, header), ...rows.map((r) => rowH(r, data))];
|
|
282
|
+
const rowH = (cells, size, role) => Math.max(minRow, ...cells.map((t, i) => G.textHeight(plainText(t), ((widths[i] || w) - cell * 2) / S.wide(role, plainText(t)), size, 1.45) + cell * 2));
|
|
283
|
+
const heights = [rowH(headers, header, "label"), ...rows.map((r) => rowH(r, data, "body"))];
|
|
269
284
|
const tableH = heights.reduce((a, b) => a + b, 0);
|
|
270
285
|
return { kind: "table", heights, widths, header, data, tableH, h: tableH + captionHeight(item.caption, w, S) };
|
|
271
286
|
}
|
|
@@ -420,9 +435,9 @@ function textAt(paras, R, S, state, o = {}) {
|
|
|
420
435
|
function visualWithText(ctx, visuals, texts, share, o = {}) {
|
|
421
436
|
const { S, state, Z } = ctx;
|
|
422
437
|
const out = [];
|
|
423
|
-
// A visual alone (
|
|
438
|
+
// A visual alone (no takeaways): a table opens its rows; on the compact step a
|
|
424
439
|
// chart carries its values under it.
|
|
425
|
-
if (!texts.length) return ((S.tok.tight &&
|
|
440
|
+
if (!texts.length) return ((S.tok.tight && valuesUnder(visuals, Z, S, state)) || openRows(stack(visuals, Z, S, state).placed, Z, S));
|
|
426
441
|
if (!visuals.length) return stack(texts, Z, S, state).placed;
|
|
427
442
|
// Takeaways too short for their column give the visual columns: a narrower column wraps them onto more
|
|
428
443
|
// lines and a wider chart or picture grows, down to a three-column takeaway.
|
|
@@ -477,16 +492,17 @@ function visualWithText(ctx, visuals, texts, share, o = {}) {
|
|
|
477
492
|
// The strip closes the column once the visual's own height is settled.
|
|
478
493
|
if (col.strip && col.end <= col.R.bottom + 0.5 && v.bottom > col.end) t.placed.push(...col.strip.placed.map((q) => ({ ...q, strip: true })));
|
|
479
494
|
// A table keeps its own height, so beside it a short takeaway leaves the body half empty. When
|
|
480
|
-
// that band is over the density cap and the takeaway fits under the table, it goes there
|
|
481
|
-
// points side by side in two columns
|
|
495
|
+
// that band is over the density cap and the takeaway fits under the table, it goes there as notes on
|
|
496
|
+
// the table above: points that pair off side by side in two columns, an odd count down one column at the
|
|
497
|
+
// reading measure, so neither column stops a point short of the other.
|
|
482
498
|
if (v.placed.every((p) => !p.flex) && (Z.bottom - Math.max(v.bottom, t.bottom)) / (Z.bottom - Z.y) > S.tok.cap) {
|
|
483
499
|
const top = stack(visuals, Z, S, state);
|
|
484
500
|
const below = { ...Z, y: top.bottom + state.blockGap };
|
|
485
501
|
const paras = texts.flatMap(itemParas);
|
|
486
502
|
const half = Math.ceil(paras.length / 2);
|
|
487
|
-
const notes = paras.length >= 2
|
|
503
|
+
const notes = paras.length >= 2 && paras.length % 2 === 0
|
|
488
504
|
? splitRegion(S, below, 0.5).map((R, i) => textAt(i ? paras.slice(half) : paras.slice(0, half), R, S, state))
|
|
489
|
-
:
|
|
505
|
+
: [textAt(paras, region(S, Z.a, Math.min(Z.b, Z.a + 8), below.y, below.bottom), S, state)];
|
|
490
506
|
const end = Math.max(...notes.map((q) => q.y + q.h));
|
|
491
507
|
if (end <= Z.bottom) {
|
|
492
508
|
// Still short of the density band: the table's rows open up first (half the room, at most
|
|
@@ -564,7 +580,7 @@ function takeawayColumn(ctx, texts, visuals, full, visualBottom) {
|
|
|
564
580
|
}
|
|
565
581
|
|
|
566
582
|
/**
|
|
567
|
-
* A table standing alone in its region (
|
|
583
|
+
* A table standing alone in its region (no takeaways beside it) opens its rows, at most one
|
|
568
584
|
* minimum row height each, while the region leaves more than the density band empty under it.
|
|
569
585
|
*/
|
|
570
586
|
function openRows(placed, Z, S) {
|
|
@@ -617,13 +633,13 @@ const PLANS = {
|
|
|
617
633
|
const items = ctx.items;
|
|
618
634
|
// Running text keeps its measure: cols 1-9 on the presented ramp, 1-7 on the reading ramp.
|
|
619
635
|
const limit = S.tok.ramp === "presented" ? 9 : 7;
|
|
620
|
-
const R =
|
|
636
|
+
const R = region(S, Z.a, Math.min(Z.b, Z.a + limit - 1), Z.y, Z.bottom);
|
|
621
637
|
const { visuals, texts } = sortItems(items);
|
|
622
638
|
if (visuals.length && texts.length) return visualWithText(ctx, visuals, texts, 7 / 12);
|
|
623
639
|
const placed = stack(items, R, S, state).placed;
|
|
624
640
|
// Two to five points that each open with a bold head, and that leave the body more than the
|
|
625
641
|
// density band empty, read as rows: the head in its own column, the point beside it.
|
|
626
|
-
const ents = !visuals.length
|
|
642
|
+
const ents = !visuals.length ? entries(texts.flatMap(itemParas)) : [];
|
|
627
643
|
// Groups of sub-points under bold heads, held to one reading measure narrower than the body,
|
|
628
644
|
// run in two columns instead: the measure stays and the body is used across.
|
|
629
645
|
const groups = ents.length >= 2 && ents.every((e) => e.head && !e.rest && e.children.length);
|
|
@@ -651,7 +667,8 @@ const PLANS = {
|
|
|
651
667
|
"text-two-column"(ctx) {
|
|
652
668
|
const { S, state, Z } = ctx;
|
|
653
669
|
const { columns, texts, visuals } = sortItems(ctx.items);
|
|
654
|
-
|
|
670
|
+
// Points after the columns are the slide's takeaways: they run under both columns.
|
|
671
|
+
if (columns) return stack([columns, ...visuals, ...texts], Z, S, state).placed;
|
|
655
672
|
const paras = texts.flatMap(itemParas);
|
|
656
673
|
const [l, r] = splitRegion(S, Z, 0.5);
|
|
657
674
|
// Break at a section boundary nearest to half the height.
|
|
@@ -727,8 +744,6 @@ const PLANS = {
|
|
|
727
744
|
|
|
728
745
|
"sidebar-note"(ctx) {
|
|
729
746
|
const { S, state, Z } = ctx;
|
|
730
|
-
// The note stands in a side title's rail: the main points take the body.
|
|
731
|
-
if (ctx.railNote) return PLANS["text-column"](ctx);
|
|
732
747
|
const { texts, visuals } = sortItems(ctx.items);
|
|
733
748
|
// The note is the fenced main-box, else a last text block after the main one; a lone text block is the main.
|
|
734
749
|
const note = texts.find((t) => t.type === "main-box") || (texts.length > 1 ? texts[texts.length - 1] : null);
|
|
@@ -752,7 +767,7 @@ const PLANS = {
|
|
|
752
767
|
if (!overflows(narrow) && shortColumn(narrow, Z.bottom, S).share <= RAIL_EMPTY && (Z.bottom - contentBottom(narrow, Z.y)) / (Z.bottom - Z.y) <= S.tok.cap) return narrow;
|
|
753
768
|
}
|
|
754
769
|
if (main.length && !visuals.length && shortColumn(out, Z.bottom, S).share > RAIL_EMPTY) {
|
|
755
|
-
// The note keeps a reading measure inside the box
|
|
770
|
+
// The note keeps a reading measure inside the box: about 32 Hangul or 70 Latin characters.
|
|
756
771
|
const korean = G.isKorean(itemParas(note).map((p) => p.text).join(""));
|
|
757
772
|
const inner = Math.min(Z.w - pad * 2, (korean ? 30 : 35) * S.sizes.lead);
|
|
758
773
|
const lead = measureText(itemParas(note).map((p) => ({ ...p, kind: "text" })), inner, S, state, { size: S.sizes.lead });
|
|
@@ -851,7 +866,7 @@ const PLANS = {
|
|
|
851
866
|
const kpi = visuals.find((v) => v.type === "kpi-table" && looksLikeKpi(v.content || {}, { basis: true }));
|
|
852
867
|
if (!kpi) return PLANS["table-insight"](ctx);
|
|
853
868
|
const others = visuals.filter((v) => v !== kpi);
|
|
854
|
-
if (
|
|
869
|
+
if (Z.b - Z.a + 1 < 10 || (state.anchor && !S.pack.shadow)) {
|
|
855
870
|
// Under a side title, or when the values take the room, they run down the body as rows:
|
|
856
871
|
// the value, then what it measures.
|
|
857
872
|
const cards = shared().kpiCards(kpi.content).slice(0, 6);
|
|
@@ -863,12 +878,13 @@ const PLANS = {
|
|
|
863
878
|
if (restTexts.length) out.push(...stack(restTexts, { ...Z, y: rowsZone.bottom + state.blockGap }, S, state).placed);
|
|
864
879
|
return out;
|
|
865
880
|
}
|
|
866
|
-
const top =
|
|
881
|
+
const top = Z.y;
|
|
867
882
|
const cardsPlan = stack([kpi, ...others, ...texts], { ...Z, y: top }, S, state);
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
//
|
|
871
|
-
|
|
883
|
+
// A row of cards with its takeaways under it that stops high on the page: the figures then run as
|
|
884
|
+
// rows down the first seven columns (value, label, basis), the caption at their foot, and the
|
|
885
|
+
// takeaways stand beside them. A pack with hard shadows keeps its cards: the bordered, shadowed card
|
|
886
|
+
// is that pack's figure.
|
|
887
|
+
if (texts.length && !others.length && !S.pack.shadow && (Z.bottom - cardsPlan.bottom) / (Z.bottom - Z.y) > S.tok.cap) {
|
|
872
888
|
const cards = shared().kpiCards(kpi.content).slice(0, 6);
|
|
873
889
|
const [fR, tR] = splitRegion(S, Z, 7 / 12);
|
|
874
890
|
const capH = kpi.caption ? captionHeight(kpi.caption, fR.w, S) + 6 : 0;
|
|
@@ -902,7 +918,7 @@ const PLANS = {
|
|
|
902
918
|
Object.assign(b, { pitch: a.pitch, value: a.value, labelSize: a.labelSize, noteSize: a.noteSize, h: b.rows.length ? b.rows[b.rows.length - 1].y - b.y + (a.h - (a.rows[a.rows.length - 1].y - a.y)) : 0 });
|
|
903
919
|
}
|
|
904
920
|
// A band left open between the rows and the takeaways under them is worse than a column whose points share
|
|
905
|
-
// the room beside the rows, when that sharing fills the column
|
|
921
|
+
// the room beside the rows, when that sharing fills the column.
|
|
906
922
|
const band = (Z.bottom - notesH - state.blockGap - capH - Math.max(...across.map((q) => q.y + q.h))) / (Z.bottom - Z.y);
|
|
907
923
|
if (band > S.tok.cap) {
|
|
908
924
|
const filled = structuredClone(out);
|
|
@@ -937,17 +953,12 @@ const PLANS = {
|
|
|
937
953
|
const capH = table.caption ? captionHeight(table.caption, nR.w, S) + 6 : 0;
|
|
938
954
|
const blockH = size * 1.15 + 12 + labelH + capH;
|
|
939
955
|
const out = [];
|
|
940
|
-
// The number opens the body, its evidence beside it or under it.
|
|
941
|
-
|
|
942
|
-
const y = ctx.cls === "bottom" ? Z.bottom - blockH : Z.y;
|
|
956
|
+
// The number opens the body, its evidence beside it or under it.
|
|
957
|
+
const y = Z.y;
|
|
943
958
|
if (texts.length) {
|
|
944
959
|
const R = tR ? region(S, tR.a + 1, tR.b, Z.y, Z.bottom)
|
|
945
|
-
:
|
|
946
|
-
|
|
947
|
-
// Beside a number on the floor the evidence ends level with the number block, so it reads
|
|
948
|
-
// with the number instead of hanging above it at the top of the page.
|
|
949
|
-
const lift = tR && ctx.cls === "bottom" ? Math.max(0, Math.min(y + blockH, Z.bottom) - evidence.bottom) : 0;
|
|
950
|
-
out.push(...evidence.placed.map((q) => ({ ...q, y: q.y + lift })));
|
|
960
|
+
: { ...Z, y: Z.y + blockH + state.blockGap };
|
|
961
|
+
out.push(...stack(texts, R, S, { ...state, stepped: true }).placed);
|
|
951
962
|
}
|
|
952
963
|
out.push({ kind: "number", value, label, size, labelSize, x: nR.x, y, w: nR.w, h: size * 1.15 + 12 + labelH, limit: Z.bottom });
|
|
953
964
|
if (table.caption) out.push({ kind: "caption", item: { content: table.caption }, x: nR.x, y: y + size * 1.15 + 12 + labelH + 6, w: nR.w, h: capH - 6, limit: Z.bottom });
|
|
@@ -1008,9 +1019,12 @@ const PLANS = {
|
|
|
1008
1019
|
},
|
|
1009
1020
|
|
|
1010
1021
|
comparison(ctx) {
|
|
1011
|
-
const { S, state
|
|
1022
|
+
const { S, state } = ctx;
|
|
1012
1023
|
const { columns, texts, visuals } = sortItems(ctx.items);
|
|
1013
1024
|
if (visuals.length && !columns) return visualWithText(ctx, visuals, texts, 7 / 12);
|
|
1025
|
+
// Beside written columns the loose points are the slide's takeaways: they run under the rows.
|
|
1026
|
+
const notes = columns && texts.length ? measureText(texts.flatMap(itemParas), ctx.Z.w, S, state) : null;
|
|
1027
|
+
const Z = notes ? { ...ctx.Z, bottom: ctx.Z.bottom - notes.h - state.blockGap } : ctx.Z;
|
|
1014
1028
|
let halves;
|
|
1015
1029
|
if (columns) halves = columns.columns.map((col) => (col || []).filter(isText).flatMap(itemParas));
|
|
1016
1030
|
else {
|
|
@@ -1031,8 +1045,7 @@ const PLANS = {
|
|
|
1031
1045
|
const LABEL = /^([^::]{1,24})[::]\s*(.+)$/u;
|
|
1032
1046
|
const isLabel = (t) => { const m = LABEL.exec(plainText(t)); return Boolean(m) && m[1].trim().split(/\s+/u).length <= 3; };
|
|
1033
1047
|
let criteria = null;
|
|
1034
|
-
|
|
1035
|
-
if ((S.tok.tight || ctx.rail) && sides.length === 2 && sides.every((sd) => sd.items.length && sd.items.every(isLabel))) {
|
|
1048
|
+
if (S.tok.tight && sides.length === 2 && sides.every((sd) => sd.items.length && sd.items.every(isLabel))) {
|
|
1036
1049
|
const n = Math.max(...sides.map((sd) => sd.items.length));
|
|
1037
1050
|
const sets = Array.from({ length: n }, (_, r) => [...new Set(sides.map((sd) => sd.items[r] && LABEL.exec(plainText(sd.items[r]))[1].trim()).filter(Boolean))]);
|
|
1038
1051
|
const labels = sets.map((set) => set.join(" / "));
|
|
@@ -1042,12 +1055,9 @@ const PLANS = {
|
|
|
1042
1055
|
for (const sd of sides) sd.items = sd.items.map((t) => LABEL.exec(plainText(t))[2]);
|
|
1043
1056
|
}
|
|
1044
1057
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
const
|
|
1048
|
-
const floor = inRail ? Math.min(Z.bottom, ctx.rail.bottom) : Z.bottom;
|
|
1049
|
-
const critR = inRail ? { ...ctx.rail, bottom: floor } : criteria ? region(S, Z.a, Z.a + 1, Z.y, Z.bottom) : null;
|
|
1050
|
-
const parts = criteria && !inRail ? splitRegion(S, region(S, Z.a + 2, Z.b, Z.y, Z.bottom), 0.5) : splitRegion(S, Z, 0.5);
|
|
1058
|
+
const floor = Z.bottom;
|
|
1059
|
+
const critR = criteria ? region(S, Z.a, Z.a + 1, Z.y, Z.bottom) : null;
|
|
1060
|
+
const parts = criteria ? splitRegion(S, region(S, Z.a + 2, Z.b, Z.y, Z.bottom), 0.5) : splitRegion(S, Z, 0.5);
|
|
1051
1061
|
const headSize = bodySize(S, state, "lead");
|
|
1052
1062
|
// Up to three rows of short points are set a step or two larger when that still keeps a dozen
|
|
1053
1063
|
// glyphs a line and fits the body; a two-row comparison otherwise holds a third of the page.
|
|
@@ -1072,10 +1082,9 @@ const PLANS = {
|
|
|
1072
1082
|
});
|
|
1073
1083
|
top = Z.y + headH + 30;
|
|
1074
1084
|
}
|
|
1075
|
-
if (inRail) top = Math.max(top, ctx.rail.y);
|
|
1076
1085
|
// Parallel points share a row, so they are read across; the rows run down to the floor.
|
|
1077
1086
|
const n = Math.max(...sides.map((sd) => sd.items.length));
|
|
1078
|
-
const labelH = (r) => (criteria ? G.textHeight(criteria[r], critR.w / S.wide("title"), itemSize) : 0);
|
|
1087
|
+
const labelH = (r) => (criteria ? G.textHeight(criteria[r], critR.w / S.wide("title", criteria[r]), itemSize) : 0);
|
|
1079
1088
|
// Measured on the lines the text frame will set (word by word), so a narrow side keeps its lines.
|
|
1080
1089
|
const sideH = (t, i) => {
|
|
1081
1090
|
const w = parts[i].w / S.wide("body");
|
|
@@ -1084,18 +1093,18 @@ const PLANS = {
|
|
|
1084
1093
|
const rowH = Array.from({ length: n }, (_, r) => Math.max(labelH(r), ...sides.map((sd, i) => (sd.items[r] ? sideH(sd.items[r], i) : 0))));
|
|
1085
1094
|
const pitch = rowPitch(n, Math.max(...rowH), { y: top, bottom: floor }, state.groupGap, Math.max(112, Math.max(...rowH) + 72));
|
|
1086
1095
|
// On the compact step each row starts one even gap under the one before (rows of two and three
|
|
1087
|
-
// lines alike). The gap opens up to a
|
|
1088
|
-
// never ends the rows higher than a pitch of the tallest row plus 48 pt would.
|
|
1096
|
+
// lines alike). The gap opens up to a fifth of the body so four one-line rows across the full body
|
|
1097
|
+
// width reach the floor, and never ends the rows higher than a pitch of the tallest row plus 48 pt would.
|
|
1089
1098
|
const sum = rowH.reduce((a, b) => a + b, 0);
|
|
1090
1099
|
const reach = n > 1 ? ((n - 1) * (Math.max(...rowH) + 48) + rowH[n - 1] - sum) / (n - 1) : 0;
|
|
1091
|
-
const gap = n > 1 ? Math.max(state.groupGap, Math.min(Math.max(48, (floor - Z.y) /
|
|
1100
|
+
const gap = n > 1 ? Math.max(state.groupGap, Math.min(Math.max(48, (floor - Z.y) / 5, reach), (floor - top - sum) / (n - 1))) : 0;
|
|
1092
1101
|
const ys = rowH.map((_, r) => (S.tok.tight ? top + rowH.slice(0, r).reduce((a, b) => a + b, 0) + r * gap : top + r * pitch));
|
|
1093
1102
|
for (let r = 0; r < n; r++) {
|
|
1094
1103
|
const y = ys[r];
|
|
1095
1104
|
if (r > 0 && (S.deco.has("hairline-rule") || S.deco.has("column-hairlines"))) {
|
|
1096
1105
|
out.push({ decor: true, kind: "rect", x: Z.x, y: y - Math.min(18, (y - ys[r - 1] - rowH[r - 1]) / 2), w: Z.w, h: 0.5, fill: "line" });
|
|
1097
1106
|
}
|
|
1098
|
-
if (criteria) out.push({ kind: "text", paras: [{ kind: "text", text: criteria[r], inset: 0, h: labelH(r), gap: 0 }], size: itemSize, x: critR.x, y, w: critR.w, h: labelH(r), limit: floor, bold: true
|
|
1107
|
+
if (criteria) out.push({ kind: "text", paras: [{ kind: "text", text: criteria[r], inset: 0, h: labelH(r), gap: 0 }], size: itemSize, x: critR.x, y, w: critR.w, h: labelH(r), limit: floor, bold: true });
|
|
1099
1108
|
sides.forEach((sd, i) => {
|
|
1100
1109
|
if (!sd.items[r]) return;
|
|
1101
1110
|
const h = sideH(sd.items[r], i);
|
|
@@ -1103,6 +1112,7 @@ const PLANS = {
|
|
|
1103
1112
|
});
|
|
1104
1113
|
}
|
|
1105
1114
|
if (S.deco.has("column-hairlines")) out.push(columnHairline(S, parts[1], Z));
|
|
1115
|
+
if (notes) out.push({ ...notes, x: Z.x, y: Z.bottom + state.blockGap, limit: ctx.Z.bottom });
|
|
1106
1116
|
return out;
|
|
1107
1117
|
},
|
|
1108
1118
|
|
|
@@ -1136,23 +1146,28 @@ const PLANS = {
|
|
|
1136
1146
|
// The same readable measure as the stacked rows (the gate counts spaces as characters). That
|
|
1137
1147
|
// measure at body size leaves the body's right columns empty, so what happens is set a step up (lead)
|
|
1138
1148
|
// when the rows still fit: the measure grows with the size and reaches across.
|
|
1139
|
-
|
|
1149
|
+
// `flush` sets what happens against the body's right edge and gives the step's name the columns
|
|
1150
|
+
// between, so a body-size measure still reaches across when the lead step does not fit.
|
|
1151
|
+
const at = (size, flush = false) => {
|
|
1140
1152
|
const restW = Math.min(Z.x + Z.w - restX, (korean ? 26 : 35) * size);
|
|
1153
|
+
const rx = flush ? Z.x + Z.w - restW : restX;
|
|
1154
|
+
const headW = rx - S.grid.gutter - headR.x;
|
|
1141
1155
|
const rows = steps.map((st) => {
|
|
1142
|
-
const hh = G.textHeight(st.head,
|
|
1156
|
+
const hh = G.textHeight(st.head, headW, headSize);
|
|
1143
1157
|
const rh = st.rest ? G.textHeight(st.rest, restW, size) : 0;
|
|
1144
1158
|
return { num: st.n, head: st.head, rest: st.rest, hh, rh, h: Math.max(numSize * 1.15, hh, rh) };
|
|
1145
1159
|
});
|
|
1146
1160
|
const tallest = Math.max(...rows.map((r) => r.h));
|
|
1147
1161
|
const pitch = rowPitch(rows.length, tallest, Z, state.groupGap, tallest + 48);
|
|
1148
1162
|
rows.forEach((r, i) => { r.y = Z.y + i * pitch; });
|
|
1149
|
-
return { kind: "rows", rows, x: Z.x, numW, textX: headR.x, textW:
|
|
1163
|
+
return { kind: "rows", rows, x: Z.x, numW, textX: headR.x, textW: headW, restX: rx, restW, head: headSize, rest: size, numSize, pitch, rule: true,
|
|
1150
1164
|
y: Z.y, h: (rows.length - 1) * pitch + rows[rows.length - 1].h, limit: Z.bottom };
|
|
1151
1165
|
};
|
|
1152
1166
|
const body = at(restSize);
|
|
1153
1167
|
if ((Z.x + Z.w - (restX + body.restW)) / Z.w > 0.25) {
|
|
1154
1168
|
const lead = at(S.sizes.lead);
|
|
1155
1169
|
if (lead.y + lead.h <= Z.bottom + 0.5) return [lead];
|
|
1170
|
+
return [at(restSize, true)];
|
|
1156
1171
|
}
|
|
1157
1172
|
return [body];
|
|
1158
1173
|
}
|
|
@@ -1167,7 +1182,6 @@ const PLANS = {
|
|
|
1167
1182
|
rows.forEach((r, i) => { r.y = Z.y + i * pitch; });
|
|
1168
1183
|
return [{ kind: "rows", rows, x: Z.x, numW, textX, textW, head: headSize, rest: restSize, numSize, pitch, rule: true, y: Z.y, h: (rows.length - 1) * pitch + rows[rows.length - 1].h, limit: Z.bottom }];
|
|
1169
1184
|
};
|
|
1170
|
-
if (ctx.cls === "side") return withNotes(asRows());
|
|
1171
1185
|
const slots = equalParts(S, Z, steps.length);
|
|
1172
1186
|
const placedSteps = steps.map((s, i) => {
|
|
1173
1187
|
const R = slots[i];
|
|
@@ -1247,7 +1261,7 @@ const PLANS = {
|
|
|
1247
1261
|
const M = wide ? region(S, Z.a, Z.b - 4, Z.y, Z.bottom) : Z;
|
|
1248
1262
|
const notesH = texts.length && !wide ? stack(texts, Z, S, state).bottom - Z.y + state.blockGap : 0;
|
|
1249
1263
|
const mBottom = M.bottom - notesH;
|
|
1250
|
-
const labelCols =
|
|
1264
|
+
const labelCols = wide ? 1 : 2;
|
|
1251
1265
|
const labels = region(S, M.a, M.a + labelCols - 1, M.y, mBottom);
|
|
1252
1266
|
const grid = region(S, M.a + labelCols, M.b, M.y, mBottom);
|
|
1253
1267
|
const capH = table.caption ? captionHeight(table.caption, grid.w, S) + 6 : 0;
|
|
@@ -1283,13 +1297,13 @@ const PLANS = {
|
|
|
1283
1297
|
const by = paras.find((p) => /^[—–-]\s*/u.test(plainText(p.text)));
|
|
1284
1298
|
const said = paras.filter((p) => p !== by).map((p) => p.text).join(" ");
|
|
1285
1299
|
const markW = S.grid.span(1, 1).w;
|
|
1286
|
-
const R = region(S, Math.min(Z.b, Z.a + 1), Math.max(Z.a + 1, Z.b -
|
|
1300
|
+
const R = region(S, Math.min(Z.b, Z.a + 1), Math.max(Z.a + 1, Z.b - 2), Z.y + 24, Z.bottom);
|
|
1287
1301
|
const size = S.sizes.title;
|
|
1288
1302
|
const h = G.textHeight(plainText(said), R.w, size, 1.3);
|
|
1289
1303
|
const blockH = h + (by ? 18 + S.sizes.label * 1.4 : 0);
|
|
1290
1304
|
// Beside a side title the quote is the slide's display element: it sits in the middle of the
|
|
1291
1305
|
// body, as a statement does, instead of at the top of an empty page.
|
|
1292
|
-
const y =
|
|
1306
|
+
const y = R.y;
|
|
1293
1307
|
return [{ kind: "quote", text: said, by: by ? plainText(by.text) : "", size, x: R.x, w: R.w, y, h: blockH, textH: h, markX: Z.x, markW, limit: Z.bottom }];
|
|
1294
1308
|
},
|
|
1295
1309
|
|
|
@@ -1339,16 +1353,22 @@ const PLANS = {
|
|
|
1339
1353
|
const size = visuals.length === 1 && visuals[0].type === "image" && visuals[0].imageSize;
|
|
1340
1354
|
if (S.tok.tight && size && size.width / size.height >= 2 && Z.b - Z.a + 1 >= 8) {
|
|
1341
1355
|
const cap = captionHeight(visuals[0].caption, Z.w, S);
|
|
1342
|
-
for (const [share, n] of [[0.5, 2], [0.5, 3], [0.42, 3], [0.36, 3]]) {
|
|
1356
|
+
for (const [share, n] of [[0.5, 2], [0.45, 2], [0.5, 3], [0.42, 3], [0.36, 3]]) {
|
|
1343
1357
|
const figH = Math.min(Math.round(Z.w * size.height / size.width) + cap, Math.round((Z.bottom - Z.y) * share));
|
|
1344
1358
|
place({ ...Z, bottom: Z.y + figH });
|
|
1345
|
-
|
|
1359
|
+
// Measured at their closest step first: whether the terms fit under the figure is not a question of
|
|
1360
|
+
// how far they may then open to fill the room.
|
|
1361
|
+
const under = { ...Z, y: Z.y + figH + state.blockGap };
|
|
1362
|
+
let cols = defsInColumns(defs, under, n, false);
|
|
1346
1363
|
// The source strip closes the shortest column when it fits there.
|
|
1347
1364
|
const last = cols.reduce((m, q) => (q.y + q.h < m.y + m.h ? q : m));
|
|
1348
1365
|
const strip = ctx.strip ? stripPlacements(ctx.strip, { ...Z, x: last.x, w: last.w }, S) : null;
|
|
1349
1366
|
const closes = strip && last.y + last.h * 1.1 + S.tok.block + strip.h <= Z.bottom;
|
|
1350
1367
|
// A figure across the body is wider at 130 pt than it stands beside its terms at full column height.
|
|
1351
1368
|
if (figH - cap >= 130 && cols.every((q) => q.y + q.h * 1.1 <= Z.bottom) && (!strip || closes)) {
|
|
1369
|
+
// Then the terms open over the room left above the strip, when they still fit there.
|
|
1370
|
+
const roomy = defsInColumns(defs, { ...under, bottom: strip ? Z.bottom - strip.h - S.tok.block : Z.bottom }, n);
|
|
1371
|
+
if (roomy.every((q) => q.y + q.h * 1.05 <= (strip ? Z.bottom - strip.h - S.tok.block : Z.bottom))) cols = roomy;
|
|
1352
1372
|
return [...out, ...cols.map((q) => ({ ...q, limit: Z.bottom - q.h * 0.1 })), ...(strip ? strip.placed.map((q) => ({ ...q, strip: true })) : [])];
|
|
1353
1373
|
}
|
|
1354
1374
|
out.length = 0;
|
|
@@ -1397,14 +1417,18 @@ const PLANS = {
|
|
|
1397
1417
|
|
|
1398
1418
|
// The terms split between two columns at the entry that brings the first nearest half the height (three
|
|
1399
1419
|
// columns: the cuts that keep the tallest column lowest).
|
|
1400
|
-
function defsInColumns(list, R, n = 2) {
|
|
1420
|
+
function defsInColumns(list, R, n = 2, open = true) {
|
|
1401
1421
|
const ents = entries(list);
|
|
1402
1422
|
const flat = (part) => part.flatMap((e) => [{ kind: e.kind === "section" ? "section" : "bullet", text: e.head ? `**${e.head}** ${e.rest}`.trim() : e.rest },
|
|
1403
1423
|
...e.children.map((c) => ({ kind: "child", text: c }))]);
|
|
1404
1424
|
const cols = equalParts(S, R, n);
|
|
1405
1425
|
const fits = (st) => cols.every((C) => C.y + measureText(flat(ents), C.w, S, st).h / n <= C.bottom);
|
|
1406
|
-
// Terms keep a 12 pt step between them when the columns hold it, else the item gap.
|
|
1407
|
-
|
|
1426
|
+
// Terms keep a 12 pt step between them when the columns hold it, else the item gap. Terms that leave
|
|
1427
|
+
// more than the density band empty under them open the step, up to 36 pt, while the columns hold it.
|
|
1428
|
+
const at = (g) => ({ ...state, itemGap: Math.max(state.itemGap, g) });
|
|
1429
|
+
const loose = (st) => (C) => (C.bottom - C.y - measureText(flat(ents), C.w, S, st).h / n) / (C.bottom - C.y) > S.tok.cap;
|
|
1430
|
+
let gapped = fits(at(12)) ? at(12) : state;
|
|
1431
|
+
if (open && gapped !== state && cols.every(loose(gapped))) gapped = [36, 30, 24, 18].map(at).find(fits) || gapped;
|
|
1408
1432
|
const tall = (part) => (part.length ? measureText(flat(part), cols[0].w, S, gapped).h : 0);
|
|
1409
1433
|
if (n === 3 && ents.length >= 3) {
|
|
1410
1434
|
let best = null;
|
|
@@ -1629,8 +1653,8 @@ function figureRows(cards, Z, S, state, spread = null) {
|
|
|
1629
1653
|
}
|
|
1630
1654
|
|
|
1631
1655
|
/**
|
|
1632
|
-
* A big-number slide as a data panel: the panel takes seven of twelve columns
|
|
1633
|
-
*
|
|
1656
|
+
* A big-number slide as a data panel: the panel takes seven of twelve columns under the title down to
|
|
1657
|
+
* the zone's floor. With two to four figures (a KPI table
|
|
1634
1658
|
* whose second body row gives each figure's basis) the panel holds one row per figure: the value at
|
|
1635
1659
|
* title size, its label and its basis beside it. One figure is set at title size with its label on a
|
|
1636
1660
|
* panel as tall as they are. The
|
|
@@ -1643,7 +1667,7 @@ function numberPanel(ctx, table, texts) {
|
|
|
1643
1667
|
const c = table.content || {};
|
|
1644
1668
|
const cards = shared().kpiCards(c).slice(0, 6).filter((k) => plainText(k.value));
|
|
1645
1669
|
const device = S.pack.display.number;
|
|
1646
|
-
const [pR, eR] = texts.length ? splitRegion(S, Z,
|
|
1670
|
+
const [pR, eR] = texts.length ? splitRegion(S, Z, 7 / 12) : [Z, null];
|
|
1647
1671
|
const pad = Math.max(18, S.tok.pad);
|
|
1648
1672
|
const inner = pR.w - pad * 2;
|
|
1649
1673
|
const panelH = pR.bottom - pR.y;
|
|
@@ -1881,8 +1905,8 @@ function drawPlaced(slide, S, p, ctx) {
|
|
|
1881
1905
|
return;
|
|
1882
1906
|
}
|
|
1883
1907
|
if (p.kind === "box") {
|
|
1884
|
-
if (p.outline) packRect(slide, S, p, { line: "ink", lineWidth: 1 });
|
|
1885
|
-
else if (p.tint) packRect(slide, S, p, { fill: "accent-tint", radius: S.pack.radius });
|
|
1908
|
+
if (p.outline) packRect(slide, S, p, { line: "ink", lineWidth: S.pack["edge-weight"] || 1, ...hardEdge(S, "ground") });
|
|
1909
|
+
else if (p.tint) packRect(slide, S, p, { fill: "accent-tint", radius: S.pack.radius, ...hardEdge(S) });
|
|
1886
1910
|
else surfaceRect(slide, S, p);
|
|
1887
1911
|
return;
|
|
1888
1912
|
}
|
|
@@ -1908,7 +1932,7 @@ function drawPlaced(slide, S, p, ctx) {
|
|
|
1908
1932
|
return;
|
|
1909
1933
|
}
|
|
1910
1934
|
if (p.kind === "callout") {
|
|
1911
|
-
packRect(slide, S, p, { fill: "accent-tint", radius: S.pack.radius });
|
|
1935
|
+
packRect(slide, S, p, { fill: "accent-tint", radius: S.pack.radius, ...hardEdge(S) });
|
|
1912
1936
|
packText(slide, S, plainText(c), { x: p.x + p.pad, y: p.y + p.pad, w: p.w - p.pad * 2, h: p.h - p.pad * 2 },
|
|
1913
1937
|
{ size: p.size, bold: true, colour: S.colour("accent-deep") });
|
|
1914
1938
|
return;
|
|
@@ -2178,7 +2202,7 @@ function drawPackTable(slide, S, c, p, title) {
|
|
|
2178
2202
|
const border = (ri) => {
|
|
2179
2203
|
const last = ri === rows.length - 1;
|
|
2180
2204
|
switch (t.rules) {
|
|
2181
|
-
case "grid": return [
|
|
2205
|
+
case "grid": return Array(4).fill(S.pack["edge-weight"] ? { type: "solid", pt: S.pack["edge-weight"], color: S.colour(S.pack["edge-colour"] || "line") } : hair);
|
|
2182
2206
|
case "booktabs": return [ri === -1 ? heavy(1) : none, none, ri === -1 ? heavy(0.5) : last ? heavy(1) : none, none];
|
|
2183
2207
|
case "top-bottom": return [ri === -1 ? hair : none, none, last ? hair : ri === -1 ? hair : none, none];
|
|
2184
2208
|
case "header-rule": return [none, none, ri === -1 ? heavy(2) : none, none];
|
|
@@ -2189,7 +2213,7 @@ function drawPackTable(slide, S, c, p, title) {
|
|
|
2189
2213
|
};
|
|
2190
2214
|
const cellMargin = [IN(6), IN(6), IN(6), IN(6)];
|
|
2191
2215
|
// Cell text that wraps is broken at spaces here, as in body text (see G.keepLines).
|
|
2192
|
-
const keep = (text, ci, ri, size, role) => G.keepLines(text, ((p.widths[ci] || p.w) - 12) / S.wide(role) / size,
|
|
2216
|
+
const keep = (text, ci, ri, size, role) => G.keepLines(text, ((p.widths[ci] || p.w) - 12) / S.wide(role, text) / size,
|
|
2193
2217
|
Math.floor(((p.heights[ri + 1] || 0) - 12) / (size * 1.45) + 0.01));
|
|
2194
2218
|
const tableRows = [headers.map((h, ci) => ({
|
|
2195
2219
|
text: keep(plainText(h), ci, -1, p.header, "label"),
|
|
@@ -2225,15 +2249,13 @@ function overflows(placed) {
|
|
|
2225
2249
|
|
|
2226
2250
|
/**
|
|
2227
2251
|
* The zones of one treatment frame: body Z, the class and where the empty band is measured from.
|
|
2228
|
-
*
|
|
2229
|
-
* The rail under a side title is another matter: it carries the slide's supporting content (see railOf).
|
|
2252
|
+
* Every treatment's body is the shared body zone under the title zone, so the class is "top".
|
|
2230
2253
|
*/
|
|
2231
2254
|
function zonesOf(frame, S) {
|
|
2232
2255
|
const t = frame.title;
|
|
2233
|
-
const cls = frame.id === "side-rail" ? "side" : frame.id === "bottom-anchor" ? "bottom" : "top";
|
|
2234
2256
|
if (!frame.body) return { cls: "none" };
|
|
2235
2257
|
const Z = region(S, frame.body.a, frame.body.b, frame.body.y, frame.body.bottom);
|
|
2236
|
-
return { cls
|
|
2258
|
+
return { cls: "top", Z, bandTop: Math.min(t.y + t.h, Z.y) };
|
|
2237
2259
|
}
|
|
2238
2260
|
|
|
2239
2261
|
/**
|
|
@@ -2279,112 +2301,18 @@ function stripPlacements(lines, Z, S) {
|
|
|
2279
2301
|
] };
|
|
2280
2302
|
}
|
|
2281
2303
|
|
|
2282
|
-
//
|
|
2283
|
-
// and the visual takes the body (a table opens its rows; how full the body ends up still decides the title).
|
|
2284
|
-
const RAIL_TAKEAWAYS = new Set(["chart-insight", "full-chart", "table-insight", "figure-academic", "image-split",
|
|
2285
|
-
"kpi-row", "big-number", "kpi-over-chart", "dashboard-grid", "photo-grid", "figure-pair", "asymmetric-feature"]);
|
|
2286
|
-
// A rail more empty than this, from the title down, costs the slide the side rail.
|
|
2304
|
+
// A column more empty than this, beside a longer one, costs the slide its title.
|
|
2287
2305
|
const RAIL_EMPTY = 0.4;
|
|
2288
2306
|
|
|
2289
|
-
/**
|
|
2290
|
-
* The rail under a side title: columns 1-4 from just under the title frame (which keeps a spare line for
|
|
2291
|
-
* a title that wraps once more than estimated) to the floor. It holds the slide's supporting content: the
|
|
2292
|
-
* source strip at its foot, the takeaways of a visual when they fit, a comparison's criteria labels level
|
|
2293
|
-
* with their rows.
|
|
2294
|
-
*/
|
|
2295
|
-
function railOf(frame, S) {
|
|
2296
|
-
const t = frame.title;
|
|
2297
|
-
const s = S.grid.span(1, 4);
|
|
2298
|
-
// `top` is where the rail's empty share is measured from: the title frame's foot, as the gate reads it.
|
|
2299
|
-
return { a: 1, b: 4, x: s.x, w: s.w, y: Math.ceil(t.y + t.h + S.tok.group), top: t.y + t.h, bottom: G.ZONE.floor };
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
/** The largest empty stretch of the rail under the title frame as a share of its height (rail placements only). */
|
|
2303
|
-
function railBand(rail, placed) {
|
|
2304
|
-
let gap = 0;
|
|
2305
|
-
let cursor = rail.top;
|
|
2306
|
-
for (const q of placed.filter((p) => p.rail && !p.decor).sort((a, b) => a.y - b.y)) {
|
|
2307
|
-
gap = Math.max(gap, q.y - cursor);
|
|
2308
|
-
cursor = Math.max(cursor, q.y + q.h);
|
|
2309
|
-
}
|
|
2310
|
-
return Math.max(gap, rail.bottom - cursor) / Math.max(1, rail.bottom - rail.top);
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
2307
|
function planSlide(spec, frame, S, items) {
|
|
2314
2308
|
const z = zonesOf(frame, S);
|
|
2315
2309
|
if (spec.lead && spec.family !== "summary-box-list") items = [{ type: "main-box", content: spec.lead }, ...items];
|
|
2316
2310
|
if (z.cls === "none") return { placed: [], band: 0, applied: [], before: 0, z };
|
|
2317
2311
|
const full = z.Z;
|
|
2318
|
-
const rail = z.cls === "side" ? railOf(frame, S) : null;
|
|
2319
|
-
const railPlaced = [];
|
|
2320
2312
|
const taken = takeStrip(spec, items);
|
|
2321
|
-
// The source strip:
|
|
2322
|
-
|
|
2323
|
-
let lines = taken.lines;
|
|
2313
|
+
// The source strip: offered to a takeaway column beside a visual, and across the body's foot when no column takes it.
|
|
2314
|
+
const lines = taken.lines;
|
|
2324
2315
|
items = taken.items;
|
|
2325
|
-
if (lines.length && rail) {
|
|
2326
|
-
const strip = stripPlacements(lines, rail, S);
|
|
2327
|
-
railPlaced.push(...strip.placed.map((q) => ({ ...q, rail: true })));
|
|
2328
|
-
rail.bottom -= strip.h + S.tok.block;
|
|
2329
|
-
lines = [];
|
|
2330
|
-
}
|
|
2331
|
-
// A visual's takeaways stand in the rail when they fit there, a chart's values table under them; the
|
|
2332
|
-
// visual then takes the body. Takeaways that leave the rail more than RAIL_EMPTY empty at body size are
|
|
2333
|
-
// set a step up (lead) when they fit: the rail is a display column, the one place the compact step
|
|
2334
|
-
// sets text above body size.
|
|
2335
|
-
if (rail && RAIL_TAKEAWAYS.has(spec.family) && !spec.stacked) {
|
|
2336
|
-
const { texts, visuals } = sortItems(items);
|
|
2337
|
-
const paras = texts.filter((t) => t.type !== "main-box").flatMap(itemParas);
|
|
2338
|
-
// A table keeps its own height: its takeaways leave the body only when the table, its rows opened,
|
|
2339
|
-
// still fills the body to the density band.
|
|
2340
|
-
const fills = () => {
|
|
2341
|
-
// Figure rows and number panels spread to the floor; so do charts and pictures.
|
|
2342
|
-
if (["kpi-row", "big-number"].includes(spec.family) || visuals.some((v) => v.type !== "kpi-table" || (v.content || {}).chart)) return true;
|
|
2343
|
-
const alone = openRows(stack(visuals, full, S, { stepped: false, itemGap: S.tok.item, groupGap: S.tok.group, blockGap: S.tok.block }).placed, full, S);
|
|
2344
|
-
return (full.bottom - contentBottom(alone, full.y)) / (full.bottom - full.y) <= S.tok.cap;
|
|
2345
|
-
};
|
|
2346
|
-
if (visuals.length && paras.length && fills()) {
|
|
2347
|
-
const state = { stepped: false, itemGap: S.tok.item, groupGap: S.tok.group, blockGap: S.tok.block };
|
|
2348
|
-
const sets = [S.sizes.body, S.sizes.lead].map((size) => {
|
|
2349
|
-
const m = { ...measureText(paras, rail.w, S, state, { size }), x: rail.x, y: rail.y, limit: rail.bottom, rail: true };
|
|
2350
|
-
const values = S.tok.tight ? chartValues(visuals, rail, S, state, rail.y + m.h) : null;
|
|
2351
|
-
const set = [m, ...(values ? [{ ...values, rail: true }] : [])];
|
|
2352
|
-
return { set, fits: set.every((q) => q.y + q.h <= rail.bottom + 0.5), band: railBand({ ...rail, bottom: G.ZONE.floor }, [...set, ...railPlaced]) };
|
|
2353
|
-
}).filter((x) => x.fits);
|
|
2354
|
-
// Body size when it fills the rail; else whichever size leaves the rail least empty.
|
|
2355
|
-
const pick = sets.find((x) => x.band <= RAIL_EMPTY) || sets.reduce((a, b) => (b.band < a.band ? b : a), sets[0]);
|
|
2356
|
-
if (pick) {
|
|
2357
|
-
// Takeaways that still leave a quarter of the rail empty share that room between them, at most a
|
|
2358
|
-
// sixth of the rail a gap: the rail reads as one column down to its source.
|
|
2359
|
-
const [m] = pick.set;
|
|
2360
|
-
const breaks = m.paras.filter((para, i) => i > 0 && para.kind !== "child");
|
|
2361
|
-
const room = rail.bottom - S.tok.block - (pick.set[pick.set.length - 1].y + pick.set[pick.set.length - 1].h);
|
|
2362
|
-
if (pick.band > 0.25 && pick.set.length === 1 && breaks.length && room > 0) {
|
|
2363
|
-
const per = Math.min(room / (breaks.length + 1), (G.ZONE.floor - rail.top) / 6);
|
|
2364
|
-
for (const para of breaks) para.gap += per;
|
|
2365
|
-
m.h += per * breaks.length;
|
|
2366
|
-
}
|
|
2367
|
-
railPlaced.push(...pick.set);
|
|
2368
|
-
items = items.filter((i) => !(isText(i) && i.type !== "main-box"));
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
2372
|
-
// A sidebar note is the slide's supporting content: under a side title it stands in the rail, at lead size
|
|
2373
|
-
// when it fits there.
|
|
2374
|
-
if (rail && spec.family === "sidebar-note") {
|
|
2375
|
-
const texts = sortItems(items).texts;
|
|
2376
|
-
const note = texts.find((t) => t.type === "main-box") || (texts.length > 1 ? texts[texts.length - 1] : null);
|
|
2377
|
-
if (note) {
|
|
2378
|
-
const state = { stepped: false, itemGap: S.tok.item, groupGap: S.tok.group, blockGap: S.tok.block };
|
|
2379
|
-
const paras = itemParas(note).map((p) => ({ ...p, kind: p.kind === "child" ? "child" : "text" }));
|
|
2380
|
-
const m = [S.sizes.lead, S.sizes.body].map((size) => ({ ...measureText(paras, rail.w, S, state, { size }), x: rail.x, y: rail.y, limit: rail.bottom, rail: true, note: true }))
|
|
2381
|
-
.find((q) => q.y + q.h <= rail.bottom + 0.5);
|
|
2382
|
-
if (m) {
|
|
2383
|
-
railPlaced.push(m);
|
|
2384
|
-
items = items.filter((i) => i !== note);
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
2316
|
const across = lines.length ? stripPlacements(lines, full, S) : null;
|
|
2389
2317
|
const tok = S.tok;
|
|
2390
2318
|
const plan = (spec.stacked && PLANS[STACKS[spec.family]]) || PLANS[spec.family] || PLANS["text-column"];
|
|
@@ -2394,15 +2322,14 @@ function planSlide(spec, frame, S, items) {
|
|
|
2394
2322
|
const zone = () => (across && !offer ? { ...full, bottom: full.bottom - across.h - S.tok.block } : full);
|
|
2395
2323
|
// A later run (another fill state) that leaves the offered strip is refused like an overflow.
|
|
2396
2324
|
const run = (settle = false) => {
|
|
2397
|
-
const ctx = { spec, S, state, Z: zone(),
|
|
2398
|
-
railValues: railPlaced.some((q) => q.item && q.item.values), railNote: railPlaced.some((q) => q.note) };
|
|
2325
|
+
const ctx = { spec, S, state, Z: zone(), family: spec.family, items, strip: offer ? lines : null };
|
|
2399
2326
|
const placed = plan(ctx);
|
|
2400
2327
|
const lost = offer && !placed.some((q) => q.strip);
|
|
2401
2328
|
if (lost && settle) {
|
|
2402
2329
|
offer = false;
|
|
2403
2330
|
return run();
|
|
2404
2331
|
}
|
|
2405
|
-
return { placed, lost, bottom: contentBottom(placed
|
|
2332
|
+
return { placed, lost, bottom: contentBottom(placed, zone().y) };
|
|
2406
2333
|
};
|
|
2407
2334
|
const band = (o) => Math.max(0, Math.min(1, (zone().bottom - o.bottom) / Math.max(1, zone().bottom - z.bandTop)));
|
|
2408
2335
|
let out = run(true);
|
|
@@ -2449,15 +2376,10 @@ function planSlide(spec, frame, S, items) {
|
|
|
2449
2376
|
fillColumn(short, floor, S, state);
|
|
2450
2377
|
short = shortColumn([...out.placed, ...(across && !offer ? across.placed : [])], floor, S);
|
|
2451
2378
|
}
|
|
2452
|
-
|
|
2453
|
-
const field = (frame.decor || []).some((d) => d.kind === "rail" && d.role === "field");
|
|
2454
|
-
const placed = [...out.placed, ...railPlaced, ...(across && !offer ? across.placed : [])].map((q) => (q.rail && field ? { ...q, onField: true } : q));
|
|
2379
|
+
const placed = [...out.placed, ...(across && !offer ? across.placed : [])];
|
|
2455
2380
|
const overflow = overflows(placed);
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
const display = ["quote", "statement"].includes(spec.family);
|
|
2459
|
-
return { ...out, placed, band: band(out), before, applied, z, overflow, rail: railEmpty, short: short.share,
|
|
2460
|
-
unfit: overflow || (railEmpty > RAIL_EMPTY && !display) || short.share > RAIL_EMPTY };
|
|
2381
|
+
return { ...out, placed, band: band(out), before, applied, z, overflow, short: short.share,
|
|
2382
|
+
unfit: overflow || short.share > RAIL_EMPTY };
|
|
2461
2383
|
}
|
|
2462
2384
|
|
|
2463
2385
|
/** The frame of a treatment for this slide (a quote under a statement title says the quote). */
|
|
@@ -2473,8 +2395,8 @@ function frameFor(spec, treatment) {
|
|
|
2473
2395
|
* on and a large one past about a third of the deck, so filling never collapses a deck into one look. The variance dial keeps the number of
|
|
2474
2396
|
* distinct treatments under its maximum.
|
|
2475
2397
|
*/
|
|
2476
|
-
//
|
|
2477
|
-
//
|
|
2398
|
+
// The bottom-anchor style (the title frame closed by a heavy rule) is for a slide led by a chart, a picture,
|
|
2399
|
+
// a big number, a row of figures or a timeline axis, not for a table.
|
|
2478
2400
|
const drawn = (spec) => ["big-number", "kpi-row", "timeline"].includes(spec.family) || (spec.items || []).some((i) => (i.type === "kpi-table" && (i.content || {}).chart) || i.type === "image" || i.type === "image-grid");
|
|
2479
2401
|
|
|
2480
2402
|
function chooseTreatment(spec, S, deck) {
|
|
@@ -2484,9 +2406,8 @@ function chooseTreatment(spec, S, deck) {
|
|
|
2484
2406
|
// Titles that fit come first; a table goes under a bottom title only when nothing else fits it.
|
|
2485
2407
|
const fit = tries.filter((x) => !x.plan.unfit);
|
|
2486
2408
|
const proper = fit.filter((x) => x.t !== "bottom-anchor" || drawn(spec));
|
|
2487
|
-
// When every title leaves the slide unfit, the one that overflows least and leaves
|
|
2488
|
-
|
|
2489
|
-
const badness = (x) => (x.plan.overflow ? 10 : 0) + Math.max(0, x.plan.rail - RAIL_EMPTY) + Math.max(0, (x.plan.short || 0) - RAIL_EMPTY);
|
|
2409
|
+
// When every title leaves the slide unfit, the one that overflows least and leaves a column least empty stands.
|
|
2410
|
+
const badness = (x) => (x.plan.overflow ? 10 : 0) + Math.max(0, (x.plan.short || 0) - RAIL_EMPTY);
|
|
2490
2411
|
const pool = proper.length ? proper : fit.length ? fit : [...tries].sort((m, n) => badness(m) - badness(n));
|
|
2491
2412
|
const first = pool[0];
|
|
2492
2413
|
if (first.t === options[0] && first.plan.band <= S.tok.cap && first.t !== deck.prev && (deck.count[first.t] || 0) < deck.limit) return first;
|
|
@@ -2598,6 +2519,8 @@ function spreadTreatments(choices, S, pack, slideCount) {
|
|
|
2598
2519
|
const layoutOf = (t, plan) => `${ZONE_OF[t]}/${partitionOf(plan, S)}`;
|
|
2599
2520
|
// The check also fails a deck where one layout covers more than 40 % of those slides, so while
|
|
2600
2521
|
// one does, a slide of that layout moves to a title that gives a layout still under the share.
|
|
2522
|
+
// A title style never moves the body (every treatment shares the title zone), so any slide whose
|
|
2523
|
+
// source leaves its title open may take another here, not only a data slide.
|
|
2601
2524
|
const shareCap = 0.4 * counted.length;
|
|
2602
2525
|
for (let guard = 0; guard < counted.length; guard++) {
|
|
2603
2526
|
const keys = {};
|
|
@@ -2610,7 +2533,7 @@ function spreadTreatments(choices, S, pack, slideCount) {
|
|
|
2610
2533
|
for (const spec of counted) {
|
|
2611
2534
|
const current = choices.get(spec);
|
|
2612
2535
|
const from = layoutOf(current.t, current.plan);
|
|
2613
|
-
if (
|
|
2536
|
+
if (keys[from] < 2 || (!short && from !== topKey)) continue;
|
|
2614
2537
|
for (const t of spec.candidates || []) {
|
|
2615
2538
|
if (t === current.t || (t === "bottom-anchor" && !drawn(spec))) continue;
|
|
2616
2539
|
const inUse = new Set([...choices.values()].map((c) => c.t));
|
|
@@ -2619,8 +2542,10 @@ function spreadTreatments(choices, S, pack, slideCount) {
|
|
|
2619
2542
|
const to = layoutOf(t, plan);
|
|
2620
2543
|
if (plan.unfit || plan.band > Math.max(S.tok.cap, current.plan.band) || to === from) continue;
|
|
2621
2544
|
// A move that adds a layout comes first; else one that relieves the crowded layout.
|
|
2622
|
-
|
|
2623
|
-
const
|
|
2545
|
+
const adds = short && !keys[to];
|
|
2546
|
+
const relieves = crowded && from === topKey && (keys[to] || 0) + 1 <= shareCap;
|
|
2547
|
+
if (!adds && !relieves) continue;
|
|
2548
|
+
const loss = plan.band - current.plan.band - (adds ? 2 : 0) - (relieves ? 1 : 0);
|
|
2624
2549
|
if (!best || loss < best.loss) best = { spec, t, plan, loss, added: !keys[to] };
|
|
2625
2550
|
}
|
|
2626
2551
|
}
|
|
@@ -2712,13 +2637,15 @@ function drawnBoxes(p, S) {
|
|
|
2712
2637
|
*/
|
|
2713
2638
|
function partitionOf(plan, S) {
|
|
2714
2639
|
const Z = plan.z && plan.z.Z;
|
|
2715
|
-
|
|
2716
|
-
const placed = (plan.placed || []).filter((q) => !q.decor && !q.rail);
|
|
2640
|
+
const placed = (plan.placed || []).filter((q) => !q.decor);
|
|
2717
2641
|
if (!Z || !placed.length) return "one";
|
|
2718
2642
|
const all = placed.flatMap((q) => drawnBoxes(q, S)).filter((b) => b.w > 0 && b.h > 0);
|
|
2719
2643
|
const cards = all.filter((b) => b.card);
|
|
2720
2644
|
const inCard = (b) => cards.some((c) => c !== b && b.x + b.w / 2 >= c.x - 1 && b.x + b.w / 2 <= c.x + c.w + 1 && b.y + b.h / 2 >= c.y - 1 && b.y + b.h / 2 <= c.y + c.h + 1);
|
|
2721
|
-
|
|
2645
|
+
// A one-line caption or source on the body floor is the slide's foot, not a column (as the gate reads it).
|
|
2646
|
+
const body = SG.zone(S.grid.canvas, "content", "body");
|
|
2647
|
+
const floor = body.y + body.h;
|
|
2648
|
+
const blocks = all.filter((b) => !inCard(b) && !(!b.card && b.h <= 16 && b.y + b.h >= floor - 2));
|
|
2722
2649
|
if (!blocks.length) return "one";
|
|
2723
2650
|
const bodyW = S.grid.W - 2 * S.grid.margin;
|
|
2724
2651
|
let narrow = blocks.filter((b) => b.w < 0.7 * bodyW);
|
|
@@ -2763,7 +2690,7 @@ const TEXT_KINDS = new Set(["text", "caption", "callout", "box", "panel"]);
|
|
|
2763
2690
|
const SHORT_KINDS = new Set([...TEXT_KINDS, "image"]);
|
|
2764
2691
|
function shortColumn(placed, floor, S) {
|
|
2765
2692
|
const flat = (list) => list.flatMap((q) => (q.kind === "group" ? flat(q.parts) : [q]));
|
|
2766
|
-
const owners = flat(placed.filter((q) => !q.decor
|
|
2693
|
+
const owners = flat(placed.filter((q) => !q.decor));
|
|
2767
2694
|
// A matrix's cells are drawn whole on the page, whatever its composition estimate reads of them.
|
|
2768
2695
|
const boxesOf = (q) => (q.kind === "matrix" ? q.cells.map((c) => ({ x: c.x, y: c.y, w: c.w, h: c.h })) : drawnBoxes(q, S));
|
|
2769
2696
|
// A chart's values table under its takeaways counts with their column.
|
|
@@ -2785,7 +2712,7 @@ function shortColumn(placed, floor, S) {
|
|
|
2785
2712
|
for (const [col, other] of [[a, c], [c, a]]) {
|
|
2786
2713
|
if (!col.every((b) => b.text)) continue;
|
|
2787
2714
|
// Beside figure rows (not on their own panel) the column runs to the floor: the rows stop where their
|
|
2788
|
-
// spacing ends, not where the slide's content does
|
|
2715
|
+
// spacing ends, not where the slide's content does.
|
|
2789
2716
|
const figures = other.some((b) => ["figrows", "number"].includes(b.q.kind)) && !other.some((b) => b.q.kind === "panel");
|
|
2790
2717
|
const reach = figures ? floor : Math.min(floor, Math.max(...other.map((b) => b.y + b.h)));
|
|
2791
2718
|
if (reach - top < 0.75 * (floor - top)) continue;
|
|
@@ -2847,9 +2774,11 @@ function fillColumn(short, floor, S, state) {
|
|
|
2847
2774
|
/** Families whose visual may take its takeaways under it across the body, and the plan that does. */
|
|
2848
2775
|
const STACKS = { "table-insight": "ledger-table", "chart-insight": "full-chart" };
|
|
2849
2776
|
|
|
2850
|
-
/**
|
|
2851
|
-
|
|
2852
|
-
|
|
2777
|
+
/**
|
|
2778
|
+
* The composition title zone of each treatment. Every treatment styles the one title frame of the
|
|
2779
|
+
* slide grid, so a slide's title "zone" in its signature is its treatment, as the gate reads it.
|
|
2780
|
+
*/
|
|
2781
|
+
const ZONE_OF = Object.fromEntries(G.TREATMENTS.map((t) => [t, t]));
|
|
2853
2782
|
|
|
2854
2783
|
// ── Content, cover and section slides ───────────────────────────────────────
|
|
2855
2784
|
|
|
@@ -2867,7 +2796,7 @@ function renderContent(slide, spec, S, ctx, chosen) {
|
|
|
2867
2796
|
const items = spec.items || [];
|
|
2868
2797
|
if (!f.image) drawDecor(slide, S, f.decor);
|
|
2869
2798
|
if (f.numeral) {
|
|
2870
|
-
packText(slide, S, f.numeral.text, f.numeral, { face: "numeral", size: f.numeral.size, colour: S.colour("accent"), lineSpacing: 1.15 });
|
|
2799
|
+
packText(slide, S, f.numeral.text, f.numeral, { face: "numeral", size: f.numeral.size, colour: S.colour("accent"), lineSpacing: 1.15, align: f.numeral.align, name: "numeral@kicker" });
|
|
2871
2800
|
}
|
|
2872
2801
|
const dctx = { sourceDir: ctx.sourceDir, title: spec.title };
|
|
2873
2802
|
let ink = t.colour;
|
|
@@ -2883,15 +2812,8 @@ function renderContent(slide, spec, S, ctx, chosen) {
|
|
|
2883
2812
|
} else if (f.support) {
|
|
2884
2813
|
const text = spec.supportText || spec.lead || items.filter(isText).flatMap(itemParas).map((p) => plainText(p.text)).join(" ");
|
|
2885
2814
|
const supportH = text ? G.textHeight(plainText(text), f.support.w, S.sizes.body) : 0;
|
|
2886
|
-
// A tall statement with a support line moves up so the line ends on the body floor.
|
|
2887
|
-
const over = f.support.y + supportH - G.ZONE.floor;
|
|
2888
|
-
if (over > 0) {
|
|
2889
|
-
const dy = Math.min(over, t.y - G.ZONE.top);
|
|
2890
|
-
t.y -= dy;
|
|
2891
|
-
f.support.y -= dy;
|
|
2892
|
-
}
|
|
2893
2815
|
ink = statementDevice(slide, S, f, supportH);
|
|
2894
|
-
if (text) packText(slide, S, text, { ...f.support, h: supportH }, { colour: S.colour(ink === "on-field" ? "on-field" : "ink-muted") });
|
|
2816
|
+
if (text) packText(slide, S, text, { ...f.support, h: supportH }, { colour: S.colour(ink === "on-field" ? "on-field" : "ink-muted"), name: "support@statement" });
|
|
2895
2817
|
} else if (f.body) {
|
|
2896
2818
|
const plan = chosen.plan;
|
|
2897
2819
|
if (plan.before > S.tok.cap || plan.band > S.tok.cap) {
|
|
@@ -2902,46 +2824,45 @@ function renderContent(slide, spec, S, ctx, chosen) {
|
|
|
2902
2824
|
}
|
|
2903
2825
|
packText(slide, S, chosen.t === "statement" && spec.statementText ? spec.statementText : spec.title, t, {
|
|
2904
2826
|
// Every title that wraps is set in balanced lines, never ending on one word.
|
|
2905
|
-
face: t.face, size: t.size, colour: S.colour(ink), lineSpacing: 1.15, title: true, name: `title@${chosen.t}`, balance: true,
|
|
2827
|
+
face: t.face, size: t.size, colour: S.colour(ink), lineSpacing: t.lineSpacing || 1.15, valign: t.valign, title: true, name: `title@${chosen.t}`, balance: true,
|
|
2906
2828
|
});
|
|
2907
2829
|
}
|
|
2908
2830
|
|
|
2909
2831
|
/**
|
|
2910
2832
|
* What carries a statement, from the pack's display keys: nothing (open), the field over the whole
|
|
2911
|
-
* page (drench), the field
|
|
2912
|
-
* above the sentence and one under its support line (rules),
|
|
2913
|
-
* the
|
|
2833
|
+
* page (drench), the field from just above the sentence's zone to the page foot (plate), a hairline
|
|
2834
|
+
* above the sentence and one under its support line (rules), the primaries' composition in the side
|
|
2835
|
+
* zone (geometry), or the rail colour filling the side zone (offset). The sentence stays in the display
|
|
2836
|
+
* title zone whichever it is. Returns the colour role the sentence takes.
|
|
2914
2837
|
*/
|
|
2915
2838
|
function statementDevice(slide, S, f, supportH) {
|
|
2916
2839
|
const g = S.grid;
|
|
2917
2840
|
const t = f.title;
|
|
2841
|
+
const side = SG.zone(g.canvas, "display", "side");
|
|
2918
2842
|
const device = (S.pack.display || {}).statement || "open";
|
|
2919
2843
|
if (device === "drench") {
|
|
2920
2844
|
packRect(slide, S, { x: 0, y: 0, w: g.W, h: g.H }, { fill: "field" });
|
|
2921
2845
|
return "on-field";
|
|
2922
2846
|
}
|
|
2923
2847
|
if (device === "plate") {
|
|
2924
|
-
|
|
2925
|
-
// of the page, so the white above stays the page and the plate reads as the sentence's ground.
|
|
2926
|
-
// A sentence too tall to sit that low keeps the open page.
|
|
2927
|
-
const top = Math.round((376 - t.h / 2) / 6) * 6;
|
|
2928
|
-
if (top - 24 < 282 || top + t.h + 24 + supportH > G.ZONE.floor) return t.colour;
|
|
2929
|
-
f.support.y += top - t.y;
|
|
2930
|
-
t.y = top;
|
|
2931
|
-
packRect(slide, S, { x: 0, y: top - 24, w: g.W, h: g.H - top + 24 }, { fill: "field" });
|
|
2848
|
+
packRect(slide, S, { x: 0, y: t.y - 24, w: g.W, h: g.H - t.y + 24 }, { fill: "field" });
|
|
2932
2849
|
return "on-field";
|
|
2933
2850
|
}
|
|
2934
2851
|
if (device === "rules") {
|
|
2852
|
+
// The rules follow the sentence centred in its zone, not the zone's edges.
|
|
2935
2853
|
const full = g.span(1, 12);
|
|
2936
|
-
const
|
|
2937
|
-
|
|
2854
|
+
const top = t.y + (t.h - t.textH) / 2;
|
|
2855
|
+
const below = supportH ? f.support.y + supportH + 24 : top + t.textH + 30;
|
|
2856
|
+
packRect(slide, S, { x: full.x, y: top - 30, w: full.w, h: 0.75 }, { fill: "line" });
|
|
2938
2857
|
packRect(slide, S, { x: full.x, y: below, w: full.w, h: 0.75 }, { fill: "line" });
|
|
2939
2858
|
return t.colour;
|
|
2940
2859
|
}
|
|
2860
|
+
if (device === "geometry") {
|
|
2861
|
+
geometry(slide, S, side);
|
|
2862
|
+
return t.colour;
|
|
2863
|
+
}
|
|
2941
2864
|
if (device === "offset") {
|
|
2942
|
-
|
|
2943
|
-
// and at the body floor, so the footer zone stays the page's.
|
|
2944
|
-
packRect(slide, S, { x: 0, y: 0, w: t.x - g.gutter / 2 - g.pitch, h: G.ZONE.floor }, { fill: S.pack.rail === "field" ? "field" : "surface" });
|
|
2865
|
+
packRect(slide, S, { ...side, h: G.ZONE.floor }, { fill: S.pack.rail === "field" ? "field" : "surface" });
|
|
2945
2866
|
return t.colour;
|
|
2946
2867
|
}
|
|
2947
2868
|
return t.colour;
|
|
@@ -2951,12 +2872,48 @@ function metaLine(meta) {
|
|
|
2951
2872
|
return [meta.date, meta.department, meta.presenter].filter(Boolean).join(" · ");
|
|
2952
2873
|
}
|
|
2953
2874
|
|
|
2954
|
-
/**
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2875
|
+
/**
|
|
2876
|
+
* The cover's words, the same places in every tonality (slide-grid cover zones): the meta line in the
|
|
2877
|
+
* header zone, the title standing on the title zone's foot at the largest step that fits it (from the
|
|
2878
|
+
* hero step while the pack's hero is on), and the
|
|
2879
|
+
* subtitle in the subtitle zone. A device only paints what lies behind or beside them.
|
|
2880
|
+
*/
|
|
2881
|
+
function coverWords(slide, spec, S, meta, o = {}) {
|
|
2882
|
+
const Z = SG.zones(S.grid.canvas).cover;
|
|
2883
|
+
const soft = S.colour(o.soft || "ink-muted");
|
|
2884
|
+
const line = metaLine(meta);
|
|
2885
|
+
if (line) packText(slide, S, line, Z.header, { size: S.sizes.label, colour: S.colour(o.meta || o.soft || "ink-muted"), lineSpacing: 1.2, name: "header@cover" });
|
|
2886
|
+
if (meta.subtitle) {
|
|
2887
|
+
const size = G.textHeight(meta.subtitle, Z.subtitle.w, S.sizes.lead) <= Z.subtitle.h ? S.sizes.lead : S.sizes.body;
|
|
2888
|
+
packText(slide, S, meta.subtitle, Z.subtitle, { name: "subtitle@cover", size, colour: soft });
|
|
2889
|
+
}
|
|
2890
|
+
const steps = [...new Set([o.start || (S.pack.hero ? S.sizes.hero : S.sizes.cover), S.sizes.cover, S.sizes.display])].sort((a, b) => b - a);
|
|
2891
|
+
const fits = (z) => S.lines(plainText(spec.title), Z.title.w, z, "display") * z * 1.1 <= Z.title.h;
|
|
2892
|
+
const size = steps.find(fits) || S.sizes.display;
|
|
2893
|
+
packText(slide, S, spec.title, Z.title, { face: "display", size, colour: S.colour(o.ink || "ink"), lineSpacing: 1.1, title: true, balance: true, valign: "bottom", name: "title@cover" });
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
/** A row of keys over short labels in the cover's strip zone (the deck's parts or its headline figures), under a hairline. */
|
|
2897
|
+
function coverStrip(slide, S, parts) {
|
|
2898
|
+
const g = S.grid;
|
|
2899
|
+
const Z = SG.zones(g.canvas).cover.strip;
|
|
2900
|
+
const row = parts.slice(0, 4);
|
|
2901
|
+
const cols = equalParts(S, region(S, 1, 12, Z.y, Z.y + Z.h), row.length);
|
|
2902
|
+
packRect(slide, S, { x: Z.x, y: Z.y - 9, w: Z.w, h: 0.75 }, { fill: "line" });
|
|
2903
|
+
row.forEach(([key, text], i) => {
|
|
2904
|
+
const R = cols[i];
|
|
2905
|
+
const k = [S.sizes.title, S.sizes.lead].find((z) => S.ems(key, "numeral") * z * 1.1 <= R.w) || S.sizes.body;
|
|
2906
|
+
packText(slide, S, key, { x: R.x, y: Z.y, w: R.w, h: k * 1.15 }, { face: "numeral", size: k, colour: S.colour(i === 0 ? "accent" : "ink"), lineSpacing: 1.15 });
|
|
2907
|
+
const top = Z.y + k * 1.32 + 4;
|
|
2908
|
+
packText(slide, S, text, { x: R.x, y: top, w: R.w, h: Math.max(S.sizes.body * 1.4, Z.y + Z.h - top) }, { size: S.sizes.body, colour: S.colour("ink-muted") });
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
/** The deck's parts, else its headline figures, as strip entries. */
|
|
2913
|
+
function stripParts(facts) {
|
|
2914
|
+
if ((facts.index || []).length >= 2) return facts.index.map((name, i) => [String(i + 1).padStart(2, "0"), name]);
|
|
2915
|
+
if ((facts.figures || []).length >= 2) return facts.figures.map((f) => [f.value, f.label]);
|
|
2916
|
+
return [];
|
|
2960
2917
|
}
|
|
2961
2918
|
|
|
2962
2919
|
function coverImage(slide, box, src, sourceDir) {
|
|
@@ -2974,80 +2931,41 @@ function croppedTo(file, box) {
|
|
|
2974
2931
|
return { x: IN(box.x), y: IN(box.y), w: IN(box.w), h: IN(box.w * ratio), sizing: { type: "cover", w: IN(box.w), h: IN(box.h) } };
|
|
2975
2932
|
}
|
|
2976
2933
|
|
|
2934
|
+
/** The field behind the title zone and what stands on it, from `top` down to 6 pt under the title's foot. */
|
|
2935
|
+
function titleField(slide, S, top) {
|
|
2936
|
+
const T = SG.zones(S.grid.canvas).cover.title;
|
|
2937
|
+
packRect(slide, S, { x: 0, y: top, w: S.grid.W, h: T.y + T.h + 6 - top }, { fill: "field" });
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2977
2940
|
/**
|
|
2978
|
-
* A type-led cover drawn with the pack's display device
|
|
2979
|
-
*
|
|
2980
|
-
*
|
|
2981
|
-
*
|
|
2941
|
+
* A type-led cover drawn with the pack's display device. Every device leaves the words where
|
|
2942
|
+
* coverWords puts them and paints only around them: the field over the whole page (drench), a field
|
|
2943
|
+
* plate from over the title to the page foot (plate), a journal page between rules (rules), the
|
|
2944
|
+
* composition in the side zone (geometry), or the plain page.
|
|
2982
2945
|
*/
|
|
2983
2946
|
const TYPE_COVERS = {
|
|
2984
2947
|
drench(slide, spec, S, meta) {
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
const line = metaLine(meta);
|
|
2988
|
-
if (line) packText(slide, S, line, { ...g.span(1, 8), y: 36, h: 24 }, { size: S.sizes.label, colour: S.colour("on-field"), lineSpacing: 1.2 });
|
|
2989
|
-
const span = g.span(1, 11);
|
|
2990
|
-
const { size, h } = coverTitle(S, spec.title, span.w, 3, S.pack.hero ? S.sizes.hero : S.sizes.cover);
|
|
2991
|
-
const sub = g.span(1, 8);
|
|
2992
|
-
const subH = meta.subtitle ? G.textHeight(meta.subtitle, sub.w, S.sizes.lead) + 18 : 0;
|
|
2993
|
-
// The title block ends just above the footer zone, so the field holds the title from below.
|
|
2994
|
-
const y = Math.round((456 - subH - h) / 6) * 6;
|
|
2995
|
-
if (meta.subtitle) packText(slide, S, meta.subtitle, { ...sub, y: y + h + 18, h: subH - 18 }, { name: "subtitle@cover", size: S.sizes.lead, colour: S.colour("on-field") });
|
|
2996
|
-
packText(slide, S, spec.title, { ...span, y, h }, { face: "display", size, colour: S.colour("on-field"), lineSpacing: 1.1, title: true, balance: true, name: "title@cover" });
|
|
2948
|
+
packRect(slide, S, { x: 0, y: 0, w: S.grid.W, h: S.grid.H }, { fill: "field" });
|
|
2949
|
+
coverWords(slide, spec, S, meta, { ink: "on-field", soft: "on-field", start: S.pack.hero ? S.sizes.hero : S.sizes.cover });
|
|
2997
2950
|
},
|
|
2998
2951
|
plate(slide, spec, S, meta) {
|
|
2999
|
-
// The plate
|
|
3000
|
-
const
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
const line = metaLine(meta);
|
|
3004
|
-
if (line) packText(slide, S, line, { ...g.span(1, 8), y: 36, h: 24 }, { size: S.sizes.label, colour: S.colour("ink-muted"), lineSpacing: 1.2 });
|
|
3005
|
-
const span = g.span(1, 10);
|
|
3006
|
-
const { size, h } = coverTitle(S, spec.title, span.w, 2);
|
|
3007
|
-
const y = top + 30;
|
|
3008
|
-
if (meta.subtitle && y + h + 18 + S.sizes.body * 1.4 <= 462) {
|
|
3009
|
-
const sub = g.span(1, 8);
|
|
3010
|
-
packText(slide, S, meta.subtitle, { ...sub, y: y + h + 18, h: G.textHeight(meta.subtitle, sub.w, S.sizes.body) }, { name: "subtitle@cover", colour: S.colour("on-field") });
|
|
3011
|
-
}
|
|
3012
|
-
packText(slide, S, spec.title, { ...span, y, h }, { face: "display", size, colour: S.colour("on-field"), lineSpacing: 1.1, title: true, balance: true, name: "title@cover" });
|
|
2952
|
+
// The plate runs from over the title to the page foot; the meta line stays on the ground above it.
|
|
2953
|
+
const top = SG.zones(S.grid.canvas).cover.title.y - 24;
|
|
2954
|
+
packRect(slide, S, { x: 0, y: top, w: S.grid.W, h: S.grid.H - top }, { fill: "field" });
|
|
2955
|
+
coverWords(slide, spec, S, meta, { ink: "on-field", soft: "on-field", meta: "ink-muted" });
|
|
3013
2956
|
},
|
|
3014
2957
|
rules(slide, spec, S, meta) {
|
|
3015
|
-
const
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
packRect(slide, S, { x: full.x, y: ruleY, w: full.w, h: 0.75 }, { fill: "line" });
|
|
3024
|
-
// Under the rule, as on a journal's title page: who presents, then where and when, in one frame.
|
|
3025
|
-
const place = [meta.department, meta.date].filter(Boolean).join(" · ");
|
|
3026
|
-
const runs = [
|
|
3027
|
-
...(meta.presenter ? [{ text: meta.presenter, options: { fontFace: S.faces.title.font, fontSize: S.sizes.lead, bold: true, color: S.colour("ink"), breakLine: Boolean(place) } }] : []),
|
|
3028
|
-
...(place ? [{ text: place, options: { fontFace: S.faces.label.font, fontSize: S.sizes.label, color: S.colour("ink-muted") } }] : []),
|
|
3029
|
-
];
|
|
3030
|
-
let at = ruleY + 18;
|
|
3031
|
-
if (runs.length) {
|
|
3032
|
-
// A frame as tall as an office renderer sets two lines of mixed sizes (about 1.5 of each).
|
|
3033
|
-
const hh = (meta.presenter ? S.sizes.lead * 1.5 : 0) + (place ? S.sizes.label * 1.5 : 0);
|
|
3034
|
-
slide.addText(runs, { x: IN(span.x), y: IN(at), w: IN(span.w), h: IN(hh), align: "left", valign: "top", margin: [0, 0, 0, 0], lineSpacingMultiple: 1.2 });
|
|
3035
|
-
at += hh;
|
|
3036
|
-
}
|
|
3037
|
-
if (meta.subtitle) packText(slide, S, meta.subtitle, { ...span, y: at + 18, h: G.textHeight(meta.subtitle, span.w, S.sizes.label, 1.3) }, { name: "subtitle@cover", size: S.sizes.label, colour: S.colour("ink-muted"), lineSpacing: 1.3 });
|
|
3038
|
-
packText(slide, S, spec.title, { ...span, y, h }, { face: "display", size, lineSpacing: 1.1, title: true, balance: true, name: "title@cover" });
|
|
2958
|
+
const Z = SG.zones(S.grid.canvas).cover;
|
|
2959
|
+
packRect(slide, S, { x: Z.strip.x, y: Z.title.y - 18, w: Z.strip.w, h: 2 }, { fill: "ink" });
|
|
2960
|
+
packRect(slide, S, { x: Z.strip.x, y: Z.strip.y - 9, w: Z.strip.w, h: 0.75 }, { fill: "line" });
|
|
2961
|
+
coverWords(slide, spec, S, meta);
|
|
2962
|
+
},
|
|
2963
|
+
geometry(slide, spec, S, meta) {
|
|
2964
|
+
geometry(slide, S, SG.zones(S.grid.canvas).cover.side);
|
|
2965
|
+
coverWords(slide, spec, S, meta);
|
|
3039
2966
|
},
|
|
3040
2967
|
plain(slide, spec, S, meta) {
|
|
3041
|
-
|
|
3042
|
-
const span = g.span(1, 9);
|
|
3043
|
-
const { size, h } = coverTitle(S, spec.title, span.w, 3);
|
|
3044
|
-
if (meta.subtitle) {
|
|
3045
|
-
const sub = g.span(1, 8);
|
|
3046
|
-
packText(slide, S, meta.subtitle, { ...sub, y: 144 + h + 24, h: G.textHeight(meta.subtitle, sub.w, S.sizes.lead) }, { name: "subtitle@cover", size: S.sizes.lead, colour: S.colour("ink-muted") });
|
|
3047
|
-
}
|
|
3048
|
-
const line = metaLine(meta);
|
|
3049
|
-
if (line) packText(slide, S, line, { ...g.span(1, 6), y: 438, h: 24 }, { size: S.sizes.label, colour: S.colour("ink-muted"), lineSpacing: 1.2 });
|
|
3050
|
-
packText(slide, S, spec.title, { ...span, y: 144, h }, { face: "display", size, lineSpacing: 1.1, title: true, balance: true, name: "title@cover" });
|
|
2968
|
+
coverWords(slide, spec, S, meta);
|
|
3051
2969
|
},
|
|
3052
2970
|
};
|
|
3053
2971
|
|
|
@@ -3062,333 +2980,178 @@ const COVERS = {
|
|
|
3062
2980
|
return (TYPE_COVERS[device] || TYPE_COVERS.plain)(slide, spec, S, meta);
|
|
3063
2981
|
},
|
|
3064
2982
|
"cover-split-field"(slide, spec, S, meta) {
|
|
3065
|
-
const
|
|
3066
|
-
|
|
3067
|
-
packRect(slide, S, { x, y: 0, w: g.W - x, h: g.H }, { fill: "field" });
|
|
2983
|
+
const side = SG.zones(S.grid.canvas).cover.side;
|
|
2984
|
+
packRect(slide, S, side, { fill: "field" });
|
|
3068
2985
|
if (spec.cover.numeral) {
|
|
3069
|
-
const s =
|
|
2986
|
+
const s = S.grid.span(side.a, side.b);
|
|
3070
2987
|
packText(slide, S, spec.cover.numeral, { ...s, y: 384, h: S.sizes.title * 1.15 }, { face: "numeral", size: S.sizes.title, colour: S.colour("on-field"), lineSpacing: 1.15 });
|
|
3071
2988
|
}
|
|
3072
|
-
|
|
2989
|
+
coverWords(slide, spec, S, meta);
|
|
3073
2990
|
},
|
|
3074
2991
|
"cover-split-image"(slide, spec, S, meta) {
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
coverImage(slide, { x, y: 0, w: g.W - x, h: g.H }, spec.cover.image, spec.sourceDir);
|
|
3078
|
-
splitText(slide, spec, S, meta, 6);
|
|
2992
|
+
coverImage(slide, SG.zones(S.grid.canvas).cover.side, spec.cover.image, spec.sourceDir);
|
|
2993
|
+
coverWords(slide, spec, S, meta);
|
|
3079
2994
|
},
|
|
3080
2995
|
"cover-full-image"(slide, spec, S, meta) {
|
|
3081
2996
|
const g = S.grid;
|
|
3082
2997
|
coverImage(slide, { x: 0, y: 0, w: g.W, h: g.H }, spec.cover.image, spec.sourceDir);
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
const line = [meta.subtitle, metaLine(meta)].filter(Boolean).join(" · ");
|
|
3088
|
-
const lineH = line ? G.textHeight(line, span.w, S.sizes.label, 1.3) : 0;
|
|
3089
|
-
const y = Math.round(G.ZONE.floor - 24 - (line ? lineH + 18 : 0) - h);
|
|
3090
|
-
packRect(slide, S, { x: 0, y: y - 24, w: span.x + span.w + 24, h: g.H - y + 24 }, { fill: "field", alpha: 12 });
|
|
3091
|
-
packText(slide, S, spec.title, { ...span, y, h }, { face: "display", size, colour: S.colour("on-field"), lineSpacing: 1.1, title: true, balance: true, name: "title@cover" });
|
|
3092
|
-
if (line) packText(slide, S, line, { ...span, y: y + h + 18, h: lineH }, { size: S.sizes.label, colour: S.colour("on-field"), lineSpacing: 1.3 });
|
|
2998
|
+
// A translucent field panel under the words' columns, the full page height; the picture keeps the side zone.
|
|
2999
|
+
const side = SG.zones(g.canvas).cover.side;
|
|
3000
|
+
packRect(slide, S, { x: 0, y: 0, w: side.x, h: g.H }, { fill: "field", alpha: 12 });
|
|
3001
|
+
coverWords(slide, spec, S, meta, { ink: "on-field", soft: "on-field" });
|
|
3093
3002
|
},
|
|
3094
3003
|
"cover-band"(slide, spec, S, meta) {
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
const
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
const subH = meta.subtitle ? G.textHeight(meta.subtitle, g.span(1, 8).w, S.sizes.lead) : 0;
|
|
3101
|
-
if (meta.subtitle) packText(slide, S, meta.subtitle, { ...g.span(1, 8), y: 252, h: subH }, { name: "subtitle@cover", size: S.sizes.lead, colour: S.colour("ink-muted") });
|
|
3102
|
-
// Under the band, the deck's parts (else its headline figures) run across the page as the
|
|
3103
|
-
// issue's contents, between a hairline and the meta line.
|
|
3104
|
-
const parts = (spec.cover.index || []).length >= 2 ? spec.cover.index.map((name, i) => [String(i + 1).padStart(2, "0"), name])
|
|
3105
|
-
: (spec.cover.figures || []).length >= 2 ? spec.cover.figures.map((f) => [f.value, f.label]) : [];
|
|
3106
|
-
if (parts.length) {
|
|
3107
|
-
const top = Math.max(300, 252 + subH + 36);
|
|
3108
|
-
const cols = equalParts(S, region(S, 1, 12, top, 420), Math.min(parts.length, 4));
|
|
3109
|
-
packRect(slide, S, { x: g.margin, y: top - 18, w: g.span(1, 12).w, h: 0.75 }, { fill: "line" });
|
|
3110
|
-
parts.slice(0, 4).forEach(([key, text], i) => {
|
|
3111
|
-
const R = cols[i];
|
|
3112
|
-
const k = [S.sizes.title, S.sizes.lead].find((z) => S.ems(key, "numeral") * z * 1.1 <= R.w) || S.sizes.lead;
|
|
3113
|
-
packText(slide, S, key, { x: R.x, y: top, w: R.w, h: k * 1.15 }, { face: "numeral", size: k, colour: S.colour("accent"), lineSpacing: 1.15 });
|
|
3114
|
-
packText(slide, S, text, { x: R.x, y: top + k * 1.15 + 8, w: R.w, h: G.textHeight(text, R.w, S.sizes.body) }, { size: S.sizes.body });
|
|
3115
|
-
});
|
|
3116
|
-
}
|
|
3117
|
-
const line = metaLine(meta);
|
|
3118
|
-
if (line) packText(slide, S, line, { ...g.span(1, 8), y: 438, h: 24 }, { size: S.sizes.label, colour: S.colour("ink-muted"), lineSpacing: 1.2 });
|
|
3004
|
+
// The header band deepened behind the title; the subtitle and the deck's parts sit on the ground under it.
|
|
3005
|
+
titleField(slide, S, 0);
|
|
3006
|
+
const parts = stripParts(spec.cover || {});
|
|
3007
|
+
if (parts.length) coverStrip(slide, S, parts);
|
|
3008
|
+
coverWords(slide, spec, S, meta, { ink: "on-field", meta: "on-field" });
|
|
3119
3009
|
},
|
|
3120
3010
|
"cover-numeral"(slide, spec, S, meta) {
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
const
|
|
3125
|
-
packText(slide, S,
|
|
3126
|
-
|
|
3127
|
-
const { size, h } = coverTitle(S, spec.title, span.w, 3, S.sizes.display);
|
|
3128
|
-
const y = 96 + numeral * 1.1 + 24;
|
|
3129
|
-
packText(slide, S, spec.title, { ...span, y, h }, { face: "display", size, lineSpacing: 1.1, title: true, balance: true, name: "title@cover" });
|
|
3130
|
-
const line = metaLine(meta);
|
|
3131
|
-
if (line) packText(slide, S, line, { ...g.span(1, 6), y: 438, h: 24 }, { size: S.sizes.label, colour: S.colour("ink-muted"), lineSpacing: 1.2 });
|
|
3011
|
+
// A number is never the display element: title size at most. It stands in the
|
|
3012
|
+
// side zone level with the title zone's top.
|
|
3013
|
+
const Z = SG.zones(S.grid.canvas).cover;
|
|
3014
|
+
const s = S.grid.span(Z.side.a, Z.side.b);
|
|
3015
|
+
packText(slide, S, spec.cover.numeral, { ...s, y: Z.title.y, h: S.sizes.title * 1.15 }, { face: "numeral", size: S.sizes.title, colour: S.colour("accent"), align: "right", lineSpacing: 1.15 });
|
|
3016
|
+
coverWords(slide, spec, S, meta);
|
|
3132
3017
|
},
|
|
3133
3018
|
"cover-index"(slide, spec, S, meta) {
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
const { size, h } = coverTitle(S, spec.title, span.w, 4, S.sizes.display);
|
|
3137
|
-
packText(slide, S, spec.title, { ...span, y: 144, h }, { face: "display", size, lineSpacing: 1.1, title: true, balance: true, name: "title@cover" });
|
|
3138
|
-
const list = g.span(8, 12);
|
|
3139
|
-
// The part names run from the title's top down to the meta line, at most 96 pt apart.
|
|
3140
|
-
const pitch = Math.min(96, (438 - 144) / Math.max(1, spec.cover.index.length - 1));
|
|
3141
|
-
packRect(slide, S, { x: list.x - 12, y: 144, w: 0.75, h: pitch * spec.cover.index.length }, { fill: "line" });
|
|
3142
|
-
spec.cover.index.forEach((name, i) => {
|
|
3143
|
-
packText(slide, S, `${String(i + 1).padStart(2, "0")} ${name}`, { ...list, y: 144 + i * pitch, h: G.textHeight(name, list.w, S.sizes.lead, 1.3) }, { size: S.sizes.lead, colour: S.colour("ink"), lineSpacing: 1.3 });
|
|
3144
|
-
});
|
|
3145
|
-
const line = metaLine(meta);
|
|
3146
|
-
if (line) packText(slide, S, line, { ...span, y: 438, h: G.textHeight(line, span.w, S.sizes.label, 1.3) }, { size: S.sizes.label, colour: S.colour("ink-muted"), lineSpacing: 1.3 });
|
|
3019
|
+
coverStrip(slide, S, stripParts({ index: spec.cover.index }));
|
|
3020
|
+
coverWords(slide, spec, S, meta);
|
|
3147
3021
|
},
|
|
3148
3022
|
"cover-figures"(slide, spec, S, meta) {
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
const { size, h } = coverTitle(S, spec.title, span.w, 2, S.sizes.display);
|
|
3152
|
-
// The figures sit between the title and the meta line, with a hairline above them; the title
|
|
3153
|
-
// is drawn last, so the slide's first shape is an unnamed one that carries the family.
|
|
3154
|
-
const figs = spec.cover.figures.slice(0, 4);
|
|
3155
|
-
// The row stands in the middle of the room between the title and the meta line.
|
|
3156
|
-
const rowH = S.sizes.title * 1.3 + 6 + S.sizes.body * 1.4;
|
|
3157
|
-
const top = Math.max(240, 96 + h + 36, Math.round((96 + h + 438 - rowH) / 2 / 6) * 6);
|
|
3158
|
-
const parts = equalParts(S, region(S, 1, 12, top, top + 120), figs.length);
|
|
3159
|
-
packRect(slide, S, { x: g.margin, y: top - 18, w: g.span(1, 12).w, h: 0.75 }, { fill: "line" });
|
|
3160
|
-
figs.forEach((f, i) => {
|
|
3161
|
-
const R = parts[i];
|
|
3162
|
-
const v = [S.sizes.title, S.sizes.lead, S.sizes.body].find((s) => S.ems(f.value, "numeral") * s * 1.1 <= R.w) || S.sizes.body;
|
|
3163
|
-
packText(slide, S, f.value, { x: R.x, y: top, w: R.w, h: v * 1.15 }, { face: "numeral", size: v, colour: S.colour(i === 0 ? "accent" : "ink"), lineSpacing: 1.15 });
|
|
3164
|
-
packText(slide, S, f.label, { x: R.x, y: top + v * 1.3 + 6, w: R.w, h: S.sizes.body * 1.4 }, { size: S.sizes.body, colour: S.colour("ink-muted"), lineSpacing: 1.3 });
|
|
3165
|
-
});
|
|
3166
|
-
const line = metaLine(meta);
|
|
3167
|
-
if (line) packText(slide, S, line, { ...g.span(1, 6), y: 438, h: 24 }, { size: S.sizes.label, colour: S.colour("ink-muted"), lineSpacing: 1.2 });
|
|
3168
|
-
packText(slide, S, spec.title, { ...span, y: 96, h }, { face: "display", size, lineSpacing: 1.1, title: true, balance: true, name: "title@cover" });
|
|
3023
|
+
coverStrip(slide, S, stripParts({ figures: spec.cover.figures }));
|
|
3024
|
+
coverWords(slide, spec, S, meta);
|
|
3169
3025
|
},
|
|
3170
3026
|
"cover-rail"(slide, spec, S, meta) {
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
const side = g.span(1, 3);
|
|
3175
|
-
const lines = [meta.department, meta.presenter, meta.date].filter(Boolean);
|
|
3176
|
-
lines.forEach((l, i) => packText(slide, S, l, { ...side, y: 402 + i * 24, h: 22 }, { size: S.sizes.label, colour: S.colour("on-field"), lineSpacing: 1.2 }));
|
|
3177
|
-
const span = g.span(5, 12);
|
|
3178
|
-
const { size, h } = coverTitle(S, spec.title, span.w, 3);
|
|
3179
|
-
packText(slide, S, spec.title, { ...span, y: 144, h }, { face: "display", size, lineSpacing: 1.1, title: true, balance: true, name: "title@cover" });
|
|
3180
|
-
if (meta.subtitle) packText(slide, S, meta.subtitle, { ...span, y: 144 + h + 24, h: G.textHeight(meta.subtitle, span.w, S.sizes.lead) }, { name: "subtitle@cover", size: S.sizes.lead, colour: S.colour("ink-muted") });
|
|
3027
|
+
// The rail is the left margin in the field, the page's height; the words keep their zones.
|
|
3028
|
+
packRect(slide, S, { x: 0, y: 0, w: 12, h: S.grid.H }, { fill: "field" });
|
|
3029
|
+
coverWords(slide, spec, S, meta);
|
|
3181
3030
|
},
|
|
3182
3031
|
};
|
|
3183
3032
|
|
|
3184
|
-
/** Title and meta in columns 1-k beside a field or image (the split covers). */
|
|
3185
|
-
function splitText(slide, spec, S, meta, k) {
|
|
3186
|
-
const g = S.grid;
|
|
3187
|
-
const span = g.span(1, k);
|
|
3188
|
-
const { size, h } = coverTitle(S, spec.title, span.w, 4, S.sizes.display);
|
|
3189
|
-
packText(slide, S, spec.title, { ...span, y: 144, h }, { face: "display", size, lineSpacing: 1.1, title: true, balance: true, name: "title@cover" });
|
|
3190
|
-
if (meta.subtitle) packText(slide, S, meta.subtitle, { ...span, y: 144 + h + 24, h: G.textHeight(meta.subtitle, span.w, S.sizes.body) }, { name: "subtitle@cover", colour: S.colour("ink-muted") });
|
|
3191
|
-
const line = metaLine(meta);
|
|
3192
|
-
if (line) packText(slide, S, line, { ...span, y: 438, h: G.textHeight(line, span.w, S.sizes.label, 1.3) }, { size: S.sizes.label, colour: S.colour("ink-muted"), lineSpacing: 1.3 });
|
|
3193
|
-
}
|
|
3194
|
-
|
|
3195
3033
|
/**
|
|
3196
|
-
* Section slides
|
|
3197
|
-
*
|
|
3198
|
-
*
|
|
3199
|
-
*
|
|
3200
|
-
*
|
|
3034
|
+
* Section slides use the display zones (slide-grid), the same in every tonality: the part number in
|
|
3035
|
+
* the header zone, the part title centred in the title zone, and what the part opens with (its slide
|
|
3036
|
+
* titles, or their first lines) in the support zone. A part number shows only when the deck says how
|
|
3037
|
+
* many parts there are; without one the title takes the cover step (the hero step in a hero tonality).
|
|
3038
|
+
* A variant paints only the device around the words.
|
|
3201
3039
|
*/
|
|
3202
3040
|
const SECTIONS = {
|
|
3203
3041
|
"section-field"(slide, spec, S) {
|
|
3204
3042
|
const g = S.grid;
|
|
3205
3043
|
packRect(slide, S, { x: 0, y: 0, w: g.W, h: g.H }, { fill: "field" });
|
|
3206
|
-
|
|
3044
|
+
sectionWords(slide, spec, S, { onField: true });
|
|
3207
3045
|
},
|
|
3208
3046
|
"section-numeral"(slide, spec, S) {
|
|
3209
3047
|
// With the agenda known, the numeral's page is the index page: the title is never alone on it.
|
|
3210
3048
|
if (spec.parts && ((spec.cover || {}).index || []).length >= 2) { sectionIndex(slide, spec, S); return; }
|
|
3211
|
-
|
|
3049
|
+
sectionWords(slide, spec, S);
|
|
3212
3050
|
},
|
|
3213
3051
|
"section-rule"(slide, spec, S) {
|
|
3214
3052
|
if (showsIndex(spec, S)) { sectionIndex(slide, spec, S); return; }
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
packRect(slide, S, { x: full.x, y: box.y + box.h + 18, w: full.w, h: 0.75 }, { fill: "line" });
|
|
3219
|
-
} });
|
|
3053
|
+
const Z = SG.zones(S.grid.canvas).display;
|
|
3054
|
+
packRect(slide, S, { ...S.grid.span(1, 12), y: Z.support.y - 12, h: 0.75 }, { fill: "line" });
|
|
3055
|
+
sectionWords(slide, spec, S);
|
|
3220
3056
|
},
|
|
3221
3057
|
"section-band"(slide, spec, S) {
|
|
3222
|
-
// The header band deepened to
|
|
3223
|
-
// index
|
|
3058
|
+
// The header band deepened to hold the header zone. With the deck's part index the band carries that
|
|
3059
|
+
// index (this part marked) in place of the part number.
|
|
3224
3060
|
const g = S.grid;
|
|
3225
|
-
|
|
3061
|
+
const Z = SG.zones(g.canvas).display;
|
|
3062
|
+
packRect(slide, S, { x: 0, y: 0, w: g.W, h: Z.header.y + Z.header.h + 6 }, { fill: "field" });
|
|
3226
3063
|
const index = showsIndex(spec, S);
|
|
3227
|
-
if (index)
|
|
3228
|
-
|
|
3229
|
-
partIndex(slide, S, spec, { ...g.span(1, 12), y: 216 - 36 - h }, true);
|
|
3230
|
-
}
|
|
3231
|
-
// Under a band that carries the index, the title is a statement over what the part opens with.
|
|
3232
|
-
if (index) sectionOpener(slide, spec, S, { top: 252, centre: 351 });
|
|
3233
|
-
else sectionTitle(slide, spec, S, { top: 240 });
|
|
3064
|
+
if (index) partIndex(slide, S, spec, Z.header, true);
|
|
3065
|
+
sectionWords(slide, spec, S, { headerOnField: true, numeral: !index });
|
|
3234
3066
|
},
|
|
3235
3067
|
"section-rail"(slide, spec, S) {
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
packRect(slide, S, { x: 0, y: 0, w: rail.x + rail.w + g.gutter / 2, h: g.H }, { fill: "field" });
|
|
3239
|
-
const numeral = S.sizes.title;
|
|
3240
|
-
if (spec.parts) partNumeral(slide, S, spec, { ...g.span(1, 3), y: 216, h: numeral * 1.15 }, numeral, "on-field");
|
|
3241
|
-
else if ((spec.contents || []).length) {
|
|
3242
|
-
// No part count: the rail names what the section opens with, one entry under the next at lead size
|
|
3243
|
-
// with a step between them, so the list reads as the part's contents and not as one wrapped block.
|
|
3244
|
-
const w = g.span(1, 3).w;
|
|
3245
|
-
let y = 216;
|
|
3246
|
-
for (const entry of spec.contents) {
|
|
3247
|
-
const h = G.textHeight(entry, w, S.sizes.lead, 1.3);
|
|
3248
|
-
packText(slide, S, entry, { ...g.span(1, 3), y, h }, { size: S.sizes.lead, colour: S.colour("on-field"), lineSpacing: 1.3 });
|
|
3249
|
-
y += h + S.tok.group;
|
|
3250
|
-
}
|
|
3251
|
-
}
|
|
3252
|
-
const span = g.span(5, 12);
|
|
3253
|
-
const display = S.sizes.display;
|
|
3254
|
-
const h = Math.ceil(S.lines(plainText(spec.title), span.w, display, "display") * display * 1.15);
|
|
3255
|
-
packText(slide, S, spec.title, { ...span, y: Math.round((303 - h / 2) / 6) * 6, h }, { face: "display", size: display, lineSpacing: 1.15, title: true, balance: true, name: "title@section" });
|
|
3068
|
+
packRect(slide, S, { x: 0, y: 0, w: 12, h: S.grid.H }, { fill: "field" });
|
|
3069
|
+
sectionWords(slide, spec, S);
|
|
3256
3070
|
},
|
|
3257
3071
|
"section-image"(slide, spec, S) {
|
|
3258
|
-
//
|
|
3072
|
+
// A translucent field panel under the words' columns, the page's height; the picture keeps the side zone.
|
|
3259
3073
|
const g = S.grid;
|
|
3260
3074
|
coverImage(slide, { x: 0, y: 0, w: g.W, h: g.H }, spec.cover.image, spec.sourceDir);
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
const h = Math.ceil(S.lines(plainText(spec.title), s.w, display, "display") * display * 1.15);
|
|
3264
|
-
const y = Math.round(G.ZONE.floor - 18 - h);
|
|
3265
|
-
const numY = y - 24;
|
|
3266
|
-
packRect(slide, S, { x: 0, y: (spec.parts ? numY : y) - 18, w: s.x + s.w + 24, h: g.H - (spec.parts ? numY : y) + 18 }, { fill: "field", alpha: 12 });
|
|
3267
|
-
if (spec.parts) packText(slide, S, String(spec.part).padStart(2, "0"), { ...s, y: numY, h: S.sizes.label * 1.4 }, { size: S.sizes.label, colour: S.colour("on-field"), lineSpacing: 1.3 });
|
|
3268
|
-
packText(slide, S, spec.title, { ...s, y, h }, { face: "display", size: display, colour: S.colour("on-field"), lineSpacing: 1.15, title: true, balance: true, name: "title@section" });
|
|
3075
|
+
packRect(slide, S, { x: 0, y: 0, w: SG.zones(g.canvas).display.side.x, h: g.H }, { fill: "field", alpha: 12 });
|
|
3076
|
+
sectionWords(slide, spec, S, { onField: true });
|
|
3269
3077
|
},
|
|
3270
3078
|
};
|
|
3271
3079
|
|
|
3272
|
-
function
|
|
3273
|
-
|
|
3274
|
-
const
|
|
3275
|
-
const
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
const
|
|
3280
|
-
const
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
if (o.numeral !== false) {
|
|
3284
|
-
partNumeral(slide, S, spec, { ...g.span(1, spec.parts ? 6 : 3), y: y - numeralH, h: size * 1.15 }, size, o.numeralColour || "accent");
|
|
3285
|
-
}
|
|
3286
|
-
const box = { ...span, y, h };
|
|
3287
|
-
// Anything drawn with the title (a rule under it) goes first, so the title stays the last shape.
|
|
3288
|
-
if (o.under) o.under(box);
|
|
3289
|
-
packText(slide, S, spec.title, box, { face: "display", size: display, colour: S.colour(o.colour || "ink"), lineSpacing: 1.15, title: true, balance: true, name: "title@section" });
|
|
3290
|
-
return box;
|
|
3291
|
-
}
|
|
3292
|
-
|
|
3293
|
-
/**
|
|
3294
|
-
* A section the deck gives no part count (one section, no agenda): the title at the cover step (the
|
|
3295
|
-
* hero step in a hero tonality) as a statement centred in the body, and under it the titles of the
|
|
3296
|
-
* slides the section opens, so the page says what comes next instead of showing a bare count. With
|
|
3297
|
-
* `rule` the pack's hairline runs between them; `top` and `centre` place it under a band.
|
|
3298
|
-
*/
|
|
3299
|
-
function sectionOpener(slide, spec, S, o = {}) {
|
|
3300
|
-
const g = S.grid;
|
|
3301
|
-
const span = g.span(1, 10);
|
|
3302
|
-
const size = S.pack.hero ? S.sizes.hero : S.sizes.cover;
|
|
3303
|
-
const h = Math.ceil(S.lines(plainText(spec.title), span.w, size, "display") * size * 1.1);
|
|
3304
|
-
const onField = o.colour === "on-field";
|
|
3305
|
-
const digest = spec.digest || [];
|
|
3306
|
-
if (digest.some((d) => d.lines.length)) return sectionDigestPage(slide, spec, S, o, { span, size, h, digest, onField });
|
|
3307
|
-
const text = (spec.contents || []).join("\n");
|
|
3308
|
-
const lead = S.sizes.lead;
|
|
3309
|
-
const ch = text ? G.textHeight(text, span.w, lead, 1.4) : 0;
|
|
3310
|
-
const gap = text ? (o.rule ? 60 : 30) : 0;
|
|
3311
|
-
const y = Math.max(o.top || 0, Math.round(((o.centre || 303) - (h + gap + ch) / 2) / 6) * 6);
|
|
3312
|
-
if (text && o.rule) packRect(slide, S, { ...g.span(1, 12), y: y + h + 30, h: 0.75 }, { fill: "line" });
|
|
3313
|
-
if (text) packText(slide, S, text, { ...span, y: y + h + gap, h: ch }, { size: lead, colour: S.colour(onField ? "on-field" : "ink-muted"), lineSpacing: 1.4 });
|
|
3314
|
-
const box = { ...span, y, h };
|
|
3315
|
-
packText(slide, S, spec.title, box, { face: "display", size, colour: S.colour(o.colour || "ink"), lineSpacing: 1.1, title: true, balance: true, name: "title@section" });
|
|
3316
|
-
return box;
|
|
3080
|
+
function sectionWords(slide, spec, S, o = {}) {
|
|
3081
|
+
const Z = SG.zones(S.grid.canvas).display;
|
|
3082
|
+
const ink = S.colour(o.onField ? "on-field" : "ink");
|
|
3083
|
+
const soft = S.colour(o.onField ? "on-field" : "ink-muted");
|
|
3084
|
+
if (spec.parts && o.numeral !== false) partNumeral(slide, S, spec, Z.header, S.sizes.title, o.onField || o.headerOnField ? "on-field" : "accent");
|
|
3085
|
+
// A numbered part sets its title at the display step; an unnumbered one at the cover step, down to display to fit.
|
|
3086
|
+
const steps = spec.parts ? [S.sizes.display] : [...new Set([S.pack.hero ? S.sizes.hero : S.sizes.cover, S.sizes.cover, S.sizes.display])].sort((a, b) => b - a);
|
|
3087
|
+
const fits = (z) => S.lines(plainText(spec.title), Z.title.w, z, "display") * z * 1.1 <= Z.title.h;
|
|
3088
|
+
const size = steps.find(fits) || S.sizes.display;
|
|
3089
|
+
sectionSupport(slide, spec, S, Z.support, ink, soft);
|
|
3090
|
+
packText(slide, S, spec.title, Z.title, { face: "display", size, colour: ink, lineSpacing: 1.1, title: true, balance: true, valign: "middle", name: "title@section" });
|
|
3317
3091
|
}
|
|
3318
3092
|
|
|
3319
3093
|
/**
|
|
3320
|
-
*
|
|
3321
|
-
*
|
|
3322
|
-
*
|
|
3094
|
+
* What an unnumbered section opens with, in the support zone: one row per opened slide (its title, then
|
|
3095
|
+
* up to two of its first lines) when the slides carry body lines, else their titles one to a line. Lines
|
|
3096
|
+
* and then rows drop until the preview fits the zone.
|
|
3323
3097
|
*/
|
|
3324
|
-
function
|
|
3325
|
-
const g = S.grid;
|
|
3098
|
+
function sectionSupport(slide, spec, S, Z, ink, soft) {
|
|
3326
3099
|
const head = S.sizes.lead;
|
|
3327
3100
|
const body = S.sizes.body;
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
const
|
|
3336
|
-
const
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
const
|
|
3340
|
-
const
|
|
3101
|
+
const digest = (spec.digest || []).filter((d) => d.lines.length);
|
|
3102
|
+
if (!digest.length) {
|
|
3103
|
+
const text = (spec.contents || []).join("\n");
|
|
3104
|
+
if (text) packText(slide, S, text, Z, { size: head, colour: soft, lineSpacing: 1.4 });
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
// About 70 characters of body text at most: the widest column span inside the zone under that.
|
|
3108
|
+
const korean = G.isKorean(digest.map((e) => e.title + e.lines.join("")).join(""));
|
|
3109
|
+
const cap = 70 * body * (korean ? 0.94 : 0.5);
|
|
3110
|
+
let k = Z.b;
|
|
3111
|
+
while (k > Z.a && S.grid.span(Z.a, k).w > cap) k--;
|
|
3112
|
+
const span = S.grid.span(Z.a, k);
|
|
3113
|
+
const gap = 12;
|
|
3341
3114
|
const build = (entries, per) => entries.map((e) => {
|
|
3342
3115
|
const text = e.lines.slice(0, per).join("\n");
|
|
3343
|
-
const headH = G.textHeight(e.title,
|
|
3344
|
-
const textH = text ? G.textHeight(plainText(text),
|
|
3345
|
-
return { ...e, text, headH, textH, h: headH + (text ?
|
|
3116
|
+
const headH = G.textHeight(e.title, span.w, head, 1.3);
|
|
3117
|
+
const textH = text ? G.textHeight(plainText(text), span.w, body) : 0;
|
|
3118
|
+
return { ...e, text, headH, textH, h: headH + (text ? 4 + textH : 0) };
|
|
3346
3119
|
});
|
|
3347
3120
|
const height = (rows) => rows.reduce((a, r) => a + r.h, 0) + gap * (rows.length - 1);
|
|
3348
3121
|
let rows = null;
|
|
3349
|
-
for (let n =
|
|
3122
|
+
for (let n = digest.length; n >= 1 && !rows; n--) {
|
|
3350
3123
|
for (let per = 2; per >= 1; per--) {
|
|
3351
|
-
const tried = build(
|
|
3352
|
-
if (height(tried) <=
|
|
3124
|
+
const tried = build(digest.slice(0, n), per);
|
|
3125
|
+
if (height(tried) <= Z.h) { rows = tried; break; }
|
|
3353
3126
|
}
|
|
3354
3127
|
}
|
|
3355
|
-
if (!rows) rows = build(
|
|
3356
|
-
|
|
3357
|
-
// Title, hairline and preview form one block centred in the body (under `top` when a band sits
|
|
3358
|
-
// above), like a statement that says what the part holds.
|
|
3359
|
-
const total = d.h + 54 + sum + gap * (rows.length - 1);
|
|
3360
|
-
const y = Math.max(o.top || 0, G.ZONE.body, Math.round(((o.centre || 303) - total / 2) / 6) * 6);
|
|
3361
|
-
const listTop = y + d.h + 54;
|
|
3362
|
-
packRect(slide, S, { ...g.span(1, 12), y: listTop - 18, h: 0.75 }, { fill: d.onField ? "on-field" : "line" });
|
|
3363
|
-
let at = listTop;
|
|
3128
|
+
if (!rows) rows = build(digest.slice(0, 1), 0);
|
|
3129
|
+
let at = Z.y;
|
|
3364
3130
|
for (const r of rows) {
|
|
3365
|
-
packText(slide, S, r.title, { ...
|
|
3366
|
-
if (r.text) packText(slide, S, r.text, { ...
|
|
3131
|
+
packText(slide, S, r.title, { ...span, y: at, h: r.headH }, { size: head, bold: true, colour: ink, lineSpacing: 1.3 });
|
|
3132
|
+
if (r.text) packText(slide, S, r.text, { ...span, y: at + r.headH + 4, h: r.textH }, { size: body, colour: soft });
|
|
3367
3133
|
at += r.h + gap;
|
|
3368
3134
|
}
|
|
3369
|
-
const box = { ...d.span, y, h: d.h };
|
|
3370
|
-
packText(slide, S, spec.title, box, { face: "display", size: d.size, colour: S.colour(o.colour || "ink"), lineSpacing: 1.1, title: true, balance: true, name: "title@section" });
|
|
3371
|
-
return box;
|
|
3372
3135
|
}
|
|
3373
3136
|
|
|
3374
3137
|
/**
|
|
3375
|
-
* A section in a deck with an agenda, set as its index page: the part
|
|
3376
|
-
*
|
|
3377
|
-
*
|
|
3138
|
+
* A section in a deck with an agenda, set as its index page: the part number in the header zone over the
|
|
3139
|
+
* part title, and the agenda down the side zone, one part a line from the title zone's top, a hairline
|
|
3140
|
+
* above each and a heavier accent rule above this part, which is set in the accent.
|
|
3378
3141
|
*/
|
|
3379
3142
|
function sectionIndex(slide, spec, S) {
|
|
3380
|
-
const
|
|
3381
|
-
const
|
|
3382
|
-
const right = g.span(8, 12);
|
|
3143
|
+
const Z = SG.zones(S.grid.canvas).display;
|
|
3144
|
+
const right = S.grid.span(Z.side.a, Z.side.b);
|
|
3383
3145
|
const names = spec.cover.index;
|
|
3384
3146
|
const lead = S.sizes.lead;
|
|
3385
3147
|
const rowH = lead * 1.4;
|
|
3386
3148
|
const label = (name, i) => `${String(i + 1).padStart(2, "0")} ${name}`;
|
|
3387
3149
|
const heights = names.map((name, i) => G.textHeight(label(name, i), right.w, lead, 1.4));
|
|
3388
3150
|
const sum = heights.reduce((a, h) => a + h, 0);
|
|
3389
|
-
const
|
|
3151
|
+
const floor = Z.support.y + Z.support.h;
|
|
3152
|
+
const after = Math.max(12, Math.min(60 - rowH, (floor - Z.title.y - sum) / Math.max(1, names.length - 1)));
|
|
3390
3153
|
const listH = sum + (names.length - 1) * after;
|
|
3391
|
-
const listY =
|
|
3154
|
+
const listY = Z.title.y + 12;
|
|
3392
3155
|
const quiet = S.colour("ink-muted");
|
|
3393
3156
|
const runs = [];
|
|
3394
3157
|
let ruleY = listY - 12;
|
|
@@ -3405,16 +3168,10 @@ function sectionIndex(slide, spec, S) {
|
|
|
3405
3168
|
// The gap goes before each part after the first, so the frame ends on the last line.
|
|
3406
3169
|
slide.addText(runs, { x: IN(right.x), y: IN(listY), w: IN(right.w), h: IN(listH), align: "left", valign: "top", margin: [0, 0, 0, 0],
|
|
3407
3170
|
lineSpacing: +rowH.toFixed(1) });
|
|
3408
|
-
|
|
3409
|
-
const display = S.sizes.display;
|
|
3410
|
-
const h = Math.ceil(S.lines(plainText(spec.title), left.w, display, "display") * display * 1.15);
|
|
3411
|
-
const blockH = size * 1.15 + 24 + h;
|
|
3412
|
-
const y = Math.round((303 - blockH / 2) / 6) * 6;
|
|
3413
|
-
partNumeral(slide, S, spec, { ...left, y, h: size * 1.15 }, size, "accent");
|
|
3414
|
-
packText(slide, S, spec.title, { ...left, y: y + size * 1.15 + 24, h }, { face: "display", size: display, lineSpacing: 1.15, title: true, balance: true, name: "title@section" });
|
|
3171
|
+
sectionWords(slide, spec, S);
|
|
3415
3172
|
}
|
|
3416
3173
|
|
|
3417
|
-
/** The part number as the agenda writes it ("03"), followed by the part count when it is known. */
|
|
3174
|
+
/** The part number as the agenda writes it ("03"), followed by the part count when it is known, standing on the box's foot. */
|
|
3418
3175
|
function partNumeral(slide, S, spec, box, size, colour) {
|
|
3419
3176
|
const face = S.faces.numeral;
|
|
3420
3177
|
const base = { fontFace: face.font, bold: Boolean(face.bold), color: S.colour(colour) };
|
|
@@ -3424,14 +3181,14 @@ function partNumeral(slide, S, spec, box, size, colour) {
|
|
|
3424
3181
|
// The count is a step below the number, so the number stays the display element.
|
|
3425
3182
|
if (spec.parts) runs.push({ text: ` / ${pad(spec.parts)}`, options: { ...base, fontSize: S.sizes.title, color: S.colour(colour === "on-field" ? "on-field" : "ink-muted") } });
|
|
3426
3183
|
slide.addText(runs, { x: IN(box.x), y: IN(box.y), w: IN(box.w), h: IN(box.h), align: "left", valign: "bottom", margin: [0, 0, 0, 0],
|
|
3427
|
-
lineSpacing: +(size * 1.15).toFixed(1) });
|
|
3184
|
+
lineSpacing: +(size * 1.15).toFixed(1), objectName: "header@section" });
|
|
3428
3185
|
}
|
|
3429
3186
|
|
|
3430
3187
|
function showsIndex(spec, S) {
|
|
3431
3188
|
return Boolean((S.pack.display || {}).index && spec.parts && ((spec.cover || {}).index || []).length >= 2);
|
|
3432
3189
|
}
|
|
3433
3190
|
|
|
3434
|
-
/** The deck's parts in one line, numbered as in the agenda; this section's part bold in the accent. */
|
|
3191
|
+
/** The deck's parts in one line, numbered as in the agenda; this section's part bold in the accent, on the box's foot. */
|
|
3435
3192
|
function partIndex(slide, S, spec, box, onField = false) {
|
|
3436
3193
|
const size = S.sizes.body;
|
|
3437
3194
|
const quiet = onField ? mix(S.P["on-field"], S.P.field, 0.3) : S.P["ink-muted"];
|
|
@@ -3444,13 +3201,50 @@ function partIndex(slide, S, spec, box, onField = false) {
|
|
|
3444
3201
|
color: current ? S.colour(onField ? "on-field" : "accent") : shared().hex(quiet),
|
|
3445
3202
|
} });
|
|
3446
3203
|
});
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
slide.addText(runs, { x: IN(box.x), y: IN(box.y), w: IN(box.w), h: IN(h), align: "left", valign: "top", margin: [0, 0, 0, 0],
|
|
3450
|
-
lineSpacing: +(size * 1.4).toFixed(1) });
|
|
3204
|
+
slide.addText(runs, { x: IN(box.x), y: IN(box.y), w: IN(box.w), h: IN(box.h), align: "left", valign: "bottom", margin: [0, 0, 0, 0],
|
|
3205
|
+
lineSpacing: +(size * 1.4).toFixed(1), objectName: "header@section" });
|
|
3451
3206
|
}
|
|
3452
3207
|
|
|
3453
3208
|
/** The sample-data tag in the footer zone, on the grid's left edge, in the pack's colours. */
|
|
3209
|
+
/**
|
|
3210
|
+
* The grid shown on purpose: a tick and a two-digit number over each column start in the top margin,
|
|
3211
|
+
* and the slide's place in the deck at the right, all in the label face and the muted ink.
|
|
3212
|
+
*/
|
|
3213
|
+
function gridMarks(slide, S, n, total) {
|
|
3214
|
+
const g = S.grid;
|
|
3215
|
+
const size = S.sizes.source;
|
|
3216
|
+
const y = 12;
|
|
3217
|
+
for (let a = 1; a <= 12; a++) {
|
|
3218
|
+
const x = g.colX(a);
|
|
3219
|
+
packRect(slide, S, { x, y, w: 0.75, h: size * 1.2 }, { fill: "line", name: "grid-mark" });
|
|
3220
|
+
if (a < 11) packText(slide, S, String(a).padStart(2, "0"), { x: x + 4, y, w: g.col - 4, h: size * 1.2 }, { face: "label", size, colour: S.colour("ink-muted"), lineSpacing: 1.2, name: "grid-mark" });
|
|
3221
|
+
}
|
|
3222
|
+
const index = `[${String(n).padStart(2, "0")}/${String(total).padStart(2, "0")}]`;
|
|
3223
|
+
const right = g.span(11, 12);
|
|
3224
|
+
packText(slide, S, index, { ...right, y, h: size * 1.2 }, { face: "label", size, colour: S.colour("ink"), align: "right", lineSpacing: 1.2, name: "grid-mark index" });
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
/**
|
|
3228
|
+
* Elementary geometry in the primaries, in a region of the page that holds no text: a square in the
|
|
3229
|
+
* surface colour, a circle in the accent, a bar in the ink and a triangle in the field, one per cell
|
|
3230
|
+
* of a two-by-two grid, so no shape overlaps another or the text.
|
|
3231
|
+
*/
|
|
3232
|
+
function geometry(slide, S, region) {
|
|
3233
|
+
const cw = region.w / 2;
|
|
3234
|
+
const ch = region.h / 2;
|
|
3235
|
+
const d = Math.min(cw, ch);
|
|
3236
|
+
const cell = (i, j) => ({ x: region.x + i * cw, y: region.y + j * ch });
|
|
3237
|
+
const shape = (kind, box, fill) => slide.addShape(kind, { x: IN(box.x), y: IN(box.y), w: IN(box.w), h: IN(box.h), fill: { color: S.colour(fill) }, line: { type: "none" } });
|
|
3238
|
+
const a = cell(0, 0);
|
|
3239
|
+
shape("rect", { x: a.x, y: a.y, w: cw, h: ch }, "accent-tint");
|
|
3240
|
+
const b = cell(1, 0);
|
|
3241
|
+
shape("ellipse", { x: b.x + (cw - d) / 2, y: b.y + (ch - d) / 2, w: d, h: d }, "accent");
|
|
3242
|
+
const c = cell(0, 1);
|
|
3243
|
+
shape("rect", { x: c.x + cw * 0.38, y: c.y, w: cw * 0.24, h: ch }, "ink");
|
|
3244
|
+
const e = cell(1, 1);
|
|
3245
|
+
shape("triangle", { x: e.x, y: e.y + (ch - d), w: cw, h: d }, "field");
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3454
3248
|
function packNotice(slide, S, text) {
|
|
3455
3249
|
const label = plainText(text);
|
|
3456
3250
|
const size = S.sizes.source;
|
|
@@ -3536,11 +3330,13 @@ async function renderPack(resolved, templateObj, outputPath) {
|
|
|
3536
3330
|
// Nothing unnamed was drawn (a statement alone): an empty marker carries the family.
|
|
3537
3331
|
slide.addShape("rect", { x: IN(S.grid.margin), y: IN(G.ZONE.top), w: 0, h: 0, fill: { type: "none" }, line: { type: "none" }, objectName: `family@${spec.family}` });
|
|
3538
3332
|
}
|
|
3333
|
+
// The marks read on the ground; a statement page may be drenched in the field, so it goes without.
|
|
3334
|
+
if (spec.kind === "content" && chosenBy[spec.index] !== "statement" && S.deco.has("grid-marks")) gridMarks(slide, S, spec.index + 1, (resolved.slides || []).length);
|
|
3539
3335
|
if (notice) packNotice(slide, S, notice);
|
|
3540
3336
|
}
|
|
3541
3337
|
for (const line of log) console.log(line);
|
|
3542
3338
|
console.log(`Treatments: ${Object.entries(chosenBy).map(([i, t]) => `${Number(i) + 1}:${t}`).join(" ")}`);
|
|
3543
|
-
await writeWithEastAsianFaces(pptx, S.faces.body.font, outputPath);
|
|
3339
|
+
await writeWithEastAsianFaces(pptx, S.faces.body.font, outputPath, Object.values(S.faces).filter((f) => f.ea), Boolean(S.pack.shadow));
|
|
3544
3340
|
return outputPath;
|
|
3545
3341
|
}
|
|
3546
3342
|
|
|
@@ -3549,10 +3345,21 @@ async function renderPack(resolved, templateObj, outputPath) {
|
|
|
3549
3345
|
* Chart text otherwise carries only a Latin face, and a renderer sets Hangul axis labels in
|
|
3550
3346
|
* whatever system font it finds. Korean runs are tagged ko-KR on the way (see writeTagged).
|
|
3551
3347
|
*/
|
|
3552
|
-
async function writeWithEastAsianFaces(pptx, face, outputPath) {
|
|
3553
|
-
await shared().writeTagged(pptx, outputPath, (name, xml) =>
|
|
3554
|
-
|
|
3555
|
-
|
|
3348
|
+
async function writeWithEastAsianFaces(pptx, face, outputPath, latinOnly = [], hardShadows = false) {
|
|
3349
|
+
await shared().writeTagged(pptx, outputPath, (name, xml) => {
|
|
3350
|
+
if (/^ppt\/(charts\/chart\d+|theme\/theme\d+)\.xml$/u.test(name)) {
|
|
3351
|
+
xml = xml.replace(/<a:ea typeface=""\/>/gu, `<a:ea typeface="${face}"/>`).replace(/(<a:latin typeface="([^"]*)"\/>)(?!<a:ea)/gu, `$1<a:ea typeface="$2"/>`);
|
|
3352
|
+
}
|
|
3353
|
+
// A Latin-only face names its East Asian face for the Hangul in its runs.
|
|
3354
|
+
if (/^ppt\/(slides\/slide\d+|charts\/chart\d+)\.xml$/u.test(name)) {
|
|
3355
|
+
for (const f of latinOnly) xml = xml.split(`<a:ea typeface="${f.font}"`).join(`<a:ea typeface="${f.ea}"`);
|
|
3356
|
+
}
|
|
3357
|
+
// pptxgenjs reads a blur of 0 as "unset" and writes its default; a pack's shadow is drawn with none.
|
|
3358
|
+
if (hardShadows && /^ppt\/slides\/slide\d+\.xml$/u.test(name)) {
|
|
3359
|
+
xml = xml.replace(/(<a:outerShdw\b[^>]*?\bblurRad=")\d+"/gu, '$10"');
|
|
3360
|
+
}
|
|
3361
|
+
return xml;
|
|
3362
|
+
});
|
|
3556
3363
|
}
|
|
3557
3364
|
|
|
3558
3365
|
module.exports = { renderPack, PLANS, COVERS, SECTIONS };
|