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:
ColorPaletteAssetis a named list of color entries, each carrying a name, a color, an optionalColorRolesemantic tag (Primary, Background, Destructive, and so on), an optional set of free-form string tags, and a “locked” flag for protected entries.ColorGradientAssetis a color curve with separate color stops and alpha stops, an explicit color-space mode (RGB, HSV, Oklab), and per-stop interpolation curves.ColorSwatchCollectionAssetis 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
Colorliterals. - 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.Instanceis the single entry point. It discovers everyColorPaletteAssetandColorGradientAssetunder any module’sResources/Color/Palettes/andResources/Color/Gradients/folder on first access. - Lookup by name or by tag.
GetPalette(name),TryGetPalette(name, out), andGetPalettesByTag(tag)cover the lookup surface. The same three exist for gradients. All comparisons are case-insensitive. - Three asset types.
ColorPaletteAssetfor named entry lists,ColorGradientAssetfor color curves with separate alpha stops,ColorSwatchCollectionAssetfor parallel-array swatch groups. ColorRolesemantic 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.
ColorPaletteMappingresolves 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), andGetNearestEntry(color)snap arbitrary inputs to the closest palette entry. The basis for downsampling dynamic content to a fixed palette. - Mutable runtime builders.
MutableColorPaletteandMutableColorGradientallow programmatic authoring with Add/Remove/SetColor/SetRole/Reorder, thenBuild()to produce the immutable runtime instance. AChangedevent fires on every mutation. - Gradient color space selection.
ColorGradientColorSpacepicks the interpolation space: RGB (the Unity default, fast), HSV (smoother hue transitions), or Oklab (perceptually uniform). - Per-stop interpolation curves.
ColorGradientStopInterpolationselects 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
ColorGradientAlphaStopinstances (matching Unity’sGradientsemantics), independent of the color stops. - Static gradient factories.
ColorGradient.TwoColor,FromPalette,SteppedFromPalette, andFromUnityGradientproduce 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 exposesGetPalettesByTag/GetGradientsByTag. Used by the editor for palette pickers and by runtime code for theme groups (for example “UI”, “Retro”, “Default”). IsFallbackdiagnostic. WhenResources.LoadAllfinds 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
| Asset | Runtime type | Stores |
|---|---|---|
ColorPaletteAsset | ColorPalette (immutable) | Named list of ColorPaletteEntry (name, color, role, tags, locked). Plus a palette-level name and tag set. |
ColorGradientAsset | ColorGradient (immutable) | Color stops + alpha stops, color space (RGB/HSV/Oklab), gradient mode (Smooth/Stepped), per-stop interpolation. |
ColorSwatchCollectionAsset | ColorSwatchCollection | Parallel 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.
| Role | Intent |
|---|---|
None | No semantic role. Default for retro and generic palettes. |
Background | Page or screen background. |
Foreground | Primary text and foreground element color. |
Border | Border and divider color. |
Ring | Focus ring color for accessibility indicators. |
Primary / PrimaryForeground | Main brand color and the text/icon color on top of it. |
Secondary / SecondaryForeground | Subtle secondary background and text/icon color on top of it. |
Accent / AccentForeground | Highlight color (hover states) and text/icon color on top of it. |
Muted / MutedForeground | Disabled background and placeholder/disabled text color. |
Card / CardForeground | Card/panel background and text/icon color on top of it. |
Destructive / DestructiveForeground | Error/danger background and text/icon color on top of it. |
Success | Positive indicator color. |
Warning | Caution indicator color. |
Error | Error indicator color. |
Info | Informational 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
ColorRolefor 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
ColorPaletteandColorGradientreferences at startup. Registry lookups are dictionary-backed and fast, but referencing a palette every frame still adds an unnecessary indirection. - Cache
ColorPaletteMappingper 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
IsFallbackin 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, andSteppedFromPaletteproduce ready-to-use gradients without authoring an asset. ReserveColorGradientAssetfor 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 thangradient.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.AllPalettesand.AllGradientsonce during a loading screen so theResources.LoadAllcost 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-lineGetPalettesByTagcall. - 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 throughColorPaletteAsset.Data. - Migrate off the static
ColorPalettesaccessor. It is deprecated and may be removed. Move call sites toColorAssetRegistry.Instance.GetPalette(...)orGetPalettesByTag(...).
Pitfalls
Related
- Tween Utils
- Architecture Overview
- API reference:
Scylla.Core.Util.Palette.ColorAssetRegistry - API reference:
Scylla.Core.Util.Palette.ColorPalette - API reference:
Scylla.Core.Util.Palette.ColorPaletteAsset - API reference:
Scylla.Core.Util.Palette.ColorPaletteEntry - API reference:
Scylla.Core.Util.Palette.ColorGradient - API reference:
Scylla.Core.Util.Palette.ColorGradientAsset - API reference:
Scylla.Core.Util.Palette.ColorGradientStop - API reference:
Scylla.Core.Util.Palette.ColorGradientAlphaStop - API reference:
Scylla.Core.Util.Palette.ColorSwatchCollection - API reference:
Scylla.Core.Util.Palette.ColorSwatchCollectionAsset - API reference:
Scylla.Core.Util.Palette.ColorPaletteMapping - API reference:
Scylla.Core.Util.Palette.MutableColorPalette - API reference:
Scylla.Core.Util.Palette.MutableColorGradient - API reference:
Scylla.Core.Util.Palette.ColorRole - API reference:
Scylla.Core.Util.Palette.ColorGradientMode - API reference:
Scylla.Core.Util.Palette.ColorGradientColorSpace - API reference:
Scylla.Core.Util.Palette.ColorGradientStopInterpolation - API reference:
Scylla.Core.Util.Palette.ColorStopBlendMode