@codename/deck 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,41 @@ export declare function kitFromTemplate(bytes: Uint8Array, { name }?: {
9
9
  kit: import('./kit.js').Kit;
10
10
  problems: string[];
11
11
  }>;
12
+ export type Art = {
13
+ scheme: Record<string, number[] | null>;
14
+ map: Record<string, string>;
15
+ rels: Map<string, string>;
16
+ themeRels: Map<string, string>;
17
+ files: Map<string, () => Promise<Uint8Array>>;
18
+ size: number[];
19
+ styles: {
20
+ fill: string[];
21
+ line: string[];
22
+ bg: string[];
23
+ };
24
+ faces: {
25
+ major: string;
26
+ minor: string;
27
+ };
28
+ pictures: Map<string, Promise<string | null>>;
29
+ used: Set<string>;
30
+ noted: Set<string>;
31
+ ids: {
32
+ n: number;
33
+ };
34
+ defs: string[];
35
+ group?: string;
36
+ };
37
+ export type Part = {
38
+ name: string;
39
+ tag: string;
40
+ inner: string;
41
+ xml: string;
42
+ };
43
+ export type Colour = {
44
+ rgb: number[];
45
+ alpha: number;
46
+ };
12
47
  /**
13
48
  * A kit's tokens in the W3C's Design Tokens format (DTCG 2025.10), as design tools read them: its colours light and
14
49
  * dark, its chart colours and its faces; `tokensFrom()` reads them back. @param {Record<string, any>} tokens
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.0";
1
+ export declare const VERSION = "0.3.0";
package/docs/host.md CHANGED
@@ -40,9 +40,9 @@ Every block that can be changed by hand carries `data-edit="from-to"`, the offse
40
40
  const { blob, report } = await toPptx(view, { PptxGenJS, JSZip })
41
41
  ```
42
42
 
43
- A kit from a PowerPoint template, and a kit's tokens out, are `@codename/deck/template`'s (`kitFromTemplate`, `tokensToDtcg`, [kits.md](kits.md)), which a host imports only when it reads or writes one; `logoOf` and `CORNERS` check a kit's logo and its corner before it is kept.
43
+ A kit from a PowerPoint template, and a kit's tokens out, are `@codename/deck/template`'s (`kitFromTemplate`, `tokensToDtcg`, [kits.md](kits.md)), which a host imports only when it reads or writes one; `logoOf` and `CORNERS` check a kit's logo and its corner before it is kept, and `rgbOf` a colour as deck reads it.
44
44
 
45
- PptxGenJS 4 (its bundle carries JSZip) is the host's to load, on demand. The file is made from the deck as drawn: a slide layout per deck layout, named by its label in the deck's language, with a placeholder for its title and each slot; lists as lists with their levels; tables; bars, lines, areas, pies, rings, points and radars as native charts with their numbers, other charts and diagrams as pictures, and pictures, alone on their line or beside the words, as pictures; notes; transitions; the kit's colours as the theme. `report` counts slides, charts, tables, pictures and placeholders.
45
+ PptxGenJS 4 (its bundle carries JSZip) is the host's to load, on demand. The file is made from the deck as drawn: a slide layout per deck layout, named by its label in the deck's language, with a placeholder for its title and each slot; lists as lists with their levels; tables; bars, lines, areas, pies, rings, points and radars as native charts with their numbers, other charts and diagrams as pictures, and pictures, alone on their line or beside the words, as pictures; a layout's art as one picture on its slide layout; notes; transitions; the kit's colours as the theme. `report` counts slides, charts, tables, pictures and placeholders.
46
46
 
47
47
  ## Files
48
48
 
package/docs/kits.md CHANGED
@@ -27,7 +27,7 @@ Flat:
27
27
 
28
28
  | Token | What it is |
29
29
  |---|---|
30
- | `paper`, `ink`, `primary` | Colours, as `#rgb` or `#rrggbb` |
30
+ | `paper`, `ink`, `primary` | Colours, as `#rgb`, `#rrggbb`, `rgb()` or `rgba()` (`rgb(0 85 164 / 50%)` too, or `#rrggbbaa`); one see-through is seen over the paper it is on |
31
31
  | `muted`, `soft` | Optional: secondary words, a pale fill |
32
32
  | `voice` | `calm`, `elegant`, `bold`, `friendly`: the device's own faces for headings and text |
33
33
  | `fonts` | Optional: `{ heading: [...], body: [...] }`, face stacks, over the voice |
@@ -57,15 +57,24 @@ A logo shows in its corner, small, or big where a layout says (`logo`: a cover,
57
57
  | `logo` | `small`, `big`, `none`: how the kit's logo shows | `small`; `big` on `cover`, `intro`, `end`; `none` on `image`, `full`, `none` |
58
58
  | `pptx` | Title Slide, Title and Content, Section Header, Two Content, Comparison, Title Only, Blank, Content with Caption, Picture with Caption | by its slots |
59
59
  | `numbered` | `false` for no slide number | `true` |
60
+ | `art` | Up to 8 names of the kit's `pictures`, drawn over the whole slide, layer over layer, behind the words, as a template draws them | none |
60
61
  | `label` | Words, or words by language: `{ en: …, fr: … }`; PowerPoint's layout is named so | the name |
61
62
  | `intent`, `use_when`, `avoid_when` | Words for the brain choosing a layout | none |
62
63
  | budgets | `required`, `max_words`, `max_chars`, `items: [min, max]`, `item_max_words`, `heading_max_words`, `kind` | none |
63
64
 
65
+ A layout with art is laid out as its template is: its grid edge to edge (its margins are `.` tracks), left to right whatever the language, its number too, the words keeping theirs. In PowerPoint, its art is one picture on its slide layout.
66
+
64
67
  A catalog's `types` are read as `layouts`. A layout given as `false` turns one of deck's own off (`image: false`), but `default`. Budgets are kept with each layout for a linter and for the brain; deck does not check them yet.
65
68
 
69
+ ## Pictures
70
+
71
+ A kit's `pictures` are names (`a-z`, `0-9`, `-`) of pictures inside the kit: PNG, JPEG, GIF, WebP or SVG as a `data:` address, base64, or an SVG written out (`data:image/svg+xml,…`), 3 MB each at most, never an address to fetch. A layout's `art` names them, so a layer many layouts draw (a template's master) is kept once.
72
+
66
73
  ## From a PowerPoint template, and out
67
74
 
68
- `@codename/deck/template`, a module of its own that a host loads only when it needs it, reads a company's template (`.potx`, `.pptx`, or an Office theme, `.thmx`) into a kit: `kitFromTemplate(bytes, { name })` gives `{ kit, problems }`. Its theme's first light is `paper`, its first dark `ink`, its first accent `primary`, its second dark and light `muted` and `soft`, its six accents `series`, its heading and body faces `fonts`; the first picture on its slide master a browser draws is its `logo`, in the corner it sits in. Each slide layout is a layout of the kit by its name: PowerPoint's Title Slide, Title and Content, Section Header, Two Content, Title Only and Blank take the place of `cover`, `default`, `section`, `two-cols-header`, `center` and `none`; the others are added, slugged (`content-with-caption`). Its placeholders become a grid: the title, with the box under it across its columns, the `default` slot; the other boxes `left`, `middle` and `right` side by side, else `s1`, `s2`…, or the names deck gave them when deck wrote the file (`body-right`); the margins are deck's. Its paper is its background's colour by its role, its title `huge` for a title slide's; a layout whose placeholders make no grid is left out, with a reason.
75
+ `@codename/deck/template`, a module of its own that a host loads only when it needs it, reads a company's template (`.potx`, `.pptx`, or an Office theme, `.thmx`) into a kit: `kitFromTemplate(bytes, { name })` gives `{ kit, problems }`. Its theme's first light is `paper`, its first dark `ink`, its first accent `primary`, its second dark and light `muted` and `soft`, its six accents `series`, its heading and body faces `fonts`; the first picture on its slide master a browser draws is its `logo`, in the corner it sits in. Each slide layout is a layout of the kit by its name: PowerPoint's Title Slide, Title and Content, Section Header, Two Content, Title Only and Blank take the place of `cover`, `default`, `section`, `two-cols-header`, `center` and `none`; the others are added, slugged (`content-with-caption`). Its placeholders become a grid: the title, with the box under it across its columns, the `default` slot; the other boxes `left`, `middle` and `right` side by side, else `s1`, `s2`…, or the names deck gave them when deck wrote the file (`body-right`); the margins are deck's. Its paper is its background's colour by its role, its title `huge` for a title slide's, its words aligned as its title's; a layout whose placeholders make no grid is left out, with a reason.
76
+
77
+ Each layout's art is what a slide on it shows besides its placeholders, as SVG layers among the kit's `pictures`: its background where it isn't a colour of the theme (another colour, a gradient, a picture), the master's shapes unless the layout hides them, then its own, in their order. Shapes are PowerPoint's usual ones and any of its own geometry, filled with a colour, a gradient or a picture and outlined as the template says (their style's fill and line where they give none); pictures cropped, see-through and recoloured (duotone, brightness, contrast) as PowerPoint draws them, an SVG beside a picture in its stead; groups scaled into place; a shape's words in its own size, weight, colour and face, in capitals where it says, aligned and wrapped in its box; fields (a slide's number, its date) are deck's. A layout with art keeps its words where the template has them; light words over it are on `ink`; where the master's logo is in its art, deck draws none of its own; and the kit's paper and words by night are its own by day, as the template has one look. Pictures browsers don't draw (EMF, WMF, TIFF) and shapes deck doesn't know are left out, with a reason; so is a picture over 2 MB.
69
78
 
70
79
  `tokensToDtcg(tokens)` writes a kit's tokens in the W3C's Design Tokens format, which `tokensFrom()` reads back.
71
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codename/deck",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Slide decks as one web page: Markdown in Slidev's form inside a .deck.html file any browser presents, a renderer that runs anywhere, kits of layouts and looks, and an editable PowerPoint out",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/deck.css CHANGED
@@ -58,6 +58,13 @@
58
58
  :is(.deck[data-scheme="light"] :is(.deck-slide[data-paper="ink"], .deck-class-invert), .deck[data-scheme="dark"] .deck-slide:is([data-paper="paper"], [data-paper="soft"]):not(.deck-class-invert), .deck[data-fill="dark"] .deck-slide[data-paper="fill"]) .deck-logo.is-dark { display: block; }
59
59
  :is(.deck[data-scheme="light"] :is(.deck-slide[data-paper="ink"], .deck-class-invert), .deck[data-scheme="dark"] .deck-slide:is([data-paper="paper"], [data-paper="soft"]):not(.deck-class-invert), .deck[data-fill="dark"] .deck-slide[data-paper="fill"]) .deck-logo:not(.has-dark, .is-dark) { background: #fff; padding: 0.5cqi; border-radius: 0.5cqi; }
60
60
 
61
+ /* A layout's art (docs/kits.md), as its template draws it, behind words that sit where the template has them: its grid edge to edge, the slide laid out left to right, its number too. */
62
+ .deck-art { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; pointer-events: none; }
63
+ .deck-slide.has-art { direction: ltr; }
64
+ .has-art .deck-canvas { padding: 0; gap: 0; }
65
+ .has-art .deck-slot { padding: 0.5cqi 1cqi; }
66
+ .deck[dir="rtl"] .has-art .deck-slot { direction: rtl; }
67
+
61
68
  /* Layouts from their kit's definition: the paper they are on, where their words sit, how big their heading is. */
62
69
  .deck-slide[data-paper="fill"] { background: var(--deck-fill); color: var(--deck-on-fill); }
63
70
  .deck-slide[data-paper="ink"] { background: var(--deck-ink); color: var(--deck-paper); }
package/src/deck.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // the deck changes or is saved.
9
9
  import { parseDeck } from './parse.js'
10
10
  import { renderSlide, h } from './render.js'
11
- import { tokensOf, schemeOf, VOICES, hex, contrast } from './look.js'
11
+ import { tokensOf, schemeOf, VOICES, rgbOf, contrast } from './look.js'
12
12
  import { mergeKits } from './kit.js'
13
13
  import { stringsFor } from './strings.js'
14
14
  import { Lexer } from 'marked'
@@ -108,7 +108,7 @@ export function createDeck(root, options) {
108
108
  })) root.style.setProperty(`--deck-${name}`, value)
109
109
  root.dataset.scheme = scheme
110
110
  // Whether the primary as a fill is dark, for a logo's dark one there.
111
- root.dataset.fill = ((fill) => (contrast(fill, [255, 255, 255]) > contrast(fill, [0, 0, 0]) ? 'dark' : 'light'))(hex(tokens.primary) ?? [247, 197, 49])
111
+ root.dataset.fill = ((fill) => (contrast(fill, [255, 255, 255]) > contrast(fill, [0, 0, 0]) ? 'dark' : 'light'))(rgbOf(tokens.primary) ?? [247, 197, 49])
112
112
  root.dispatchEvent(new CustomEvent('deck-scheme', { detail: { scheme, colours, tokens } }))
113
113
  }
114
114
  const chooseScheme = () => {
package/src/index.js CHANGED
@@ -3,7 +3,7 @@ export { createDeck, markdownOf } from './deck.js'
3
3
  export { parseDeck } from './parse.js'
4
4
  export { renderSlide } from './render.js'
5
5
  export { STANDARD, CORNERS, mergeKits, layoutOf, logoOf, inferLayout, labelOf, tokensFrom } from './kit.js'
6
- export { tokensOf, schemeOf, LOOKS, readable, contrast } from './look.js'
6
+ export { tokensOf, schemeOf, LOOKS, readable, contrast, rgbOf } from './look.js'
7
7
  export { readDeckFile, writeDeckFile, rendererUrl, escape, unescape } from './file.js'
8
8
  export { toPptx } from './pptx.js'
9
9
  export { defaultBlocks, libraryAt } from './blocks.js'
package/src/kit.js CHANGED
@@ -3,13 +3,14 @@
3
3
  // deck's own kit holds the usual layouts; a host's (an organisation's, a project's) and the deck
4
4
  // file's own `kit:` go over it, in that order. A layout is a grid: its slots by name, the areas
5
5
  // they take, its tracks, how its words sit, what paper it is on, how big its logo shows, and the
6
- // PowerPoint layout it is nearest to; a kit may turn one of the usual layouts off. Its tokens may
7
- // hold a logo, a picture inside the kit and never an address. Everything is checked against a small
8
- // grammar; what fails is dropped, with a reason. Pure.
6
+ // PowerPoint layout it is nearest to, and the art a template draws behind it, layers among the kit's
7
+ // pictures; a kit may turn one of the usual layouts off. Its tokens may hold a logo, a picture inside
8
+ // the kit and never an address, as its pictures are. Everything is checked against a small grammar;
9
+ // what fails is dropped, with a reason. Pure.
9
10
 
10
11
  /** @typedef {{ required?: boolean, max_words?: number, max_chars?: number, items?: [number, number], item_max_words?: number, heading_max_words?: number, kind?: string }} Budget */
11
- /** @typedef {{ label?: Record<string, string> | string, intent?: string, use_when?: string, avoid_when?: string, slots: string[], budgets: Record<string, Budget>, areas: string[][], columns: string[], rows: string[], align: 'start' | 'center' | 'end', justify: 'start' | 'center' | 'end', paper: 'paper' | 'fill' | 'ink' | 'soft', title: 'normal' | 'large' | 'huge', logo: 'big' | 'small' | 'none', pptx: string, numbered: boolean }} Layout */
12
- /** @typedef {{ name?: string, tokens: Record<string, any>, layouts: Record<string, Layout> }} Kit */
12
+ /** @typedef {{ label?: Record<string, string> | string, intent?: string, use_when?: string, avoid_when?: string, slots: string[], budgets: Record<string, Budget>, areas: string[][], columns: string[], rows: string[], align: 'start' | 'center' | 'end', justify: 'start' | 'center' | 'end', paper: 'paper' | 'fill' | 'ink' | 'soft', title: 'normal' | 'large' | 'huge', logo: 'big' | 'small' | 'none', pptx: string, numbered: boolean, art?: string[] }} Layout */
13
+ /** @typedef {{ name?: string, tokens: Record<string, any>, layouts: Record<string, Layout>, pictures: Record<string, string> }} Kit */
13
14
 
14
15
  const NAME = /^[a-z][a-z0-9-]{0,31}$/
15
16
  const TRACK = /^(auto|min-content|max-content|\d{1,2}(\.\d)?fr|\d{1,3}%)$/
@@ -24,6 +25,12 @@ export const CORNERS = ['top-end', 'top-start', 'bottom-end', 'bottom-start']
24
25
  */
25
26
  export const logoOf = (value) => (typeof value === 'string' && value.length <= 700_000 && /^data:image\/(png|jpeg|gif|webp|svg\+xml);base64,[a-z0-9+/]+=*$/i.test(value) ? value : null)
26
27
 
28
+ /**
29
+ * A picture of a kit's, a layer of a layout's art drawn behind its words over the whole slide: a picture inside the kit as a
30
+ * logo is, or an SVG written out (`data:image/svg+xml,…`), of 3 MB at most, never an address to fetch; else null. @param {unknown} value
31
+ */
32
+ export const artOf = (value) => (typeof value === 'string' && value.length <= 3_145_728 && /^data:image\/(?:(?:png|jpeg|gif|webp|svg\+xml);base64,[a-z0-9+/]+=*|svg\+xml,[\w.!~*'()%/+=-]*)$/i.test(value) ? value : null)
33
+
27
34
  /** The names brains give the usual layouts, for the ones deck has. */
28
35
  export const ALIASES = /** @type {Record<string, string>} */ ({
29
36
  title: 'cover', 'title-slide': 'cover', 'two-columns': 'two-cols', 'two-column': 'two-cols', columns: 'two-cols', comparison: 'two-cols-header',
@@ -34,6 +41,7 @@ export const ALIASES = /** @type {Record<string, string>} */ ({
34
41
  export const STANDARD = /** @type {Kit} */ ({
35
42
  name: 'standard',
36
43
  tokens: {},
44
+ pictures: {},
37
45
  layouts: Object.fromEntries(Object.entries(/** @type {Record<string, any>} */ ({
38
46
  default: { label: { en: 'Title and content', fr: 'Titre et contenu' }, pptx: 'Title and Content' },
39
47
  cover: { label: { en: 'Cover', fr: 'Couverture' }, justify: 'center', paper: 'fill', title: 'huge', logo: 'big', pptx: 'Title Slide', numbered: false },
@@ -98,12 +106,14 @@ export function layoutOf(given) {
98
106
  return value
99
107
  }
100
108
  const label = typeof given.label === 'string' || (given.label && typeof given.label === 'object') ? given.label : undefined
109
+ const art = Array.isArray(given.art) && given.art.length <= 8 && given.art.every((/** @type {unknown} */ one) => typeof one === 'string' && NAME.test(one)) ? given.art : undefined
110
+ if (given.art !== undefined && !art) problems.push('art is not 8 names at most')
101
111
  const words = (/** @type {any} */ v) => (typeof v === 'string' ? v.slice(0, 300) : undefined)
102
112
  return {
103
113
  layout: /** @type {Layout} */ ({
104
114
  label, intent: words(given.intent), use_when: words(given.use_when), avoid_when: words(given.avoid_when), slots, budgets, areas: rows, columns, rows: rowTracks,
105
115
  align: pick('align', 'start'), justify: pick('justify', 'start'), paper: pick('paper', 'paper'), title: pick('title', 'normal'), logo: pick('logo', 'small'),
106
- pptx: PPTX.includes(given.pptx) ? given.pptx : slots.length > 1 ? 'Comparison' : 'Title and Content', numbered: given.numbered !== false,
116
+ pptx: PPTX.includes(given.pptx) ? given.pptx : slots.length > 1 ? 'Comparison' : 'Title and Content', numbered: given.numbered !== false, art,
107
117
  }),
108
118
  problems,
109
119
  }
@@ -123,14 +133,15 @@ function clean(given) {
123
133
  /**
124
134
  * Design tokens as the W3C's Design Tokens format (DTCG 2025.10) writes them, read into a kit's:
125
135
  * `color.light` and `color.dark` groups (bg, surface, fg, muted, accent) or flat colours, `font.sans`
126
- * and the like; each `$value` a hex string or a colour object with its `hex`. Tokens already flat
136
+ * and the like; each `$value` a colour string or a colour object with its `hex` and `alpha`. Tokens already flat
127
137
  * pass through. @param {any} given @returns {Record<string, any>}
128
138
  */
129
139
  export function tokensFrom(given) {
130
140
  if (!given || typeof given !== 'object') return {}
131
141
  if (!JSON.stringify(given).includes('"$value"')) return given
132
142
  const value = (/** @type {any} */ t) => (t && typeof t === 'object' && '$value' in t ? t.$value : t)
133
- const colour = (/** @type {any} */ t) => { const v = value(t); return typeof v === 'string' ? v : typeof v?.hex === 'string' ? v.hex : undefined }
143
+ // A colour object's alpha kept, as #rrggbbaa.
144
+ const colour = (/** @type {any} */ t) => { const v = value(t); return typeof v === 'string' ? v : typeof v?.hex === 'string' ? `${v.hex}${typeof v.alpha === 'number' && v.alpha < 1 ? Math.round(Math.max(0, v.alpha) * 255).toString(16).padStart(2, '0') : ''}` : undefined }
134
145
  const face = (/** @type {any} */ t) => { const v = value(t); return Array.isArray(v) ? v.map(String) : typeof v === 'string' ? [v] : undefined }
135
146
  const NAMES = /** @type {const} */ ({ bg: 'paper', background: 'paper', paper: 'paper', fg: 'ink', text: 'ink', ink: 'ink', accent: 'primary', primary: 'primary', muted: 'muted', surface: 'soft', soft: 'soft' })
136
147
  const group = (/** @type {any} */ g) => Object.fromEntries(Object.entries(g ?? {}).filter(([k]) => k in NAMES).map(([k, t]) => [NAMES[/** @type {keyof typeof NAMES} */ (k)], colour(t)]).filter(([, v]) => v))
@@ -146,13 +157,13 @@ export function tokensFrom(given) {
146
157
  }
147
158
 
148
159
  /**
149
- * Kits merged, the later over the earlier: their tokens, their dark ones within theirs, and their layouts by name, each
150
- * checked; a layout given as false is taken away, but the default. A logo that isn't a picture inside the kit is dropped.
151
- * @param {...any} kits @returns {{ kit: Kit, problems: string[] }}
160
+ * Kits merged, the later over the earlier: their tokens, their dark ones within theirs, their layouts by name and their
161
+ * pictures, each checked; a layout given as false is taken away, but the default. A logo that isn't a picture inside the
162
+ * kit is dropped. @param {...any} kits @returns {{ kit: Kit, problems: string[] }}
152
163
  */
153
164
  export function mergeKits(...kits) {
154
165
  /** @type {Kit} */
155
- const kit = { name: 'standard', tokens: {}, layouts: { ...STANDARD.layouts } }
166
+ const kit = { name: 'standard', tokens: {}, layouts: { ...STANDARD.layouts }, pictures: {} }
156
167
  const problems = []
157
168
  for (const given of kits) {
158
169
  if (!given || typeof given !== 'object') continue
@@ -161,6 +172,10 @@ export function mergeKits(...kits) {
161
172
  const next = tokensFrom(given.tokens), dark = next.dark && typeof next.dark === 'object' ? { ...kit.tokens.dark, ...next.dark } : kit.tokens.dark
162
173
  kit.tokens = { ...kit.tokens, ...next, ...(dark && { dark }) }
163
174
  }
175
+ for (const [name, picture] of Object.entries(given.pictures ?? {})) {
176
+ if (NAME.test(name) && artOf(picture)) kit.pictures[name] = picture
177
+ else problems.push(`picture ${name}: not a picture inside the kit`)
178
+ }
164
179
  for (const [name, definition] of Object.entries(given.layouts ?? given.types ?? {})) {
165
180
  if (!NAME.test(name)) { problems.push(`layout ${name}: not a-z, 0-9 and -`); continue }
166
181
  if (definition === false) { if (name !== 'default') delete kit.layouts[name]; continue }
package/src/look.js CHANGED
@@ -41,12 +41,12 @@ export function tokensOf(settings, project = {}) {
41
41
  function clean(given) {
42
42
  /** @type {Partial<Tokens>} */
43
43
  const kept = {}
44
- for (const key of /** @type {const} */ (['paper', 'ink', 'primary', 'muted', 'soft'])) if (typeof given[key] === 'string' && hex(given[key])) kept[key] = given[key]
45
- if (given.dark && typeof given.dark === 'object') kept.dark = Object.fromEntries(Object.entries(given.dark).filter(([, v]) => typeof v === 'string' && hex(v)))
44
+ for (const key of /** @type {const} */ (['paper', 'ink', 'primary', 'muted', 'soft'])) if (typeof given[key] === 'string' && rgbOf(given[key])) kept[key] = given[key]
45
+ if (given.dark && typeof given.dark === 'object') kept.dark = Object.fromEntries(Object.entries(given.dark).filter(([, v]) => typeof v === 'string' && rgbOf(v)))
46
46
  if (given.fonts && Array.isArray(given.fonts.heading) && Array.isArray(given.fonts.body)) kept.fonts = { heading: given.fonts.heading.map(String), body: given.fonts.body.map(String) }
47
47
  if (typeof given.voice === 'string') kept.voice = given.voice
48
48
  if (typeof given.radius === 'number') kept.radius = given.radius
49
- if (Array.isArray(given.series) && given.series.length && given.series.every((/** @type {unknown} */ c) => typeof c === 'string' && hex(c))) kept.series = given.series.slice(0, 6)
49
+ if (Array.isArray(given.series) && given.series.length && given.series.every((/** @type {unknown} */ c) => typeof c === 'string' && rgbOf(c))) kept.series = given.series.slice(0, 6)
50
50
  return kept
51
51
  }
52
52
 
@@ -55,30 +55,36 @@ function clean(given) {
55
55
  * @param {Tokens} tokens @param {'light' | 'dark'} scheme @returns {Scheme}
56
56
  */
57
57
  export function schemeOf(tokens, scheme) {
58
- const light = hex(tokens.paper) ?? [255, 253, 248], dark = hex(tokens.ink) ?? [29, 27, 22]
58
+ // A see-through colour is seen over the paper it is on.
59
+ const light = rgbOf(tokens.paper) ?? [255, 253, 248], dark = rgbOf(tokens.ink, light) ?? [29, 27, 22]
59
60
  // A kit's own dark colours where it gives them (a design system's dark mode), else worked out.
60
61
  const own = scheme === 'dark' ? tokens.dark ?? {} : {}
61
- const paper = round(hex(own.paper ?? '') ?? (scheme === 'dark' ? mix(dark, [0, 0, 0], 0.35) : light))
62
- const ink = round(hex(own.ink ?? '') ?? (scheme === 'dark' ? mix(light, [255, 255, 255], 0.2) : dark))
63
- const primary = hex(own.primary ?? tokens.primary) ?? [247, 197, 49]
62
+ const paper = round(rgbOf(own.paper ?? '', scheme === 'dark' ? [0, 0, 0] : [255, 255, 255]) ?? (scheme === 'dark' ? mix(dark, [0, 0, 0], 0.35) : light))
63
+ const ink = round(rgbOf(own.ink ?? '', paper) ?? (scheme === 'dark' ? mix(light, [255, 255, 255], 0.2) : dark))
64
+ const primary = rgbOf(own.primary ?? tokens.primary, paper) ?? [247, 197, 49]
64
65
  // The primary as a fill keeps its own colour; as words or marks it is made to read on the paper.
65
66
  const text = readable(primary, paper, 4.5)
66
67
  const onPrimary = contrast(ink, primary) >= contrast(paper, primary) ? ink : paper
67
68
  // A chart's colours: the kit's own (a template's accents), else the primary turned round the wheel; each made to read at 3:1.
68
- const given = (tokens.series ?? []).map(hex).filter((c) => c !== null)
69
+ const given = (tokens.series ?? []).map((c) => rgbOf(c, paper)).filter((c) => c !== null)
69
70
  const series = [0, 1, 2, 3, 4, 5].map((n) => readable(/** @type {number[]} */ (given[n % (given.length || 1)] ?? rotate(primary, n * 57)), paper, 3))
70
71
  return {
71
- paper: css(paper), ink: css(ink), muted: css(readable(round(hex(own.muted ?? (scheme === 'light' ? tokens.muted ?? '' : '')) ?? mix(ink, paper, 0.38)), paper, 4.5)), line: css(mix(ink, paper, 0.82)),
72
- primary: css(text), onPrimary: css(onPrimary), soft: css(hex(own.soft ?? (scheme === 'light' ? tokens.soft ?? '' : '')) ?? mix(primary, paper, 0.78)), series: series.map(css),
72
+ paper: css(paper), ink: css(ink), muted: css(readable(round(rgbOf(own.muted ?? (scheme === 'light' ? tokens.muted ?? '' : ''), paper) ?? mix(ink, paper, 0.38)), paper, 4.5)), line: css(mix(ink, paper, 0.82)),
73
+ primary: css(text), onPrimary: css(onPrimary), soft: css(rgbOf(own.soft ?? (scheme === 'light' ? tokens.soft ?? '' : ''), paper) ?? mix(primary, paper, 0.78)), series: series.map(css),
73
74
  }
74
75
  }
75
76
 
76
- /** @param {string} value @returns {[number, number, number] | null} */
77
- export function hex(value) {
78
- const m = /^#?([\da-f]{3}|[\da-f]{6})$/i.exec(String(value).trim())
79
- if (!m) return null
80
- const h = m[1].length === 3 ? [...m[1]].map((c) => c + c).join('') : m[1]
81
- return /** @type {[number, number, number]} */ ([0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)))
77
+ /**
78
+ * A colour as deck reads one, as red, green and blue: `#rgb` or `#rrggbb` (`#rgba`, `#rrggbbaa` with an alpha), `rgb()` or
79
+ * `rgba()` as CSS writes them, by commas or spaces, the alpha after a comma or a slash; one see-through is seen over `under`,
80
+ * white unless said. @param {string} value @param {number[]} [under] @returns {[number, number, number] | null}
81
+ */
82
+ export function rgbOf(value, under = [255, 255, 255]) {
83
+ const text = String(value).trim(), digits = /^#?([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.exec(text)?.[1], css = /^rgba?\(([^)]*)\)$/i.exec(text)?.[1].split(/[\s,/]+/).filter(Boolean)
84
+ const share = (/** @type {string} */ part, /** @type {number} */ whole) => (part.endsWith('%') ? (parseFloat(part) / 100) * whole : Number(part))
85
+ const [r, g, b, a = 1] = digits ? (digits.length < 5 ? [...digits].map((c) => c + c) : /** @type {string[]} */ (digits.match(/../g))).map((h, i) => parseInt(h, 16) / (i > 2 ? 255 : 1))
86
+ : css?.length === 3 || css?.length === 4 ? css.map((part, i) => share(part, i > 2 ? 1 : 255)) : []
87
+ return r !== undefined && [r, g, b].every((c) => c >= 0 && c <= 255) && a >= 0 && a <= 1 ? round(mix(under, [r, g, b], a)) : null
82
88
  }
83
89
 
84
90
  /** @param {number[]} rgb */
package/src/pptx.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // each of its slots, so a slide added there looks like the deck's; words as PowerPoint's own text,
4
4
  // a list's levels as its levels; a table as a table; a chart of bars, lines, areas, pies, rings,
5
5
  // points or a radar as PowerPoint's own chart, its numbers in the file to change; anything else (a
6
- // diagram, another chart, a picture) as a picture; the speaker's notes as notes; each slide's
6
+ // diagram, another chart, a picture) as a picture, a layout's art too, on its slide layout; the speaker's notes as notes; each slide's
7
7
  // transition as PowerPoint's (Morph where it has one, a fade where it hasn't); the look's colours
8
8
  // as the file's theme. Read from the deck as drawn, each box where the browser put it, so the file
9
9
  // looks like the slides, in the scheme shown. PptxGenJS (with its JSZip) is the host's to load.
@@ -72,6 +72,9 @@ export async function toPptx(view, { PptxGenJS, JSZip }) {
72
72
  const masterName = `${definition ? labelOf(definition, layout, view.lang) : layout}${element.classList.contains('deck-class-invert') ? ' (dark)' : ''}`
73
73
  if (!masters.has(masterName)) {
74
74
  const objects = []
75
+ // The layout's art, its layers as one picture on its paper behind the rest: a slide added in PowerPoint has it too.
76
+ const art = [...element.querySelectorAll('img.deck-art')].map((one) => /** @type {HTMLImageElement} */ (one).src), backdrop = art.length && await pictureOf(art, W * 150, H * 150, false, style.backgroundColor)
77
+ if (backdrop) { objects.push({ image: { data: backdrop, x: 0, y: 0, w: W, h: H } }); report.pictures++ }
75
78
  if (title) objects.push({ placeholder: { options: { name: 'title', type: 'title', ...across(title), valign: 'bottom', margin: 0, fontFace: pres.theme.headFontFace, color: hexOf(getComputedStyle(title).color), align: rtl ? 'right' : 'left' }, text: '' } })
76
79
  for (const slot of slots) {
77
80
  const r = place(slot)
@@ -296,21 +299,34 @@ function transitionXml(kind, rtl) {
296
299
  return '<p:transition spd="med"><p:fade/></p:transition>'
297
300
  }
298
301
 
299
- /** A picture's data as PNG, drawn at a size; null when it can't be read (another site's, unshared). @param {string} src @param {number} w @param {number} h @param {boolean} [cover] */
300
- async function pictureOf(src, w, h, cover = false) {
302
+ /**
303
+ * A picture's data as PNG, drawn at a size, or a layout's art, its layers, as JPEG on its paper; null when it can't be read
304
+ * (another site's, unshared). @param {string | string[]} src @param {number} w @param {number} h @param {boolean} [cover] @param {string} [paper]
305
+ */
306
+ async function pictureOf(src, w, h, cover = false, paper = '') {
301
307
  try {
302
- const img = new Image()
303
- img.crossOrigin = 'anonymous'
304
- img.src = src
305
- await img.decode()
308
+ const images = await Promise.all([src].flat().map(async (one) => {
309
+ const img = new Image()
310
+ img.crossOrigin = 'anonymous'
311
+ img.src = one
312
+ await img.decode()
313
+ return img
314
+ }))
315
+ const img = images[0]
306
316
  const canvas = document.createElement('canvas')
307
317
  canvas.width = Math.max(1, Math.round(w))
308
318
  canvas.height = Math.max(1, Math.round(h))
309
319
  const ctx = /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'))
310
320
  const iw = img.naturalWidth || w, ih = img.naturalHeight || h
311
321
  const scale = cover ? Math.max(canvas.width / iw, canvas.height / ih) : Math.min(canvas.width / iw, canvas.height / ih)
312
- ctx.drawImage(img, (canvas.width - iw * scale) / 2, (canvas.height - ih * scale) / 2, iw * scale, ih * scale)
313
- return canvas.toDataURL('image/png')
322
+ if (!paper) ctx.drawImage(img, (canvas.width - iw * scale) / 2, (canvas.height - ih * scale) / 2, iw * scale, ih * scale)
323
+ else {
324
+ // Art, stretched over the slide as it is drawn there, on its paper, as a JPEG: photographs stay small.
325
+ ctx.fillStyle = paper
326
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
327
+ for (const layer of images) ctx.drawImage(layer, 0, 0, canvas.width, canvas.height)
328
+ }
329
+ return canvas.toDataURL(paper ? 'image/jpeg' : 'image/png', 0.9)
314
330
  } catch {
315
331
  return null
316
332
  }
package/src/render.js CHANGED
@@ -43,11 +43,14 @@ export function renderSlide(slide, context) {
43
43
  const counter = { step: 0 }
44
44
  const slots = new Map(slide.slots.map((slot) => [slot.name, slot]))
45
45
  const names = definition.slots
46
+ const art = (definition.art ?? []).map((name) => context.kit.pictures?.[name]).filter(Boolean)
46
47
  const section = h('section', {
47
- class: ['deck-slide', `deck-layout-${layout}`, ...classes.map((name) => `deck-class-${name}`)].join(' '),
48
+ class: ['deck-slide', `deck-layout-${layout}`, ...classes.map((name) => `deck-class-${name}`), ...(art.length ? ['has-art'] : [])].join(' '),
48
49
  'data-index': slide.index, 'data-layout': layout, 'data-paper': definition.paper, 'data-align': definition.align, 'data-justify': definition.justify, 'data-title': definition.title,
49
50
  'aria-roledescription': context.slide, role: 'group',
50
51
  })
52
+ // Its art, as its template draws it, layer over layer behind the rest.
53
+ for (const src of art) section.append(h('img', { class: 'deck-art', src, alt: '', 'aria-hidden': 'true' }))
51
54
  // Its grid, from the layout's definition, through the CSSOM: areas named s-<slot>, as `default` is no name CSS allows.
52
55
  const canvas = h('div', { class: 'deck-canvas' })
53
56
  canvas.style.setProperty('grid-template-areas', definition.areas.map((row) => `"${row.map((cell) => (cell === '.' ? '.' : `s-${cell}`)).join(' ')}"`).join(' '))