paintcan 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paintcan-0.1.0/LICENSE +21 -0
- paintcan-0.1.0/PKG-INFO +113 -0
- paintcan-0.1.0/README.md +91 -0
- paintcan-0.1.0/pyproject.toml +50 -0
- paintcan-0.1.0/src/paintcan/__init__.py +2 -0
- paintcan-0.1.0/src/paintcan/__init__.pyi +2 -0
- paintcan-0.1.0/src/paintcan/__main__.py +62 -0
- paintcan-0.1.0/src/paintcan/__main__.pyi +6 -0
- paintcan-0.1.0/src/paintcan/color_scheme.py +229 -0
- paintcan-0.1.0/src/paintcan/color_scheme.pyi +29 -0
- paintcan-0.1.0/src/paintcan/hsba_color.py +100 -0
- paintcan-0.1.0/src/paintcan/hsba_color.pyi +17 -0
paintcan-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 B.T. Franklin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
paintcan-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: paintcan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Pythonic library for creating beautiful, algorithmic color schemes.
|
|
5
|
+
Author-Email: "B.T. Franklin" <brandon.franklin@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
17
|
+
Project-URL: Homepage, https://github.com/btfranklin/paintcan
|
|
18
|
+
Project-URL: Issues, https://github.com/btfranklin/paintcan/issues
|
|
19
|
+
Project-URL: Repository, https://github.com/btfranklin/paintcan.git
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# PaintCan
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
**PaintCan** is a high-quality, Pythonic library for working with HSBA colors and generating beautiful, algorithmic color schemes. It provides robust tools for color manipulation and harmony generation.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **HSBAColor**: A value-type representation of color using Hue, Saturation, Brightness, and Alpha (0.0 - 1.0).
|
|
32
|
+
- Cyclic Hue adjustment (wrapping).
|
|
33
|
+
- Clamping and Overflow support for Saturation and Brightness.
|
|
34
|
+
- Pythonic unpacking: `h, s, b, a = color`.
|
|
35
|
+
- **ColorScheme**: A collection of colors generated by color theory rules.
|
|
36
|
+
- Supports the Sequence protocol (`len(scheme)`, `scheme[0]`, iteration).
|
|
37
|
+
- 8 factory methods for generating harmonious schemes:
|
|
38
|
+
- Analogous
|
|
39
|
+
- Accented Analogous
|
|
40
|
+
- Complementary
|
|
41
|
+
- Compound
|
|
42
|
+
- Monochromatic
|
|
43
|
+
- Shades
|
|
44
|
+
- Split Complementary
|
|
45
|
+
- Triadic
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
Using PDM:
|
|
50
|
+
```bash
|
|
51
|
+
pdm add paintcan
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Using pip:
|
|
55
|
+
```bash
|
|
56
|
+
pip install paintcan
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### Working with HSBAColor
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from paintcan import HSBAColor
|
|
65
|
+
|
|
66
|
+
# Create a color (Red)
|
|
67
|
+
red = HSBAColor(hue=0.0, saturation=1.0, brightness=1.0, alpha=1.0)
|
|
68
|
+
|
|
69
|
+
# Adjust Hue (wraps around)
|
|
70
|
+
orange = red.adjust_hue(0.08)
|
|
71
|
+
|
|
72
|
+
# Get the complement (Cyan-ish)
|
|
73
|
+
cyan = red.complement()
|
|
74
|
+
|
|
75
|
+
# Pythonic Unpacking
|
|
76
|
+
h, s, b, a = cyan
|
|
77
|
+
print(f"Hue: {h}, Saturation: {s}")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Generating Color Schemes
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from paintcan import HSBAColor, ColorScheme
|
|
84
|
+
|
|
85
|
+
# Start with a base color
|
|
86
|
+
base = HSBAColor(0.5, 0.8, 0.9, 1.0) # Cyan-ish
|
|
87
|
+
|
|
88
|
+
# Generate a Split Complementary scheme
|
|
89
|
+
scheme = ColorScheme.from_split_complementary(base)
|
|
90
|
+
|
|
91
|
+
# Access colors (behaves like a tuple)
|
|
92
|
+
print(f"Scheme has {len(scheme)} colors.")
|
|
93
|
+
theme = scheme.theme_color # First color is always the theme color
|
|
94
|
+
|
|
95
|
+
for color in scheme:
|
|
96
|
+
print(color)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Demo
|
|
100
|
+
|
|
101
|
+
To see the color schemes in action in your terminal (using ANSI colors):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# If installed via PDM/Dev
|
|
105
|
+
pdm run python -m paintcan
|
|
106
|
+
|
|
107
|
+
# If installed in your environment
|
|
108
|
+
python -m paintcan
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
paintcan-0.1.0/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# PaintCan
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
**PaintCan** is a high-quality, Pythonic library for working with HSBA colors and generating beautiful, algorithmic color schemes. It provides robust tools for color manipulation and harmony generation.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **HSBAColor**: A value-type representation of color using Hue, Saturation, Brightness, and Alpha (0.0 - 1.0).
|
|
10
|
+
- Cyclic Hue adjustment (wrapping).
|
|
11
|
+
- Clamping and Overflow support for Saturation and Brightness.
|
|
12
|
+
- Pythonic unpacking: `h, s, b, a = color`.
|
|
13
|
+
- **ColorScheme**: A collection of colors generated by color theory rules.
|
|
14
|
+
- Supports the Sequence protocol (`len(scheme)`, `scheme[0]`, iteration).
|
|
15
|
+
- 8 factory methods for generating harmonious schemes:
|
|
16
|
+
- Analogous
|
|
17
|
+
- Accented Analogous
|
|
18
|
+
- Complementary
|
|
19
|
+
- Compound
|
|
20
|
+
- Monochromatic
|
|
21
|
+
- Shades
|
|
22
|
+
- Split Complementary
|
|
23
|
+
- Triadic
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Using PDM:
|
|
28
|
+
```bash
|
|
29
|
+
pdm add paintcan
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Using pip:
|
|
33
|
+
```bash
|
|
34
|
+
pip install paintcan
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Working with HSBAColor
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from paintcan import HSBAColor
|
|
43
|
+
|
|
44
|
+
# Create a color (Red)
|
|
45
|
+
red = HSBAColor(hue=0.0, saturation=1.0, brightness=1.0, alpha=1.0)
|
|
46
|
+
|
|
47
|
+
# Adjust Hue (wraps around)
|
|
48
|
+
orange = red.adjust_hue(0.08)
|
|
49
|
+
|
|
50
|
+
# Get the complement (Cyan-ish)
|
|
51
|
+
cyan = red.complement()
|
|
52
|
+
|
|
53
|
+
# Pythonic Unpacking
|
|
54
|
+
h, s, b, a = cyan
|
|
55
|
+
print(f"Hue: {h}, Saturation: {s}")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Generating Color Schemes
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from paintcan import HSBAColor, ColorScheme
|
|
62
|
+
|
|
63
|
+
# Start with a base color
|
|
64
|
+
base = HSBAColor(0.5, 0.8, 0.9, 1.0) # Cyan-ish
|
|
65
|
+
|
|
66
|
+
# Generate a Split Complementary scheme
|
|
67
|
+
scheme = ColorScheme.from_split_complementary(base)
|
|
68
|
+
|
|
69
|
+
# Access colors (behaves like a tuple)
|
|
70
|
+
print(f"Scheme has {len(scheme)} colors.")
|
|
71
|
+
theme = scheme.theme_color # First color is always the theme color
|
|
72
|
+
|
|
73
|
+
for color in scheme:
|
|
74
|
+
print(color)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Demo
|
|
78
|
+
|
|
79
|
+
To see the color schemes in action in your terminal (using ANSI colors):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# If installed via PDM/Dev
|
|
83
|
+
pdm run python -m paintcan
|
|
84
|
+
|
|
85
|
+
# If installed in your environment
|
|
86
|
+
python -m paintcan
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "paintcan"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A Pythonic library for creating beautiful, algorithmic color schemes."
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "B.T. Franklin", email = "brandon.franklin@gmail.com" },
|
|
7
|
+
]
|
|
8
|
+
dependencies = []
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
"Topic :: Multimedia :: Graphics",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.license]
|
|
25
|
+
text = "MIT"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/btfranklin/paintcan"
|
|
29
|
+
Issues = "https://github.com/btfranklin/paintcan/issues"
|
|
30
|
+
Repository = "https://github.com/btfranklin/paintcan.git"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = [
|
|
34
|
+
"pdm-backend",
|
|
35
|
+
]
|
|
36
|
+
build-backend = "pdm.backend"
|
|
37
|
+
|
|
38
|
+
[tool.pdm]
|
|
39
|
+
distribution = true
|
|
40
|
+
|
|
41
|
+
[tool.pdm.build]
|
|
42
|
+
excludes = [
|
|
43
|
+
"tests/**",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[dependency-groups]
|
|
47
|
+
dev = [
|
|
48
|
+
"pytest>=9.0.2",
|
|
49
|
+
"flake8>=7.3.0",
|
|
50
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from .hsba_color import HSBAColor
|
|
2
|
+
from .color_scheme import ColorScheme
|
|
3
|
+
|
|
4
|
+
def print_color(color: HSBAColor, text: str = " "):
|
|
5
|
+
# Simple HSBA to RGB conversion for demo purposes
|
|
6
|
+
# Source: https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
|
|
7
|
+
h = color.hue * 360
|
|
8
|
+
s = color.saturation
|
|
9
|
+
v = color.brightness
|
|
10
|
+
|
|
11
|
+
c = v * s
|
|
12
|
+
x = c * (1 - abs((h / 60) % 2 - 1))
|
|
13
|
+
m = v - c
|
|
14
|
+
|
|
15
|
+
if 0 <= h < 60:
|
|
16
|
+
r, g, b = c, x, 0
|
|
17
|
+
elif 60 <= h < 120:
|
|
18
|
+
r, g, b = x, c, 0
|
|
19
|
+
elif 120 <= h < 180:
|
|
20
|
+
r, g, b = 0, c, x
|
|
21
|
+
elif 180 <= h < 240:
|
|
22
|
+
r, g, b = 0, x, c
|
|
23
|
+
elif 240 <= h < 300:
|
|
24
|
+
r, g, b = x, 0, c
|
|
25
|
+
else:
|
|
26
|
+
r, g, b = c, 0, x
|
|
27
|
+
|
|
28
|
+
r = int((r + m) * 255)
|
|
29
|
+
g = int((g + m) * 255)
|
|
30
|
+
b = int((b + m) * 255)
|
|
31
|
+
|
|
32
|
+
# ANSI escape code for background color
|
|
33
|
+
print(f"\033[48;2;{r};{g};{b}m{text}\033[0m", end=" ")
|
|
34
|
+
|
|
35
|
+
def print_scheme(name: str, scheme: ColorScheme):
|
|
36
|
+
print(f"{name:<15}: ", end="")
|
|
37
|
+
for color in scheme:
|
|
38
|
+
print_color(color, " ")
|
|
39
|
+
print(f" (Theme Hue: {scheme.theme_color.hue:.2f})")
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
# Start with a random bright color
|
|
43
|
+
print("Generating random base color...")
|
|
44
|
+
base = HSBAColor.random(saturation_range=(0.7, 1.0), brightness_range=(0.8, 1.0))
|
|
45
|
+
|
|
46
|
+
print("\nBetterColors Demo")
|
|
47
|
+
print("=================")
|
|
48
|
+
print(f"{'Base Color':<15}: ", end="")
|
|
49
|
+
print_color(base, " ")
|
|
50
|
+
print("\n")
|
|
51
|
+
|
|
52
|
+
print_scheme("Analogous", ColorScheme.from_analogous(base))
|
|
53
|
+
print_scheme("Complementary", ColorScheme.from_complementary(base))
|
|
54
|
+
print_scheme("Triadic", ColorScheme.from_triadic(base))
|
|
55
|
+
print_scheme("Split Compl.", ColorScheme.from_split_complementary(base))
|
|
56
|
+
print_scheme("Monochromatic", ColorScheme.from_monochromatic(base))
|
|
57
|
+
print_scheme("Compound", ColorScheme.from_compound(base))
|
|
58
|
+
print_scheme("Shades", ColorScheme.from_shades(base))
|
|
59
|
+
print_scheme("Accented Analog", ColorScheme.from_accented_analogous(base))
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Tuple, Self, Iterator
|
|
3
|
+
from .hsba_color import HSBAColor
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class ColorScheme:
|
|
7
|
+
"""
|
|
8
|
+
A collection of colors comprising a color scheme.
|
|
9
|
+
Behaves like a read-only sequence of HSBAColor.
|
|
10
|
+
"""
|
|
11
|
+
colors: Tuple[HSBAColor, ...]
|
|
12
|
+
|
|
13
|
+
def __post_init__(self):
|
|
14
|
+
if not self.colors:
|
|
15
|
+
raise ValueError("ColorScheme colors cannot be empty")
|
|
16
|
+
|
|
17
|
+
def __len__(self) -> int:
|
|
18
|
+
return len(self.colors)
|
|
19
|
+
|
|
20
|
+
def __getitem__(self, index: int) -> HSBAColor:
|
|
21
|
+
return self.colors[index]
|
|
22
|
+
|
|
23
|
+
def __iter__(self) -> Iterator[HSBAColor]:
|
|
24
|
+
return iter(self.colors)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def theme_color(self) -> HSBAColor:
|
|
28
|
+
return self.colors[0]
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_analogous(cls, theme_color: HSBAColor, spacing: float = 0.05) -> Self:
|
|
32
|
+
if not (0.0 <= spacing < 0.2):
|
|
33
|
+
raise ValueError("Spacing must be between 0 and 0.2")
|
|
34
|
+
|
|
35
|
+
colors = []
|
|
36
|
+
colors.append(theme_color)
|
|
37
|
+
|
|
38
|
+
# Color 2
|
|
39
|
+
colors.append(theme_color
|
|
40
|
+
.adjust_saturation(-0.05, floor=0.10)
|
|
41
|
+
.adjust_hue(spacing)
|
|
42
|
+
.adjust_brightness(-0.05, floor=0.20))
|
|
43
|
+
|
|
44
|
+
# Color 3
|
|
45
|
+
colors.append(theme_color
|
|
46
|
+
.adjust_saturation(-0.05, floor=0.10)
|
|
47
|
+
.adjust_hue(spacing * 2)
|
|
48
|
+
.adjust_brightness(0, floor=0.20))
|
|
49
|
+
|
|
50
|
+
# Color 4
|
|
51
|
+
colors.append(theme_color
|
|
52
|
+
.adjust_saturation(-0.05, floor=0.10)
|
|
53
|
+
.adjust_hue(-spacing)
|
|
54
|
+
.adjust_brightness(-0.05, floor=0.20))
|
|
55
|
+
|
|
56
|
+
# Color 5
|
|
57
|
+
colors.append(theme_color
|
|
58
|
+
.adjust_saturation(-0.05, floor=0.10)
|
|
59
|
+
.adjust_hue(-(spacing * 2))
|
|
60
|
+
.adjust_brightness(0, floor=0.20))
|
|
61
|
+
|
|
62
|
+
return cls(tuple(colors))
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_accented_analogous(cls, theme_color: HSBAColor, spacing: float = 0.05) -> Self:
|
|
66
|
+
if not (0.0 <= spacing < 0.2):
|
|
67
|
+
raise ValueError("Spacing must be between 0 and 0.2")
|
|
68
|
+
|
|
69
|
+
colors = []
|
|
70
|
+
colors.append(theme_color)
|
|
71
|
+
|
|
72
|
+
# Color 2
|
|
73
|
+
colors.append(theme_color
|
|
74
|
+
.adjust_saturation(-0.05, floor=0.10)
|
|
75
|
+
.adjust_hue(spacing)
|
|
76
|
+
.adjust_brightness(-0.05, floor=0.20))
|
|
77
|
+
|
|
78
|
+
# Color 3
|
|
79
|
+
colors.append(theme_color
|
|
80
|
+
.adjust_saturation(-0.05, floor=0.10)
|
|
81
|
+
.adjust_hue(-spacing)
|
|
82
|
+
.adjust_brightness(-0.05, floor=0.20))
|
|
83
|
+
|
|
84
|
+
# Accent 1
|
|
85
|
+
colors.append(theme_color
|
|
86
|
+
.adjust_saturation(-0.05, floor=0.10)
|
|
87
|
+
.adjust_hue(spacing * 2)
|
|
88
|
+
.complement()
|
|
89
|
+
.adjust_brightness(0, floor=0.20))
|
|
90
|
+
|
|
91
|
+
# Accent 2
|
|
92
|
+
colors.append(theme_color
|
|
93
|
+
.adjust_saturation(-0.05, floor=0.10)
|
|
94
|
+
.adjust_hue(-(spacing * 2))
|
|
95
|
+
.complement()
|
|
96
|
+
.adjust_brightness(0, floor=0.20))
|
|
97
|
+
|
|
98
|
+
return cls(tuple(colors))
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_complementary(cls, theme_color: HSBAColor) -> Self:
|
|
102
|
+
colors = []
|
|
103
|
+
colors.append(theme_color)
|
|
104
|
+
|
|
105
|
+
colors.append(theme_color
|
|
106
|
+
.adjust_saturation(0.10)
|
|
107
|
+
.adjust_brightness(-0.30, floor=0.20, overflow=True))
|
|
108
|
+
|
|
109
|
+
colors.append(theme_color
|
|
110
|
+
.adjust_saturation(-0.10)
|
|
111
|
+
.adjust_brightness(0.30))
|
|
112
|
+
|
|
113
|
+
colors.append(theme_color
|
|
114
|
+
.complement()
|
|
115
|
+
.adjust_saturation(0.20)
|
|
116
|
+
.adjust_brightness(-0.30, floor=0.20, overflow=True))
|
|
117
|
+
|
|
118
|
+
colors.append(theme_color.complement())
|
|
119
|
+
|
|
120
|
+
return cls(tuple(colors))
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_compound(cls, theme_color: HSBAColor) -> Self:
|
|
124
|
+
colors = []
|
|
125
|
+
colors.append(theme_color)
|
|
126
|
+
|
|
127
|
+
colors.append(theme_color
|
|
128
|
+
.adjust_hue(0.1)
|
|
129
|
+
.adjust_saturation(-0.10, floor=0.10)
|
|
130
|
+
.adjust_brightness(-0.20, floor=0.20))
|
|
131
|
+
|
|
132
|
+
colors.append(theme_color
|
|
133
|
+
.adjust_hue(0.1)
|
|
134
|
+
.adjust_saturation(-0.40, floor=0.10, ceiling=0.90)
|
|
135
|
+
.adjust_brightness(-0.40, floor=0.20))
|
|
136
|
+
|
|
137
|
+
colors.append(theme_color
|
|
138
|
+
.adjust_hue(-0.05)
|
|
139
|
+
.complement()
|
|
140
|
+
.adjust_saturation(-0.25, floor=0.10)
|
|
141
|
+
.adjust_brightness(0.05, floor=0.20))
|
|
142
|
+
|
|
143
|
+
colors.append(theme_color
|
|
144
|
+
.adjust_hue(-0.1)
|
|
145
|
+
.complement()
|
|
146
|
+
.adjust_saturation(0.10, ceiling=0.90)
|
|
147
|
+
.adjust_brightness(-0.20, floor=0.20))
|
|
148
|
+
|
|
149
|
+
return cls(tuple(colors))
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def from_monochromatic(cls, theme_color: HSBAColor) -> Self:
|
|
153
|
+
colors = []
|
|
154
|
+
colors.append(theme_color)
|
|
155
|
+
|
|
156
|
+
colors.append(theme_color
|
|
157
|
+
.adjust_brightness(-0.50, floor=0.20, overflow=True))
|
|
158
|
+
|
|
159
|
+
colors.append(theme_color
|
|
160
|
+
.adjust_saturation(-0.30, floor=0.10, ceiling=0.70, overflow=True))
|
|
161
|
+
|
|
162
|
+
colors.append(theme_color
|
|
163
|
+
.adjust_brightness(-0.50, floor=0.20, overflow=True)
|
|
164
|
+
.adjust_saturation(-0.3, floor=0.10, ceiling=0.70, overflow=True))
|
|
165
|
+
|
|
166
|
+
colors.append(theme_color
|
|
167
|
+
.adjust_brightness(-0.20, floor=0.20, overflow=True))
|
|
168
|
+
|
|
169
|
+
return cls(tuple(colors))
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def from_shades(cls, theme_color: HSBAColor) -> Self:
|
|
173
|
+
colors = []
|
|
174
|
+
colors.append(theme_color)
|
|
175
|
+
|
|
176
|
+
colors.append(theme_color.adjust_brightness(-0.25, floor=0.20, overflow=True))
|
|
177
|
+
colors.append(theme_color.adjust_brightness(-0.50, floor=0.20, overflow=True))
|
|
178
|
+
colors.append(theme_color.adjust_brightness(-0.75, floor=0.20, overflow=True))
|
|
179
|
+
colors.append(theme_color.adjust_brightness(-0.10, floor=0.20))
|
|
180
|
+
|
|
181
|
+
return cls(tuple(colors))
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def from_split_complementary(cls, theme_color: HSBAColor, spacing: float = 0.05) -> Self:
|
|
185
|
+
colors = []
|
|
186
|
+
colors.append(theme_color)
|
|
187
|
+
|
|
188
|
+
colors.append(theme_color
|
|
189
|
+
.adjust_saturation(0.10)
|
|
190
|
+
.adjust_brightness(-0.30, floor=0.20, overflow=True))
|
|
191
|
+
|
|
192
|
+
colors.append(theme_color
|
|
193
|
+
.adjust_saturation(-0.10)
|
|
194
|
+
.adjust_brightness(0.30))
|
|
195
|
+
|
|
196
|
+
colors.append(theme_color
|
|
197
|
+
.complement()
|
|
198
|
+
.adjust_hue(spacing))
|
|
199
|
+
|
|
200
|
+
colors.append(theme_color
|
|
201
|
+
.complement()
|
|
202
|
+
.adjust_hue(-spacing))
|
|
203
|
+
|
|
204
|
+
return cls(tuple(colors))
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_triadic(cls, theme_color: HSBAColor) -> Self:
|
|
208
|
+
colors = []
|
|
209
|
+
colors.append(theme_color)
|
|
210
|
+
|
|
211
|
+
colors.append(theme_color
|
|
212
|
+
.adjust_saturation(0.10)
|
|
213
|
+
.adjust_brightness(-0.30, floor=0.20, overflow=True))
|
|
214
|
+
|
|
215
|
+
colors.append(theme_color
|
|
216
|
+
.adjust_hue(0.33)
|
|
217
|
+
.adjust_saturation(-0.10))
|
|
218
|
+
|
|
219
|
+
colors.append(theme_color
|
|
220
|
+
.adjust_hue(0.66)
|
|
221
|
+
.adjust_saturation(-0.10)
|
|
222
|
+
.adjust_brightness(-0.20))
|
|
223
|
+
|
|
224
|
+
colors.append(theme_color
|
|
225
|
+
.adjust_hue(0.66)
|
|
226
|
+
.adjust_saturation(-0.05)
|
|
227
|
+
.adjust_brightness(-0.30, floor=0.40, overflow=True))
|
|
228
|
+
|
|
229
|
+
return cls(tuple(colors))
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .hsba_color import HSBAColor as HSBAColor
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Iterator, Self
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class ColorScheme:
|
|
7
|
+
colors: tuple[HSBAColor, ...]
|
|
8
|
+
def __post_init__(self) -> None: ...
|
|
9
|
+
def __len__(self) -> int: ...
|
|
10
|
+
def __getitem__(self, index: int) -> HSBAColor: ...
|
|
11
|
+
def __iter__(self) -> Iterator[HSBAColor]: ...
|
|
12
|
+
@property
|
|
13
|
+
def theme_color(self) -> HSBAColor: ...
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_analogous(cls, theme_color: HSBAColor, spacing: float = 0.05) -> Self: ...
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_accented_analogous(cls, theme_color: HSBAColor, spacing: float = 0.05) -> Self: ...
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_complementary(cls, theme_color: HSBAColor) -> Self: ...
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_compound(cls, theme_color: HSBAColor) -> Self: ...
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_monochromatic(cls, theme_color: HSBAColor) -> Self: ...
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_shades(cls, theme_color: HSBAColor) -> Self: ...
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_split_complementary(cls, theme_color: HSBAColor, spacing: float = 0.05) -> Self: ...
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_triadic(cls, theme_color: HSBAColor) -> Self: ...
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import random
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class HSBAColor:
|
|
7
|
+
"""
|
|
8
|
+
A color represented by Hue, Saturation, Brightness, and Alpha components.
|
|
9
|
+
All components are floats in the range [0.0, 1.0].
|
|
10
|
+
"""
|
|
11
|
+
hue: float
|
|
12
|
+
saturation: float
|
|
13
|
+
brightness: float
|
|
14
|
+
alpha: float
|
|
15
|
+
|
|
16
|
+
def __iter__(self):
|
|
17
|
+
"""Allows unpacking: h, s, b, a = color"""
|
|
18
|
+
return iter((self.hue, self.saturation, self.brightness, self.alpha))
|
|
19
|
+
|
|
20
|
+
def adjust_hue(self, change: float) -> Self:
|
|
21
|
+
"""
|
|
22
|
+
Returns a new HSBAColor with the hue adjusted by the given amount.
|
|
23
|
+
The result is wrapped to the [0.0, 1.0] range cyclically.
|
|
24
|
+
Param `change` must be between -1.0 and 1.0.
|
|
25
|
+
"""
|
|
26
|
+
if not (-1.0 <= change <= 1.0):
|
|
27
|
+
raise ValueError("Hue adjustment must be between -1.0 and 1.0")
|
|
28
|
+
|
|
29
|
+
new_hue = self.hue + change
|
|
30
|
+
|
|
31
|
+
if new_hue > 1.0:
|
|
32
|
+
new_hue -= 1.0
|
|
33
|
+
elif new_hue < 0.0:
|
|
34
|
+
new_hue += 1.0
|
|
35
|
+
|
|
36
|
+
return HSBAColor(new_hue, self.saturation, self.brightness, self.alpha)
|
|
37
|
+
|
|
38
|
+
def complement(self) -> Self:
|
|
39
|
+
"""Returns the complementary color (180 degrees reduced hue)."""
|
|
40
|
+
return self.adjust_hue(0.5)
|
|
41
|
+
|
|
42
|
+
def adjust_saturation(self, change: float, floor: float = 0.0, ceiling: float = 1.0, overflow: bool = False) -> Self:
|
|
43
|
+
"""
|
|
44
|
+
Returns a new HSBAColor with adjusted saturation.
|
|
45
|
+
If overflow is True, values outside [floor, ceiling] wrap around.
|
|
46
|
+
If overflow is False, values are clamped.
|
|
47
|
+
"""
|
|
48
|
+
new_saturation = self.saturation + change
|
|
49
|
+
new_saturation = self._apply_bounds(new_saturation, floor, ceiling, overflow)
|
|
50
|
+
return HSBAColor(self.hue, new_saturation, self.brightness, self.alpha)
|
|
51
|
+
|
|
52
|
+
def adjust_brightness(self, change: float, floor: float = 0.0, ceiling: float = 1.0, overflow: bool = False) -> Self:
|
|
53
|
+
"""
|
|
54
|
+
Returns a new HSBAColor with adjusted brightness.
|
|
55
|
+
"""
|
|
56
|
+
new_brightness = self.brightness + change
|
|
57
|
+
new_brightness = self._apply_bounds(new_brightness, floor, ceiling, overflow)
|
|
58
|
+
return HSBAColor(self.hue, self.saturation, new_brightness, self.alpha)
|
|
59
|
+
|
|
60
|
+
def adjust_alpha(self, change: float, floor: float = 0.0, ceiling: float = 1.0) -> Self:
|
|
61
|
+
"""
|
|
62
|
+
Returns a new HSBAColor with adjusted alpha.
|
|
63
|
+
Alpha is always clamped, never overflows.
|
|
64
|
+
"""
|
|
65
|
+
new_alpha = self.alpha + change
|
|
66
|
+
if new_alpha > ceiling:
|
|
67
|
+
new_alpha = ceiling
|
|
68
|
+
elif new_alpha < floor:
|
|
69
|
+
new_alpha = floor
|
|
70
|
+
|
|
71
|
+
return HSBAColor(self.hue, self.saturation, self.brightness, new_alpha)
|
|
72
|
+
|
|
73
|
+
def _apply_bounds(self, value: float, floor: float, ceiling: float, overflow: bool) -> float:
|
|
74
|
+
"""Helper to apply floor/ceiling logic with optional overflow."""
|
|
75
|
+
if overflow:
|
|
76
|
+
if value > ceiling:
|
|
77
|
+
value -= (ceiling - floor)
|
|
78
|
+
elif value < floor:
|
|
79
|
+
value += (ceiling - floor)
|
|
80
|
+
else:
|
|
81
|
+
if value > ceiling:
|
|
82
|
+
value = ceiling
|
|
83
|
+
elif value < floor:
|
|
84
|
+
value = floor
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def random(cls, saturation_range: tuple[float, float] = (0.0, 1.0),
|
|
89
|
+
brightness_range: tuple[float, float] = (0.0, 1.0)) -> Self:
|
|
90
|
+
"""
|
|
91
|
+
Generates a random HSBAColor.
|
|
92
|
+
Hue is always random [0.0, 1.0].
|
|
93
|
+
Alpha is 1.0.
|
|
94
|
+
"""
|
|
95
|
+
return cls(
|
|
96
|
+
hue=random.uniform(0.0, 1.0),
|
|
97
|
+
saturation=random.uniform(*saturation_range),
|
|
98
|
+
brightness=random.uniform(*brightness_range),
|
|
99
|
+
alpha=1.0
|
|
100
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class HSBAColor:
|
|
6
|
+
hue: float
|
|
7
|
+
saturation: float
|
|
8
|
+
brightness: float
|
|
9
|
+
alpha: float
|
|
10
|
+
def __iter__(self): ...
|
|
11
|
+
def adjust_hue(self, change: float) -> Self: ...
|
|
12
|
+
def complement(self) -> Self: ...
|
|
13
|
+
def adjust_saturation(self, change: float, floor: float = 0.0, ceiling: float = 1.0, overflow: bool = False) -> Self: ...
|
|
14
|
+
def adjust_brightness(self, change: float, floor: float = 0.0, ceiling: float = 1.0, overflow: bool = False) -> Self: ...
|
|
15
|
+
def adjust_alpha(self, change: float, floor: float = 0.0, ceiling: float = 1.0) -> Self: ...
|
|
16
|
+
@classmethod
|
|
17
|
+
def random(cls, saturation_range: tuple[float, float] = (0.0, 1.0), brightness_range: tuple[float, float] = (0.0, 1.0)) -> Self: ...
|