#[non_exhaustive]pub enum Tint {
None,
Multiply {
r: u8,
g: u8,
b: u8,
},
Mix {
r: u8,
g: u8,
b: u8,
amount: u8,
},
}Expand description
How a sprite’s own pixels are recoloured at draw time.
A sprite is composited from the artwork’s pixels, and a cell’s
Style::fg does not touch it (see
Surface::put_span). A tint is the separate channel that does,
so one piece of artwork can serve a biome variant, a damage flash, or a shadowed copy of
itself without a second sprite in the sheet.
Pixel backends only. Cell backends have no sprite to recolour and ignore a tint entirely;
they draw the cell’s glyph in its own Style, as always.
§Why not reuse fg
Tinting a sprite by the cell’s foreground colour is what most tileset libraries do, and it works for them because their foreground colour has exactly one job and defaults to white, the identity of a multiply.
Neither holds here. Color::Default means “whatever foreground the
terminal is configured for”, not white, so it has no sensible reading as a modulation value.
More importantly, a cell drawn as a sprite by a pixel backend is drawn as an fg-coloured
glyph by a cell backend, and the colour that reads correctly as a solid character is not
the colour that reads correctly multiplied onto artwork that already has colour of its own.
One field cannot serve both.
§Choosing an operation
Multiply is the workhorse and can only darken: every channel scales
toward zero. It preserves the artwork’s own shading, which is what makes it right for
variants of one material (grass to savanna, stone to mossy stone) and for lighting.
Mix blends toward a colour and can therefore brighten, which multiply cannot
express at all. It is also the only one of the two a caller could not approximate for
themselves, since doing so needs the sprite’s pixels. Mix at full strength replaces the
artwork’s colour outright while keeping its alpha, which is how a white-on-transparent mask
sheet gets recoloured.
Alpha is never touched by either: a tint changes what the sprite’s opaque pixels look like, never which of them are opaque. Compositing and the cell background showing through transparent pixels behave identically tinted or not.
§Scope: what Tint is not for
Tint is per-cell and per-draw, not per-sheet or per-frame. “Is this sheet art
or a mask” is a different, fixed-at-load-time question, answered once by
retroglyph_window::tileset::SheetColor rather than by this type. The two compose instead of
collapsing into one flag (see retroglyph_window::sprite_cache::SpriteTint, which resolves
both in one place), because “is this sheet art or a mask” (fixed when the asset is authored)
and “what colour to flash this cell right now” (fixed per frame) are different questions that
would conflict if merged into a single modulate(bool)-style flag: a sheet declared
art-not-mask still needs to be flashable.
Frame- or layer-level colour transforms (day/night cycles, fog of war, a “remembered” map
render) are not a use case for Tint either. Those apply to everything already drawn,
every frame, so routing them through per-cell Tint would mean writing the same value into a
side-table entry for every cell of every layer, every frame: the wrong lever for a
screen-wide effect. That is tracked as its own, not-yet-designed concern in retroglyph#562;
it is out of scope here.
Tint is #[non_exhaustive] so more operations (add, screen, replace) can be added later
without breaking either backend: the GL encoder already falls through to “no recolour” on an
operation it does not recognize.
§Examples
use retroglyph_core::color::Tint;
// Grass artwork, dimmed toward its own shadow.
let shadowed = Tint::multiply(128, 128, 128);
assert_eq!(shadowed.apply((200, 180, 60)), (100, 90, 30));
// The same pixels, flashed most of the way to white.
let hit = Tint::mix(255, 255, 255, 192);
assert_eq!(hit.apply((200, 180, 60)), (241, 236, 207));
// The default costs nothing and changes nothing.
assert_eq!(Tint::None.apply((200, 180, 60)), (200, 180, 60));Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
None
Composite the sprite’s pixels verbatim.
Multiply
Scale each channel by rgb / 255, darkening toward black.
(255, 255, 255) is the identity and behaves as None, just less cheaply.
Mix
Blend each channel amount / 255 of the way toward rgb.
amount of 0 is the identity; 255 replaces the sprite’s colour outright, keeping its
alpha.
Implementations§
Source§impl Tint
impl Tint
Sourcepub const fn multiply(r: u8, g: u8, b: u8) -> Self
pub const fn multiply(r: u8, g: u8, b: u8) -> Self
A Multiply tint scaling each channel by rgb / 255.
Sourcepub const fn mix(r: u8, g: u8, b: u8, amount: u8) -> Self
pub const fn mix(r: u8, g: u8, b: u8, amount: u8) -> Self
A Mix tint blending amount / 255 of the way toward rgb.
Sourcepub const fn is_identity(self) -> bool
pub const fn is_identity(self) -> bool
Whether this tint leaves every pixel exactly as authored.
True for None and for the identity of either operation, so a renderer can
take its untinted fast path for a tint that would do nothing.
Sourcepub const fn apply(self, rgb: (u8, u8, u8)) -> (u8, u8, u8)
pub const fn apply(self, rgb: (u8, u8, u8)) -> (u8, u8, u8)
Applies this tint to one straight-alpha RGB triple, returning the recoloured channels.
The reference implementation of the operation. Both pixel backends produce their output from this, the software renderer by calling it per pixel and the GL renderer by matching its arithmetic in the sprite fragment shader, so that a sprite tinted on one backend matches the same sprite tinted on the other.
Alpha is not an input and not an output: a tint never changes which pixels are opaque.
Sourcepub const fn apply_rgb888(self, px: Rgb888) -> Rgb888
pub const fn apply_rgb888(self, px: Rgb888) -> Rgb888
Applies this tint to an [Rgb888], the same operation as apply but
without the channel-order round trip through a bare (u8, u8, u8) tuple.
const because [Rgb888::to_rgb][gem::rgb::Rgb::to_rgb] is a const inherent method
(gem 0.2.0): the equivalent by way of the HasRed-family traits
cannot be, since trait methods aren’t const-callable on stable.
use retroglyph_core::color::Tint;
use gem::rgb::Rgb888;
const PX: Rgb888 = Tint::Multiply { r: 128, g: 128, b: 128 }
.apply_rgb888(Rgb888::from_rgb(200, 180, 60));
assert_eq!(PX, Rgb888::from_rgb(100, 90, 30));Sourcepub const fn multiply_color(c: Color, default: (u8, u8, u8)) -> Self
pub const fn multiply_color(c: Color, default: (u8, u8, u8)) -> Self
A Multiply tint by color’s resolved RGB, falling back to default
for Color::Default (which has no intrinsic reading as a
modulation value). Built for retroglyph-window’s sheet-level recolouring: a
SheetColor::Mask sheet is tinted by the cell’s own foreground colour this way before
the cell’s own Tint is applied on top.
Trait Implementations§
impl Copy for Tint
impl Eq for Tint
impl StructuralPartialEq for Tint
Auto Trait Implementations§
impl Freeze for Tint
impl RefUnwindSafe for Tint
impl Send for Tint
impl Sync for Tint
impl Unpin for Tint
impl UnsafeUnpin for Tint
impl UnwindSafe for Tint
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<C> WithAlpha for Cwhere
C: Copy,
impl<C> WithAlpha for Cwhere
C: Copy,
§fn with_alpha_first<A>(self, alpha: A) -> AlphaFirst<A, Self>
fn with_alpha_first<A>(self, alpha: A) -> AlphaFirst<A, Self>
self with alpha, storing alpha before the color in memory
(see [AlphaFirst]).§fn with_alpha_last<A>(self, alpha: A) -> AlphaLast<A, Self>
fn with_alpha_last<A>(self, alpha: A) -> AlphaLast<A, Self>
self with alpha, storing alpha after the color in memory
(see [AlphaLast]).