Skip to content

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 RoleCSS Custom PropertyPrimary PurposeHigh-Contrast Pairing
primary--oim-color-primaryDominant brand color, primary Call-To-Action (CTA) fills, main badges.on-primary
on-primary--oim-color-on-primaryTypography, icons, and glyphs resting directly on top of primary surfaces.primary
secondary--oim-color-secondarySecondary brand color, supporting badges, chips, category tags.on-secondary
on-secondary--oim-color-on-secondaryTypography and icons resting directly on top of secondary surfaces.secondary
surface--oim-color-surfaceBackground color for container cards, panels, and window-mode masks.on-surface
on-surface--oim-color-on-surfacePrimary body text, titles, and neutral icons resting on surface backgrounds.surface
outline--oim-color-outlineStructural borders, vector outlines, divider lines, and card strokes.N/A
accent--oim-color-accentExpressive highlights, attention-grabbing details, rating stars, alert badges.N/A
text-primary--oim-color-text-primaryHeadlines and large display text.N/A
text-secondary--oim-color-text-secondarySubtitles and body copy.N/A
text-muted--oim-color-text-mutedSmall print, labels and metadata.N/A
text-on-accent--oim-color-text-on-accentText resting on accent fills.accent

TypeScript Contract (@oim-media/core) ​

typescript
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:

typescript
export function resolveCssColor(
  role: DynamicColorRole | string | undefined | null,
  fallback?: string | null
): string

Resolution Rules ​

  1. Role Active & Fallback Provided:

    typescript
    resolveCssColor("accent", "#2563EB")
    // Returns: "var(--oim-color-accent, var(--oim-palette-accent, #2563EB))"

    The browser uses --oim-color-accent if 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.

  2. Role Active & No Fallback:

    typescript
    resolveCssColor("surface", undefined)
    // Returns: "var(--oim-color-surface)"
  3. No Role Assigned (Fixed Author Color):

    typescript
    resolveCssColor(undefined, "#10B981")
    // Returns: "#10B981"

    The slot renders strictly with the author's specified color and ignores host page theming.

  4. Malicious Fallback Payload Detected:

    typescript
    resolveCssColor("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.

  5. No Role & Malicious Fallback:

    typescript
    resolveCssColor(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:

text
// 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:

typescript
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 fieldAuthor's colour (fallback)
fillColorRolefillColor
colorRolefillColor (text)
strokeColorRolestrokeColor
textStrokeColorRoletextStrokeColor
effectColorRoleeffectColor (glow)
maskBgColorRolemaskBgColor
maskStrokeColorRolemaskStrokeColor

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:

css
/* 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:

  1. the palette attribute on <oim-player> (for example palette="dark");
  2. if the file carries both a dark and a light palette, the one matching the reader's system setting (prefers-color-scheme);
  3. 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.

html
<!-- 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:

html
<!-- 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.