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 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.
@@ -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
+ ![PaintCan Social Preview](.github/social%20preview/paintcan_social_preview.jpg)
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.
@@ -0,0 +1,91 @@
1
+ # PaintCan
2
+
3
+ ![PaintCan Social Preview](.github/social%20preview/paintcan_social_preview.jpg)
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,2 @@
1
+ from .hsba_color import HSBAColor
2
+ from .color_scheme import ColorScheme
@@ -0,0 +1,2 @@
1
+ from .color_scheme import ColorScheme as ColorScheme
2
+ from .hsba_color import HSBAColor as HSBAColor
@@ -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,6 @@
1
+ from .color_scheme import ColorScheme as ColorScheme
2
+ from .hsba_color import HSBAColor as HSBAColor
3
+
4
+ def print_color(color: HSBAColor, text: str = ' '): ...
5
+ def print_scheme(name: str, scheme: ColorScheme): ...
6
+ def main() -> None: ...
@@ -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: ...