diff options
Diffstat (limited to 'thcolor/angles.py')
-rw-r--r-- | thcolor/angles.py | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/thcolor/angles.py b/thcolor/angles.py new file mode 100644 index 0000000..6dd36a6 --- /dev/null +++ b/thcolor/angles.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +# ***************************************************************************** +# Copyright (C) 2019-2022 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +# This file is part of the thcolor project, which is MIT-licensed. +# ***************************************************************************** +""" Angle representation and conversions. """ + +from math import pi as _pi +from typing import Optional as _Optional + +__all__ = [ + 'Angle', 'DegreesAngle', 'GradiansAngle', 'RadiansAngle', 'TurnsAngle', +] + + +class Angle: + """ Abstract class representing an angle within thcolor. + + Used for some color representations (most notably hue). + """ + + __slots__ = () + + def __init__(self): + pass + + def __repr__(self): + params = ( + (key, getattr(self, key)) for key in dir(self) + if not key.startswith('_') and not callable(getattr(self, key)) + ) + + return ( + f'{self.__class__.__name__}(' + f"{', '.join(f'{key}={val!r}' for key, val in params)})" + ) + + def __eq__(self, other): + if not isinstance(other, Angle): + return False + + return ( + round(self.asturns().turns, 3) == round(other.asturns().turns, 3) + ) + + def asdegrees(self) -> 'DegreesAngle': + """ Get the current angle as a degrees angle. """ + + try: + value = self._value + ob = self._bottom + ot = self._top + except AttributeError: + raise NotImplementedError from None + + nb = DegreesAngle._bottom + nt = DegreesAngle._top + + return DegreesAngle((value - ob) / (ot - ob) * (nt - nb) + nb) + + def asgradians(self) -> 'GradiansAngle': + """ Get the current angle as a gradians angle. """ + + try: + value = self._value + ob = self._bottom + ot = self._top + except AttributeError: + raise NotImplementedError from None + + nb = GradiansAngle._bottom + nt = GradiansAngle._top + + return GradiansAngle((value - ob) / (ot - ob) * (nt - nb) + nb) + + def asradians(self) -> 'RadiansAngle': + """ Get the current angle as a radians angle. """ + + try: + value = self._value + ob = self._bottom + ot = self._top + except AttributeError: + raise NotImplementedError from None + + nb = RadiansAngle._bottom + nt = RadiansAngle._top + + return RadiansAngle((value - ob) / (ot - ob) * (nt - nb) + nb) + + def asturns(self) -> 'TurnsAngle': + """ Get the current angle as a turns angle. """ + + try: + value = self._value + ob = self._bottom + ot = self._top + except AttributeError: + raise NotImplementedError from None + + nb = TurnsAngle._bottom + nt = TurnsAngle._top + + return TurnsAngle((value - ob) / (ot - ob) * (nt - nb) + nb) + + @classmethod + def fromtext( + cls, + expr: str, + decoder: _Optional = None, + ) -> 'Angle': + """ Create a color from a string. + + :param expr: The expression to decode. + """ + + if decoder is None: + from .decoders.builtin import DefaultColorDecoder + + decoder = DefaultColorDecoder() + + results = decoder.decode(expr, prefer_angles=True) + + if len(results) != 1 or not isinstance(results[0], cls): + raise ValueError( + f'result of expression was not an instance of {cls.__name__}: ' + f'single color: {results!r}', + ) + + return results[0] + + +class DegreesAngle(Angle): + """ An angle expressed in degrees. + + A 270° angle can be created the following way: + + .. code-block:: python + + angle = DegreesAngle(270) + + :param degrees: Degrees; canonical values are between 0 and 360 + excluded. + """ + + __slots__ = ('_value') + + _bottom = 0 + _top = 360.0 + + def __init__(self, degrees: float): + self._value = float(degrees) # % 360.0 + + def __str__(self): + x = self._value + return f'{int(x) if x == int(x) else x}deg' + + @property + def degrees(self) -> float: + """ Degrees. """ + + return self._value + + +class GradiansAngle(Angle): + """ An angle expressed in gradians. + + A 565.5 gradians angle can be created the following way: + + .. code-block:: python + + angle = GradiansAngle(565.5) + + :param gradians: Gradians; canonical values are between + 0 and 400.0 excluded. + """ + + __slots__ = ('_value') + + _bottom = 0 + _top = 400.0 + + def __init__(self, gradians: float): + self._value = float(gradians) # % 400.0 + + def __str__(self): + x = self._value + return f'{int(x) if x == int(x) else x}grad' + + @property + def gradians(self) -> float: + """ Gradians. """ + + return self._value + + +class RadiansAngle(Angle): + """ An angle expressed in radians. + + A π radians angle can be created the following way: + + .. code-block:: python + + from math import pi + angle = RadiansAngle(pi) + + :param radians: Radians; canonical are between 0 and 2π + excluded. + """ + + __slots__ = ('_value') + + _bottom = 0 + _top = 2 * _pi + + def __init__(self, radians: float): + self._value = float(radians) # % (2 * _pi) + + def __str__(self): + x = self._value + return f'{int(x) if x == int(x) else x}rad' + + def __repr__(self): + r = self.radians / _pi + ir = int(r) + if r == ir: + r = ir + + return f"{self.__class__.__name__}(radians = {f'{r}π' if r else '0'})" + + @property + def radians(self) -> float: + """ Radians. """ + + return self._value + + +class TurnsAngle(Angle): + """ An angle expressed in turns. + + A 3.5 turns angle can be created the following way: + + .. code-block:: python + + angle = TurnsAngle(3.5) + + :param turns: Turns; canonical values are between 0 and 1 + excluded. + """ + + __slots__ = ('_value') + + _bottom = 0 + _top = 1 + + def __init__(self, turns: float): + self._value = float(turns) # % 1.0 + + def __str__(self): + x = self._value + return f'{int(x) if x == int(x) else x}turn' + + @property + def turns(self) -> float: + """ Turns. """ + + return self._value + +# End of file. |