0.12.0a5 add OKLab and oklch

This commit is contained in:
Yusur 2025-12-25 11:01:10 +01:00
parent ef645bd4da
commit b1d0c62b44
4 changed files with 105 additions and 12 deletions

View file

@ -1,11 +1,11 @@
# Changelog # Changelog
## 0.12.0 ## 0.12.0 "The Color Update"
* All `AuthSrc()` derivatives, deprecated and never used, have been removed. * All `AuthSrc()` derivatives, deprecated and never used, have been removed.
* New module `mat` adds a shallow reimplementation of `Matrix()` in order to implement matrix multiplication * New module `mat` adds a shallow reimplementation of `Matrix()` in order to implement matrix multiplication
* Removed obsolete `configparse` implementation that has been around since 0.3 and shelved since 0.4. * Removed obsolete `configparse` implementation that has been around since 0.3 and shelved since 0.4.
* `color`: added support for conversion from RGB to sRGB, XYZ * `color`: added support for conversion from RGB to sRGB, XYZ, OKLab and OKLCH.
## 0.11.2 ## 0.11.2

View file

@ -35,20 +35,19 @@ from .strtools import PrefixIdentifier
from .validators import matches, not_less_than, not_greater_than, yesno from .validators import matches, not_less_than, not_greater_than, yesno
from .redact import redact_url_password from .redact import redact_url_password
from .http import WantsContentType from .http import WantsContentType
from .color import chalk, WebColor from .color import OKLabColor, chalk, WebColor, RGBColor, SRGBColor, XYZColor, OKLabColor
from .mat import Matrix from .mat import Matrix
__version__ = "0.12.0a4" __version__ = "0.12.0a5"
__all__ = ( __all__ = (
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue', 'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',
'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n', 'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n',
'Matrix', 'Matrix', 'MissingConfigError', 'MissingConfigWarning', 'OKLabColor',
'MissingConfigError', 'MissingConfigWarning', 'PrefixIdentifier', 'PrefixIdentifier', 'RGBColor', 'SRGBColor',
'RGBColor', 'SRGBColor',
'Siq', 'SiqCache', 'SiqGen', 'SiqType', 'Snowflake', 'SnowflakeGen', 'Siq', 'SiqCache', 'SiqGen', 'SiqType', 'Snowflake', 'SnowflakeGen',
'StringCase', 'TimedDict', 'TomlI18n', 'UserSigner', 'Wanted', 'WantsContentType', 'StringCase', 'TimedDict', 'TomlI18n', 'UserSigner', 'Wanted', 'WantsContentType',
'WebColor', 'WebColor', 'XYZColor',
'addattr', 'additem', 'age_and_days', 'alru_cache', 'b2048decode', 'b2048encode', 'addattr', 'additem', 'age_and_days', 'alru_cache', 'b2048decode', 'b2048encode',
'b32ldecode', 'b32lencode', 'b64encode', 'b64decode', 'cb32encode', 'b32ldecode', 'b32lencode', 'b64encode', 'b64decode', 'cb32encode',
'cb32decode', 'chalk', 'count_ones', 'dei_args', 'deprecated', 'cb32decode', 'chalk', 'count_ones', 'dei_args', 'deprecated',

View file

@ -21,6 +21,7 @@ from __future__ import annotations
from collections import namedtuple from collections import namedtuple
from functools import lru_cache from functools import lru_cache
import math
from suou.mat import Matrix from suou.mat import Matrix
@ -100,6 +101,9 @@ class RGBColor(namedtuple('_WebColor', 'red green blue')):
Representation of a color in the RGB TrueColor space. Representation of a color in the RGB TrueColor space.
Useful for theming. Useful for theming.
*Changed in 0.12.0*: name is now RGBColor, with WebColor being an alias.
Added conversions to and from OKLCH, OKLab, sRGB, and XYZ.
""" """
def lighten(self, *, factor = .75): def lighten(self, *, factor = .75):
""" """
@ -149,7 +153,8 @@ class RGBColor(namedtuple('_WebColor', 'red green blue')):
(-1 if i < 0 else 1) * (((abs(i) + 0.55)) / 1.055) ** 2.4) for i in self (-1 if i < 0 else 1) * (((abs(i) + 0.55)) / 1.055) ** 2.4) for i in self
)) ))
def to_oklab(self):
return self.to_xyz().to_oklab()
__add__ = blend_with __add__ = blend_with
@ -165,6 +170,11 @@ class RGBColor(namedtuple('_WebColor', 'red green blue')):
def to_xyz(self): def to_xyz(self):
return XYZColor(*(self.RGB_TO_XYZ @ Matrix.as_column(self)).get_column()) return XYZColor(*(self.RGB_TO_XYZ @ Matrix.as_column(self)).get_column())
def to_oklch(self):
return self.to_xyz().to_oklch()
def to_oklab(self):
return self.to_xyz().to_oklab()
WebColor = RGBColor WebColor = RGBColor
@ -192,12 +202,15 @@ class SRGBColor(namedtuple('_SRGBColor', 'red green blue')):
def to_xyz(self): def to_xyz(self):
return self.to_rgb().to_xyz() return self.to_rgb().to_xyz()
def to_oklab(self):
return self.to_rgb().to_oklab()
class XYZColor(namedtuple('_XYZColor', 'x y z')): class XYZColor(namedtuple('_XYZColor', 'x y z')):
""" """
Represent a color in the XYZ color space. Represent a color in the XYZ color space.
*New in 0.12.0*
""" """
XYZ_TO_RGB = Matrix([ XYZ_TO_RGB = Matrix([
@ -206,10 +219,89 @@ class XYZColor(namedtuple('_XYZColor', 'x y z')):
[ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786] [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
]) ])
XYZ_TO_LMS = Matrix([
[0.8190224379967030, 0.3619062600528904, -0.1288737815209879],
[0.0329836539323885, 0.9292868615863434, 0.0361446663506424],
[0.0481771893596242, 0.2642395317527308, 0.6335478284694309]
])
LMSG_TO_OKLAB = Matrix([
[0.2104542683093140, 0.7936177747023054, -0.0040720430116193],
[1.9779985324311684, -2.4285922420485799, 0.4505937096174110],
[0.0259040424655478, 0.7827717124575296, -0.8086757549230774]
])
def to_rgb(self): def to_rgb(self):
return RGBColor(*(self.XYZ_TO_RGB @ Matrix.as_column(self)).get_column()) return RGBColor(*(self.XYZ_TO_RGB @ Matrix.as_column(self)).get_column())
def to_oklab(self):
lms = (self.XYZ_TO_LMS @ Matrix.as_column(self)).get_column()
lmsg = [math.cbrt(i) for i in lms]
oklab = (self.LMSG_TO_OKLAB @ Matrix.as_column(self)).get_column()
return OKLabColor(*oklab)
def to_oklch(self):
return self.to_oklab().to_oklch()
class OKLabColor(namedtuple('_OKLabColor', 'l a b')):
"""
Represent a color in the OKLab color space.
__all__ = ('chalk', 'WebColor', "RGBColor", 'SRGBColor') *New in 0.12.0*
"""
OKLAB_TO_LMSG = Matrix([
[1., 0.3963377773761749, 0.2158037573099136],
[1., -0.1055613458156586, -0.0638541728258133],
[1., -0.0894841775298119, -1.2914855480194092]
])
LMS_TO_XYZ = Matrix([
[ 1.2268798758459243, -0.5578149944602171, 0.2813910456659647],
[-0.0405757452148008, 1.1122868032803170, -0.0717110580655164],
[-0.0763729366746601, -0.4214933324022432, 1.5869240198367816]
])
def to_xyz(self):
lmsg = (self.OKLAB_TO_LMSG @ Matrix.as_column(self)).get_column()
lms = [i ** 3 for i in lmsg]
xyz = (self.LMS_TO_XYZ @ Matrix.as_column(lms)).get_column()
return XYZColor(*xyz)
def to_oklch(self):
return OKLCHColor(
self.l,
math.sqrt(self.a ** 2 + self.b ** 2),
0 if abs(self.a) < .0002 and abs(self.b) < .0002 else (((math.atan2(self.b, self.a) * 180) / math.pi % 360) + 360) % 360
)
def to_rgb(self):
return self.to_xyz().to_rgb()
class OKLCHColor(namedtuple('_OKLCHColor', 'l c h')):
"""
Represent a color in the OKLCH color space.
*Warning*: conversion to RGB is not bound checked yet!
*New in 0.12.0*
"""
def __str__(self):
l, c, h = round(self.l, 4), round(self.c, 4), round(self.h, 4)
return f'oklch({l}, {c}, {h})'
def to_oklab(self):
return OKLabColor(
self.l,
self.c * math.cos(self.h * math.pi / 180),
self.h * math.cos(self.h * math.pi / 180)
)
def to_rgb(self):
return self.to_oklab().to_rgb()
__all__ = ('chalk', 'WebColor', "RGBColor", 'SRGBColor', 'XYZColor', 'OKLabColor')

View file

@ -24,7 +24,9 @@ _T = TypeVar('_T')
class Matrix(Collection[_T]): class Matrix(Collection[_T]):
""" """
Shallow reimplementation of numpy's matrices in pure Python. Minimalist reimplementation of matrices in pure Python.
This to avoid adding numpy as a dependency.
*New in 0.12.0* *New in 0.12.0*
""" """