From 1412d72fc3dc5eee48aee453a784d524f488ea07 Mon Sep 17 00:00:00 2001 From: Brad Estey Date: Sat, 26 Sep 2026 19:08:37 -0400 Subject: [PATCH] Add optional background color and masking to panel definitions. --- README.md | 34 +++++++++++++++++++++++++++- src/lexicons/work.sequential.defs.ts | 33 +++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 11a21d2..8ed353d 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,25 @@ Panel positions, versions and credits are shared by every language, so translati Length limits count graphemes (visible characters) rather than bytes, so CJK text gets the same room as English. +### Guided Reading + +Pages and strips can be read panel by panel. Each panel in `panels` has three optional pieces that control how an app shows it: + +- **`bounds`**: a simple rectangle around the panel. The app pans and zooms to fit it in the viewport. +- **`background`**: fills the viewport around the panel when the panel's aspect ratio doesn't match the viewport's. It's a color plus an opacity (`a`, 0–100). At `0`, the app pans to the panel and the surrounding page stays visible. At `100`, the page is hidden behind a solid color so the panel stands on its own. Values in between dim the page. Omitting it means fully transparent. +- **`mask`**: a black and white PNG of the panel's exact shape, for round or irregular panels, or art that bleeds past the panel border. White shows the art, black hides it, and grays blend for soft edges. + +The mask covers the panel's `bounds`, at the same size or at least the same aspect ratio. Draw the bounds around everything the panel shows, including art that bleeds past its border, and the mask trims that rectangle to the panel's real shape. A mask needs bounds to line up with, so apps ignore it on panels without bounds. Changing a panel's bounds means exporting a new mask. + +```json +{ + "bounds": { "x": 1200, "y": 800, "width": 4000, "height": 4000 }, + "background": { "r": 0, "g": 0, "b": 0, "a": 100 }, + "mask": { "$type": "blob", "mimeType": "image/png", ... }, + "alt": [{ "lang": "en", "text": "A round panel of the moon over the city." }] +} +``` + ### Alt Text Alt text is never stored on an image. It's a list of `defs#localizedAlt` entries, so it can be translated independently of which localized images exist. Slices and covers have an `alt` list on the record. Pages and strips have one on each panel. @@ -297,12 +316,25 @@ Shared definitions. #### `defs#panel` -A panel on a page or strip. It carries the panel's alt text and, optionally, its position for guided panel-by-panel reading. A splash page or single-panel strip is a single panel. Creators can write panel alt text without mapping positions, because `bounds` is optional. +A panel on a page or strip. It carries the panel's alt text and, optionally, how to show it during guided reading. A splash page or single-panel strip is a single panel. Every field is optional, so creators can write panel alt text without mapping positions. See [Guided Reading](#guided-reading). | Property | Type | Required | Description | | --- | --- | --- | --- | | `bounds` | `defs#bounds` | no | Where the panel sits on the image. | | `alt` | array of `defs#localizedAlt` | no | Localized alt text for the panel. | +| `background` | `defs#rgba` | no | Fills the viewport around the panel. Opacity `0` keeps the surrounding page visible, `100` hides it behind a solid color. Fully transparent when omitted. | +| `mask` | blob (png, max 4 MiB) | no | Black and white mask of the panel's exact shape, covering the panel's `bounds`. White shows, black hides. Ignored without `bounds`. | + +#### `defs#rgba` + +A color with opacity. + +| Property | Type | Required | Description | +| --- | --- | --- | --- | +| `r` | integer (0–255) | yes | Red. | +| `g` | integer (0–255) | yes | Green. | +| `b` | integer (0–255) | yes | Blue. | +| `a` | integer (0–100) | yes | Opacity, from 0 (fully transparent) to 100 (fully opaque). | #### `defs#bounds` diff --git a/src/lexicons/work.sequential.defs.ts b/src/lexicons/work.sequential.defs.ts index e7ab745..ff47cfa 100644 --- a/src/lexicons/work.sequential.defs.ts +++ b/src/lexicons/work.sequential.defs.ts @@ -105,10 +105,43 @@ export const workSequentialDefs = lx.lexicon('work.sequential.defs', { }), alt: lx.array(lx.ref('work.sequential.defs#localizedAlt'), { description: 'Localized alt text for this panel: describes the art and transcribes the dialogue, captions and sound effects.' + }), + background: lx.ref('#rgba', { + description: 'Fills the viewport around the panel during guided reading, when the panel\'s aspect ratio differs from the viewport\'s. The alpha is the opacity: 0 pans to the panel with the surrounding page still visible, 100 hides the page behind a solid color so the panel stands on its own. Treated as fully transparent when omitted.' + }), + mask: lx.blob({ + maxSize: 4 * MiB, + accept: ['image/png'], + description: 'Black and white mask of the panel\'s exact shape, for panels that aren\'t rectangles or whose art bleeds past the panel border. Covers the panel\'s bounds, at the same size or aspect ratio, so the bounds must enclose everything the mask shows. Ignored when bounds are omitted. White shows the art, black hides it, and grays blend for soft edges.' }) }, { description: 'A panel on a page or strip. Carries the image\'s alt text, one description per panel, and optionally its position for guided (panel-by-panel) reading. A splash page or single-panel strip is a single panel.' }), + rgba: lx.object({ + r: lx.integer({ + minimum: 0, + maximum: 255, + required: true + }), + g: lx.integer({ + minimum: 0, + maximum: 255, + required: true + }), + b: lx.integer({ + minimum: 0, + maximum: 255, + required: true + }), + a: lx.integer({ + minimum: 0, + maximum: 100, + required: true, + description: 'Opacity, from 0 (fully transparent) to 100 (fully opaque).' + }) + }, { + description: 'A color with opacity.' + }), bounds: lx.object({ x: lx.integer({ minimum: 0, -- 2.54.0