diff --git a/Tests/Functions/test_BlendModes.py b/Tests/Functions/test_BlendModes.py new file mode 100644 index 0000000..2916494 --- /dev/null +++ b/Tests/Functions/test_BlendModes.py @@ -0,0 +1,29 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# - Package Imports - +# ---------------------------------------------------------------------------------------------------------------------- +# General Packages +from __future__ import annotations +import unittest + +# Custom Library +from AthenaColor.Objects.Color.ColorSystem import RGB, RGBA +from AthenaColor.Functions.BlendModes import * + + +# Custom Packages + +# ---------------------------------------------------------------------------------------------------------------------- +# - Code - +# ---------------------------------------------------------------------------------------------------------------------- +class Functions_BlendModes(unittest.TestCase): + def test_blend_normal(self): + self.assertEqual( + RGBA(255,255,255,255), + blend_normal(RGB(128,128,128), RGB(255,255,255)) + ) + + def test_blend_multiply(self): + self.assertEqual( + RGBA(82, 77, 45,255), + blend_multiply(RGB(246, 231, 134), RGB(85,85,85)) + ) \ No newline at end of file diff --git a/setup.py b/setup.py index 26c46c2..63d3f6a 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setuptools.setup( name="AthenaColor", - version="4.1.2", + version="4.2.0", author="Andreas Sas", author_email="", description="Package to support full usage of RGB colors in the Console.", diff --git a/src/AthenaColor/Functions/BlendModes.py b/src/AthenaColor/Functions/BlendModes.py new file mode 100644 index 0000000..ae87eab --- /dev/null +++ b/src/AthenaColor/Functions/BlendModes.py @@ -0,0 +1,141 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# - Package Imports - +# ---------------------------------------------------------------------------------------------------------------------- +# General Packages +from __future__ import annotations + +import math +from typing import Callable + +# Custom Library + +# Custom Packages +from AthenaColor.Objects.Color.ColorSystem import ColorSystem, RGBA +from AthenaColor.Objects.Color.ColorObjectConversion import to_RGBA +from AthenaColor.Objects.Color.ColorTupleConversion import NormalizeRgba + +# ---------------------------------------------------------------------------------------------------------------------- +# - Support Code - +# ---------------------------------------------------------------------------------------------------------------------- +__all__ = [ + "blend_normal", "blend_linearburn", "blend_colordodge", "blend_difference", "blend_lineardodge", "blend_screen", + "blend_darken", "blend_linearlight", "blend_vividlight", "blend_colorburn", "blend_multiply", "blend_lighten", + "blend_overlay", "blend_exclusion", "blend_hardlight", "blend_softlight", "blend_pinlight" +] + +# ---------------------------------------------------------------------------------------------------------------------- +# - Support Code - +# ---------------------------------------------------------------------------------------------------------------------- +def _blend_function(color1:ColorSystem,color2:ColorSystem, formula:Callable) -> RGBA: + color1_tuple = NormalizeRgba(*to_RGBA(color1).export()) + color2_tuple = NormalizeRgba(*to_RGBA(color2).export()) + + # WARNING below values are normalized (aka between 0 and 1) + normalized_outcome = (formula(a, b) for a, b in zip(color1_tuple, color2_tuple)) + return RGBA(*(n * 255 for n in normalized_outcome)) # need to de normalize them again + +# ---------------------------------------------------------------------------------------------------------------------- +# - Code - +# ---------------------------------------------------------------------------------------------------------------------- +def blend_normal(color1:ColorSystem, color2:ColorSystem) -> RGBA: + # possible because to_RGBA creates a new object + return to_RGBA(color2) + +def blend_darken(color1:ColorSystem, color2:ColorSystem) -> RGBA: + formula = lambda a, b: min(a, b) + return _blend_function(color1,color2,formula) + +def blend_multiply(color1:ColorSystem, color2:ColorSystem) -> RGBA: + formula = lambda a,b : a*b + return _blend_function(color1,color2,formula) + +def blend_colorburn(color1:ColorSystem, color2:ColorSystem) -> RGBA: + formula = lambda a,b : 1-(1-a)/b + return _blend_function(color1,color2,formula) + +def blend_linearburn(color1:ColorSystem, color2:ColorSystem) -> RGBA: + formula = lambda a,b : a+b-1 + return _blend_function(color1,color2,formula) + +def blend_lighten(color1:ColorSystem, color2:ColorSystem) -> RGBA: + formula = lambda a,b : max(a,b) + return _blend_function(color1,color2,formula) + +def blend_screen(color1:ColorSystem, color2:ColorSystem) -> RGBA: + formula = lambda a,b : 1-(1-a)(1-b) + return _blend_function(color1,color2,formula) + +def blend_colordodge(color1:ColorSystem, color2:ColorSystem) -> RGBA: + formula = lambda a,b : a/(1-b) + return _blend_function(color1,color2,formula) + +def blend_lineardodge(color1:ColorSystem, color2:ColorSystem) -> RGBA: + formula = lambda a,b : a+b + return _blend_function(color1,color2,formula) + +def blend_overlay(color1:ColorSystem, color2:ColorSystem) -> RGBA: + def formula(a: float, b: float): + if a < 0.5: + return 1 - 2 * (1 - a) * (1 - b) + else: + return 2 * a * b + + return _blend_function(color1,color2,formula) + +def blend_softlight(color1:ColorSystem, color2:ColorSystem) -> RGBA: + def formula(a: float, b: float): + if a <= 0.25: + g_w3c = ((16*a-12)*a+4)*a + else: + g_w3c = math.sqrt(a) + + if b <=0.5: + return a-(1-2*b)*a*(1-a) + else: + return a+(2*b-1)*(g_w3c-a) + + return _blend_function(color1,color2,formula) + +def blend_hardlight(color1:ColorSystem, color2:ColorSystem) -> RGBA: + def formula(a: float, b: float): + if b < 0.5: + return a*(2*b) + else: + return 1-(1-a)*(1-2*(b-0.5)) + + return _blend_function(color1,color2,formula) + +def blend_vividlight(color1:ColorSystem, color2:ColorSystem) -> RGBA: + def formula(a: float, b: float): + if b < 0.5: + return 1-(1-a)/(2*b) + else: + return a/(1-2*(b-.5)) + + return _blend_function(color1,color2,formula) + +def blend_linearlight(color1:ColorSystem, color2:ColorSystem) -> RGBA: + def formula(a: float, b: float): + if b < 0.5: + return a+2*b-1 + else: + return a+2*(b-.5) + + return _blend_function(color1,color2,formula) + +def blend_pinlight(color1:ColorSystem, color2:ColorSystem) -> RGBA: + def formula(a: float, b: float): + if b < 0.5: + return min(a,2*b) + else: + return max(a,2*(b-.5)) + + return _blend_function(color1,color2,formula) + +def blend_difference(color1:ColorSystem, color2:ColorSystem) -> RGBA: + formula = lambda a,b : abs(a-b) + return _blend_function(color1,color2,formula) + +def blend_exclusion(color1:ColorSystem, color2:ColorSystem) -> RGBA: + formula = lambda a,b : 0.5-2*(a-.5)*(2-.5) + return _blend_function(color1,color2,formula) \ No newline at end of file diff --git a/src/AthenaColor/Objects/Color/ColorTupleConversion.py b/src/AthenaColor/Objects/Color/ColorTupleConversion.py index 3252038..280bde1 100644 --- a/src/AthenaColor/Objects/Color/ColorTupleConversion.py +++ b/src/AthenaColor/Objects/Color/ColorTupleConversion.py @@ -30,6 +30,8 @@ # ---------------------------------------------------------------------------------------------------------------------- def NormalizeRgb(r:int,g:int,b:int) -> Tuple[float, ...]: return r/255,g/255,b/255 +def NormalizeRgba(r:int,g:int,b:int,a:int) -> Tuple[float, ...]: + return r/255,g/255,b/255, a/255 numbers = (int,float)