from colorsys import hsv_to_rgb, rgb_to_hsv
+from math import atan2, cos, pi, sin, sqrt, tau
from functools import partial
import pygame
-from ui import Child, Label, Slider, Spinner, TextInput
+from ui import Child, Label, Slider, Spinner, TabBar, TextInput
from .base_menu import BaseMenu
pygame.draw.rect(self.surf, self.color, self.rect)
+def draw_cursor(surf, color, pos):
+ r = 16
+ pygame.draw.polygon(
+ surf,
+ color,
+ (
+ (pos[0], pos[1] - r),
+ (pos[0] + r, pos[1]),
+ (pos[0], pos[1] + r),
+ (pos[0] - r, pos[1]),
+ )
+ )
+
+
+class ColorCircle(Child):
+ def render_hue_circle(self):
+ surf = pygame.Surface(self.rect.size, 0, 24)
+ center = tuple(a // 2 for a in surf.get_size())
+ rs = self.radii_squared
+ with pygame.PixelArray(surf) as pa:
+ for x, col in enumerate(pa):
+ for y in range(len(col)):
+ rel = (x - center[0], y - center[1])
+ if rs[0] <= rel[0] ** 2 + rel[1] ** 2 <= rs[1]:
+ angle = atan2(-rel[1], rel[0])
+ if angle < 0:
+ angle += tau
+ pa[x][y] = ColorMenu.hsv_to_color(
+ (int(angle * 255 / tau), 255, 255)
+ )
+ return surf
+
+ def get_overlay_surfs(self):
+ size = self.hue_surf.get_size()
+ overlay_surfs = (
+ pygame.Surface(size, pygame.SRCALPHA, 32),
+ pygame.Surface(size, pygame.SRCALPHA, 32),
+ )
+ for i, surf in enumerate(overlay_surfs):
+ with pygame.PixelArray(surf) as pa:
+ for x, col in enumerate(pa):
+ for y in range(len(col)):
+ pa[x][y] = pygame.Color(
+ y // 2, y // 2, y // 2, 255 - (y, x)[i] * 255 // max(size),
+ )
+ return overlay_surfs
+
+ def __init__(self, parent, rect):
+ super().__init__(parent)
+ self.rect = rect
+ self.radii = (378, min(rect.size) // 2)
+ self.radii_squared = tuple(r ** 2 for r in self.radii)
+ self.avg_radius = sum(self.radii) / len(self.radii)
+ self.hue_circle = self.render_hue_circle()
+ self.hue_angle = 0
+ sz = (512, 512)
+ self.hue_surf = pygame.Surface(sz, pygame.SRCALPHA, 32)
+ self.hue_surf_rect = pygame.Rect(
+ (self.rect.centerx - sz[0] // 2, self.rect.centery - sz[1] // 2), sz
+ )
+ self.sv_pos = (0, 0)
+ self.overlay_surfs = self.get_overlay_surfs()
+ self.pressed = None
+
+ def get_hue_surf_angle(self):
+ angle = self.hue_angle + tau / 8
+ if angle >= tau:
+ angle -= tau
+ return angle
+
+ def draw(self):
+ self.surf.blit(self.hue_circle, self.rect.topleft)
+ draw_cursor(
+ self.surf,
+ "black",
+ (
+ self.rect.centerx + cos(self.hue_angle) * self.avg_radius,
+ self.rect.centery - sin(self.hue_angle) * self.avg_radius,
+ ),
+ )
+ color = ColorMenu.hsv_to_color((self.hue_angle * 255 / tau, 255, 255))
+ self.hue_surf.fill(color)
+ self.hue_surf.blit(self.overlay_surfs[0], (0, 0))
+ self.hue_surf.blit(self.overlay_surfs[1], (0, 0))
+ angle = self.get_hue_surf_angle()
+ rotated_hue_surf = pygame.transform.rotate(self.hue_surf, angle * 360 / tau)
+ sz = rotated_hue_surf.get_size()
+ self.surf.blit(
+ rotated_hue_surf,
+ (self.rect.centerx - sz[0] // 2, self.rect.centery - sz[1] // 2)
+ )
+
+ sz = self.hue_surf_rect.size
+ sv_rel = (self.sv_pos[0] - sz[0] // 2, self.sv_pos[1] - sz[1] // 2)
+ rel_dist = sqrt(sv_rel[0] ** 2 + sv_rel[1] ** 2)
+ rel_angle = atan2(sv_rel[1], sv_rel[0]) - angle
+ flipped_hue_angle = self.hue_angle - pi / 2
+ if flipped_hue_angle < 0:
+ flipped_hue_angle += tau
+ draw_cursor(
+ self.surf,
+ ColorMenu.hsv_to_color((flipped_hue_angle * 255 / tau, 255, 255)),
+ (
+ self.rect.centerx + cos(rel_angle) * rel_dist,
+ self.rect.centery + sin(rel_angle) * rel_dist,
+ ),
+ )
+
+ def handle_mousebuttondown(self, ev):
+ if ev.button != 1 or not self.rect.collidepoint(ev.pos):
+ return
+ rel = (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery)
+ rs = self.radii_squared
+ sq = rel[0] ** 2 + rel[1] ** 2
+ if rs[0] <= sq <= rs[1]:
+ self.pressed = 1
+ self.set_rel_hue(rel)
+ elif rs[0] > sq:
+ self.pressed = 2
+ self.set_rel_sv(rel, sq)
+
+ def handle_mousemotion(self, ev):
+ if self.pressed == 1:
+ self.set_rel_hue(
+ (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery)
+ )
+ elif self.pressed == 2:
+ self.set_rel_sv(
+ (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery)
+ )
+
+ def handle_mousebuttonup(self, ev):
+ if self.pressed == 1:
+ self.set_rel_hue(
+ (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery)
+ )
+ self.pressed = None
+ elif self.pressed == 2:
+ self.set_rel_sv(
+ (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery)
+ )
+ self.pressed = None
+
+ def set_rel_hue(self, rel):
+ angle = atan2(-rel[1], rel[0])
+ if angle < 0:
+ angle += tau
+ self.hue_angle = angle
+ self.parent.set_channel(angle * 255 / tau, "h", self)
+
+ def set_rel_sv(self, rel, sq=None):
+ rel_dist = sqrt(sq if sq is not None else rel[0] ** 2 + rel[1] ** 2)
+ rel_angle = atan2(rel[1], rel[0]) + self.get_hue_surf_angle()
+ sz = self.hue_surf_rect.size
+ x, y = (
+ cos(rel_angle) * rel_dist + sz[0] // 2,
+ sin(rel_angle) * rel_dist + sz[1] // 2,
+ )
+ if x < 0:
+ x = 0
+ elif x > sz[0]:
+ x = sz[0]
+ if y < 0:
+ y = 0
+ elif y > sz[1]:
+ y = sz[1]
+ self.sv_pos = (x, y)
+ self.parent.set_channel(x * 255 / sz[0], "s", self)
+ self.parent.set_channel(y * 255 / sz[1], "v", self)
+
+
+class HSMap(Child):
+ @staticmethod
+ def get_hs_surf(size):
+ surf = pygame.Surface(size, 0, 24)
+ with pygame.PixelArray(surf) as pa:
+ for x, col in enumerate(pa):
+ for y in range(len(col)):
+ col[y] = pygame.Color(
+ *(
+ int(a * 255)
+ for a in hsv_to_rgb(x / size[0], 1 - y / (size[1] - 1), 1.0)
+ )
+ )
+ return surf
+
+ def __init__(self, parent, rect):
+ super().__init__(parent)
+ self.rect = rect
+ self.hs_surf = self.get_hs_surf(rect.size)
+ self.hs_pos = (0, 0)
+
+ def draw(self):
+ pygame.draw.rect(self.surf, "green", self.rect, 1)
+ self.surf.blit(self.hs_surf, self.rect.topleft)
+ color = ColorMenu.hsv_to_color(
+ (self.hs_pos[0] * 255 / self.rect.width, 255, 255)
+ )
+ draw_cursor(
+ self.surf,
+ color,
+ (self.rect.left + self.hs_pos[0], self.rect.bottom - (1 + self.hs_pos[1])),
+ )
+
+ def set_hsv(self, value, dest):
+ self.parent.set_hsv(value, dest, type(self))
+
+
+class HSSlider(Slider):
+ def draw_cursor(self):
+ draw_cursor(self.surf, "blue", self.get_cursor_rect()[0].center)
+
+
class ColorMenu(BaseMenu):
+ def prev_tab(self):
+ if self.tab_bar.current_tab > 0:
+ self.tab_bar.click(self.tab_bar.current_tab - 1)
+
+ def next_tab(self):
+ if self.tab_bar.current_tab < len(self.tab_bar.buttons) - 1:
+ self.tab_bar.click(self.tab_bar.current_tab + 1)
+
+ key_methods = {
+ frozenset(): {
+ pygame.K_LEFT: prev_tab,
+ pygame.K_RIGHT: next_tab,
+ **BaseMenu.key_methods[frozenset()],
+ }
+ }
+
+ def add_control(self, ui_type, *args, **kwargs):
+ dest = kwargs.pop("dest")
+ instance = ui_type(self, *args, None, **kwargs)
+ instance.callback = partial(self.set_channel, dest=dest, sender=instance)
+ self.channel_slots[dest].append(instance)
+ return instance
+
def __init__(self, parent):
super().__init__(parent)
size = self.surf.get_size()
slider_right = label_right + label_width
input_right = slider_right + slider_width
- y += button_height * 2
- Label(
- self,
- pygame.Rect((label_left, y), (label_width, button_height)),
- "R",
- Label.HAlign.CENTER,
- )
- self.red_slider = Slider(
- self,
+ y += button_height + 16
+ tabbar_y = y
+ y += (button_height + 16) * 2
+
+ self.channel_slots = {"r": [], "g": [], "b": [], "h": [], "s": [], "v": []}
+ red_slider = self.add_control(
+ Slider,
pygame.Rect((slider_left, y), (slider_width, button_height)),
Slider.Direction.HORIZONTAL,
0,
button_height,
- partial(self.set_rgb, dest="r", sender_type=Slider),
+ dest="r",
)
- self.red_input = Spinner(
- self,
+ red_spinner = self.add_control(
+ Spinner,
pygame.Rect((input_left, y), (input_width, button_height)),
- partial(self.set_rgb, dest="r", sender_type=Spinner),
+ dest="r",
)
- Label(
- self,
- pygame.Rect((label_right, y), (label_width, button_height)),
- "H",
- Label.HAlign.CENTER,
- )
- self.hue_slider = Slider(
- self,
+ hue_slider = self.add_control(
+ Slider,
pygame.Rect((slider_right, y), (slider_width, button_height)),
Slider.Direction.HORIZONTAL,
0,
button_height,
- partial(self.set_hsv, dest=0, sender_type=Slider),
+ dest="h",
)
- self.hue_input = Spinner(
- self,
+ hue_spinner = self.add_control(
+ Spinner,
pygame.Rect((input_right, y), (input_width, button_height)),
- partial(self.set_hsv, dest=0, sender_type=Spinner),
+ dest="h",
)
-
y += button_height + 16
- Label(
- self,
- pygame.Rect((label_left, y), (label_width, button_height)),
- "G",
- Label.HAlign.CENTER,
- )
- self.green_slider = Slider(
- self,
+ green_slider = self.add_control(
+ Slider,
pygame.Rect((slider_left, y), (slider_width, button_height)),
Slider.Direction.HORIZONTAL,
0,
button_height,
- partial(self.set_rgb, dest="g", sender_type=Slider),
+ dest="g",
)
- self.green_input = Spinner(
- self,
+ green_spinner = self.add_control(
+ Spinner,
pygame.Rect((input_left, y), (input_width, button_height)),
- partial(self.set_rgb, dest="g", sender_type=Spinner),
+ dest="g",
)
- Label(
- self,
- pygame.Rect((label_right, y), (label_width, button_height)),
- "S",
- Label.HAlign.CENTER,
- )
- self.saturation_slider = Slider(
- self,
+ saturation_slider = self.add_control(
+ Slider,
pygame.Rect((slider_right, y), (slider_width, button_height)),
Slider.Direction.HORIZONTAL,
0,
button_height,
- partial(self.set_hsv, dest=1, sender_type=Slider),
+ dest="s",
)
- self.saturation_input = Spinner(
- self,
+ saturation_spinner = self.add_control(
+ Spinner,
pygame.Rect((input_right, y), (input_width, button_height)),
- partial(self.set_hsv, dest=1, sender_type=Spinner),
+ dest="s",
)
-
y += button_height + 16
- Label(
- self,
- pygame.Rect((label_left, y), (label_width, button_height)),
- "B",
- Label.HAlign.CENTER,
- )
- self.blue_slider = Slider(
- self,
+ blue_slider = self.add_control(
+ Slider,
pygame.Rect((slider_left, y), (slider_width, button_height)),
Slider.Direction.HORIZONTAL,
0,
button_height,
- partial(self.set_rgb, dest="b", sender_type=Slider),
+ dest="b",
)
- self.blue_input = Spinner(
- self,
+ blue_spinner = self.add_control(
+ Spinner,
pygame.Rect((input_left, y), (input_width, button_height)),
- partial(self.set_rgb, dest="b", sender_type=Spinner),
+ dest="b",
)
- Label(
- self,
- pygame.Rect((label_right, y), (label_width, button_height)),
- "V",
- Label.HAlign.CENTER,
- )
- self.value_slider = Slider(
- self,
+ value_slider = self.add_control(
+ Slider,
pygame.Rect((slider_right, y), (slider_width, button_height)),
Slider.Direction.HORIZONTAL,
0,
button_height,
- partial(self.set_hsv, dest=2, sender_type=Slider),
+ dest="v",
)
- self.value_input = Spinner(
- self,
+ value_spinner = self.add_control(
+ Spinner,
pygame.Rect((input_right, y), (input_width, button_height)),
- partial(self.set_hsv, dest=2, sender_type=Spinner),
+ dest="v",
+ )
+ self.color_circle = ColorCircle(
+ self,
+ pygame.Rect(
+ (self.modal_rect.centerx - 450, tabbar_y + button_height + 16),
+ (900, 900),
+ ),
+ )
+ hs_map_width = 1336
+ hs_slider_width = 64
+ self.hs_map = HSMap(
+ self,
+ pygame.Rect(
+ (
+ self.modal_rect.centerx - hs_map_width // 2,
+ tabbar_y + button_height + 16,
+ ),
+ (hs_map_width - hs_slider_width - 16, 900),
+ ),
+ )
+ value_slider2 = self.add_control(
+ HSSlider,
+ pygame.Rect(
+ (
+ self.modal_rect.centerx + hs_map_width // 2 - hs_slider_width,
+ tabbar_y + button_height + 16,
+ ),
+ (hs_slider_width, 900),
+ ),
+ Slider.Direction.VERTICAL,
+ 0,
+ button_height // 2,
+ dest="v",
)
- y += button_height * 2
+ self.tab_bar = TabBar(
+ self,
+ pygame.Rect(
+ (self.modal_rect.left, tabbar_y),
+ (self.modal_rect.width, button_height),
+ ),
+ (
+ "Sliders",
+ "Hue Circle",
+ "Hue-Saturation Map",
+ ),
+ (
+ (
+ Label(
+ self,
+ pygame.Rect(
+ (label_left, red_slider.rect.top),
+ (label_width, button_height),
+ ),
+ "R",
+ Label.HAlign.CENTER,
+ ),
+ red_slider,
+ red_spinner,
+ Label(
+ self,
+ pygame.Rect(
+ (label_right, hue_slider.rect.top),
+ (label_width, button_height)
+ ),
+ "H",
+ Label.HAlign.CENTER,
+ ),
+ hue_slider,
+ hue_spinner,
+ Label(
+ self,
+ pygame.Rect(
+ (label_left, green_slider.rect.top),
+ (label_width, button_height),
+ ),
+ "G",
+ Label.HAlign.CENTER,
+ ),
+ green_slider,
+ green_spinner,
+ Label(
+ self,
+ pygame.Rect(
+ (label_right, saturation_slider.rect.top),
+ (label_width, button_height),
+ ),
+ "S",
+ Label.HAlign.CENTER,
+ ),
+ saturation_slider,
+ saturation_spinner,
+ Label(
+ self,
+ pygame.Rect(
+ (label_left, blue_slider.rect.top),
+ (label_width, button_height),
+ ),
+ "B",
+ Label.HAlign.CENTER,
+ ),
+ blue_slider,
+ blue_spinner,
+ Label(
+ self,
+ pygame.Rect(
+ (label_right, value_slider.rect.top),
+ (label_width, button_height),
+ ),
+ "V",
+ Label.HAlign.CENTER,
+ ),
+ value_slider,
+ value_spinner,
+ ),
+ (self.color_circle,),
+ (self.hs_map, value_slider2,),
+ ),
+ )
+
+ y = tabbar_y + button_height + 16 + 900 + 16
half_width = self.modal_rect.width // 2
self.hex_input = TextInput(
self,
*(int(c * 255) for c in hsv_to_rgb(*(x / 255 for x in hsv)))
)
- def update_controls(self, skip_hsv=False):
- color = self.parent.draw_image.color
- self.color_label.color = color
- self.hex_input.value = f"{int(color) >> 8:06x}"
- controls = (
- (self.red_slider, self.red_input, color.r),
- (self.green_slider, self.green_input, color.g),
- (self.blue_slider, self.blue_input, color.b),
- )
- if not skip_hsv:
+ def update_controls(self, color=None, hsv=None, sender=None):
+ if hsv is None:
+ if color is None:
+ color = self.parent.draw_image.color
+ else:
+ self.parent.draw_image.color = color
hsv = self.color_to_hsv(color)
- controls = (
- *controls,
- (self.hue_slider, self.hue_input, int(hsv[0])),
- (self.saturation_slider, self.saturation_input, int(hsv[1])),
- (self.value_slider, self.value_input, int(hsv[2])),
- )
- for slider, spinner, value in controls:
- slider.value = slider.extent * value // 255
- spinner.value = value
+ else:
+ assert color is None
+ color = self.hsv_to_color(hsv)
+ self.parent.draw_image.color = color
+ self.hex_input.value = f"{int(color) >> 8:06x}"
+ self.color_label.color = color
+ assert tuple(color)[:3] == (color.r, color.g, color.b)
+ values = {
+ "r": color.r,
+ "g": color.g,
+ "b": color.b,
+ "h": hsv[0],
+ "s": hsv[1],
+ "v": hsv[2],
+ }
+ for key, controls in self.channel_slots.items():
+ value = values[key]
+ for control in controls:
+ if control is sender:
+ continue
+ control.value = (
+ value
+ if not isinstance(control, Slider)
+ else value * control.extent // 255
+ )
+ if sender != self.color_circle:
+ self.color_circle.hue_angle = values["h"] * tau / 255
+ sz = self.color_circle.hue_surf_rect.size
+ self.color_circle.sv_pos = (values["s"] * sz[0] / 255, values["v"] * sz[1] / 255)
+ if sender != self.hs_map:
+ ...
def draw_modal(self):
super().draw_modal()
pygame.draw.rect(self.surf, "black", rect)
pygame.draw.rect(self.surf, "gray", rect, 1)
- def set_rgb(self, value, dest, sender_type):
- slider, spinner = {
- "r": (self.red_slider, self.red_input),
- "g": (self.green_slider, self.green_input),
- "b": (self.blue_slider, self.blue_input),
- }[dest]
- if sender_type is Slider:
- value = value * 255 // slider.extent
- elif sender_type is Spinner:
- pass
- else:
- raise KeyError(sender_type)
- setattr(self.parent.draw_image.color, dest, max(min(value, 255), 0))
- self.update_controls()
- self.dirty = True
+ RGB_LETTERS = ("r", "g", "b")
+ HSV_LETTERS = ("h", "s", "v")
- def set_hsv(self, value, dest, sender_type):
- slider, spinner = (
- (self.hue_slider, self.hue_input),
- (self.saturation_slider, self.saturation_input),
- (self.value_slider, self.value_input),
- )[dest]
- if sender_type is Slider:
- limited_value = max(min(value, slider.extent), 0)
- if limited_value != value:
- value = limited_value
- slider.value = value
- value = value * 255 // slider.extent
- spinner.value = int(value)
- elif sender_type is Spinner:
- limited_value = max(min(int(value), 255), 0)
- if limited_value != value:
- value = limited_value
- spinner.value = value
- slider.value = value * slider.extent // 255
+ def set_channel(self, value, dest, sender):
+ if isinstance(sender, Slider):
+ value = max(min(value, sender.extent), 0)
+ if getattr(sender, "value", value) != value:
+ sender.value = value
+ value = value * 255 // sender.extent
else:
- raise KeyError(sender_type)
- self.parent.draw_image.color = self.hsv_to_color(
- (
- value if dest == 0 else self.hue_input.value,
- value if dest == 1 else self.saturation_input.value,
- value if dest == 2 else self.value_input.value,
- )
- )
- self.update_controls(True)
+ value = max(min(value, 255), 0)
+ if getattr(sender, "value", value) != value:
+ sender.value = value
+ kwargs = {}
+ if dest in self.RGB_LETTERS:
+ color = pygame.Color(self.parent.draw_image.color)
+ if getattr(color, dest) == value:
+ return
+ setattr(color, dest, value)
+ kwargs["color"] = color
+ elif dest in self.HSV_LETTERS:
+ hsv_controls = [
+ next(
+ d for d in self.channel_slots[c] if not isinstance(d, Slider)
+ )
+ for c in self.HSV_LETTERS
+ ]
+ index = self.HSV_LETTERS.index(dest)
+ kwargs["hsv"] = [d.value for d in hsv_controls]
+ if sender != hsv_controls[index]:
+ if kwargs["hsv"][index] == value:
+ return
+ kwargs["hsv"][index] = value
+ self.update_controls(sender=sender, **kwargs)
self.dirty = True
- def set_color(self, value):
- try:
- color = pygame.Color(value)
- except:
- color = pygame.Color(f"0x{value}")
+ def set_color(self, color_like):
+ if isinstance(color_like, pygame.Color):
+ color = color_like
+ else:
+ try:
+ color = pygame.Color(color_like)
+ except:
+ color = pygame.Color(f"0x{color_like}")
self.parent.draw_image.color = color
self.update_controls()
self.dirty = True