pygkit 1.0.1__py3-none-any.whl

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.
@@ -0,0 +1,185 @@
1
+ import math
2
+ from typing import Callable
3
+
4
+ from pygame import SRCALPHA, Surface, draw
5
+
6
+ from ..utils.interpolation import ease_in_out
7
+
8
+
9
+ class _WipeBase:
10
+ __slots__ = ("_easing", "_elapsed", "_duration", "_done", "_mask")
11
+
12
+ def __init__(self, easing: Callable[[float], float] = ease_in_out) -> None:
13
+ self._easing = easing
14
+ self._elapsed = 0.0
15
+ self._duration = 0.0
16
+ self._done = False
17
+ self._mask: Surface | None = None
18
+
19
+ def start(self, duration: float) -> None:
20
+ self._elapsed = 0.0
21
+ self._duration = duration
22
+ self._done = False
23
+
24
+ @property
25
+ def done(self) -> bool:
26
+ return self._done
27
+
28
+ def reset(self) -> None:
29
+ self._elapsed = 0.0
30
+ self._duration = 0.0
31
+ self._done = False
32
+ self._mask = None
33
+
34
+
35
+ class IrisIn(_WipeBase):
36
+ """
37
+ Circle closes in from the edges to the center, revealing black (or a color).
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ color: tuple[int, int, int] = (0, 0, 0),
43
+ easing: Callable[[float], float] = ease_in_out,
44
+ ) -> None:
45
+ super().__init__(easing)
46
+ self._color = color
47
+
48
+ def update(self, dt: float) -> None:
49
+ if self._done:
50
+ return
51
+ self._elapsed += dt
52
+ if self._elapsed >= self._duration:
53
+ self._elapsed = self._duration
54
+ self._done = True
55
+
56
+ def render(
57
+ self,
58
+ screen: Surface,
59
+ source: Surface | None = None,
60
+ target: Surface | None = None,
61
+ ) -> None:
62
+ if source is not None:
63
+ screen.blit(source, (0, 0))
64
+
65
+ w, h = screen.get_size()
66
+ max_radius = int(math.ceil(math.sqrt(w * w + h * h) / 2))
67
+
68
+ t = self._easing(self._elapsed / self._duration) if self._duration > 0 else 1.0
69
+ radius = int(max_radius * (1.0 - t))
70
+
71
+ if radius <= 0:
72
+ screen.fill(self._color)
73
+ return
74
+
75
+ cx, cy = w // 2, h // 2
76
+
77
+ if self._mask is None or self._mask.get_size() != (w, h):
78
+ self._mask = Surface((w, h), SRCALPHA)
79
+
80
+ self._mask.fill(self._color)
81
+ draw.circle(self._mask, (0, 0, 0, 0), (cx, cy), radius)
82
+ screen.blit(self._mask, (0, 0))
83
+
84
+
85
+ class IrisOut(_WipeBase):
86
+ """
87
+ Circle expands from the center outward, revealing the target.
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ easing: Callable[[float], float] = ease_in_out,
93
+ ) -> None:
94
+ super().__init__(easing)
95
+
96
+ def update(self, dt: float) -> None:
97
+ if self._done:
98
+ return
99
+ self._elapsed += dt
100
+ if self._elapsed >= self._duration:
101
+ self._elapsed = self._duration
102
+ self._done = True
103
+
104
+ def render(
105
+ self,
106
+ screen: Surface,
107
+ source: Surface | None = None,
108
+ target: Surface | None = None,
109
+ ) -> None:
110
+ w, h = screen.get_size()
111
+ max_radius = int(math.ceil(math.sqrt(w * w + h * h) / 2))
112
+
113
+ t = self._easing(self._elapsed / self._duration) if self._duration > 0 else 1.0
114
+ radius = int(max_radius * t)
115
+ cx, cy = w // 2, h // 2
116
+
117
+ if target is not None:
118
+ screen.blit(target, (0, 0))
119
+
120
+ if radius < max_radius:
121
+ if self._mask is None or self._mask.get_size() != (w, h):
122
+ self._mask = Surface((w, h), SRCALPHA)
123
+
124
+ self._mask.fill((0, 0, 0, 255))
125
+ draw.circle(self._mask, (0, 0, 0, 0), (cx, cy), radius)
126
+ screen.blit(self._mask, (0, 0))
127
+
128
+
129
+ class Slide(_WipeBase):
130
+ """
131
+ Source slides offscreen while target slides in from the opposite side.
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ direction: str = "left",
137
+ ) -> None:
138
+ super().__init__()
139
+ if direction not in ("left", "right", "up", "down"):
140
+ raise ValueError(
141
+ f"Invalid direction '{direction}'. Must be left, right, up, or down."
142
+ )
143
+ self._direction = direction
144
+ self._buffer: Surface | None = None
145
+
146
+ def update(self, dt: float) -> None:
147
+ if self._done:
148
+ return
149
+ self._elapsed += dt
150
+ if self._elapsed >= self._duration:
151
+ self._elapsed = self._duration
152
+ self._done = True
153
+
154
+ def render(
155
+ self,
156
+ screen: Surface,
157
+ source: Surface | None = None,
158
+ target: Surface | None = None,
159
+ ) -> None:
160
+ w, h = screen.get_size()
161
+ t = self._elapsed / self._duration if self._duration > 0 else 1.0
162
+
163
+ if self._direction == "left":
164
+ out_offset = (-w * t, 0)
165
+ in_offset = (w * (1.0 - t), 0)
166
+ elif self._direction == "right":
167
+ out_offset = (w * t, 0)
168
+ in_offset = (-w * (1.0 - t), 0)
169
+ elif self._direction == "up":
170
+ out_offset = (0, -h * t)
171
+ in_offset = (0, h * (1.0 - t))
172
+ else:
173
+ out_offset = (0, h * t)
174
+ in_offset = (0, -h * (1.0 - t))
175
+
176
+ if self._buffer is None or self._buffer.get_size() != (w, h):
177
+ self._buffer = Surface((w, h))
178
+
179
+ self._buffer.fill((0, 0, 0))
180
+ if target is not None:
181
+ self._buffer.blit(target, in_offset)
182
+ if source is not None:
183
+ self._buffer.blit(source, out_offset)
184
+
185
+ screen.blit(self._buffer, (0, 0))
pygkit/ui/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from .base import UIBase, UIOptions, BoxModel, BoxModelResult, generate_box_model
2
+ from .container import Container
3
+ from .cooldown import CooldownOverlay
4
+ from .progressbar import ProgressBarUI
5
+
6
+ __all__ = [
7
+ "UIBase",
8
+ "UIOptions",
9
+ "BoxModel",
10
+ "BoxModelResult",
11
+ "generate_box_model",
12
+ "ProgressBarUI",
13
+ "CooldownOverlay",
14
+ "Container",
15
+ ]
pygkit/ui/base.py ADDED
@@ -0,0 +1,125 @@
1
+ from typing import Any, Callable, List, TypedDict
2
+
3
+ import pygame
4
+ from pygame import SRCALPHA, Surface
5
+ from pygame.typing import ColorLike
6
+
7
+
8
+ class BoxModel(TypedDict, total=False):
9
+ margin_x: int
10
+ margin_y: int
11
+ padding_x: int
12
+ padding_y: int
13
+ width: int
14
+ height: int
15
+
16
+
17
+ class BoxModelResult(TypedDict, total=True):
18
+ left: int
19
+ top: int
20
+ offset_x: int
21
+ offset_y: int
22
+ full_width: int
23
+ full_height: int
24
+ content_width: int
25
+ content_height: int
26
+
27
+
28
+ class UIOptions(BoxModel, total=False):
29
+ border_radius: int
30
+ border_color: ColorLike
31
+ background: ColorLike
32
+ fill_color: ColorLike
33
+
34
+
35
+ def generate_box_model(model: BoxModel) -> BoxModelResult:
36
+ width = model.get("width", 0)
37
+ height = model.get("height", 0)
38
+ padding_x = model.get("padding_x", 0)
39
+ padding_y = model.get("padding_y", 0)
40
+ margin_x = model.get("margin_x", 0)
41
+ margin_y = model.get("margin_y", 0)
42
+ border_width = model.get("border_width", 0)
43
+
44
+ inner_x = 2 * (padding_x + border_width)
45
+ inner_y = 2 * (padding_y + border_width)
46
+
47
+ return {
48
+ "left": padding_x + border_width,
49
+ "top": padding_y + border_width,
50
+ "offset_x": margin_x,
51
+ "offset_y": margin_y,
52
+ "full_width": width,
53
+ "full_height": height,
54
+ "content_width": width - inner_x,
55
+ "content_height": height - inner_y,
56
+ }
57
+
58
+
59
+ class UIBase:
60
+ def __init__(self, options: UIOptions) -> None:
61
+ self.colors: dict[str, ColorLike] = {
62
+ "bg": options.get("background", (0, 0, 0, 0))
63
+ }
64
+
65
+ self.box_model = generate_box_model(options)
66
+
67
+ self.border: dict[str, Any] = {
68
+ "radius": options.get("border_radius", 0),
69
+ "width": options.get("border_width", 0),
70
+ "color": options.get("border_color", (0, 0, 0, 0)),
71
+ }
72
+
73
+ local_size = self.box_model["full_width"], self.box_model["full_height"]
74
+ self.local_surface = Surface(local_size, SRCALPHA)
75
+
76
+ self.renderable_plugins: List[Callable[[Surface], Any]] = []
77
+
78
+ def add_plugin(self, cb: Callable[[Surface], Any]) -> None:
79
+ self.renderable_plugins.append(cb)
80
+
81
+ def draw_base(self) -> None:
82
+ local_surf = self.local_surface
83
+ local_surf.fill((0, 0, 0, 0))
84
+
85
+ bg_color = self.colors["bg"]
86
+ border_width = self.border["width"]
87
+ border_radius = self.border["radius"]
88
+ border_color = self.border["color"]
89
+
90
+ content_pos = self.box_model["left"], self.box_model["top"]
91
+ content_size = self.box_model["content_width"], self.box_model["content_height"]
92
+ surface_size = local_surf.get_size()
93
+
94
+ if border_width > 0:
95
+ pygame.draw.rect(
96
+ local_surf,
97
+ border_color,
98
+ (0, 0, *surface_size),
99
+ width=border_width,
100
+ border_radius=border_radius,
101
+ )
102
+
103
+ pygame.draw.rect(
104
+ local_surf,
105
+ bg_color,
106
+ (*content_pos, *content_size),
107
+ border_radius=max(0, border_radius - border_width),
108
+ )
109
+
110
+ @property
111
+ def size(self) -> tuple[int, int]:
112
+ return (self.box_model["full_width"], self.box_model["full_height"])
113
+
114
+ def render(self, screen: Surface, pos_offset: tuple[int, int] = (0, 0)) -> None:
115
+ self.draw_base()
116
+ for plugin in self.renderable_plugins:
117
+ plugin(self.local_surface)
118
+ pos = (
119
+ self.box_model["offset_x"] + pos_offset[0],
120
+ self.box_model["offset_y"] + pos_offset[1],
121
+ )
122
+ screen.blit(self.local_surface, pos)
123
+
124
+ def update(self) -> None:
125
+ pass
pygkit/ui/container.py ADDED
@@ -0,0 +1,21 @@
1
+ from typing import List, Tuple
2
+
3
+ from pygame import Surface
4
+
5
+ from .base import UIBase
6
+
7
+
8
+ class Container:
9
+ def __init__(self) -> None:
10
+ self.children: List[Tuple[UIBase, Tuple[int, int]]] = []
11
+
12
+ def add(self, child: UIBase, pos: Tuple[int, int] = (0, 0)) -> None:
13
+ self.children.append((child, pos))
14
+
15
+ def update(self) -> None:
16
+ for child, _ in self.children:
17
+ child.update()
18
+
19
+ def render(self, screen: Surface) -> None:
20
+ for child, offset in self.children:
21
+ child.render(screen, offset)
pygkit/ui/cooldown.py ADDED
@@ -0,0 +1,107 @@
1
+ import math
2
+ from typing import List, Optional, Tuple, Unpack
3
+
4
+ import pygame
5
+ from pygame import SRCALPHA, Surface, draw, transform
6
+
7
+ from ..utils.timer import Timer
8
+ from .base import UIBase, UIOptions
9
+
10
+
11
+ class CooldownOverlay(UIBase):
12
+ def __init__(
13
+ self,
14
+ timer: Timer,
15
+ size: int | float,
16
+ icon: Optional[Surface] = None,
17
+ overlay_color: tuple[int, int, int, int] = (0, 0, 0, 255),
18
+ disabled_color: tuple[int, int, int, int] = (50, 50, 55, 200),
19
+ **overrides: Unpack[UIOptions],
20
+ ) -> None:
21
+ super().__init__({"width": int(size), "height": int(size), **overrides})
22
+
23
+ self.timer = timer
24
+ self.overlay_color = overlay_color
25
+ self.disabled_color = disabled_color
26
+ self.disabled = False
27
+
28
+ content_w = self.box_model["content_width"]
29
+ content_h = self.box_model["content_height"]
30
+
31
+ r = min(content_w, content_h) / 2
32
+ self.radius = r
33
+ self.scalar: float = 1.0
34
+ b = overrides.get("border_radius", 0)
35
+ if b < r:
36
+ self.scalar = (math.sqrt(2 * (r - b) ** 2) + b) / r
37
+
38
+ self.overlay_parent = Surface((content_w, content_h), SRCALPHA)
39
+ self._disabled_surf: Optional[Surface] = None
40
+ self.add_plugin(self._draw_overlay)
41
+ self.add_plugin(self._draw_disabled_overlay)
42
+ if icon is not None:
43
+ scale = content_w / icon.width, content_h / icon.height
44
+ self.icon = transform.scale_by(icon, scale)
45
+ self.add_plugin(self._draw_icon)
46
+
47
+ def _draw_icon(self, surface: Surface) -> None:
48
+ icon: Optional[Surface] = getattr(self, "icon", None)
49
+ if icon is None:
50
+ return
51
+
52
+ center_x = self.box_model["content_width"] // 2
53
+ center_y = self.box_model["content_height"] // 2
54
+
55
+ rect = icon.get_rect(
56
+ center=(
57
+ self.box_model["left"] + center_x,
58
+ self.box_model["top"] + center_y,
59
+ )
60
+ )
61
+
62
+ if self.disabled:
63
+ icon.set_alpha(80)
64
+ elif not self.timer.reached():
65
+ icon.set_alpha(150)
66
+ elif icon.get_alpha() != 255:
67
+ icon.set_alpha(255)
68
+ surface.blit(icon, rect.topleft)
69
+
70
+ def _draw_overlay(self, surface: Surface) -> None:
71
+ if self.disabled or self.timer.reached():
72
+ return
73
+
74
+ progress = 1.0 - self.timer.ratio()
75
+
76
+ center = (self.radius, self.radius)
77
+ points: List[Tuple[float, float]] = [center]
78
+
79
+ end_degrees = int(progress * 360)
80
+
81
+ for degree in range(-90, end_degrees - 90):
82
+ angle = math.radians(degree)
83
+ x = self.radius + self.radius * math.cos(angle) * self.scalar
84
+ y = self.radius + self.radius * math.sin(angle) * self.scalar
85
+ points.append((x, y))
86
+
87
+ op = self.overlay_parent
88
+ op.fill((0, 0, 0, 0))
89
+
90
+ if len(points) > 2:
91
+ draw.polygon(op, self.overlay_color, points)
92
+
93
+ pos = self.box_model["left"], self.box_model["top"]
94
+ surface.blit(op, pos)
95
+
96
+ def _draw_disabled_overlay(self, surface: Surface) -> None:
97
+ if not self.disabled:
98
+ return
99
+
100
+ w = self.box_model["content_width"]
101
+ h = self.box_model["content_height"]
102
+ if self._disabled_surf is None or self._disabled_surf.get_size() != (w, h):
103
+ self._disabled_surf = Surface((w, h), SRCALPHA)
104
+
105
+ self._disabled_surf.fill(self.disabled_color)
106
+ pos = self.box_model["left"], self.box_model["top"]
107
+ surface.blit(self._disabled_surf, pos)
@@ -0,0 +1,64 @@
1
+ from typing import Unpack
2
+
3
+ import pygame
4
+ from pygame import Surface
5
+
6
+ from ..utils.interpolation import SimpleInterpolation
7
+ from .base import UIBase, UIOptions
8
+
9
+ PROGRESSBAR_DEFAULTS: UIOptions = {
10
+ "width": 250,
11
+ "height": 24,
12
+ "border_width": 2,
13
+ "border_radius": 6,
14
+ "padding_x": 2,
15
+ "padding_y": 2,
16
+ "margin_x": 0,
17
+ "margin_y": 0,
18
+ "border_color": (40, 44, 52, 255),
19
+ "background": (33, 37, 43, 255),
20
+ "fill_color": (255, 255, 255, 255),
21
+ }
22
+
23
+
24
+ class ProgressBarUI(UIBase):
25
+ def __init__(self, **overrides: Unpack[UIOptions]) -> None:
26
+ options: UIOptions = {**PROGRESSBAR_DEFAULTS, **overrides}
27
+ super().__init__(options)
28
+ self.colors["fill"] = options.get("fill_color", (255, 255, 255, 255))
29
+ self.interpolation = SimpleInterpolation(speed=0.05)
30
+
31
+ def set_progress(self, value: float) -> None:
32
+ self.interpolation.set(value)
33
+
34
+ def get_progress(self) -> float:
35
+ return self.interpolation.current
36
+
37
+ def update(self) -> None:
38
+ self.interpolation.update()
39
+
40
+ def render(self, screen: Surface, pos_offset: tuple[int, int] = (0, 0)) -> None:
41
+ self.draw_base()
42
+
43
+ intrp_current = self.interpolation.current
44
+ if intrp_current > 0:
45
+ fill_width = int(self.box_model["content_width"] * intrp_current)
46
+ inner_radius = max(0, self.border["radius"] - self.border["width"])
47
+
48
+ pygame.draw.rect(
49
+ self.local_surface,
50
+ self.colors["fill"],
51
+ (
52
+ self.box_model["left"],
53
+ self.box_model["top"],
54
+ fill_width,
55
+ self.box_model["content_height"],
56
+ ),
57
+ border_radius=inner_radius,
58
+ )
59
+
60
+ pos = (
61
+ self.box_model["offset_x"] + pos_offset[0],
62
+ self.box_model["offset_y"] + pos_offset[1],
63
+ )
64
+ screen.blit(self.local_surface, pos)
@@ -0,0 +1,19 @@
1
+ from .interpolation import SimpleInterpolation, ease_in, ease_in_out, ease_out, lerp, smoothstep
2
+ from .text import fit, load_font, outline, render_multiline, shadow, wrap
3
+ from .timer import Timer
4
+
5
+ __all__ = [
6
+ "Timer",
7
+ "SimpleInterpolation",
8
+ "lerp",
9
+ "smoothstep",
10
+ "ease_in_out",
11
+ "ease_out",
12
+ "ease_in",
13
+ "load_font",
14
+ "outline",
15
+ "shadow",
16
+ "wrap",
17
+ "render_multiline",
18
+ "fit",
19
+ ]
@@ -0,0 +1,45 @@
1
+ import math
2
+
3
+
4
+ def lerp(a: float, b: float, t: float) -> float:
5
+ return a + (b - a) * t
6
+
7
+
8
+ def smoothstep(t: float) -> float:
9
+ return t * t * (3 - 2 * t)
10
+
11
+
12
+ def ease_in(t: float) -> float:
13
+ return t * t
14
+
15
+
16
+ def ease_out(t: float) -> float:
17
+ return 1 - (1 - t) ** 2
18
+
19
+
20
+ def ease_in_out(t: float) -> float:
21
+ if t < 0.5:
22
+ return 2 * t * t
23
+ return 1 - ((-2 * t + 2) ** 2) / 2
24
+
25
+
26
+ class SimpleInterpolation:
27
+ __slots__ = ("current", "target", "speed")
28
+
29
+ def __init__(self, value: float = 1.0, speed: float = 0.1) -> None:
30
+ self.current = value
31
+ self.target = value
32
+ self.speed = speed
33
+
34
+ def set(self, target: float) -> None:
35
+ self.target = max(0, min(target, 1.0))
36
+
37
+ def update(self) -> None:
38
+ diff = self.target - self.current
39
+ if abs(diff) < 0.001:
40
+ self.current = self.target
41
+ else:
42
+ self.current += diff * self.speed
43
+
44
+ def finished(self) -> bool:
45
+ return abs(self.current - self.target) < 0.001