Dynamic Colors Engine
The Dynamic Colors Engine allows OIM interactive assets to dynamically adapt their color scheme to the embedding host application's brand, theme, or dark/light mode, without modifying the underlying asset file.
In traditional digital publishing, an interactive banner designed with purple brand accents cannot be embedded on a blue corporate website without visual discordance. Updating it requires the author to manually edit the artwork, re-export, and re-publish.
With OIM Dynamic Colors, authors assign semantic roles to text, vector fills, outlines, and window masks. At runtime, <oim-player> maps these roles to standard CSS custom properties defined by the host page, while guaranteeing rock-solid fallback defaults and impenetrable security against CSS injection attacks.
The 12 Semantic Roles
OIM adopts a 12-role semantic palette inspired by modern design token standards (such as Material Design 3 and open design token specifications):
| Semantic Role | CSS Custom Property | Primary Purpose | High-Contrast Pairing |
|---|---|---|---|
primary | --oim-color-primary | Dominant brand color, primary Call-To-Action (CTA) fills, main badges. | on-primary |
on-primary | --oim-color-on-primary | Typography, icons, and glyphs resting directly on top of primary surfaces. | primary |
secondary | --oim-color-secondary | Secondary brand color, supporting badges, chips, category tags. | on-secondary |
on-secondary | --oim-color-on-secondary | Typography and icons resting directly on top of secondary surfaces. | secondary |
surface | --oim-color-surface | Background color for container cards, panels, and window-mode masks. | on-surface |
on-surface | --oim-color-on-surface | Primary body text, titles, and neutral icons resting on surface backgrounds. | surface |
outline | --oim-color-outline | Structural borders, vector outlines, divider lines, and card strokes. | N/A |
accent | --oim-color-accent | Expressive highlights, attention-grabbing details, rating stars, alert badges. | N/A |
text-primary | --oim-color-text-primary | Headlines and large display text. | N/A |
text-secondary | --oim-color-text-secondary | Subtitles and body copy. | N/A |
text-muted | --oim-color-text-muted | Small print, labels and metadata. | N/A |
text-on-accent | --oim-color-text-on-accent | Text resting on accent fills. | accent |
TypeScript Contract (@oim-media/core)
export type DynamicColorRole =
| "primary"
| "on-primary"
| "secondary"
| "on-secondary"
| "surface"
| "on-surface"
| "outline"
| "accent"
| "text-primary"
| "text-secondary"
| "text-muted"
| "text-on-accent";
export const DYNAMIC_COLOR_ROLES: readonly DynamicColorRole[] = [
"primary",
"on-primary",
"secondary",
"on-secondary",
"surface",
"on-surface",
"outline",
"accent",
"text-primary",
"text-secondary",
"text-muted",
"text-on-accent",
] as const;Runtime Resolution Pipeline (resolveCssColor)
The core engine provides a single, centralized resolution function (resolveCssColor) through which all 13 rendering sinks in the player (ImageEngine) resolve colors:
export function resolveCssColor(
role: DynamicColorRole | string | undefined | null,
fallback?: string | null
): stringResolution Rules
Role Active & Fallback Provided:
typescriptresolveCssColor("accent", "#2563EB") // Returns: "var(--oim-color-accent, var(--oim-palette-accent, #2563EB))"The browser uses
--oim-color-accentif defined in the host page's CSS. If not, it uses the file's palette colour for that role, and if the file has none, the author's original#2563EB.Role Active & No Fallback:
typescriptresolveCssColor("surface", undefined) // Returns: "var(--oim-color-surface)"No Role Assigned (Fixed Author Color):
typescriptresolveCssColor(undefined, "#10B981") // Returns: "#10B981"The slot renders strictly with the author's specified color and ignores host page theming.
Malicious Fallback Payload Detected:
typescriptresolveCssColor("accent", "red; background: url(//evil.com/beacon)") // Returns: "var(--oim-color-accent, var(--oim-palette-accent))"The corrupted fallback is immediately discarded. The CSS variable is preserved, but the envenomed fallback is stripped.
No Role & Malicious Fallback:
typescriptresolveCssColor(undefined, "red; background: url(//evil.com/beacon)") // Returns: ""The unsafe value is rejected, rendering an empty string rather than masking tampering.
Security Architecture: Strict Positive Whitelist (isSafeCssColor)
In an .oim file, colors originate from untrusted JSON payloads created by third parties. If color strings were interpolated directly into an HTML element's inline style attribute without sanitization, an attacker could craft an injection payload:
// MALICIOUS INPUT IN OIM METADATA:
"strokeColor": "red; background-image: url(https://evil.com/tracking-beacon)"
// RESULTING INSECURE DOM INJECTION (WITHOUT SANITIZATION):
<div style="stroke: red; background-image: url(https://evil.com/tracking-beacon)"></div>This would cause the browser to parse two distinct CSS declarations, triggering an unauthorized HTTP request to an external server (tracking beacon, data exfiltration, or CSRF).
To eradicate this attack vector, OIM enforces isSafeCssColor, a strict positive whitelist tested against hundreds of real-world production projects:
export function isSafeCssColor(valor: unknown, profundidad = 0): boolean {
if (typeof valor !== "string") return false;
const v = valor.trim();
// Guard 1: String length <= 128 and recursion depth <= 4
if (!v || v.length > 128 || profundidad > 4) return false;
// Guard 2: Reject control characters, quotes, and escape slashes
if (/[;{}<>"]/.test(v) || v.includes(String.fromCharCode(92))) return false;
// Guard 3: Reject dangerous CSS functions and comments
if (/url\(|expression\(|\/\*/i.test(v)) return false;
// Positive Whitelist Patterns:
if (HEX.test(v)) return true; // #RGB, #RGBA, #RRGGBB, #RRGGBBAA
if (PALABRAS.has(v.toLowerCase())) return true; // transparent, currentcolor, none, inherit
if (FUNCION_COLOR.test(v)) return true; // rgb(...), rgba(...), hsl(...), hsla(...)
if (VAR_SIMPLE.test(v)) return true; // var(--custom-prop)
// Recursive fallback validation for nested var()
const conRespaldo = VAR_CON_RESPALDO.exec(v);
if (conRespaldo) return isSafeCssColor(conRespaldo[2], profundidad + 1);
return false;
}Which layer fields carry a role
A role always travels with the colour it replaces. Tools that read an .oim (to list the roles a file uses, or the author's colour for each) should read these pairs from each layer's style, the same ones the player resolves:
| Role field | Author's colour (fallback) |
|---|---|
fillColorRole | fillColor |
colorRole | fillColor (text) |
strokeColorRole | strokeColor |
textStrokeColorRole | textStrokeColor |
effectColorRole | effectColor (glow) |
maskBgColorRole | maskBgColor |
maskStrokeColorRole | maskStrokeColor |
Host Integration Guide
Defining Theme Palettes in CSS
To customize embedded OIM assets, define the semantic custom properties on the host element or any ancestor container:
/* Light Theme */
:root {
--oim-color-primary: #2563eb;
--oim-color-on-primary: #ffffff;
--oim-color-secondary: #475569;
--oim-color-on-secondary: #f8fafc;
--oim-color-surface: #ffffff;
--oim-color-on-surface: #0f172a;
--oim-color-outline: #cbd5e1;
--oim-color-accent: #f59e0b;
}
/* Dark Theme */
[data-theme="dark"] {
--oim-color-primary: #3b82f6;
--oim-color-on-primary: #ffffff;
--oim-color-secondary: #94a3b8;
--oim-color-on-secondary: #0f172a;
--oim-color-surface: #1e293b;
--oim-color-on-surface: #f8fafc;
--oim-color-outline: #334155;
--oim-color-accent: #fbbf24;
}Palettes inside the file
An .oim can also carry its own named palettes (up to 6 per page, set in the editor or with oim_apply_theme). The player writes the chosen one as --oim-palette-<role> and picks it in this order:
- the
paletteattribute on<oim-player>(for examplepalette="dark"); - if the file carries both a
darkand alightpalette, the one matching the reader's system setting (prefers-color-scheme); - the file's
defaultPalette.
So an image with dark and light palettes follows the reader's dark mode by itself, with no CSS on the host page. A host page that defines --oim-color-* still wins over the file's palette, and allow-dynamic-colors="false" turns both off.
<!-- Force the dark palette regardless of the system setting -->
<oim-player src="piece.oim.png" palette="dark"></oim-player>Isolating Assets (allow-dynamic-colors)
If your website defines global --oim-color-* variables, but you are embedding a third-party artwork where the author's original colors must be preserved, use the allow-dynamic-colors="false" attribute:
<!-- Ignores host site variables; renders strict author fallback colors -->
<oim-player
src="independent-artwork.oim.png"
allow-dynamic-colors="false"
mode="full">
</oim-player>When allow-dynamic-colors="false" is set, the player explicitly assigns --oim-color-${role}: initial across all 12 roles within its internal Shadow DOM, neutralizing inherited properties from the document.