# SPDX-License-Identifier: GPL-3.0-only
# Copyright (C) 2026 Felix Faber /
# Otto von Guericke University Magdeburg, Thermal Process Engineering
"""
Colors and Color Gradients
--------------------------
"""
from __future__ import annotations
from typing import Self
from porescene.color.conversion import hex2rgb, nrgb2lnrgb, nrgb2rgb, rgb2hex, rgb2nrgb
[docs]
class Color:
"""
Simple representation of a color.
Parameters
----------
hexstr
HEX color string
Examples
------------
Suppose you have two :class:``Color`` instances, just mix them by literal
addition operation ``+``:
:example:
>>> blue = Color('#00f')
>>> red = Color('#f00')
>>> purple = red + blue
>>> print(purple)
Be aware that all properties of the color, which includes `alpha`, will be
mixed.
"""
def __init__(self, h: str = "#000"):
rgb = hex2rgb(h)
if len(rgb) == 3:
rgb = rgb2nrgb(rgb[0], rgb[1], rgb[2])
rgb += (1.0,)
else:
rgb = rgb2nrgb(rgb[0], rgb[1], rgb[2], rgb[3])
self.red = rgb[0]
self.green = rgb[1]
self.blue = rgb[2]
self.alpha = rgb[3]
def __add__(self, other) -> Color:
"""
Creates a new color by mixing the current instance with another.
"""
return self.mix(other)
def __str__(self) -> str:
return self.str_hex
def __repr__(self) -> str:
return f"Color('{self.str_hex}')"
[docs]
@classmethod
def from_rgb(cls, r: int, g: int, b: int, a: float = 1.0) -> Self:
ins = cls()
rgb = rgb2nrgb(r, g, b)
ins.red = rgb[0]
ins.green = rgb[1]
ins.blue = rgb[2]
ins.alpha = a
return ins
[docs]
@classmethod
def from_nrgb(cls, nr: float, ng: float, nb: float, a: float = 1.0) -> Self:
ins = cls()
ins.red = nr
ins.green = ng
ins.blue = nb
ins.alpha = a
return ins
[docs]
def mix(self, other: Self, ratio: float = 0.5) -> Color:
"""
Create a new color by mixing the current instance with others.
Parameters
----------
other
The color to mix with.
ratio
The amount of color `other` to mix to the current instance.
space
The colorspace to use for mixing the colors. Mixing in RGB space
is *subtractive*, while Lab is *additive*. Lab mixes more accurate
but still does not reach real color mixing results.
Returns
-------
A new instance of :class:``color``.
ToDo
----
Implementation of real world color mixing like described by
`MixBox <https://scrtwpns.com/mixbox.pdf>`_.
"""
r = self.red * (1 - ratio) + other.red * ratio
g = self.green * (1 - ratio) + other.green * ratio
b = self.blue * (1 - ratio) + other.blue * ratio
a = self.alpha * (1 - ratio) + other.alpha * ratio
return Color.from_nrgb(r, g, b, a)
@property
def alpha(self) -> float:
"""
Normalized value from interval [0, 1] of the transparent (A) color channel.
"""
return self._alpha
@alpha.setter
def alpha(self, arg: float):
self._alpha = arg
@property
def red(self) -> float:
"""
Normalized value from interval [0, 1] of the red (R) color channel.
"""
return self._red
@red.setter
def red(self, arg: float):
self._red = arg
@property
def green(self) -> float:
"""
Normalized value from interval [0, 1] of the green (G) color channel.
"""
return self._green
@green.setter
def green(self, arg: float):
self._green = arg
@property
def blue(self) -> float:
"""
Normalized value from interval [0, 1] of the blue (B) color channel.
"""
return self._blue
@blue.setter
def blue(self, arg: float):
self._blue = arg
@property
def hex(self) -> str:
"""HEX `str` without hash."""
return rgb2hex(*nrgb2rgb(self.red, self.green, self.blue))[1:]
@property
def hexa(self) -> str:
"""HEXA `str` without hash."""
return rgb2hex(*nrgb2rgb(self.red, self.green, self.blue, self.alpha))[1:]
@property
def lnrgb(self) -> tuple[float, float, float]:
"""
Returns current color in normalized values fom linear RGB color space.
"""
return nrgb2lnrgb(*self.nrgb)
@property
def lnrgb_str(self) -> str:
return f"lnrgb({self.nrgb[0]}, {self.nrgb[1]}, {self.nrgb[2]})"
@property
def lnrgba(self) -> tuple[float, float, float, float]:
return nrgb2lnrgb(*self.nrgba)
@property
def lnrgba_str(self) -> str:
return (
f"lnrgba({self.lnrgba[0]}, {self.lnrgba[1]}, "
f"{self.lnrgba[2]}, {self.lnrgba[3]})"
)
@property
def nrgb(self) -> tuple[float, float, float]:
"""
Returns current color in normlized values (interval [0, 1]) from sRGB
color space.
"""
return (self.red, self.green, self.blue)
@property
def nrgba(self) -> tuple[float, float, float, float]:
"""
Returns current color in normlized values (interval [0, 1]) from sRGBA color
space.
"""
return (self.red, self.green, self.blue, self.alpha)
@property
def rgb(self) -> tuple[int, int, int]:
"""
Returns current color in values (interval [0, 255]) from sRGB color space.
"""
return nrgb2rgb(*self.nrgb)
@property
def rgba(self) -> tuple[int, int, int, float]:
return nrgb2rgb(*self.nrgba)
@property
def str_hex(self) -> str:
"""HEX `str` with hash."""
return rgb2hex(*self.rgb)
@property
def str_hexa(self) -> str:
"""HEXA `str` with hash."""
return f"#{self.hexa}"
@property
def str_rgb(self) -> str:
"""
Current color formatted as `rgb(R, G, B)`.
"""
return f"rgb({self.rgb[0]}, {self.rgb[1]}, {self.rgb[2]})"
@property
def str_rgba(self) -> str:
"""
Current color formatted as `rgb(R, G, B, A)`.
"""
return f"rgba({self.rgba[0]}, {self.rgba[1]}, " f"{self.rgba[2]}, {self.rgba[3]})"