How a cell gets its color and its style, from the ANSI sixteen through truecolor, and what each of the nine style attributes actually does when the shader draws it.
Summary
Every cell carries a foreground color, a background color, and a bitfield of style attributes. The colors are ordinary Color32 values packed into the cell, so nothing stops you using whatever your game already produces. ANSIColor exists on top of that as the bridge to terminal convention: the standard sixteen by name, the 256-color extended palette by index, and truecolor by component.
That bridge matters more than it sounds like it should. Terminal escape sequences address color by index, not by RGB, so anything running through the terminal emulator picks colors out of a table. ANSIColor owns that table, which means swapping it swaps what every already-drawn indexed cell means. That is exactly how the color theme system repaints a screen: it pushes a new palette into ANSIColor and the indexed colors follow.
Attributes are the other half. Nine of them are visible to your game (bold, italic, underline, strikethrough, inverse, dim, blink, hidden and double-width), and the shader implements every one, so they cost nothing per frame. Blink genuinely blinks without any code running, because the shader animates it from a timer uniform. Inverse swaps foreground and background at draw time rather than requiring you to swap the colors yourself, which is what makes a selection highlight or a title bar a one-flag change.
Use this page when you need to:
- Pick colors that match terminal conventions rather than inventing your own palette.
- Reach the 256-color cube or truecolor from game code.
- Understand what a style attribute will actually look like before you set it.
- Swap the ANSI table under already-drawn content.
- Compose a background over what is already in a cell rather than replacing it.
Colors
There are three ways in, depending on what you have.
| Call | Range | Notes |
|---|---|---|
| Named properties | The standard sixteen | ANSIColor.Red, ANSIColor.BrightCyan and the rest. These read from the active palette, so they move when a theme changes. This is what most game code should use, because it keeps your screen consistent with anything the terminal draws. |
FromStandard(int index) | 0 to 15 | The same sixteen by index rather than by name, clamped into range. Useful when the index comes from data, a save file, or an escape sequence. |
FromIndex(int index) | 0 to 255 | The extended palette. 0 to 15 are the standard colors, 16 to 231 are a 6 by 6 by 6 color cube, and 232 to 255 are a 24-step grayscale ramp running from 8 to 238. An index outside the range returns white rather than throwing. |
FromRGB(byte r, byte g, byte b) | Anything | Truecolor. This is a convenience over constructing a Color32 with full alpha; there is no palette involved and nothing about it moves when a theme changes. |
The 6 by 6 by 6 cube and the grayscale ramp are built from the xterm-256 formulas, so an index means the same thing here as it does in a real terminal. That is what lets a program’s escape sequences produce the colors their author intended.
Nothing forces you through ANSIColor at all. Every drawing call takes a Color32, so a color from a Unity gradient, a designer-authored palette asset, or a lerp you computed this frame is equally valid. Reach for ANSIColor when you want terminal semantics or theme-following behavior, and use raw colors when you do not.
Swapping the palette
SetPalette replaces the sixteen standard colors with a Core ColorPalette, and ResetPalette puts the built-in set back. Because the named properties and FromStandard read through the active palette, this changes what already-drawn indexed content means without touching a single cell.
Two things follow from that. The extended 256-color table is rebuilt when the palette changes, so the cube and the ramp stay consistent with the sixteen. And truecolor cells are unaffected, because they were never indexed: a cell drawn with FromRGB keeps exactly the color it was given. If you want a screen that responds wholesale to a theme change, draw with named colors; if you want a color pinned regardless of theme, use truecolor.
Attributes
CellAttributes is a flags enum. Nine values are meant for your game; the rest are internal bookkeeping the renderer and the selection system use, and setting them by hand is not supported.
| Attribute | What the shader does | Notes |
|---|---|---|
Bold | Dilates the glyph’s distance field by the material’s bold weight | Faux bold rather than a second font, so it works with any font asset. The weight is expressed in atlas texels, which keeps it consistent across fonts built with different padding. |
Italic | Shears the glyph horizontally by the material’s shear amount | Also faux, and also font-independent. A real italic face will look better, but this costs nothing and works everywhere. |
Underline | Draws a rule across the cell at the configured underline position | Position and thickness are material properties, so they can be tuned per view. |
Strikethrough | Draws the same rule at the strikethrough position | Shares the underline’s thickness, so the two match by construction. |
Inverse | Swaps foreground and background at draw time | The cell’s stored colors are untouched, which is what makes this reversible with one flag. This is how a selection highlight, a title bar, or a menu cursor is usually drawn. |
Dim | Multiplies the foreground by 0.5 | A fixed factor, not configurable. Reads as “less important” without needing a second color in your palette. |
Blink | Hides the foreground for half of each cycle at the view’s text blink rate | Animated from a shader timer, so a blinking warning costs no per-frame work on the CPU. Kept on a separate timer from the cursor so the two do not have to agree. |
Hidden | Draws the foreground in the background color | The character is still there, still occupies its cell, and still comes back when the flag clears. This is the password-field behavior a terminal expects. |
DoubleWidth | Marks the cell as occupying two columns | Intended for the wide characters a terminal has to handle. Your own drawing code usually does not need it. |
Attributes combine, so Bold | Blink is a bold blinking cell and Inverse | Dim is a dimmed inverse one. They are also independent of color, which means an inverse cell picks up whatever colors it happens to hold, including ones that came from a theme change after it was drawn.
Recipes
Draw a selected menu row
Problem. Your inventory menu highlights the row under the cursor. Redrawing the row with swapped colors works until a theme change moves the colors under you and the highlight stops matching the rest of the screen.
Solution. Set Inverse and let the shader do the swap. The row keeps whatever colors it was drawn with, so it stays consistent with everything else through a retheme.
for (var i = 0; i < items.Count; i++)
{
/* One flag is the whole difference between a normal row and the highlighted one. Because
Inverse swaps at draw time, the row is still holding the ordinary text colors, so a
later theme change moves the highlight along with the rest of the screen. */
var attributes = i == selectedIndex ? CellAttributes.Inverse : CellAttributes.None;
_console.Print(2, MENU_Y + i, items[i].Name.PadRight(MENU_W),
ANSIColor.White, ANSIColor.Black, BackgroundBlendMode.Set, attributes);
}
Flash a low-health warning without a coroutine
Problem. The player’s health is critical and you want the readout to blink. Driving that from Update means a timer, a boolean, and a redraw every time it flips, all to animate one word.
Solution. Set Blink and stop thinking about it. The shader animates it from a uniform, so the cell blinks whether or not your code runs again.
/* Drawn once. The blink continues on its own at the view's text blink rate, so a paused game
or a frame spike does not desynchronize it from the rest of the interface. */
var critical = playerHealth < playerMaxHealth / 4;
_console.Print(STATS_X, STATS_Y, $"HP {playerHealth,3}", ANSIColor.BrightRed, ANSIColor.Black,
BackgroundBlendMode.Set,
critical ? CellAttributes.Bold | CellAttributes.Blink : CellAttributes.Bold);
Reach the 256-color cube for a heat map
Problem. You’re visualizing threat levels on a tactics grid and the sixteen ANSI colors are too coarse a ramp. You want a smooth gradient that still reads as terminal color rather than arbitrary RGB.
Solution. Index into the color cube. The 6 by 6 by 6 arrangement means you can walk one axis for a clean ramp.
/* The cube occupies indices 16 through 231, arranged as six levels per channel. Walking red
while holding green and blue at zero gives a six-step ramp from near-black to full red. */
for (var level = 0; level < 6; level++)
{
var index = 16 + (level * 36); /* 36 = 6 * 6, the stride of one red level. */
_console.Fill(LEGEND_X + level, LEGEND_Y, 1, 1, FULL_BLOCK,
ANSIColor.FromIndex(index), ANSIColor.Black);
}
/* The grayscale ramp at 232 through 255 is the one to reach for when you want intensity
without hue, such as a fog-of-war or an elevation overlay. */
var shade = 232 + Mathf.Clamp(elevation, 0, 23);
_console.Fill(x, y, 1, 1, FULL_BLOCK, ANSIColor.FromIndex(shade), ANSIColor.Black);
Pin a color so a theme change cannot move it
Problem. Your game’s faction colors are part of its identity and must stay the same whichever theme the player picks, while the rest of the interface should follow the theme.
Solution. Draw the faction colors with truecolor and everything else with named ANSI colors. Only the indexed colors move when the palette changes.
/* Named colors read through the active palette, so this label restyles with the theme. */
_console.Print(2, 1, "ALLEGIANCE", ANSIColor.White, ANSIColor.Black);
/* Truecolor was never indexed, so this is the same red under every theme. Use it for the
things that are part of your game's identity rather than part of its skin. */
var crimson = ANSIColor.FromRGB(178, 34, 52);
_console.Print(14, 1, faction.Name, crimson, ANSIColor.Black, BackgroundBlendMode.Set,
CellAttributes.Bold);
Mask text the way a password field does
Problem. Your in-game terminal prompts for a passcode and you do not want the characters visible, but you also do not want to store a masked copy of the string and reconcile it with the real one.
Solution. Draw the real text with Hidden. The characters occupy their cells and are simply drawn in the background color, so the cursor position and the layout stay correct.
/* The real string is on the grid, which means the cursor lands in the right place and the
field's width is honest. Clearing the flag reveals it without redrawing. */
_console.Print(PROMPT_X, PROMPT_Y, enteredPasscode, ANSIColor.Green, ANSIColor.Black,
BackgroundBlendMode.Set, CellAttributes.Hidden);
API Sketch
static class ANSIColor {
/* The standard sixteen, read through the active palette. */
Color32 Black, Red, Green, Yellow, Blue, Magenta, Cyan, White;
Color32 BrightBlack, BrightRed, BrightGreen, BrightYellow,
BrightBlue, BrightMagenta, BrightCyan, BrightWhite;
Color32 FromStandard(int index); /* 0-15, clamped */
Color32 FromIndex(int index); /* 0-255, white when out of range */
Color32 FromRGB(byte r, byte g, byte b);
ColorPalette ActivePalette { get; }
void SetPalette(ColorPalette palette); /* rebuilds the 256 table */
void ResetPalette();
}
[Flags] enum CellAttributes {
None, Bold, Italic, Underline, Strikethrough,
Inverse, Dim, Blink, Hidden, DoubleWidth
/* plus internal bits for cursor, selection and atlas index */
}
enum BackgroundBlendMode { Set, Multiply, Add, Screen, Overlay, Lighten, Darken, Alpha }
See the API reference for full signatures, remarks, and exception contracts on every member.
Best Practices
- Draw with named colors unless you have a reason not to. They follow the active palette, which means your screen restyles with the theme for free and stays consistent with anything the terminal renders.
- Use truecolor for identity, not for convenience. A color that must never change is a good candidate; reaching for
FromRGBbecause it is quicker than picking an ANSI name gives you a screen that ignores themes. - Prefer
Inverseto swapping colors by hand. One flag, reversible, and it survives a theme change. Manually swapped colors do not. - Let
Blinkblink. Animating a flash fromUpdatecosts a redraw per flip and drifts out of step with the rest of the interface, which is animated from a shared shader timer. - Pad strings you intend to invert. The highlight covers written cells only.
- Treat
Dimas a fixed step, not a tunable. It is a 0.5 multiply in the shader. When you need a specific reduced intensity, use a color. - Use the grayscale ramp for intensity overlays. Indices 232 to 255 give you 24 steps of pure luminance, which reads better for fog of war or elevation than desaturating a hue.
- Do not set the internal attribute bits. Cursor, selection, edge and atlas-index bits belong to the renderer and the selection system; writing them by hand produces cells that render wrongly or get overwritten unpredictably.
- Remember that
Hiddenis not secure. It changes what is drawn, not what is stored. - Check your font before relying on faux bold or italic. They are distance-field manipulations and look good on most monospace faces, but a font with very thin strokes can lose legibility when dilated.
Pitfalls
Related
- 01 Scylla TextMode
- 04 TextMode – Drawing
- 09 TextMode – Color Themes
- 06 TextMode – Terminal Emulation
- API reference:
Scylla.TextMode.ANSIColor - API reference:
Scylla.TextMode.CellAttributes - API reference:
Scylla.TextMode.BackgroundBlendMode