@oim-media/spec 1.27.8 → 1.27.10
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/BINARY_SPEC.md +190 -186
- package/CANONICAL_SPEC.md +409 -409
- package/LICENSE +55 -55
- package/NOTICE +6 -6
- package/README.md +458 -444
- package/action-cases.json +13 -13
- package/artistic-text-cases.json +690 -690
- package/canonical-cases.json +6 -0
- package/keyframe-cases.json +32 -32
- package/keys.json +22 -22
- package/marks-cases.json +34 -34
- package/package.json +2 -1
- package/pattern-cases.json +552 -552
- package/schema/oim.schema.json +2920 -2920
- package/schema/oim2.schema.json +2763 -0
- package/schema/ovm.schema.json +2701 -2701
- package/text-effect-cases.json +835 -835
- package/tools/parity-core.mjs +122 -122
- package/tools/parity.mjs +86 -86
- package/vectors/README.md +105 -103
- package/vectors/accessibility.expected.json +67 -67
- package/vectors/artistic-text.expected.json +84 -84
- package/vectors/asymmetric-borders.expected.json +29 -29
- package/vectors/background-pattern-order.expected.json +95 -95
- package/vectors/basic-text.expected.json +29 -29
- package/vectors/color-blending-opacity.expected.json +47 -47
- package/vectors/crop.expected.json +32 -32
- package/vectors/embedded-font-woff.expected.json +57 -57
- package/vectors/extreme-aspect-ratios.expected.json +29 -29
- package/vectors/filter-effects.expected.json +33 -33
- package/vectors/flip.expected.json +47 -47
- package/vectors/frame-shapes.expected.json +73 -73
- package/vectors/full-monty-composite.expected.json +96 -96
- package/vectors/golden/exceptions.json +3 -3
- package/vectors/golden/mask-window.web.png +0 -0
- package/vectors/golden/zone-group.web.png +0 -0
- package/vectors/groups-and-parallax.expected.json +90 -90
- package/vectors/image-fill.expected.json +54 -54
- package/vectors/interactive-chained-actions.expected.json +77 -77
- package/vectors/interactive-hover.expected.json +36 -36
- package/vectors/interactive.expected.json +121 -121
- package/vectors/layer-blur.expected.json +48 -48
- package/vectors/layer-grain.expected.json +52 -52
- package/vectors/layer-ordering-zindex.expected.json +62 -62
- package/vectors/legacy-text-chunk.expected.json +29 -29
- package/vectors/mask-window.expected.json +45 -0
- package/vectors/mask-window.oim.png +0 -0
- package/vectors/media-filters.expected.json +50 -50
- package/vectors/multi-container-layout.expected.json +45 -45
- package/vectors/multilang-cjk-rtl.expected.json +30 -30
- package/vectors/nested-groups.expected.json +68 -68
- package/vectors/palettes.expected.json +57 -57
- package/vectors/parallax-infinite.expected.json +39 -39
- package/vectors/pattern-color-roles.expected.json +68 -68
- package/vectors/reedit-english.expected.json +45 -45
- package/vectors/reveal-and-play.expected.json +107 -107
- package/vectors/shadow-effects.expected.json +34 -34
- package/vectors/shape-shadows.expected.json +62 -62
- package/vectors/strokes-and-borders.expected.json +31 -31
- package/vectors/text-runs-precalculated.expected.json +46 -46
- package/vectors/transparent-background.expected.json +30 -30
- package/vectors/typography-styling.expected.json +34 -34
- package/vectors/zone-group.expected.json +46 -0
- package/vectors/zone-group.oim.png +0 -0
package/CANONICAL_SPEC.md
CHANGED
|
@@ -1,409 +1,409 @@
|
|
|
1
|
-
# OIM (
|
|
2
|
-
|
|
3
|
-
**Contract version:** 1.27.
|
|
4
|
-
**Date:** 2026-11-02 (reviewed against `packages/core/src/schema.ts`; the source of truth is the schema, this document summarises it)
|
|
5
|
-
**Status:** Normative / In force
|
|
6
|
-
**License:** Apache-2.0
|
|
7
|
-
**Authority:** Xue Studios LLC & the OIM Architecture Team
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 1. Summary and Objectives of the Canonical Specification
|
|
12
|
-
|
|
13
|
-
The **`.oim`** format defines a multi-platform interactive image specification. The metadata layer (historically called `oimdata` or `oimdata.json`, injected into the `OIM_Data` chunk of the PNG container) uses a **strongly typed, serialisable canonical language**.
|
|
14
|
-
|
|
15
|
-
> **Note on OVM (`.ovm`):** The interactive video format (`ovm.schema.json` and `@oim-media/player-ovm`) is currently a **reserved placeholder pending specification**. The normative specification below governs **OIM (`.oim`)**.
|
|
16
|
-
|
|
17
|
-
### Canonical Design Principles
|
|
18
|
-
|
|
19
|
-
1. **Total independence from web engines (native platforms):**
|
|
20
|
-
- The format **prohibits free or arbitrary CSS** (`custom_loop_css`, `custom_keyframes`, free style strings).
|
|
21
|
-
- Every visual or dynamic attribute is parameterised in canonical types (numbers, enums, tuples or arrays). This allows native implementations on **iOS (SwiftUI / Core Animation)**, **Android (Jetpack Compose / RenderNode)**, **Desktop (Rust / Tauri / Skia)** or Web Components to paint and interact with 1:1 fidelity without requiring an embedded web browser.
|
|
22
|
-
2. **One name per concept (zero ambiguity):**
|
|
23
|
-
- The compiler canonicalises shortcuts and duplicate names before signing:
|
|
24
|
-
- `size` ➔ `fontSize`
|
|
25
|
-
- `font` ➔ `fontFamily`
|
|
26
|
-
- `color` ➔ `fillColor`
|
|
27
|
-
- `bold: true` ➔ `fontWeight: 700`
|
|
28
|
-
- `italic: true` ➔ `fontStyle: "italic"`
|
|
29
|
-
- `underline: true` ➔ `textDecoration: "underline"`
|
|
30
|
-
- `fontSizeOverrides` ➔ unified inside the `fontSizes` map.
|
|
31
|
-
- `reedicion` (`"publico" | "solo_autor" | "sellado"`) ➔ `reedit` (`"public" | "author_only" | "sealed"`, 1.23.0; legacy files are normalised on read via `normalizeLegacyKeys`).
|
|
32
|
-
3. **Determinism and security (zero-trust sandbox):**
|
|
33
|
-
- With no arbitrary CSS injection, attack vectors such as `@import`, covert network requests via `url()`, or breaking out of Shadow DOM isolation are eradicated.
|
|
34
|
-
4. **Portable or not admitted:**
|
|
35
|
-
- Every visual effect is built from primitives that any graphics engine has cheaply — opacity, translation, scale, rotation and rectangular clipping, per unit (letter, word, line or layer). What exists only in a browser (per-letter blur, animated shadows, free `clip-path`) is not admitted, and what was already there is retired or rewritten into portable primitives (`glitchLayers`, `entranceFrameAt`, `revealFrames`).
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## 2. Canonical Layer Types (`slot.type`)
|
|
40
|
-
|
|
41
|
-
Every interactive element in the `slots` array declares a canonical type:
|
|
42
|
-
|
|
43
|
-
| Type (`type`) | Purpose | Canonical Characteristics |
|
|
44
|
-
|---|---|---|
|
|
45
|
-
| `text` | Dynamic multilingual text | Supports localised `content`, sizes per language (`fontSizes`), widths per language (`maxWidths`), text marks (`marks`) and precalculated lines (`textRuns`). |
|
|
46
|
-
| `image` | Secondary or embedded bitmap | Embedded `data:image/*` URI in `content`, framing (`objectFit`, `imageCrop`), mirroring (`flipX`, `flipY`), filters and accessible `alt`/`decorative`. |
|
|
47
|
-
| `lottie` | Lottie vector animation | Embedded vector JSON. |
|
|
48
|
-
| `geo` / `shape` | Pure vector geometry or photo frame | Governed by `style.geoShape` (24 catalog shapes including 12 photo-frame silhouettes), custom `svgPath`/`svgViewBox`, or `fillType: "image"` (`fillImage`, `fillImageCrop`, `fillImageFlipX`, `fillImageFlipY`). |
|
|
49
|
-
| `draw` / `svg` | Free vector strokes | Supports standard SVG paths (`svgRawXml`, `svgPath`, `svgViewBox`, `customBlobPath`). |
|
|
50
|
-
| `artistic-text` | 3D text with parametric extrusion | Vector extrusion (`extrusionDepth`, `extrusionAngle`, `curveAmount`, `curveType`). |
|
|
51
|
-
| `pattern` | Repeating procedural pattern | Canonical presets (`clouds`, `bubbles`, `grid`, `waves`, `stars`, `stripes`, `dots`, `zigzags`, `bolt`, `leaf`, `drop`, `moon`, `crown`, `heart`). |
|
|
52
|
-
| `filter` | Colour adjustment / optical filter layer | Named presets (`filterPreset`) and normalised numeric adjustments (`filterBrightness`, `filterContrast`, `filterSaturate`, `filterHueRotate`, `filterBlur`). |
|
|
53
|
-
| `container` | Bounding box or frame | Supports geometry, fills, borders, shadows and clipping. |
|
|
54
|
-
| `group` | Hierarchical container node | Groups child layers via `style.groupId`. Controls clipping (`slotClipping`) and scene loops (`style.sceneLoop`). |
|
|
55
|
-
| `component` | Composite component instance | Encapsulated grouping of modular design. |
|
|
56
|
-
| `mask` | Clipping or die-cut mask | Clipping by a base shape (`maskShape`, `maskMode`) or multiple vector cutouts (`cuts`). |
|
|
57
|
-
| `zone` | Contextual anchor zone | Delimiter for anchoring slots to relative regions of the image. |
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## 3. Root Structure of the Slot (`slotSchema`)
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
interface OimSlot {
|
|
65
|
-
id: string; // Unique identifier (maximum 128 characters)
|
|
66
|
-
name?: string; // Name assigned by the author (max 256 chars)
|
|
67
|
-
type?: SlotType; // One of the canonical types in section 2
|
|
68
|
-
languages?: string[]; // Languages in which it is shown (default: ["*"])
|
|
69
|
-
pendingTranslation?: string[]; // Language codes pending translation
|
|
70
|
-
align?: "left" | "center" | "right";
|
|
71
|
-
position?: [number, number]; // Normalised coordinates [x, y] (percentage 0.0 to 100.0%)
|
|
72
|
-
width_px?: number; // Virtual width in reference pixels
|
|
73
|
-
height_px?: number; // Virtual height in reference pixels
|
|
74
|
-
content?: Record<string, string>; // Language -> content dictionary (e.g. { "en": "Text" })
|
|
75
|
-
style?: OimStyle; // Canonical style object (see section 4)
|
|
76
|
-
parallax?: OimParallax; // Parallax configuration (see section 5; may also travel in style.parallax)
|
|
77
|
-
interaction?: OimInteraction; // Declared interactions
|
|
78
|
-
tracking_data?: number[][]; // Tracking keyframes [time, x1..y4, cx, cy]
|
|
79
|
-
zone_id?: string; // ID of the containing zone
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## 4. Canonical Style Contract (`styleSchema`)
|
|
86
|
-
|
|
87
|
-
### 4.1. Typography and Text
|
|
88
|
-
- `fontFamily`: String with the font name (max 128 chars).
|
|
89
|
-
- `fontSize`: Pure number or container percentage (e.g. `24` or `"4.5cqw"`).
|
|
90
|
-
- `fontWeight`: Numeric weight (`100` to `900`, `400` = normal, `700` = bold) or string keyword.
|
|
91
|
-
- `fontStyle`: `"normal" | "italic" | "oblique"`.
|
|
92
|
-
- `textDecoration`: `"none" | "underline"` (max 64 chars).
|
|
93
|
-
- `textAlign`: `"left" | "center" | "right" | "justify"`.
|
|
94
|
-
- `lineHeight`: Numeric multiplier factor (e.g. `1.2`, `1.5`) or a bounded string.
|
|
95
|
-
- `letterSpacing`: A CSS length with its unit (`"2px"`, `"0.05em"`). A bare number is not a length: readers ignore it, as the browser does (section 4.8).
|
|
96
|
-
- `maxWidth`: Container percentage string (e.g. `"80cqw"`, `"none"`) or numeric value.
|
|
97
|
-
- `fontSizes`: Map of size specific to each language `{ "es": "3.0cqw", "ja": "2.5cqw" }`.
|
|
98
|
-
- `maxWidths`: Map of maximum width specific to each language `{ "en": "60cqw" }`.
|
|
99
|
-
- `textRole`: Semantic hierarchy hint (`"auto" | "title" | "subtitle" | "body"`).
|
|
100
|
-
- **Text marks (1.11.0, `text_marks` capability):** `content` can wrap phrases in `{name}…{/name}`; literal braces are doubled (`{{`, `}}`). `style.marks` defines each mark with a closed set (`color`/`colorRole`, `bold`, `italic`, `underline`, `highlight{style, color, colorRole, paddingX, paddingY, borderRadius}`). Names `^[a-z][a-z0-9-]{0,23}$`, maximum 8 per layer, not nested. A single interpretation: `parseMarks`, `markStyle` (`@oim-media/core`). The backend does not sign malformed tags (`OIM_E_TEXT_MARKS_INVALID`).
|
|
101
|
-
- `dropCap`: `{ enabled, lines: 2|3, fontFamily?, color?, marginRight? }`. Size is set with `dropCapStyle`; the rest of the text keeps its marks (`splitDropCap`).
|
|
102
|
-
- `textHighlight`: **retired** (1.12.0). Kept read-only for published files; new writers use `marks`.
|
|
103
|
-
- `textRuns`: **Precalculated lines**, only for readers **without** a paragraph layout engine. A reader that can lay out text (web, Flutter) ignores them: the exporter's line breaks can disagree with the real layout (section 4.8):
|
|
104
|
-
```json
|
|
105
|
-
"textRuns": {
|
|
106
|
-
"es": {
|
|
107
|
-
"fontSize": 24,
|
|
108
|
-
"lineHeight": 28,
|
|
109
|
-
"lines": [
|
|
110
|
-
{ "text": "First line of text", "width": 280, "yOffset": 0 },
|
|
111
|
-
{ "text": "Second line, adjusted", "width": 260, "yOffset": 28 }
|
|
112
|
-
]
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### 4.2. Colours, Dynamic Roles and Fills (`cssColor`)
|
|
118
|
-
Colours are validated against the strict `cssColor` pattern (hexadecimal `#RGB` / `#RRGGBB` / `#RRGGBBAA`, functions `rgb()`, `hsl()`, `"transparent"`, `"none"`, or dynamic colour tokens `var(--primary)`):
|
|
119
|
-
- **Dynamic colour roles (`DYNAMIC_COLOR_ROLES`):**
|
|
120
|
-
`"primary" | "on-primary" | "secondary" | "on-secondary" | "surface" | "on-surface" | "outline" | "accent" | "text-primary" | "text-secondary" | "text-muted" | "text-on-accent"`.
|
|
121
|
-
- `fillColor`: Main fill colour (or glyph colour in text layers).
|
|
122
|
-
- `fillType`: `"none" | "color" | "gradient" | "pattern" | "generative" | "image"`.
|
|
123
|
-
- `colorRole` / `fillColorRole` / `strokeColorRole` / `textStrokeColorRole` / `shadowColorRole` / `effectColorRole` / `maskBgColorRole` / `maskStrokeColorRole`: Semantic dynamic theme roles.
|
|
124
|
-
- **Pattern colour roles (1.21.0):** `fillPatternColorRole`, `fillPatternBgColorRole`, `backgroundPatternColorRole`, `backgroundPatternBgColorRole`.
|
|
125
|
-
- `fillGradient`: Canonical linear, radial or conic gradient function (`CSS_GRADIENT_PATTERN`).
|
|
126
|
-
- `fillPatternType`: Repeating pattern preset:
|
|
127
|
-
`"clouds" | "bubbles" | "grid" | "waves" | "stars" | "stripes" | "dots" | "zigzags" | "bolt" | "leaf" | "drop" | "moon" | "crown" | "heart"`.
|
|
128
|
-
- **Image fill and photo frames (1.17.0 / 1.19.0, `image_fill` and `frame_shapes` capabilities):**
|
|
129
|
-
- `fillImage`: Embedded `data:image/*` URI (up to 10 MB).
|
|
130
|
-
- `fillImageCrop`: `{ x: 0..100, y: 0..100, zoom: 1..10 }`.
|
|
131
|
-
- `fillImageFlipX` / `fillImageFlipY`: `boolean` (mirrors the photo inside the frame without moving the frame).
|
|
132
|
-
- `fillGenerative`: Algorithmic procedural fill:
|
|
133
|
-
```typescript
|
|
134
|
-
{
|
|
135
|
-
generatorId: string; // Identifier of the generative algorithm
|
|
136
|
-
seed: number; // Deterministic seed
|
|
137
|
-
params: Record<string, number | string | boolean>;
|
|
138
|
-
colors: {
|
|
139
|
-
mode: "monochrome" | "palette";
|
|
140
|
-
stops: string[]; // Array of cssColor colours (max 16)
|
|
141
|
-
direction: "forward" | "reverse" | "radial";
|
|
142
|
-
background: string;
|
|
143
|
-
backgroundVisible: boolean;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### 4.3. Shapes, Frames, Strokes and Borders
|
|
149
|
-
- `geoShape`:
|
|
150
|
-
- Base shapes: `"rectangle" | "circle" | "triangle" | "star" | "line" | "arrow" | "diamond" | "hexagon" | "heart" | "blob" | "scribble" | "custom"`.
|
|
151
|
-
- Frame shapes (1.19.0, `frame_shapes` capability): `"pill" | "octagon" | "pentagon" | "cross" | "moon" | "cloud" | "drop" | "bolt" | "bubble" | "flower" | "burst" | "arch"`.
|
|
152
|
-
- `svgPath`: Validated SVG path data (`^[MmLlHhVvCcSsQqTtAaZz0-9eE.,\s+-]*$`, max 20,000 chars) for custom frames (`1.19.0`).
|
|
153
|
-
- `svgViewBox`: Four-number coordinate box (`^-?\d+(\.\d+)?([\s,]+-?\d+(\.\d+)?){3}$`, max 64 chars).
|
|
154
|
-
- `strokeColor`: Colour of the border or stroke.
|
|
155
|
-
- `strokeWidth`: Stroke thickness in canvas units (minimum 0).
|
|
156
|
-
- `strokeStyle`: `"solid" | "dashed" | "dotted"`.
|
|
157
|
-
- `borderRadius`: Uniform numeric radius or four-value spatial form (e.g. `8` or `"1.0cqw 2.0cqw 0.0cqw 0.5cqw"`).
|
|
158
|
-
|
|
159
|
-
### 4.4. Transform, Framing, Accessibility and Hierarchy
|
|
160
|
-
- `width` / `height`: Layer dimensions in `cqw` or user units.
|
|
161
|
-
- `rotation`: Rotation in degrees.
|
|
162
|
-
- `flipX` / `flipY` (1.14.0): Mirror the layer on its own horizontal/vertical axes (`flipScale`).
|
|
163
|
-
- `opacity`: Layer opacity (`0` to `100`).
|
|
164
|
-
- `isVisible`: Initial visibility (`boolean`).
|
|
165
|
-
- `groupId`: ID of the parent `group` slot (`slotParent`, `childrenByGroup`).
|
|
166
|
-
- `objectFit`: `"contain" | "cover" | "fill" | "none" | "scale-down"`.
|
|
167
|
-
- `imageCrop` (1.15.0): `{ x: 0..100, y: 0..100, zoom: 1..10 }` (`cropPlacement`, `cropCss`).
|
|
168
|
-
- `alt` (1.16.0): `Record<string, string>` (per-language accessible description, max 300 chars per entry).
|
|
169
|
-
- `decorative` (1.16.0): `boolean` (hides decorative imagery from assistive technology).
|
|
170
|
-
|
|
171
|
-
### 4.5. Unified Effects, Shadows and Media Filters
|
|
172
|
-
- `effects`: Modular array (`max(8)`) of discriminated effect objects (`oimEffectSchema`):
|
|
173
|
-
- `texture`: `{ type: "texture", preset, intensity: 0..100, blend, scale?: 0.2..5, color?, colorRole?, enabled? }`.
|
|
174
|
-
- `drop-shadow`: `{ type: "drop-shadow", x?, y?, blur?: 0..200, spread?, color?, colorRole?, enabled? }`.
|
|
175
|
-
- `blur`: `{ type: "blur", radius: 0..100, enabled? }`.
|
|
176
|
-
- `glow`: `{ type: "glow", color?, colorRole?, radius: 0..100, intensity?: 0..100, enabled? }`.
|
|
177
|
-
- `color-grade`: `{ type: "color-grade", preset: "warm-vintage" | "cool-cinema" | "cyberpunk" | "monochrome-contrast", intensity: 0..100, enabled? }`.
|
|
178
|
-
- `shadowEnabled`, `shadowColor`, `shadowColorRole`, `shadowBlur` (`0..200`), `shadowOffsetX` (`-500..500`), `shadowOffsetY` (`-500..500`), `shadowOpacity` (`0..100`).
|
|
179
|
-
- `layerBlur`: Layer blur (`0..100`).
|
|
180
|
-
- **Media filters (1.18.0, `media_filters` capability):**
|
|
181
|
-
- `filterPreset`: `"none" | "duotone" | "grayscale" | "sepia" | "high-contrast" | "vintage" | "cool" | "warm" | "faded" | "cinema" | "nordic" | "sunset" | "noir" | "dramatic" | "cyberpunk" | "pastel" | "vhs"`.
|
|
182
|
-
- `filterBlur` (`0..100`), `filterBrightness` (`0..500`), `filterContrast` (`0..500`), `filterSaturate` (`0..500`), `filterHueRotate` (`0..360`).
|
|
183
|
-
|
|
184
|
-
### 4.6. Artistic Text and 3D Relief (`artistic-text`)
|
|
185
|
-
- `artisticPreset`: `"neon-cyberpunk" | "retro-wave-3d" | "comic-pop-extrusion" | "vintage-emboss" | "chrome-metallic" | "arcade-80s" | "custom"`.
|
|
186
|
-
- `extrusionDepth`: Geometric extrusion depth (`0` to `100`).
|
|
187
|
-
- `extrusionAngle`: Projection angle in degrees (`0` to `360`).
|
|
188
|
-
- `extrusionColor`: Colour of the extrusion volume.
|
|
189
|
-
- `extrusionShading`: Volumetric facet shading (`boolean`).
|
|
190
|
-
- `curveAmount`: Text curvature (`-100` to `100`).
|
|
191
|
-
- `curveType`: Curvature geometry (`"arc" | "wave" | "arch"`).
|
|
192
|
-
- `outerStrokeColor`, `outerStrokeWidth`, `innerStrokeColor`, `innerStrokeWidth`, `textGradientType`.
|
|
193
|
-
- **Rendering:** the layer is an SVG box of `width_px x height_px` canvas px (else `style.width`/`height`), fitted into the slot (`meet`) without clipping. `fontSize` is canvas px; a `cqw` value is a percentage of the canvas `natural_width`, not of a fixed width. Each line (`
|
|
194
|
-
`) is drawn on its own, `fontSize * lineHeight` apart around the centre. Back to front: the extrusion layers (fill and a stroke of their colour, `outerStrokeWidth` or 1 px), the outer stroke (`outerStrokeWidth * 2`), the inner stroke and then the face. With `curveAmount` the glyphs sit on the curve path, centred on it. Curve path, line offsets and extrusion layers are in `artistic-text-cases.json`.
|
|
195
|
-
|
|
196
|
-
### 4.7. Animations, Keyframes and Reveal (`animation`, `reveal`)
|
|
197
|
-
- `animation.loop`: Loop preset (`"none" | "pulse" | "float" | "bounce" | "spin" | "glow" | "custom"`).
|
|
198
|
-
- `animation.hover`: Cursor preset (`"none" | "grow" | "shrink" | "lift" | "glow" | "custom"`).
|
|
199
|
-
- `animation.duration`: Duration in seconds (`0.1` to `20`).
|
|
200
|
-
- `animation.delay`: Initial delay in seconds (`0` to `10`).
|
|
201
|
-
- `animation.trigger` (1.22.0): `"load" | "action"` (`"action"` holds the layer at its first keyframe until triggered by a `"play"` state action).
|
|
202
|
-
- `animation.keyframes`: Structured array of keyframes (`keyframeSchema`, `max(240)`):
|
|
203
|
-
```typescript
|
|
204
|
-
interface OimKeyframe {
|
|
205
|
-
t: number; // Time in seconds (0 to 60)
|
|
206
|
-
o?: number; // Opacity (0.0 to 2.0, allows elastic overshoot)
|
|
207
|
-
s?: number; // Scale multiplier (0.0 to 10.0)
|
|
208
|
-
r?: number; // Rotation in degrees (-3600 to 3600)
|
|
209
|
-
x?: number; // X displacement in canvas coordinates
|
|
210
|
-
y?: number; // Y displacement in canvas coordinates
|
|
211
|
-
b?: number; // Blur (0 to 50)
|
|
212
|
-
e?: "linear" | "ease-in" | "ease-out" | "ease-in-out" | "steps(1, end)" | [number, number, number, number];
|
|
213
|
-
}
|
|
214
|
-
```
|
|
215
|
-
- `reveal` (1.22.0): How a layer enters when shown by an action and exits when hidden (`revealSchema`):
|
|
216
|
-
```typescript
|
|
217
|
-
interface OimReveal {
|
|
218
|
-
effect: "fade" | "slide-up" | "slide-down" | "slide-left" | "slide-right" | "scale";
|
|
219
|
-
duration: number; // Duration in ms (0 to 2000)
|
|
220
|
-
easing?: KeyframeEasing;
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
- `textAnimationType`: `"none" | "typewriter" | "wave-chars" | "glitch-cyberpunk" | "glitch-slice" | "fade-chars" | "slide-up" | "bounce-chars" | "reveal-mask" | "scale-in" | "rise-words" | "blur-chars"`, with `textAnimationSpeed` (`0..200`), `textAnimationDelay` (`0..100`), `textAnimationIntensity` (`0..100`) and `typewriterCursor`.
|
|
224
|
-
- `fade-chars`, `slide-up`, `bounce-chars`, `reveal-mask`, `scale-in` and `rise-words` are entrance effects defined as portable data in `@oim-media/core` (`text-effects.ts`).
|
|
225
|
-
- `glitch-cyberpunk` and `glitch-slice` are defined as portable layer primitives in `@oim-media/core` (`glitch.ts`).
|
|
226
|
-
- `blur-chars` is **retired** (not portable): it still validates, and readers draw the text without animation.
|
|
227
|
-
|
|
228
|
-
### 4.8. Rendering rules for readers
|
|
229
|
-
|
|
230
|
-
The web player (`@oim-media/player-oim`) is the reference renderer (section 9.1). These are the rules it
|
|
231
|
-
follows that the schema alone does not say. Every reader must follow them; each one has cost a visible
|
|
232
|
-
difference between renderers before it was written down.
|
|
233
|
-
|
|
234
|
-
**Order and stacking**
|
|
235
|
-
- Layers paint in `slots` order, except the background pattern layer (`is_background_pattern: true`, or
|
|
236
|
-
id `background-pattern-slot`), which always paints behind every other layer.
|
|
237
|
-
|
|
238
|
-
**Text**
|
|
239
|
-
- The text box is a column centred vertically (`justify-content: center`). Horizontal alignment is
|
|
240
|
-
`style.textAlign`, else the slot's `align`, else `left`.
|
|
241
|
-
- The box has a right padding of `0.25em` and is anchored by its padded width: left-aligned text does not
|
|
242
|
-
move, centred text moves `0.125em` left and right-aligned text `0.25em` left.
|
|
243
|
-
- Text with no colour is black.
|
|
244
|
-
- `letterSpacing` applies only as a length with its unit (`px`, `em`); a bare number is ignored.
|
|
245
|
-
- `textRuns` are ignored by a reader that lays out text itself; it lays out `content` instead.
|
|
246
|
-
- A text layer's shadow follows the glyphs (CSS `text-shadow`).
|
|
247
|
-
- The `lineHeight` leading is split evenly above and below the glyphs, as in CSS.
|
|
248
|
-
- On a text layer, `effects` of type `drop-shadow` add one text-shadow (`x`, `y` default 0 and 4, `blur` default 12) and `glow` adds two, blurred `radius * 0.4` and `radius` (default 16, colour `#f59e0b`).
|
|
249
|
-
|
|
250
|
-
**Shapes, strokes and fills**
|
|
251
|
-
- A plain rectangle's stroke is a border outside its box: the box grows by `strokeWidth` on every side,
|
|
252
|
-
keeps its vertical centre, and is anchored horizontally by `textAlign` (left by default). The fill
|
|
253
|
-
paints under the border. Shapes drawn as SVG (catalogue shape, `svgPath`) stroke their outline instead.
|
|
254
|
-
- Borders are drawn only from `strokeColor`, `strokeWidth` and `strokeStyle`. `borderColor` and
|
|
255
|
-
`borderWidth` are not contract keys and are not drawn.
|
|
256
|
-
- A custom path (`svgPath`) is scaled from its `svgViewBox` into the layer box; a photo frame with its own
|
|
257
|
-
`svgPath` masks its colour layer to that path too, so no box of the fill colour shows around the photo.
|
|
258
|
-
- A `container` layer paints like a `geo` layer.
|
|
259
|
-
- A pattern tile is `160 × fillPatternScale` (or `backgroundPatternScale`) canvas units. A stored tile
|
|
260
|
-
(`svgRawXml`) wins over the one generated from the pattern type.
|
|
261
|
-
|
|
262
|
-
**Effects**
|
|
263
|
-
- The shadow of a non-text layer, and the `drop-shadow` effect, follow the layer's silhouette (its alpha),
|
|
264
|
-
not its bounding box, and paint under the layer.
|
|
265
|
-
- `effects` apply in file order to the whole layer, before its flip and rotation. `blur` blurs with a
|
|
266
|
-
standard deviation of `radius` (default 8). On a non-text layer `drop-shadow` casts the silhouette
|
|
267
|
-
(Gaussian sigma `blur / 2`) and `glow` casts it twice, blurred `radius * 0.4` then `radius`.
|
|
268
|
-
- A non-empty `effects` replaces the legacy `shadow*`, `layerBlur`/`blur` and `grain` fields.
|
|
269
|
-
- `texture` repeats its preset tile (`@oim-media/core` `TEXTURE_CATALOG`) at `tile size * scale` canvas units, blended
|
|
270
|
-
with `blend` (default per preset). Text takes it inside its glyphs; `svg` and `image` layers over their pixels;
|
|
271
|
-
other layers over their box, faded once more by `intensity`. In document `metadata.effects` it covers the canvas.
|
|
272
|
-
- Media filters (`filterPreset`, `filter*`) apply only to `image` layers and to the picture of an image
|
|
273
|
-
fill, never to the frame or to other layer types.
|
|
274
|
-
- `opacity` is read as `0..100`.
|
|
275
|
-
- Retired keys (section 8), such as `blendMode`, are not drawn.
|
|
276
|
-
|
|
277
|
-
---
|
|
278
|
-
|
|
279
|
-
## 5. Canonical Parallax (`parallaxSchema`)
|
|
280
|
-
|
|
281
|
-
Configuration for layers with spatial depth and reactive or continuous displacement (carried on `slot.parallax` or `slot.style.parallax`):
|
|
282
|
-
|
|
283
|
-
```typescript
|
|
284
|
-
interface OimParallax {
|
|
285
|
-
enabled: boolean; // Default: true
|
|
286
|
-
depth: number; // Focal depth (-2.0 to 2.0)
|
|
287
|
-
mode: "mouse" | "scroll" | "infinite_horizontal";
|
|
288
|
-
axis: "both" | "x_only" | "y_only";
|
|
289
|
-
infinite?: {
|
|
290
|
-
enabled?: boolean; // Default: false (true when active)
|
|
291
|
-
speedPxSec?: number; // Continuous advance speed (-500 to 500 px/s; alias: speed_px_sec)
|
|
292
|
-
direction?: "left_to_right" | "right_to_left";
|
|
293
|
-
mirror?: boolean; // Mirror alternate tiles (aliases: seamlessTile, seamless_tile)
|
|
294
|
-
pauseOnHover?: boolean; // Pause when the cursor rests on it (alias: pause_on_hover)
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
> Note: On group layers that form an editable loop scene, `style.sceneLoop` (`{ speedPxSec, direction, mirror, pauseOnHover }`) and child `style.loopDepth` (`0..1`) preserve the scene parameters for re-editing, while each moving child layer carries its resolved `parallax` (`speedPxSec = scene.speedPxSec * loopDepth`) for the player (`infiniteLayerPlan`).
|
|
300
|
-
|
|
301
|
-
---
|
|
302
|
-
|
|
303
|
-
## 6. Interactivity and State Machine (`isInteractive`, `states`)
|
|
304
|
-
|
|
305
|
-
Allows declaring interactivity without writing JavaScript:
|
|
306
|
-
|
|
307
|
-
```typescript
|
|
308
|
-
interface OimInteractiveStates {
|
|
309
|
-
hover?: OimStateConfig; // State when the cursor rests on it or a touch is held (reverted on leave)
|
|
310
|
-
active?: OimStateConfig; // State when clicked or tapped (persists)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
interface OimStateConfig {
|
|
314
|
-
scale?: number; // Percentage scale (0 to 1000%, e.g. 105 = 105%)
|
|
315
|
-
rotation?: number; // Rotation (-360 to 360 degrees)
|
|
316
|
-
opacity?: number; // Percentage opacity (0 to 100%)
|
|
317
|
-
transitionDuration?: number; // Transition time in ms (0 to 10000)
|
|
318
|
-
actions?: OimStateAction[]; // Chained actions on other layers (max 64)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
interface OimStateAction {
|
|
322
|
-
target: string; // ID of the target slot (1 to 128 chars)
|
|
323
|
-
type: "show" | "hide" | "toggle" | "setText" | "play"; // "play" added in 1.22.0
|
|
324
|
-
value?: string; // Value to apply (e.g. new text if type is setText, max 10000 chars)
|
|
325
|
-
}
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
---
|
|
329
|
-
|
|
330
|
-
## 7. Root Document Metadata (`OimMetadataSchema`)
|
|
331
|
-
|
|
332
|
-
Top-level structure carried in the `OIM_Data` payload:
|
|
333
|
-
|
|
334
|
-
| Key | Type | Description |
|
|
335
|
-
|---|---|---|
|
|
336
|
-
| `oim` | `string` | Format version of the document (e.g. `"1.27.
|
|
337
|
-
| `min_player` | `string` | Minimum player version required to rebuild the document (`minVersionFor`). |
|
|
338
|
-
| `oim_id` | `string` | Unique document identifier for re-editing and registry checks (max 128 chars). |
|
|
339
|
-
| `media_type` | `"image" \| "video"` | Media type of the OIM document. |
|
|
340
|
-
| `name` | `string` | Official title of the work (max 256 chars). |
|
|
341
|
-
| `description` | `Record<string, string>` | Per-language summary of the whole document for screen readers (1.16.0, max 1000 chars per language). |
|
|
342
|
-
| `reedit` | `"public" \| "author_only" \| "sealed"` | Authorised cryptographic re-edit policy (1.23.0; replaces deprecated Spanish `reedicion`). |
|
|
343
|
-
| `immutable_lock` | `boolean` | Cryptographic seal flag (`true` when `reedit === "sealed"`). |
|
|
344
|
-
| `canvas` | `{ natural_width: number, natural_height: number }` | Base reference dimensions of the canvas. |
|
|
345
|
-
| `lang` / `languages` | `string[]` | Catalogue of BCP-47 language codes present in the document (max 10 chars per tag). |
|
|
346
|
-
| `palettes` | `Record<string, Partial<Record<DynamicColorRole, string>>>` | Up to 6 named colour palettes mapping semantic roles to CSS colours (1.20.0). |
|
|
347
|
-
| `defaultPalette` | `string` | Name of the active palette in `palettes` (1.20.0). |
|
|
348
|
-
| `slots` | `OimSlot[]` | Array of interactive layers (maximum `MAX_SLOTS` = 150). |
|
|
349
|
-
| `zones` | `OimZone[]` | Optional catalogue of named bounding box zones. |
|
|
350
|
-
| `capabilities` | `("vector_parallax" \| "infinite_parallax" \| "text_marks" \| "image_fill" \| "media_filters" \| "frame_shapes")[]` | Capabilities used by the document. |
|
|
351
|
-
| `capabilities_required` | same as `capabilities` | Capabilities the reader **must** implement to paint with fidelity (glTF pattern). |
|
|
352
|
-
| `fonts` | `EmbeddedFont[]` | Font variants embedded in base64 for offline rendering (1.3.0): subset, static (no `fvar`, `gvar`, `avar`, `HVAR`, `MVAR`; `STAT` may stay) WOFF, `format: "woff"`, written since 1.27.0; WOFF2 (`format: "woff2"`) is read forever. The content starts with the magic of its format (`wOFF` / `wOF2`). |
|
|
353
|
-
| `image_hash` | `string` | SHA-256 hash of the base image `IDAT` chunk payloads (`imageDataHash`). |
|
|
354
|
-
| `image_hash_alg` | `"sha256-idat"` | Image sealing algorithm identifier. |
|
|
355
|
-
| `signature` | `string` | ECDSA P-256 digital signature (Base64) over RFC 8785 canonical JSON (`serializeCanonical`). |
|
|
356
|
-
| `key_version` | `string` | Public key identifier in `keys.json` (e.g. `"v1"`, `"v2"`). |
|
|
357
|
-
| `signed_at` | `string` | ISO-8601 timestamp with time zone included inside the signed payload to enforce key expiry. |
|
|
358
|
-
| `timestamp` | `string` | ISO-8601 timestamp of package creation. |
|
|
359
|
-
|
|
360
|
-
---
|
|
361
|
-
|
|
362
|
-
## 8. Retired Keys (Read-Only for Backward Compatibility)
|
|
363
|
-
|
|
364
|
-
These keys remain declared with `deprecated: true` in `oim.schema.json` so already-published files continue to validate, but **no new writer may emit them**:
|
|
365
|
-
- **In `style`:** `textTransform`, `verticalAlign`, `shader_fx`, `blendMode` (closed to 9 values), `grain`/`grainIntensity`/`grainSize`/`grainBlend` (replaced by `style.effects` texture), `textHighlight` (replaced by `style.marks`), `animation.custom_loop_css`/`custom_keyframes`/`custom_hover_css` (retired in 1.9.0).
|
|
366
|
-
- **At the root:** `reedicion` (renamed to `reedit` in 1.23.0; mapped by `normalizeLegacyKeys`), `grain` and `effects` (background grain is baked into the base image, 1.12.0), `elements` and `layers` (`slots` is the single canonical name, 1.12.0), `allowed_domains` (retired in 1.7.0), `redirect_membership_verified` and `terms_accepted` (retired in 1.1.0).
|
|
367
|
-
- **In the slot:** `p`, `s`, `st`, `c`, `t` (short aliases retired in 1.12.0).
|
|
368
|
-
|
|
369
|
-
---
|
|
370
|
-
|
|
371
|
-
## 9. Verification and Conformance
|
|
372
|
-
|
|
373
|
-
Conformance of any generator or reader with this specification is validated automatically by running the canonical vector suite and JSON Schema agreement tests:
|
|
374
|
-
|
|
375
|
-
```bash
|
|
376
|
-
pnpm conformance
|
|
377
|
-
pnpm --filter @oim-media/core test
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
### 9.1. Visual conformance
|
|
381
|
-
|
|
382
|
-
The metadata vectors cannot see pixels: two renderers can read the same JSON and paint it differently.
|
|
383
|
-
Every vector therefore also has a golden: `vectors/golden/<vector>.web.png`, rendered by the web player,
|
|
384
|
-
which is the reference renderer.
|
|
385
|
-
|
|
386
|
-
**Procedure.** 800 px wide; height `round(800 × h / w)` from the vector's PNG header; device pixel ratio 1;
|
|
387
|
-
animations disabled; white background; host font `sans-serif` for text that names no family. Goldens
|
|
388
|
-
are only valid when rendered on the CI job's Linux image (`node tools/render-web-goldens.mjs vectors/golden`),
|
|
389
|
-
because fonts rasterise differently on each system.
|
|
390
|
-
|
|
391
|
-
**Metric** (`tools/parity-core.mjs`). A pixel differs when any channel differs by more than 40. The
|
|
392
|
-
difference mask is eroded with a 3×3 kernel, which removes anti-aliasing edges. The outermost 1 px ring of the image is not measured (the kernel has no neighbours there). A render fails when the
|
|
393
|
-
remaining area exceeds **0.4 %** of the image or any 4-connected blob exceeds **576 px** (a 24 × 24 square).
|
|
394
|
-
|
|
395
|
-
The threshold was calibrated on 2026-11-07 against bugs reproduced on purpose:
|
|
396
|
-
|
|
397
|
-
| Case | Area |
|
|
398
|
-
|---|---|
|
|
399
|
-
| Largest passing vector (text anti-aliasing, `typography-styling`) | 0.265 % |
|
|
400
|
-
| Unitless `letterSpacing` ignored | 0.509 % |
|
|
401
|
-
| Square shadow on a round shape | 6.96 % |
|
|
402
|
-
| Shadow painted over the fill | 6.29 % |
|
|
403
|
-
| Pattern tile at the wrong size | 10.9 % |
|
|
404
|
-
| Pattern background painted over the layers | 24.6 % |
|
|
405
|
-
|
|
406
|
-
**Running it.** A renderer writes `<vector>.png` per vector following the procedure, then
|
|
407
|
-
`node tools/parity.mjs <renders-dir> --renderer <name> --out <diff-dir>` compares them and writes a red
|
|
408
|
-
difference image for each failure. Known differences go in `vectors/golden/exceptions.json`, per renderer,
|
|
409
|
-
with a reason; an excepted vector that passes fails the run, so a fixed bug cannot keep its exception.
|
|
1
|
+
# OIM (Object Interactive Media) Canonical Metadata Specification
|
|
2
|
+
|
|
3
|
+
**Contract version:** 1.27.10
|
|
4
|
+
**Date:** 2026-11-02 (reviewed against `packages/core/src/schema.ts`; the source of truth is the schema, this document summarises it)
|
|
5
|
+
**Status:** Normative / In force
|
|
6
|
+
**License:** Apache-2.0
|
|
7
|
+
**Authority:** Xue Studios LLC & the OIM Architecture Team
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Summary and Objectives of the Canonical Specification
|
|
12
|
+
|
|
13
|
+
The **`.oim`** format defines a multi-platform interactive image specification. The metadata layer (historically called `oimdata` or `oimdata.json`, injected into the `OIM_Data` chunk of the PNG container) uses a **strongly typed, serialisable canonical language**.
|
|
14
|
+
|
|
15
|
+
> **Note on OVM (`.ovm`):** The interactive video format (`ovm.schema.json` and `@oim-media/player-ovm`) is currently a **reserved placeholder pending specification**. The normative specification below governs **OIM (`.oim`)**.
|
|
16
|
+
|
|
17
|
+
### Canonical Design Principles
|
|
18
|
+
|
|
19
|
+
1. **Total independence from web engines (native platforms):**
|
|
20
|
+
- The format **prohibits free or arbitrary CSS** (`custom_loop_css`, `custom_keyframes`, free style strings).
|
|
21
|
+
- Every visual or dynamic attribute is parameterised in canonical types (numbers, enums, tuples or arrays). This allows native implementations on **iOS (SwiftUI / Core Animation)**, **Android (Jetpack Compose / RenderNode)**, **Desktop (Rust / Tauri / Skia)** or Web Components to paint and interact with 1:1 fidelity without requiring an embedded web browser.
|
|
22
|
+
2. **One name per concept (zero ambiguity):**
|
|
23
|
+
- The compiler canonicalises shortcuts and duplicate names before signing:
|
|
24
|
+
- `size` ➔ `fontSize`
|
|
25
|
+
- `font` ➔ `fontFamily`
|
|
26
|
+
- `color` ➔ `fillColor`
|
|
27
|
+
- `bold: true` ➔ `fontWeight: 700`
|
|
28
|
+
- `italic: true` ➔ `fontStyle: "italic"`
|
|
29
|
+
- `underline: true` ➔ `textDecoration: "underline"`
|
|
30
|
+
- `fontSizeOverrides` ➔ unified inside the `fontSizes` map.
|
|
31
|
+
- `reedicion` (`"publico" | "solo_autor" | "sellado"`) ➔ `reedit` (`"public" | "author_only" | "sealed"`, 1.23.0; legacy files are normalised on read via `normalizeLegacyKeys`).
|
|
32
|
+
3. **Determinism and security (zero-trust sandbox):**
|
|
33
|
+
- With no arbitrary CSS injection, attack vectors such as `@import`, covert network requests via `url()`, or breaking out of Shadow DOM isolation are eradicated.
|
|
34
|
+
4. **Portable or not admitted:**
|
|
35
|
+
- Every visual effect is built from primitives that any graphics engine has cheaply — opacity, translation, scale, rotation and rectangular clipping, per unit (letter, word, line or layer). What exists only in a browser (per-letter blur, animated shadows, free `clip-path`) is not admitted, and what was already there is retired or rewritten into portable primitives (`glitchLayers`, `entranceFrameAt`, `revealFrames`).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Canonical Layer Types (`slot.type`)
|
|
40
|
+
|
|
41
|
+
Every interactive element in the `slots` array declares a canonical type:
|
|
42
|
+
|
|
43
|
+
| Type (`type`) | Purpose | Canonical Characteristics |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `text` | Dynamic multilingual text | Supports localised `content`, sizes per language (`fontSizes`), widths per language (`maxWidths`), text marks (`marks`) and precalculated lines (`textRuns`). |
|
|
46
|
+
| `image` | Secondary or embedded bitmap | Embedded `data:image/*` URI in `content`, framing (`objectFit`, `imageCrop`), mirroring (`flipX`, `flipY`), filters and accessible `alt`/`decorative`. |
|
|
47
|
+
| `lottie` | Lottie vector animation | Embedded vector JSON. |
|
|
48
|
+
| `geo` / `shape` | Pure vector geometry or photo frame | Governed by `style.geoShape` (24 catalog shapes including 12 photo-frame silhouettes), custom `svgPath`/`svgViewBox`, or `fillType: "image"` (`fillImage`, `fillImageCrop`, `fillImageFlipX`, `fillImageFlipY`). |
|
|
49
|
+
| `draw` / `svg` | Free vector strokes | Supports standard SVG paths (`svgRawXml`, `svgPath`, `svgViewBox`, `customBlobPath`). |
|
|
50
|
+
| `artistic-text` | 3D text with parametric extrusion | Vector extrusion (`extrusionDepth`, `extrusionAngle`, `curveAmount`, `curveType`). |
|
|
51
|
+
| `pattern` | Repeating procedural pattern | Canonical presets (`clouds`, `bubbles`, `grid`, `waves`, `stars`, `stripes`, `dots`, `zigzags`, `bolt`, `leaf`, `drop`, `moon`, `crown`, `heart`). |
|
|
52
|
+
| `filter` | Colour adjustment / optical filter layer | Named presets (`filterPreset`) and normalised numeric adjustments (`filterBrightness`, `filterContrast`, `filterSaturate`, `filterHueRotate`, `filterBlur`). |
|
|
53
|
+
| `container` | Bounding box or frame | Supports geometry, fills, borders, shadows and clipping. |
|
|
54
|
+
| `group` | Hierarchical container node | Groups child layers via `style.groupId`. Controls clipping (`slotClipping`) and scene loops (`style.sceneLoop`). |
|
|
55
|
+
| `component` | Composite component instance | Encapsulated grouping of modular design. |
|
|
56
|
+
| `mask` | Clipping or die-cut mask | Clipping by a base shape (`maskShape`, `maskMode`) or multiple vector cutouts (`cuts`). |
|
|
57
|
+
| `zone` | Contextual anchor zone | Delimiter for anchoring slots to relative regions of the image. |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. Root Structure of the Slot (`slotSchema`)
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
interface OimSlot {
|
|
65
|
+
id: string; // Unique identifier (maximum 128 characters)
|
|
66
|
+
name?: string; // Name assigned by the author (max 256 chars)
|
|
67
|
+
type?: SlotType; // One of the canonical types in section 2
|
|
68
|
+
languages?: string[]; // Languages in which it is shown (default: ["*"])
|
|
69
|
+
pendingTranslation?: string[]; // Language codes pending translation
|
|
70
|
+
align?: "left" | "center" | "right";
|
|
71
|
+
position?: [number, number]; // Normalised coordinates [x, y] (percentage 0.0 to 100.0%)
|
|
72
|
+
width_px?: number; // Virtual width in reference pixels
|
|
73
|
+
height_px?: number; // Virtual height in reference pixels
|
|
74
|
+
content?: Record<string, string>; // Language -> content dictionary (e.g. { "en": "Text" })
|
|
75
|
+
style?: OimStyle; // Canonical style object (see section 4)
|
|
76
|
+
parallax?: OimParallax; // Parallax configuration (see section 5; may also travel in style.parallax)
|
|
77
|
+
interaction?: OimInteraction; // Declared interactions
|
|
78
|
+
tracking_data?: number[][]; // Tracking keyframes [time, x1..y4, cx, cy]
|
|
79
|
+
zone_id?: string; // ID of the containing zone
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 4. Canonical Style Contract (`styleSchema`)
|
|
86
|
+
|
|
87
|
+
### 4.1. Typography and Text
|
|
88
|
+
- `fontFamily`: String with the font name (max 128 chars).
|
|
89
|
+
- `fontSize`: Pure number or container percentage (e.g. `24` or `"4.5cqw"`).
|
|
90
|
+
- `fontWeight`: Numeric weight (`100` to `900`, `400` = normal, `700` = bold) or string keyword.
|
|
91
|
+
- `fontStyle`: `"normal" | "italic" | "oblique"`.
|
|
92
|
+
- `textDecoration`: `"none" | "underline"` (max 64 chars).
|
|
93
|
+
- `textAlign`: `"left" | "center" | "right" | "justify"`.
|
|
94
|
+
- `lineHeight`: Numeric multiplier factor (e.g. `1.2`, `1.5`) or a bounded string.
|
|
95
|
+
- `letterSpacing`: A CSS length with its unit (`"2px"`, `"0.05em"`). A bare number is not a length: readers ignore it, as the browser does (section 4.8).
|
|
96
|
+
- `maxWidth`: Container percentage string (e.g. `"80cqw"`, `"none"`) or numeric value.
|
|
97
|
+
- `fontSizes`: Map of size specific to each language `{ "es": "3.0cqw", "ja": "2.5cqw" }`.
|
|
98
|
+
- `maxWidths`: Map of maximum width specific to each language `{ "en": "60cqw" }`.
|
|
99
|
+
- `textRole`: Semantic hierarchy hint (`"auto" | "title" | "subtitle" | "body"`).
|
|
100
|
+
- **Text marks (1.11.0, `text_marks` capability):** `content` can wrap phrases in `{name}…{/name}`; literal braces are doubled (`{{`, `}}`). `style.marks` defines each mark with a closed set (`color`/`colorRole`, `bold`, `italic`, `underline`, `highlight{style, color, colorRole, paddingX, paddingY, borderRadius}`). Names `^[a-z][a-z0-9-]{0,23}$`, maximum 8 per layer, not nested. A single interpretation: `parseMarks`, `markStyle` (`@oim-media/core`). The backend does not sign malformed tags (`OIM_E_TEXT_MARKS_INVALID`).
|
|
101
|
+
- `dropCap`: `{ enabled, lines: 2|3, fontFamily?, color?, marginRight? }`. Size is set with `dropCapStyle`; the rest of the text keeps its marks (`splitDropCap`).
|
|
102
|
+
- `textHighlight`: **retired** (1.12.0). Kept read-only for published files; new writers use `marks`.
|
|
103
|
+
- `textRuns`: **Precalculated lines**, only for readers **without** a paragraph layout engine. A reader that can lay out text (web, Flutter) ignores them: the exporter's line breaks can disagree with the real layout (section 4.8):
|
|
104
|
+
```json
|
|
105
|
+
"textRuns": {
|
|
106
|
+
"es": {
|
|
107
|
+
"fontSize": 24,
|
|
108
|
+
"lineHeight": 28,
|
|
109
|
+
"lines": [
|
|
110
|
+
{ "text": "First line of text", "width": 280, "yOffset": 0 },
|
|
111
|
+
{ "text": "Second line, adjusted", "width": 260, "yOffset": 28 }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 4.2. Colours, Dynamic Roles and Fills (`cssColor`)
|
|
118
|
+
Colours are validated against the strict `cssColor` pattern (hexadecimal `#RGB` / `#RRGGBB` / `#RRGGBBAA`, functions `rgb()`, `hsl()`, `"transparent"`, `"none"`, or dynamic colour tokens `var(--primary)`):
|
|
119
|
+
- **Dynamic colour roles (`DYNAMIC_COLOR_ROLES`):**
|
|
120
|
+
`"primary" | "on-primary" | "secondary" | "on-secondary" | "surface" | "on-surface" | "outline" | "accent" | "text-primary" | "text-secondary" | "text-muted" | "text-on-accent"`.
|
|
121
|
+
- `fillColor`: Main fill colour (or glyph colour in text layers).
|
|
122
|
+
- `fillType`: `"none" | "color" | "gradient" | "pattern" | "generative" | "image"`.
|
|
123
|
+
- `colorRole` / `fillColorRole` / `strokeColorRole` / `textStrokeColorRole` / `shadowColorRole` / `effectColorRole` / `maskBgColorRole` / `maskStrokeColorRole`: Semantic dynamic theme roles.
|
|
124
|
+
- **Pattern colour roles (1.21.0):** `fillPatternColorRole`, `fillPatternBgColorRole`, `backgroundPatternColorRole`, `backgroundPatternBgColorRole`.
|
|
125
|
+
- `fillGradient`: Canonical linear, radial or conic gradient function (`CSS_GRADIENT_PATTERN`).
|
|
126
|
+
- `fillPatternType`: Repeating pattern preset:
|
|
127
|
+
`"clouds" | "bubbles" | "grid" | "waves" | "stars" | "stripes" | "dots" | "zigzags" | "bolt" | "leaf" | "drop" | "moon" | "crown" | "heart"`.
|
|
128
|
+
- **Image fill and photo frames (1.17.0 / 1.19.0, `image_fill` and `frame_shapes` capabilities):**
|
|
129
|
+
- `fillImage`: Embedded `data:image/*` URI (up to 10 MB).
|
|
130
|
+
- `fillImageCrop`: `{ x: 0..100, y: 0..100, zoom: 1..10 }`.
|
|
131
|
+
- `fillImageFlipX` / `fillImageFlipY`: `boolean` (mirrors the photo inside the frame without moving the frame).
|
|
132
|
+
- `fillGenerative`: Algorithmic procedural fill:
|
|
133
|
+
```typescript
|
|
134
|
+
{
|
|
135
|
+
generatorId: string; // Identifier of the generative algorithm
|
|
136
|
+
seed: number; // Deterministic seed
|
|
137
|
+
params: Record<string, number | string | boolean>;
|
|
138
|
+
colors: {
|
|
139
|
+
mode: "monochrome" | "palette";
|
|
140
|
+
stops: string[]; // Array of cssColor colours (max 16)
|
|
141
|
+
direction: "forward" | "reverse" | "radial";
|
|
142
|
+
background: string;
|
|
143
|
+
backgroundVisible: boolean;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 4.3. Shapes, Frames, Strokes and Borders
|
|
149
|
+
- `geoShape`:
|
|
150
|
+
- Base shapes: `"rectangle" | "circle" | "triangle" | "star" | "line" | "arrow" | "diamond" | "hexagon" | "heart" | "blob" | "scribble" | "custom"`.
|
|
151
|
+
- Frame shapes (1.19.0, `frame_shapes` capability): `"pill" | "octagon" | "pentagon" | "cross" | "moon" | "cloud" | "drop" | "bolt" | "bubble" | "flower" | "burst" | "arch"`.
|
|
152
|
+
- `svgPath`: Validated SVG path data (`^[MmLlHhVvCcSsQqTtAaZz0-9eE.,\s+-]*$`, max 20,000 chars) for custom frames (`1.19.0`).
|
|
153
|
+
- `svgViewBox`: Four-number coordinate box (`^-?\d+(\.\d+)?([\s,]+-?\d+(\.\d+)?){3}$`, max 64 chars).
|
|
154
|
+
- `strokeColor`: Colour of the border or stroke.
|
|
155
|
+
- `strokeWidth`: Stroke thickness in canvas units (minimum 0).
|
|
156
|
+
- `strokeStyle`: `"solid" | "dashed" | "dotted"`.
|
|
157
|
+
- `borderRadius`: Uniform numeric radius or four-value spatial form (e.g. `8` or `"1.0cqw 2.0cqw 0.0cqw 0.5cqw"`).
|
|
158
|
+
|
|
159
|
+
### 4.4. Transform, Framing, Accessibility and Hierarchy
|
|
160
|
+
- `width` / `height`: Layer dimensions in `cqw` or user units.
|
|
161
|
+
- `rotation`: Rotation in degrees.
|
|
162
|
+
- `flipX` / `flipY` (1.14.0): Mirror the layer on its own horizontal/vertical axes (`flipScale`).
|
|
163
|
+
- `opacity`: Layer opacity (`0` to `100`).
|
|
164
|
+
- `isVisible`: Initial visibility (`boolean`).
|
|
165
|
+
- `groupId`: ID of the parent `group` slot (`slotParent`, `childrenByGroup`).
|
|
166
|
+
- `objectFit`: `"contain" | "cover" | "fill" | "none" | "scale-down"`.
|
|
167
|
+
- `imageCrop` (1.15.0): `{ x: 0..100, y: 0..100, zoom: 1..10 }` (`cropPlacement`, `cropCss`).
|
|
168
|
+
- `alt` (1.16.0): `Record<string, string>` (per-language accessible description, max 300 chars per entry).
|
|
169
|
+
- `decorative` (1.16.0): `boolean` (hides decorative imagery from assistive technology).
|
|
170
|
+
|
|
171
|
+
### 4.5. Unified Effects, Shadows and Media Filters
|
|
172
|
+
- `effects`: Modular array (`max(8)`) of discriminated effect objects (`oimEffectSchema`):
|
|
173
|
+
- `texture`: `{ type: "texture", preset, intensity: 0..100, blend, scale?: 0.2..5, color?, colorRole?, enabled? }`.
|
|
174
|
+
- `drop-shadow`: `{ type: "drop-shadow", x?, y?, blur?: 0..200, spread?, color?, colorRole?, enabled? }`.
|
|
175
|
+
- `blur`: `{ type: "blur", radius: 0..100, enabled? }`.
|
|
176
|
+
- `glow`: `{ type: "glow", color?, colorRole?, radius: 0..100, intensity?: 0..100, enabled? }`.
|
|
177
|
+
- `color-grade`: `{ type: "color-grade", preset: "warm-vintage" | "cool-cinema" | "cyberpunk" | "monochrome-contrast", intensity: 0..100, enabled? }`.
|
|
178
|
+
- `shadowEnabled`, `shadowColor`, `shadowColorRole`, `shadowBlur` (`0..200`), `shadowOffsetX` (`-500..500`), `shadowOffsetY` (`-500..500`), `shadowOpacity` (`0..100`).
|
|
179
|
+
- `layerBlur`: Layer blur (`0..100`).
|
|
180
|
+
- **Media filters (1.18.0, `media_filters` capability):**
|
|
181
|
+
- `filterPreset`: `"none" | "duotone" | "grayscale" | "sepia" | "high-contrast" | "vintage" | "cool" | "warm" | "faded" | "cinema" | "nordic" | "sunset" | "noir" | "dramatic" | "cyberpunk" | "pastel" | "vhs"`.
|
|
182
|
+
- `filterBlur` (`0..100`), `filterBrightness` (`0..500`), `filterContrast` (`0..500`), `filterSaturate` (`0..500`), `filterHueRotate` (`0..360`).
|
|
183
|
+
|
|
184
|
+
### 4.6. Artistic Text and 3D Relief (`artistic-text`)
|
|
185
|
+
- `artisticPreset`: `"neon-cyberpunk" | "retro-wave-3d" | "comic-pop-extrusion" | "vintage-emboss" | "chrome-metallic" | "arcade-80s" | "custom"`.
|
|
186
|
+
- `extrusionDepth`: Geometric extrusion depth (`0` to `100`).
|
|
187
|
+
- `extrusionAngle`: Projection angle in degrees (`0` to `360`).
|
|
188
|
+
- `extrusionColor`: Colour of the extrusion volume.
|
|
189
|
+
- `extrusionShading`: Volumetric facet shading (`boolean`).
|
|
190
|
+
- `curveAmount`: Text curvature (`-100` to `100`).
|
|
191
|
+
- `curveType`: Curvature geometry (`"arc" | "wave" | "arch"`).
|
|
192
|
+
- `outerStrokeColor`, `outerStrokeWidth`, `innerStrokeColor`, `innerStrokeWidth`, `textGradientType`.
|
|
193
|
+
- **Rendering:** the layer is an SVG box of `width_px x height_px` canvas px (else `style.width`/`height`), fitted into the slot (`meet`) without clipping. `fontSize` is canvas px; a `cqw` value is a percentage of the canvas `natural_width`, not of a fixed width. Each line (`
|
|
194
|
+
`) is drawn on its own, `fontSize * lineHeight` apart around the centre. Back to front: the extrusion layers (fill and a stroke of their colour, `outerStrokeWidth` or 1 px), the outer stroke (`outerStrokeWidth * 2`), the inner stroke and then the face. With `curveAmount` the glyphs sit on the curve path, centred on it. Curve path, line offsets and extrusion layers are in `artistic-text-cases.json`.
|
|
195
|
+
|
|
196
|
+
### 4.7. Animations, Keyframes and Reveal (`animation`, `reveal`)
|
|
197
|
+
- `animation.loop`: Loop preset (`"none" | "pulse" | "float" | "bounce" | "spin" | "glow" | "custom"`).
|
|
198
|
+
- `animation.hover`: Cursor preset (`"none" | "grow" | "shrink" | "lift" | "glow" | "custom"`).
|
|
199
|
+
- `animation.duration`: Duration in seconds (`0.1` to `20`).
|
|
200
|
+
- `animation.delay`: Initial delay in seconds (`0` to `10`).
|
|
201
|
+
- `animation.trigger` (1.22.0): `"load" | "action"` (`"action"` holds the layer at its first keyframe until triggered by a `"play"` state action).
|
|
202
|
+
- `animation.keyframes`: Structured array of keyframes (`keyframeSchema`, `max(240)`):
|
|
203
|
+
```typescript
|
|
204
|
+
interface OimKeyframe {
|
|
205
|
+
t: number; // Time in seconds (0 to 60)
|
|
206
|
+
o?: number; // Opacity (0.0 to 2.0, allows elastic overshoot)
|
|
207
|
+
s?: number; // Scale multiplier (0.0 to 10.0)
|
|
208
|
+
r?: number; // Rotation in degrees (-3600 to 3600)
|
|
209
|
+
x?: number; // X displacement in canvas coordinates
|
|
210
|
+
y?: number; // Y displacement in canvas coordinates
|
|
211
|
+
b?: number; // Blur (0 to 50)
|
|
212
|
+
e?: "linear" | "ease-in" | "ease-out" | "ease-in-out" | "steps(1, end)" | [number, number, number, number];
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
- `reveal` (1.22.0): How a layer enters when shown by an action and exits when hidden (`revealSchema`):
|
|
216
|
+
```typescript
|
|
217
|
+
interface OimReveal {
|
|
218
|
+
effect: "fade" | "slide-up" | "slide-down" | "slide-left" | "slide-right" | "scale";
|
|
219
|
+
duration: number; // Duration in ms (0 to 2000)
|
|
220
|
+
easing?: KeyframeEasing;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
- `textAnimationType`: `"none" | "typewriter" | "wave-chars" | "glitch-cyberpunk" | "glitch-slice" | "fade-chars" | "slide-up" | "bounce-chars" | "reveal-mask" | "scale-in" | "rise-words" | "blur-chars"`, with `textAnimationSpeed` (`0..200`), `textAnimationDelay` (`0..100`), `textAnimationIntensity` (`0..100`) and `typewriterCursor`.
|
|
224
|
+
- `fade-chars`, `slide-up`, `bounce-chars`, `reveal-mask`, `scale-in` and `rise-words` are entrance effects defined as portable data in `@oim-media/core` (`text-effects.ts`).
|
|
225
|
+
- `glitch-cyberpunk` and `glitch-slice` are defined as portable layer primitives in `@oim-media/core` (`glitch.ts`).
|
|
226
|
+
- `blur-chars` is **retired** (not portable): it still validates, and readers draw the text without animation.
|
|
227
|
+
|
|
228
|
+
### 4.8. Rendering rules for readers
|
|
229
|
+
|
|
230
|
+
The web player (`@oim-media/player-oim`) is the reference renderer (section 9.1). These are the rules it
|
|
231
|
+
follows that the schema alone does not say. Every reader must follow them; each one has cost a visible
|
|
232
|
+
difference between renderers before it was written down.
|
|
233
|
+
|
|
234
|
+
**Order and stacking**
|
|
235
|
+
- Layers paint in `slots` order, except the background pattern layer (`is_background_pattern: true`, or
|
|
236
|
+
id `background-pattern-slot`), which always paints behind every other layer.
|
|
237
|
+
|
|
238
|
+
**Text**
|
|
239
|
+
- The text box is a column centred vertically (`justify-content: center`). Horizontal alignment is
|
|
240
|
+
`style.textAlign`, else the slot's `align`, else `left`.
|
|
241
|
+
- The box has a right padding of `0.25em` and is anchored by its padded width: left-aligned text does not
|
|
242
|
+
move, centred text moves `0.125em` left and right-aligned text `0.25em` left.
|
|
243
|
+
- Text with no colour is black.
|
|
244
|
+
- `letterSpacing` applies only as a length with its unit (`px`, `em`); a bare number is ignored.
|
|
245
|
+
- `textRuns` are ignored by a reader that lays out text itself; it lays out `content` instead.
|
|
246
|
+
- A text layer's shadow follows the glyphs (CSS `text-shadow`).
|
|
247
|
+
- The `lineHeight` leading is split evenly above and below the glyphs, as in CSS.
|
|
248
|
+
- On a text layer, `effects` of type `drop-shadow` add one text-shadow (`x`, `y` default 0 and 4, `blur` default 12) and `glow` adds two, blurred `radius * 0.4` and `radius` (default 16, colour `#f59e0b`).
|
|
249
|
+
|
|
250
|
+
**Shapes, strokes and fills**
|
|
251
|
+
- A plain rectangle's stroke is a border outside its box: the box grows by `strokeWidth` on every side,
|
|
252
|
+
keeps its vertical centre, and is anchored horizontally by `textAlign` (left by default). The fill
|
|
253
|
+
paints under the border. Shapes drawn as SVG (catalogue shape, `svgPath`) stroke their outline instead.
|
|
254
|
+
- Borders are drawn only from `strokeColor`, `strokeWidth` and `strokeStyle`. `borderColor` and
|
|
255
|
+
`borderWidth` are not contract keys and are not drawn.
|
|
256
|
+
- A custom path (`svgPath`) is scaled from its `svgViewBox` into the layer box; a photo frame with its own
|
|
257
|
+
`svgPath` masks its colour layer to that path too, so no box of the fill colour shows around the photo.
|
|
258
|
+
- A `container` layer paints like a `geo` layer.
|
|
259
|
+
- A pattern tile is `160 × fillPatternScale` (or `backgroundPatternScale`) canvas units. A stored tile
|
|
260
|
+
(`svgRawXml`) wins over the one generated from the pattern type.
|
|
261
|
+
|
|
262
|
+
**Effects**
|
|
263
|
+
- The shadow of a non-text layer, and the `drop-shadow` effect, follow the layer's silhouette (its alpha),
|
|
264
|
+
not its bounding box, and paint under the layer.
|
|
265
|
+
- `effects` apply in file order to the whole layer, before its flip and rotation. `blur` blurs with a
|
|
266
|
+
standard deviation of `radius` (default 8). On a non-text layer `drop-shadow` casts the silhouette
|
|
267
|
+
(Gaussian sigma `blur / 2`) and `glow` casts it twice, blurred `radius * 0.4` then `radius`.
|
|
268
|
+
- A non-empty `effects` replaces the legacy `shadow*`, `layerBlur`/`blur` and `grain` fields.
|
|
269
|
+
- `texture` repeats its preset tile (`@oim-media/core` `TEXTURE_CATALOG`) at `tile size * scale` canvas units, blended
|
|
270
|
+
with `blend` (default per preset). Text takes it inside its glyphs; `svg` and `image` layers over their pixels;
|
|
271
|
+
other layers over their box, faded once more by `intensity`. In document `metadata.effects` it covers the canvas.
|
|
272
|
+
- Media filters (`filterPreset`, `filter*`) apply only to `image` layers and to the picture of an image
|
|
273
|
+
fill, never to the frame or to other layer types.
|
|
274
|
+
- `opacity` is read as `0..100`.
|
|
275
|
+
- Retired keys (section 8), such as `blendMode`, are not drawn.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 5. Canonical Parallax (`parallaxSchema`)
|
|
280
|
+
|
|
281
|
+
Configuration for layers with spatial depth and reactive or continuous displacement (carried on `slot.parallax` or `slot.style.parallax`):
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
interface OimParallax {
|
|
285
|
+
enabled: boolean; // Default: true
|
|
286
|
+
depth: number; // Focal depth (-2.0 to 2.0)
|
|
287
|
+
mode: "mouse" | "scroll" | "infinite_horizontal";
|
|
288
|
+
axis: "both" | "x_only" | "y_only";
|
|
289
|
+
infinite?: {
|
|
290
|
+
enabled?: boolean; // Default: false (true when active)
|
|
291
|
+
speedPxSec?: number; // Continuous advance speed (-500 to 500 px/s; alias: speed_px_sec)
|
|
292
|
+
direction?: "left_to_right" | "right_to_left";
|
|
293
|
+
mirror?: boolean; // Mirror alternate tiles (aliases: seamlessTile, seamless_tile)
|
|
294
|
+
pauseOnHover?: boolean; // Pause when the cursor rests on it (alias: pause_on_hover)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
> Note: On group layers that form an editable loop scene, `style.sceneLoop` (`{ speedPxSec, direction, mirror, pauseOnHover }`) and child `style.loopDepth` (`0..1`) preserve the scene parameters for re-editing, while each moving child layer carries its resolved `parallax` (`speedPxSec = scene.speedPxSec * loopDepth`) for the player (`infiniteLayerPlan`).
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 6. Interactivity and State Machine (`isInteractive`, `states`)
|
|
304
|
+
|
|
305
|
+
Allows declaring interactivity without writing JavaScript:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
interface OimInteractiveStates {
|
|
309
|
+
hover?: OimStateConfig; // State when the cursor rests on it or a touch is held (reverted on leave)
|
|
310
|
+
active?: OimStateConfig; // State when clicked or tapped (persists)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
interface OimStateConfig {
|
|
314
|
+
scale?: number; // Percentage scale (0 to 1000%, e.g. 105 = 105%)
|
|
315
|
+
rotation?: number; // Rotation (-360 to 360 degrees)
|
|
316
|
+
opacity?: number; // Percentage opacity (0 to 100%)
|
|
317
|
+
transitionDuration?: number; // Transition time in ms (0 to 10000)
|
|
318
|
+
actions?: OimStateAction[]; // Chained actions on other layers (max 64)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface OimStateAction {
|
|
322
|
+
target: string; // ID of the target slot (1 to 128 chars)
|
|
323
|
+
type: "show" | "hide" | "toggle" | "setText" | "play"; // "play" added in 1.22.0
|
|
324
|
+
value?: string; // Value to apply (e.g. new text if type is setText, max 10000 chars)
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## 7. Root Document Metadata (`OimMetadataSchema`)
|
|
331
|
+
|
|
332
|
+
Top-level structure carried in the `OIM_Data` payload:
|
|
333
|
+
|
|
334
|
+
| Key | Type | Description |
|
|
335
|
+
|---|---|---|
|
|
336
|
+
| `oim` | `string` | Format version of the document (e.g. `"1.27.10"`), the contract version the backend signs against. |
|
|
337
|
+
| `min_player` | `string` | Minimum player version required to rebuild the document (`minVersionFor`). |
|
|
338
|
+
| `oim_id` | `string` | Unique document identifier for re-editing and registry checks (max 128 chars). |
|
|
339
|
+
| `media_type` | `"image" \| "video"` | Media type of the OIM document. |
|
|
340
|
+
| `name` | `string` | Official title of the work (max 256 chars). |
|
|
341
|
+
| `description` | `Record<string, string>` | Per-language summary of the whole document for screen readers (1.16.0, max 1000 chars per language). |
|
|
342
|
+
| `reedit` | `"public" \| "author_only" \| "sealed"` | Authorised cryptographic re-edit policy (1.23.0; replaces deprecated Spanish `reedicion`). |
|
|
343
|
+
| `immutable_lock` | `boolean` | Cryptographic seal flag (`true` when `reedit === "sealed"`). |
|
|
344
|
+
| `canvas` | `{ natural_width: number, natural_height: number }` | Base reference dimensions of the canvas. |
|
|
345
|
+
| `lang` / `languages` | `string[]` | Catalogue of BCP-47 language codes present in the document (max 10 chars per tag). |
|
|
346
|
+
| `palettes` | `Record<string, Partial<Record<DynamicColorRole, string>>>` | Up to 6 named colour palettes mapping semantic roles to CSS colours (1.20.0). |
|
|
347
|
+
| `defaultPalette` | `string` | Name of the active palette in `palettes` (1.20.0). |
|
|
348
|
+
| `slots` | `OimSlot[]` | Array of interactive layers (maximum `MAX_SLOTS` = 150). |
|
|
349
|
+
| `zones` | `OimZone[]` | Optional catalogue of named bounding box zones. A zone paints its background, corners and backdrop blur; its layers keep their own position. Flow properties in its style (`display`, `flexDirection`, `justifyContent`, `alignItems`, `gap`, `padding`) are not applied. |
|
|
350
|
+
| `capabilities` | `("vector_parallax" \| "infinite_parallax" \| "text_marks" \| "image_fill" \| "media_filters" \| "frame_shapes")[]` | Capabilities used by the document. |
|
|
351
|
+
| `capabilities_required` | same as `capabilities` | Capabilities the reader **must** implement to paint with fidelity (glTF pattern). |
|
|
352
|
+
| `fonts` | `EmbeddedFont[]` | Font variants embedded in base64 for offline rendering (1.3.0): subset, static (no `fvar`, `gvar`, `avar`, `HVAR`, `MVAR`; `STAT` may stay) WOFF, `format: "woff"`, written since 1.27.0; WOFF2 (`format: "woff2"`) is read forever. The content starts with the magic of its format (`wOFF` / `wOF2`). |
|
|
353
|
+
| `image_hash` | `string` | SHA-256 hash of the base image `IDAT` chunk payloads (`imageDataHash`). |
|
|
354
|
+
| `image_hash_alg` | `"sha256-idat"` | Image sealing algorithm identifier. |
|
|
355
|
+
| `signature` | `string` | ECDSA P-256 digital signature (Base64) over RFC 8785 canonical JSON (`serializeCanonical`). |
|
|
356
|
+
| `key_version` | `string` | Public key identifier in `keys.json` (e.g. `"v1"`, `"v2"`). |
|
|
357
|
+
| `signed_at` | `string` | ISO-8601 timestamp with time zone included inside the signed payload to enforce key expiry. |
|
|
358
|
+
| `timestamp` | `string` | ISO-8601 timestamp of package creation. |
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 8. Retired Keys (Read-Only for Backward Compatibility)
|
|
363
|
+
|
|
364
|
+
These keys remain declared with `deprecated: true` in `oim.schema.json` so already-published files continue to validate, but **no new writer may emit them**:
|
|
365
|
+
- **In `style`:** `textTransform`, `verticalAlign`, `shader_fx`, `blendMode` (closed to 9 values), `grain`/`grainIntensity`/`grainSize`/`grainBlend` (replaced by `style.effects` texture), `textHighlight` (replaced by `style.marks`), `animation.custom_loop_css`/`custom_keyframes`/`custom_hover_css` (retired in 1.9.0).
|
|
366
|
+
- **At the root:** `reedicion` (renamed to `reedit` in 1.23.0; mapped by `normalizeLegacyKeys`), `grain` and `effects` (background grain is baked into the base image, 1.12.0), `elements` and `layers` (`slots` is the single canonical name, 1.12.0), `allowed_domains` (retired in 1.7.0), `redirect_membership_verified` and `terms_accepted` (retired in 1.1.0).
|
|
367
|
+
- **In the slot:** `p`, `s`, `st`, `c`, `t` (short aliases retired in 1.12.0).
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 9. Verification and Conformance
|
|
372
|
+
|
|
373
|
+
Conformance of any generator or reader with this specification is validated automatically by running the canonical vector suite and JSON Schema agreement tests:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
pnpm conformance
|
|
377
|
+
pnpm --filter @oim-media/core test
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### 9.1. Visual conformance
|
|
381
|
+
|
|
382
|
+
The metadata vectors cannot see pixels: two renderers can read the same JSON and paint it differently.
|
|
383
|
+
Every vector therefore also has a golden: `vectors/golden/<vector>.web.png`, rendered by the web player,
|
|
384
|
+
which is the reference renderer.
|
|
385
|
+
|
|
386
|
+
**Procedure.** 800 px wide; height `round(800 × h / w)` from the vector's PNG header; device pixel ratio 1;
|
|
387
|
+
animations disabled; white background; host font `sans-serif` for text that names no family. Goldens
|
|
388
|
+
are only valid when rendered on the CI job's Linux image (`node tools/render-web-goldens.mjs vectors/golden`),
|
|
389
|
+
because fonts rasterise differently on each system.
|
|
390
|
+
|
|
391
|
+
**Metric** (`tools/parity-core.mjs`). A pixel differs when any channel differs by more than 40. The
|
|
392
|
+
difference mask is eroded with a 3×3 kernel, which removes anti-aliasing edges. The outermost 1 px ring of the image is not measured (the kernel has no neighbours there). A render fails when the
|
|
393
|
+
remaining area exceeds **0.4 %** of the image or any 4-connected blob exceeds **576 px** (a 24 × 24 square).
|
|
394
|
+
|
|
395
|
+
The threshold was calibrated on 2026-11-07 against bugs reproduced on purpose:
|
|
396
|
+
|
|
397
|
+
| Case | Area |
|
|
398
|
+
|---|---|
|
|
399
|
+
| Largest passing vector (text anti-aliasing, `typography-styling`) | 0.265 % |
|
|
400
|
+
| Unitless `letterSpacing` ignored | 0.509 % |
|
|
401
|
+
| Square shadow on a round shape | 6.96 % |
|
|
402
|
+
| Shadow painted over the fill | 6.29 % |
|
|
403
|
+
| Pattern tile at the wrong size | 10.9 % |
|
|
404
|
+
| Pattern background painted over the layers | 24.6 % |
|
|
405
|
+
|
|
406
|
+
**Running it.** A renderer writes `<vector>.png` per vector following the procedure, then
|
|
407
|
+
`node tools/parity.mjs <renders-dir> --renderer <name> --out <diff-dir>` compares them and writes a red
|
|
408
|
+
difference image for each failure. Known differences go in `vectors/golden/exceptions.json`, per renderer,
|
|
409
|
+
with a reason; an excepted vector that passes fails the run, so a fixed bug cannot keep its exception.
|