Palette Utils

29 min read

A unified registry for named color palettes, gradients, and swatch collections, so you can author UI tints, terrain biomes, and faction colors once and look them up by name, role, or tag from anywhere in your game.

Summary

The Scylla color palette utils provide a unified, asset-backed system for naming, organizing, and looking up colors so that game code can refer colors by intent (palette name, semantic role, or gradient position) rather than raw RGB triples. Gradients also provide many features where Unity’s own gradients fall short and both, color palettes and gradients can be edited with Scylla Chroma.

The system has three first-class asset types:

  • ColorPaletteAsset is a named list of color entries, each carrying a name, a color, an optional ColorRole semantic tag (Primary, Background, Destructive, and so on), an optional set of free-form string tags, and a “locked” flag for protected entries.
  • ColorGradientAsset is a color curve with separate color stops and alpha stops, an explicit color-space mode (RGB, HSV, Oklab), and per-stop interpolation curves.
  • ColorSwatchCollectionAsset is a parallel-array container for grouped color swatches.

All three are Unity ScriptableObject assets, authored in the editor and discovered at runtime.

ColorAssetRegistry is your entry point. It is a process-wide singleton (ColorAssetRegistry.Instance) that discovers every ColorPaletteAsset and ColorGradientAsset it can find under any module’s Resources/Color/Palettes/ and Resources/Color/Gradients/ folder via Resources.LoadAll. Lookup is by name or by tag, both case-insensitive. The registry transparently exposes the runtime ColorPalette and ColorGradient instances – the asset is the design-time wrapper; the registry returns the runtime view. When no assets are discovered (a fresh clone where the per-module asset generators have not run, a stripped player build that omitted the Resources folders, a test harness), the registry falls back to a minimal in-memory seed containing only an “Empty” palette and a “Black to White” gradient, and IsFallback returns true.

Beyond lookup, the system ships three richer behaviors. MutableColorPalette and MutableColorGradient are runtime builders for programmatic authoring: add entries, change colors, reorder, then Build() to produce the immutable runtime instance. ColorPaletteMapping translates between two palettes through a three-tier resolution chain (name match, role match, then perceptual nearest), which is how a UI palette swap or a retro downsample stays coherent. The ColorRole enum carries the canonical semantic vocabulary used by UI theming (Primary, Background, Destructive, Success, Warning, and so on).

Use the color palette system for:

  • Centralizing UI theme colors so a theme switch is one line.
  • Building retro-style downsamplers that snap dynamic colors to a fixed hardware palette.
  • Authoring gradient curves for heat maps, health bars, and procedural texturing.
  • Sharing color vocabularies across modules without leaking Unity Color literals.
  • Authoring color presets in the editor and looking them up by name in code.
  • Tag-based palette pickers in custom inspectors and tools.

Features

The features below apply across palettes, gradients, and swatch collections except where noted.

  • Unified asset registry. ColorAssetRegistry.Instance is the single entry point. It discovers every ColorPaletteAsset and ColorGradientAsset under any module’s Resources/Color/Palettes/ and Resources/Color/Gradients/ folder on first access.
  • Lookup by name or by tag. GetPalette(name), TryGetPalette(name, out), and GetPalettesByTag(tag) cover the lookup surface. The same three exist for gradients. All comparisons are case-insensitive.
  • Three asset types. ColorPaletteAsset for named entry lists, ColorGradientAsset for color curves with separate alpha stops, ColorSwatchCollectionAsset for parallel-array swatch groups.
  • ColorRole semantic vocabulary. A 21-value enum (Primary, Background, Destructive, Success, Warning, Info, Card, Border, Ring, Muted, and so on) used for UI theming. Palette entries can be tagged with a role; mappings between palettes use role match as a fallback when names differ.
  • Three-tier palette mapping. ColorPaletteMapping resolves source-to-target colors by trying name match first, then role match, then perceptual nearest. The right primitive for theme swaps and retro downsampling.
  • Nearest-color matching. palette.GetNearest(color), GetNearestIndex(color), and GetNearestEntry(color) snap arbitrary inputs to the closest palette entry. The basis for downsampling dynamic content to a fixed palette.
  • Mutable runtime builders. MutableColorPalette and MutableColorGradient allow programmatic authoring with Add/Remove/SetColor/SetRole/Reorder, then Build() to produce the immutable runtime instance. A Changed event fires on every mutation.
  • Gradient color space selection. ColorGradientColorSpace picks the interpolation space: RGB (the Unity default, fast), HSV (smoother hue transitions), or Oklab (perceptually uniform).
  • Per-stop interpolation curves. ColorGradientStopInterpolation selects how each stop’s interpolation curve bends: None, SmoothStep, SmootherStep, QuadIn/Out/InOut, CubicIn/Out/InOut, QuartIn/Out/InOut, SineIn/Out/InOut, ExpoIn/Out/InOut.
  • Separate alpha stops. Gradient transparency is encoded as a parallel curve of ColorGradientAlphaStop instances (matching Unity’s Gradient semantics), independent of the color stops.
  • Static gradient factories. ColorGradient.TwoColor, FromPalette, SteppedFromPalette, and FromUnityGradient produce ready-to-use gradients without authoring a ScriptableObject.
  • Bulk evaluation. gradient.Evaluate(ReadOnlySpan<float>, Span<Color>) evaluates many positions in one call; Sample(int count) produces a uniform sample array; ToPalette(int) snapshots a gradient as a palette.
  • Tag-based queries. Every asset carries an optional string[] Tags. The registry indexes them and exposes GetPalettesByTag / GetGradientsByTag. Used by the editor for palette pickers and by runtime code for theme groups (for example “UI”, “Retro”, “Default”).
  • IsFallback diagnostic. When Resources.LoadAll finds no palette or gradient assets, the registry falls back to a minimal seed (“Empty”, “Black to White”). The flag lets tests and editor tooling detect a freshly-cloned project where the per-module asset generators have not been run.

Asset types

AssetRuntime typeStores
ColorPaletteAssetColorPalette (immutable)Named list of ColorPaletteEntry (name, color, role, tags, locked). Plus a palette-level name and tag set.
ColorGradientAssetColorGradient (immutable)Color stops + alpha stops, color space (RGB/HSV/Oklab), gradient mode (Smooth/Stepped), per-stop interpolation.
ColorSwatchCollectionAssetColorSwatchCollectionParallel arrays of names and colors. Smaller surface than ColorPalette; no roles or per-entry tags.

The asset wraps the data; the registry returns the runtime instance. ColorPaletteAsset.Palette and ColorGradientAsset.Gradient are the conversion points. Most calling code never touches the asset directly because the registry surfaces only the runtime view.

Recipes

These recipes are self-contained – each one answers a single practical problem, so scan the names and jump to the one that matches what you’re building. The first recipe covers the registry setup that the others assume; if you’re new to the palette system, start there.

Look up a palette by name

Problem. You want to reference a palette from any script in your project without manually cross-referencing assets or calling Object.FindObjectOfType. You need one stable, name-keyed entry point that your gameplay code, your UI code, and your editor tools can all call without knowing where the assets live on disk.

Solution. ColorAssetRegistry.Instance is that entry point. The first access initializes the registry and runs Resources.LoadAll over every Resources/Color/Palettes/ and Resources/Color/Gradients/ folder it can find. After that, every lookup is dictionary-backed and fast.

var registry = ColorAssetRegistry.Instance;

/* Throwing lookup: raises KeyNotFoundException when the palette is not registered.
 * Use when the palette is required and a missing asset is a configuration error. */
var uiLight = registry.GetPalette("Scylla UI Light");

/* Try-lookup pattern: returns false instead of throwing.
 * Use whenever the palette is optional or might not be present yet. */
if (registry.TryGetPalette("CGA", out var cga))
{
    UseRetroPalette(cga);
}

/* Bulk discovery: iterate every registered palette.
 * Useful for building a theme picker or an editor debug list. */
foreach (var palette in registry.AllPalettes)
{
    Log.Info($"{palette.Name}: {palette.Count} colors", LogCategory.Core);
}

Read individual palette entries

Problem. You already have a palette in hand and you want to pull specific colors out of it – by position, by name, or with a safe fallback if the entry might not exist. You also need the full entry metadata (role, tags, locked flag) for an inspector tool or a debug log.

Solution. ColorPalette exposes entries by integer index and by name. The indexer is the most common access for known-good names; the TryGetColor pattern handles the uncertain case without a try/catch.

var uiLight = registry.GetPalette("Scylla UI Light");

/* Indexer by name. Case-insensitive. Throws if the name is not present. */
var primary    = uiLight["Primary"];
var background = uiLight["Background"];

/* Indexer by integer index. Throws on out-of-range. */
var first = uiLight[0];

/* Try-pattern when the entry may not exist in all palette variants. */
if (uiLight.TryGetColor("Accent", out var accent))
{
    Highlight(accent);
}

/* Full entry: name, color, role, tags, and locked flag.
 * Useful for inspector tools and diagnostic logs. */
var entry = uiLight.GetEntry(0);
Log.Info($"{entry.Name} [{entry.Role}]: {entry.Color}", LogCategory.UI);

/* Color32 variants for sprite atlases and raw pixel work. */
var primary32 = uiLight.GetColor32("Primary");

/* Probes. */
var hasAccent      = uiLight.ContainsName("Accent");
var indexOfPrimary = uiLight.IndexOf("Primary");

Theme your UI with semantic color roles

Problem. You’ve shipped a UI palette and then watched a designer rename “Primary” to “Brand” and later to “Accent”, with every code reference breaking each time. You want UI elements to ask for the right color by semantic intent – “give me the Primary color” – and have that request survive any rename.

Solution. Each palette entry can carry a ColorRole tag, and the role survives renames. Code that asks for ColorRole.Primary keeps working even after the designer renames the entry. This is the correct pattern for all color references in your UI layer; save named lookups for retro palettes and generic non-UI use.

/* Find the entry that carries the Primary role and apply its color.
 * Works correctly even after the entry is renamed from "Primary" to "Brand". */
if (uiLight.TryGetColorByRole(ColorRole.Primary, out var primaryColor))
{
    button.colors = ApplyPrimary(button.colors, primaryColor);
}

/* The index variant returns the integer position for downstream lookups.
 * Use when you need to drive a parallel array or a sprite remap table. */
if (uiLight.TryGetIndexByRole(ColorRole.Destructive, out var destructiveIndex))
{
    /* drive a warning overlay sprite index, for instance */
}

Quantize dynamic colors to a retro or fixed palette

Problem. You’re building a stylized renderer and you need any arbitrary color – from a screenshot, a procedural texture, or a player color pick – to snap to the nearest entry in your target palette. Think CGA quantization, NES-palette matching, or a Game Boy-style four-color reduction: you want the closest palette entry, perceptually, for every input color.

Solution. GetNearest, GetNearestIndex, and GetNearestEntry all do this for you. The matching metric is perceptual distance in RGB and the result is stable regardless of insertion order.

/* Snap a dynamic color to the nearest CGA palette entry. */
if (registry.TryGetPalette("CGA", out var cga))
{
    var live    = new Color(0.4f, 0.7f, 0.2f);
    var snapped = cga.GetNearest(live);

    /* Or get the matching index and entry name for diagnostic logs or
     * for driving a parallel sprite lookup table. */
    var idx   = cga.GetNearestIndex(live);
    var entry = cga.GetNearestEntry(live);
    Log.Info($"Snapped {live} to {entry.Name} (idx {idx})", LogCategory.Graphics);
}

/* Color32 overloads for byte-precision quantization: sprite atlases, raw textures. */
var snapped32 = cga.GetNearest(new Color32(102, 178, 51, 255));

Drive a health bar or heat map with a gradient

Problem. You’re building a system that needs to sample a color curve many times: a health bar that shifts from green to red as the player takes damage, a heat map painting tiles by temperature, or a minimap colorizing elevation. You want to look up a gradient asset the same way you look up a palette, then evaluate it at any position or across many positions at once without allocating per frame.

Solution. Gradients are looked up through the same registry. The runtime ColorGradient exposes Evaluate(t) for a single position and a bulk Evaluate(ReadOnlySpan<float>, Span<Color>) for many positions in one call with no allocation.

if (registry.TryGetGradient("Heat", out var heat))
{
    /* Single sample. The canonical "what color is this health fraction?" call. */
    var warm = heat.Evaluate(t: 0.7f);

    /* Color32 variant for writing into a texture or sprite atlas. */
    var warm32 = heat.Evaluate32(t: 0.7f);

    /* Bulk: evaluate 256 positions at once into caller-supplied spans.
     * No allocation; use this for painting a full lookup texture each frame. */
    Span<float> positions = stackalloc float[256];
    Span<Color> outputs   = stackalloc Color[256];
    for (var i = 0; i < positions.Length; i++)
    {
        positions[i] = i / (positions.Length - 1f);
    }
    heat.Evaluate(positions, outputs);

    /* Uniform sample without supplying positions explicitly. */
    Color[] uniform = heat.Sample(count: 32);

    /* Snapshot the gradient as a palette: useful for paletted shader lookups
     * where you want N discrete bands rather than continuous interpolation. */
    ColorPalette baked = heat.ToPalette(sampleCount: 16);

    /* Reverse for "from hot to cold" without re-authoring the asset. */
    var cool = heat.Reversed();
}

Build a procedural gradient in code

Problem. Your gradient needs to be constructed at runtime rather than baked into an asset – a hue ramp the player configures in a character creator, a debug heat map whose endpoints change based on game state, a tween-driven color flash. You want to author the gradient in code without needing a ColorGradientAsset, and you want a few static factory shortcuts for the common one-shot cases.

Solution. ColorGradient exposes four static factory methods that produce ready-to-use gradients without touching a ScriptableObject. None of them require an asset, which makes them the right tool whenever the gradient is procedural or short-lived.

/* Two-color gradient: the simplest case. Used for damage flashes,
 * faction color lerps, and any "start/end" visual transition. */
var red2green = ColorGradient.TwoColor(
    name:  "RedToGreen",
    start: Color.red,
    end:   Color.green);

/* Sample a palette as a gradient with linear interpolation between entries.
 * Useful for turning a set of authored swatches into a continuous ramp. */
var paletteGradient = ColorGradient.FromPalette(
    palette: uiLight,
    name:    "UILightGradient");

/* Stepped variant: hard transitions between palette entries, no interpolation.
 * The right shape for a CGA or Game Boy color band, or for tile-based
 * elevation shading with distinct strata. */
var stepped = ColorGradient.SteppedFromPalette(
    palette: cga,
    name:    "CGAStepped");

/* Adapt a Unity Gradient from the Inspector into a ColorGradient.
 * Useful when a designer has already authored a gradient in Unity's
 * built-in editor and you want to feed it into the Scylla pipeline. */
var fromUnity = ColorGradient.FromUnityGradient(
    name:     "FromInspectorGradient",
    gradient: _inspectorGradient);

Author a palette asset in the editor

Problem. You want a designer to own the palette – picking colors visually, assigning roles, writing tag strings – without writing any code. You need the resulting asset to be picked up automatically by the registry the next time your game runs.

Solution. Palettes and gradients are authored as Unity ScriptableObjects. Right-click in the Project view and select Create > Scylla > Core > Color Palette (or the gradient equivalent). Place the saved asset anywhere under a Resources/Color/Palettes/ folder and the registry finds it on first access.

Assets/
  YourModule/
    Resources/
      Color/
        Palettes/
          YourPalette.asset
        Gradients/
          YourGradient.asset

Generate a faction palette at runtime

Problem. You’re building a game where palettes don’t live on disk – a character creator lets the player choose colors and you need to build a full palette from those choices at runtime, inspect and mutate it live during the session, and then bake it down to an immutable form to pass to the rest of the systems.

Solution. MutableColorPalette is the runtime builder for that. Create one empty or seeded from an existing immutable palette, mutate it freely with Add/SetColor/SetRole, subscribe to the Changed event for live editor previews, then Build() to produce the immutable instance.

/* Build a palette in code: typical use case is a runtime character creator
 * or a procedurally generated faction color set. */
var builder = new MutableColorPalette(
    name:        "Custom Theme",
    description: "Generated at runtime for the level editor preview.");

builder.Add(name: "Primary",    color: new Color(0.2f, 0.4f, 0.8f), role: ColorRole.Primary);
builder.Add(name: "Background", color: new Color(0.1f, 0.1f, 0.1f), role: ColorRole.Background);
builder.Add(name: "Accent",     color: new Color(1.0f, 0.6f, 0.0f), role: ColorRole.Accent);

/* Mutate existing entries: player adjusts a hue slider in the character creator. */
builder.SetColor("Primary", new Color(0.3f, 0.5f, 0.9f));
builder.SetRole("Accent", ColorRole.Warning);

/* Subscribe to mutation events for live preview panels in an editor tool. */
builder.Changed += p => Log.Info($"Palette {p.Name} mutated", LogCategory.Editor);

/* Bake to immutable form. Pass this to any system that expects a ColorPalette. */
var custom = builder.Build();

Build a damage-visualization gradient at runtime

Problem. You need a gradient constructed entirely in code – a “cold to hot” ramp for damage visualization that you want to tune during development without an asset round-trip, with Oklab interpolation for perceptual uniformity and a mid-curve alpha dip for a pulse effect.

Solution. MutableColorGradient mirrors the palette builder. It keeps color stops and alpha stops separate (matching Unity’s gradient model), validates stop offsets in [0, 1], and sorts stops automatically as you add them.

var builder = new MutableColorGradient(
    name:        "HeatPreview",
    description: "Cold to hot for damage visualization.",
    mode:        ColorGradientMode.Smooth,
    colorSpace:  ColorGradientColorSpace.Oklab);

/* Color stops: cool blue at 0, orange at mid-point, near-white at full heat. */
builder.AddStop(offset: 0.0f, color: new Color(0.0f, 0.0f, 0.4f));
builder.AddStop(offset: 0.5f, color: new Color(0.8f, 0.2f, 0.0f), interpolation: ColorGradientStopInterpolation.SmoothStep);
builder.AddStop(offset: 1.0f, color: new Color(1.0f, 0.9f, 0.6f));

/* Alpha stops are independent: start opaque, dip mid-curve for a pulse,
 * then return to full opacity. The pulse reads as a "damage flash" peak. */
builder.AddAlphaStop(offset: 0.0f, alpha: 1.0f);
builder.AddAlphaStop(offset: 0.5f, alpha: 0.6f);
builder.AddAlphaStop(offset: 1.0f, alpha: 1.0f);

var heat = builder.Build();

Swap between light and dark themes

Problem. You’re shipping a game with swappable UI themes and you want to translate every color in your UI Light palette to its closest counterpart in UI Dark with a single precomputed structure. The same pattern covers retro palette swaps: multiple NES family palettes, Game Boy four-color sets, Sega Master System variants – you want a fast, cached source-to-target lookup that’s O(1) per query.

Solution. ColorPaletteMapping precomputes a source-to-target index table in one constructor pass. The three-tier resolution runs automatically: exact name match wins, then role match, then perceptual nearest. After construction every Resolve call is O(1).

/* Precompute: translate every UI Light entry to its closest UI Dark entry.
 * Do this once at theme-switch time, then hold the mapping for the session. */
var mapping = uiLight.CreateMapping(uiDark);

/* By source index: pairs cleanly with palette iteration for a full retint pass. */
for (var i = 0; i < uiLight.Count; i++)
{
    var darkColor = mapping.Resolve(i);
    SetDarkVariant(uiLight.GetColorName(i), darkColor);
}

/* By source name: more readable at call sites where you know the entry name. */
var translatedPrimary = mapping.Resolve("Primary");

/* Probe the target index instead of the color.
 * Useful for sprite remapping tables that index into a color array. */
var darkIndex = mapping.GetTargetIndex(sourceIndex: 0);

/* One-shot helper when you only need to translate one color and don't
 * want to hold a mapping object for a single call. */
var oneShot = uiLight.ResolveColor("Primary", uiDark);

Populate a theme picker from tagged palettes

Problem. You have a settings screen that should display “all available UI themes” or “all retro palettes” as a picker, without you hard-coding every palette name. You want to query by a label so the list stays current as artists add or remove palette assets without touching any code.

Solution. Every palette and gradient asset can carry a Tags array. The registry indexes them at discovery time and exposes tag-keyed bulk queries. Tags drive editor pickers, runtime theme groups, and any “give me everything labeled X” loop.

/* Every palette tagged "UI": populate a theme picker dropdown. */
foreach (var ui in registry.GetPalettesByTag("UI"))
{
    PopulateThemePicker(ui);
}

/* Every gradient tagged "Default": populate the editor's default-gradient slot. */
foreach (var grad in registry.GetGradientsByTag("Default"))
{
    PopulateDefaultGradients(grad);
}

/* Per-entry tag check: individual palette entries can also carry tags,
 * independent of the palette-level tags. Useful for accessibility grouping. */
var entry = uiLight.GetEntry(0);
if (entry.HasTag("Accessibility"))
{
    HighlightForA11y(entry);
}

Store a simple swatch list without role overhead

Problem. Your artist hands you a flat list of named colors – a small set of hero swatches for a character select screen, or a set of faction tints – and you don’t need roles, mapping, or per-entry tags on top. You want the lightest possible container.

Solution. ColorSwatchCollection is the parallel-array form with no roles, no per-entry tags, and no mapping support. It’s the right pick when you have “here’s a list of named swatches” data and don’t want the overhead of a full palette.

var swatches = new ColorSwatchCollection(
    name:   "Chroma",
    names:  new[] { "Sky", "Sea", "Sand" },
    colors: new[] {
        new Color(0.5f, 0.7f, 1.0f),
        new Color(0.1f, 0.3f, 0.6f),
        new Color(0.9f, 0.8f, 0.5f)
    });

for (var i = 0; i < swatches.Count; i++)
{
    var name  = swatches.GetName(i);
    var color = swatches[i];
    /* apply to your character select button or faction tint material */
}

API Sketch

The public surface groups into the registry, the palette and gradient runtime types with their asset wrappers, the mutable builders, the mapping type, and the swatch collection. Every feature class is accessible from ColorAssetRegistry.Instance; the mutable builders are standalone and do not require the registry.

namespace Scylla.Core.Util.Palette
{
    /* ------------------------------------------------------------------ */
    /* REGISTRY                                                           */
    /* ------------------------------------------------------------------ */

    public sealed class ColorAssetRegistry : ScriptableObject
    {
        public static ColorAssetRegistry Instance { get; }

        public IReadOnlyList<ColorPalette>  AllPalettes  { get; }
        public IReadOnlyList<ColorGradient> AllGradients { get; }
        public int PaletteCount  { get; }
        public int GradientCount { get; }
        public bool IsFallback   { get; }

        public ColorPalette  GetPalette(string name);
        public bool          TryGetPalette(string name, out ColorPalette palette);
        public IReadOnlyList<ColorPalette> GetPalettesByTag(string tag);

        public ColorGradient GetGradient(string name);
        public bool          TryGetGradient(string name, out ColorGradient gradient);
        public IReadOnlyList<ColorGradient> GetGradientsByTag(string tag);

        public bool RegisterPalette(ColorPaletteAsset asset);
        public bool UnregisterPalette(ColorPaletteAsset asset);
        public bool RegisterGradient(ColorGradientAsset asset);
        public bool UnregisterGradient(ColorGradientAsset asset);

        public const string PALETTE_RESOURCES_FOLDER  = "Color/Palettes";
        public const string GRADIENT_RESOURCES_FOLDER = "Color/Gradients";
    }

    /* ------------------------------------------------------------------ */
    /* PALETTE                                                            */
    /* ------------------------------------------------------------------ */

    public sealed class ColorPalette : IEquatable<ColorPalette>
    {
        public ColorPalette(string name, string description, ColorPaletteEntry[] entries);
        public ColorPalette(string name, string description, ColorPaletteEntry[] entries, string[] tags);

        public string Name        { get; }
        public string Description { get; }
        public int    Count       { get; }
        public IReadOnlyList<string> Tags { get; }

        public Color   this[string name] { get; }
        public Color   this[int index]   { get; }

        public Color   GetColor(string name);
        public Color   GetColor(int index);
        public Color32 GetColor32(string name);
        public Color32 GetColor32(int index);
        public bool    TryGetColor(string name, out Color color);
        public bool    TryGetColor32(string name, out Color32 color);

        public ColorPaletteEntry GetEntry(int index);
        public string  GetColorName(int index);
        public bool    ContainsName(string name);
        public int     IndexOf(string name);
        public string  GetTag(int index);
        public bool    HasTag(string tag);

        public bool TryGetColorByRole(ColorRole role, out Color color);
        public bool TryGetIndexByRole(ColorRole role, out int index);

        public int     GetNearestIndex(Color target);
        public int     GetNearestIndex(Color32 target);
        public Color   GetNearest(Color target);
        public Color32 GetNearest(Color32 target);
        public ColorPaletteEntry GetNearestEntry(Color target);
        public ColorPaletteEntry GetNearestEntry(Color32 target);

        public ColorPaletteMapping CreateMapping(ColorPalette target);
        public Color ResolveColor(string name, ColorPalette target);
    }

    public readonly struct ColorPaletteEntry : IEquatable<ColorPaletteEntry>
    {
        public string    Name     { get; }
        public Color     Color    { get; }
        public Color32   Color32  { get; }
        public ColorRole Role     { get; }
        public IReadOnlyList<string> Tags { get; }
        public bool      IsLocked { get; }

        public ColorPaletteEntry(string name, Color   color, ColorRole role = ColorRole.None);
        public ColorPaletteEntry(string name, Color32 color, ColorRole role = ColorRole.None);
        public ColorPaletteEntry(string name, Color   color, ColorRole role, string[] tags, bool isLocked);
        public ColorPaletteEntry(string name, Color32 color, ColorRole role, string[] tags, bool isLocked);
    }

    public sealed class ColorPaletteAsset : ScriptableObject
    {
        public ColorPalette     Palette { get; }
        public ColorPaletteData Data    { get; set; }
    }

    /* ------------------------------------------------------------------ */
    /* GRADIENT                                                           */
    /* ------------------------------------------------------------------ */

    public sealed class ColorGradient : IEquatable<ColorGradient>
    {
        public ColorGradient(string name, string description, ColorGradientMode mode, /* stops + alpha stops + color space */);

        public string Name        { get; }
        public string Description { get; }
        public ColorGradientMode      Mode       { get; }
        public ColorGradientColorSpace ColorSpace { get; }
        public int    StopCount      { get; }
        public int    AlphaStopCount { get; }
        public IReadOnlyList<string> Tags { get; }

        public Color   Evaluate(float t);
        public Color32 Evaluate32(float t);
        public void    Evaluate(ReadOnlySpan<float> positions, Span<Color> results);
        public Color[] Sample(int count);

        public ColorGradientStop GetStop(int index);
        public ColorGradient     Reversed();
        public ColorPalette      ToPalette(int sampleCount);
        public string GetTag(int index);
        public bool   HasTag(string tag);

        public static ColorGradient TwoColor(string name, Color start, Color end, /* options */);
        public static ColorGradient FromPalette(ColorPalette palette, /* options */);
        public static ColorGradient SteppedFromPalette(ColorPalette palette, /* options */);
        public static ColorGradient FromUnityGradient(string name, Gradient gradient);
    }

    public readonly struct ColorGradientStop : IEquatable<ColorGradientStop>, IComparable<ColorGradientStop>
    {
        public float                          Offset        { get; }
        public Color                          Color         { get; }
        public Color32                        Color32       { get; }
        public ColorGradientStopInterpolation Interpolation { get; }
        public float                          Midpoint      { get; }
        public ColorStopBlendMode             BlendMode     { get; }
    }

    public readonly struct ColorGradientAlphaStop : IEquatable<ColorGradientAlphaStop>, IComparable<ColorGradientAlphaStop>
    {
        public float Offset   { get; }
        public float Alpha    { get; }
        public float Midpoint { get; }
        public ColorGradientStopInterpolation Interpolation { get; }
    }

    public sealed class ColorGradientAsset : ScriptableObject
    {
        public ColorGradient      Gradient { get; }
        public ColorGradientData  Data     { get; set; }
    }

    /* ------------------------------------------------------------------ */
    /* MUTABLE BUILDERS                                                   */
    /* ------------------------------------------------------------------ */

    public sealed class MutableColorPalette
    {
        public MutableColorPalette(string name, string description);
        public MutableColorPalette(ColorPalette source);

        public string Name        { get; set; }
        public string Description { get; set; }
        public int    Count       { get; }
        public event Action<MutableColorPalette> Changed;

        public Color   this[int index] { get; set; }
        public Color   this[string name] { get; set; }
        public ColorPaletteEntry GetEntry(int index);
        public bool    TryGetColor(string name, out Color color);

        public void Add(string name, Color color, ColorRole role = ColorRole.None, /* options */);
        public bool Remove(string name);
        public void SetColor(string name, Color color);
        public void SetRole(string name, ColorRole role);
        public void SetTags(string name, string[] tags);
        public void SetLocked(string name, bool isLocked);
        public void Rename(string oldName, string newName);
        public void Reorder(int fromIndex, int toIndex);
        public void Clear();

        public ColorPalette Build();
    }

    public sealed class MutableColorGradient
    {
        public MutableColorGradient(string name, string description, /* options */);
        public MutableColorGradient(ColorGradient source);

        public int  StopCount      { get; }
        public int  AlphaStopCount { get; }
        public bool HasAlphaStops  { get; }
        public event Action<MutableColorGradient> Changed;

        public void AddStop(float offset, Color color, /* options */);
        public void RemoveStop(int index);
        public void SetStopColor(int index, Color color);
        public void SetStopOffset(int index, float offset);
        public void SetStopInterpolation(int index, ColorGradientStopInterpolation interpolation);
        public void SetStopMidpoint(int index, float midpoint);
        public void SetStopBlendMode(int index, ColorStopBlendMode blendMode);

        public void AddAlphaStop(float offset, float alpha, /* options */);
        public void RemoveAlphaStop(int index);
        public void SetAlphaStopAlpha(int index, float alpha);
        public void SetAlphaStopOffset(int index, float offset);
        public void SetAlphaStopInterpolation(int index, ColorGradientStopInterpolation interpolation);

        public void Clear();
        public void ClearAlphaStops();

        public Color Evaluate(float t);
        public ColorGradient Build();
    }

    /* ------------------------------------------------------------------ */
    /* MAPPING                                                            */
    /* ------------------------------------------------------------------ */

    public sealed class ColorPaletteMapping
    {
        public ColorPaletteMapping(ColorPalette source, ColorPalette target);

        public ColorPalette Source { get; }
        public ColorPalette Target { get; }

        public Color   Resolve(int sourceIndex);
        public Color32 Resolve32(int sourceIndex);
        public Color   Resolve(string sourceName);
        public int     GetTargetIndex(int sourceIndex);
    }

    /* ------------------------------------------------------------------ */
    /* SWATCHES                                                           */
    /* ------------------------------------------------------------------ */

    public sealed class ColorSwatchCollection
    {
        public ColorSwatchCollection(string name, string[] names, Color[] colors);

        public string Name { get; }
        public int    Count { get; }
        public ReadOnlySpan<Color>  Colors { get; }
        public ReadOnlySpan<string> Names  { get; }

        public Color this[int index] { get; }
        public string GetName(int index);
    }

    public sealed class ColorSwatchCollectionAsset : ScriptableObject
    {
        public ColorSwatchCollection      Collection { get; }
        public ColorSwatchCollectionData  Data       { get; set; }
    }

    /* ------------------------------------------------------------------ */
    /* ENUMS                                                              */
    /* ------------------------------------------------------------------ */

    public enum ColorRole { None, Background, Foreground, Border, Ring,
        Primary, PrimaryForeground, Secondary, SecondaryForeground,
        Accent, AccentForeground, Muted, MutedForeground, Card, CardForeground,
        Destructive, DestructiveForeground, Success, Warning, Error, Info }

    public enum ColorGradientMode { Smooth, Stepped }

    public enum ColorGradientColorSpace { RGB, HSV, Oklab }

    public enum ColorGradientStopInterpolation
    {
        None, SmoothStep, SmootherStep,
        QuadIn, QuadOut, QuadInOut,
        CubicIn, CubicOut, CubicInOut,
        QuartIn, QuartOut, QuartInOut,
        SineInOut, ExpoIn, ExpoOut, ExpoInOut, SineIn, SineOut
    }

    public enum ColorStopBlendMode { Normal, Multiply, Screen, Add }
}

See the API reference for full signatures, remarks, and exception contracts on every member.

ColorRole reference

The semantic vocabulary used for UI theming and palette-to-palette mapping. Roles are paired (Primary / PrimaryForeground, Card / CardForeground, and so on) so a layered UI surface has a consistent text color on top of its background.

RoleIntent
NoneNo semantic role. Default for retro and generic palettes.
BackgroundPage or screen background.
ForegroundPrimary text and foreground element color.
BorderBorder and divider color.
RingFocus ring color for accessibility indicators.
Primary / PrimaryForegroundMain brand color and the text/icon color on top of it.
Secondary / SecondaryForegroundSubtle secondary background and text/icon color on top of it.
Accent / AccentForegroundHighlight color (hover states) and text/icon color on top of it.
Muted / MutedForegroundDisabled background and placeholder/disabled text color.
Card / CardForegroundCard/panel background and text/icon color on top of it.
Destructive / DestructiveForegroundError/danger background and text/icon color on top of it.
SuccessPositive indicator color.
WarningCaution indicator color.
ErrorError indicator color.
InfoInformational indicator color.

Built-in palettes

The framework ships a set of built-in palette definitions referenced by the static ColorPalettes accessor class. The 21 entries listed there include UI themes (UILight, UIDark), demo themes (DemoDark, DemoBlue, DemoLight), a terminal palette (ANSI), and retro hardware palettes (CGA, VIC20, C64, AmigaWorkbench1, AmigaWorkbench2, ZXSpectrum, BBCMicro, AmstradCPC, MSX, NES, GameBoy, Atari8Bit, AtariST, AppleII, MacintoshII).

The definitions are realized as ColorPaletteAsset files generated by an editor tool (see DemoColorAssetGenerator in the Core editor) into Resources/Color/Palettes/. Whether they are present at runtime depends on whether the asset generator has been run in the current project. In a freshly-cloned project where the generator has not been run, the registry reports IsFallback == true and only the seed “Empty” palette is available. Calling ColorPalettes.UILight in that state throws KeyNotFoundException.

Best Practices

  • Reference colors by name through the registry, never as literals in code. Hard-coded new Color(0.2f, 0.4f, 0.8f) literals drift the first time a designer wants to retheme. Registry lookups stay correct across themes.
  • Use ColorRole for UI theming, names for everything else. Roles let a UI element ask for “Primary” and get the active theme’s primary color; entry names let retro and generic palettes keep their natural vocabulary.
  • Cache the ColorPalette and ColorGradient references at startup. Registry lookups are dictionary-backed and fast, but referencing a palette every frame still adds an unnecessary indirection.
  • Cache ColorPaletteMapping per source/target pair. The constructor walks the palette once to build the index table; per-call cost is then O(1). Building a fresh mapping per pixel defeats the design.
  • Check IsFallback in test harnesses and freshly-cloned projects. A missing palette in fallback mode is not a bug in calling code; it is a missing asset generation step. The flag is the cheapest way to distinguish the two.
  • Pick a gradient color space deliberately. RGB is the default and matches Unity’s built-in Gradient. Use HSV when smooth hue transitions matter (rainbows, heat maps with strong hue shifts). Use Oklab when perceptual uniformity matters (gradients that should look evenly stepped to the eye).
  • Use the static gradient factories for one-off procedural gradients. TwoColor, FromPalette, and SteppedFromPalette produce ready-to-use gradients without authoring an asset. Reserve ColorGradientAsset for gradients that are reused across the project.
  • Pre-bake gradients to palettes for per-pixel work. gradient.ToPalette(N) and palette indexer access is faster than gradient.Evaluate(t) for every pixel; the trade-off is N discrete colors instead of continuous interpolation.
  • Pre-warm the registry during loading screens. Touch ColorAssetRegistry.Instance.AllPalettes and .AllGradients once during a loading screen so the Resources.LoadAll cost happens off the gameplay critical path.
  • Tag palettes and gradients at authoring time. Tags drive editor pickers and runtime theme groups. The marginal cost of adding a "UI" or "Retro" tag at authoring time is zero; the value at lookup time is the difference between hard-coding every palette name and a one-line GetPalettesByTag call.
  • Prefer Mutable* for in-memory authoring; persist through *Asset. The mutable builders are not registered with the registry until you wrap them in an asset and register them explicitly. For runtime-customized colors that persist between sessions, save through ColorPaletteAsset.Data.
  • Migrate off the static ColorPalettes accessor. It is deprecated and may be removed. Move call sites to ColorAssetRegistry.Instance.GetPalette(...) or GetPalettesByTag(...).

Pitfalls