From faacaba610cb5d575b0c4a03059c4a772f15198c Mon Sep 17 00:00:00 2001 From: mar77i Date: Sat, 15 Mar 2025 02:07:33 +0100 Subject: [PATCH] yuuge update. new game, fixed slider, slow progress on bookpaint --- bookpaint/bookpaint_menu.py | 6 + bookpaint/color_chooser.py | 175 +++++++++++++++++++++++++++--- bookpaint/color_circle.py | 169 +++++++++++++++++++++++++++++ bookpaint/color_plane.py | 72 ++++++++++++ bookpaint/utils.py | 27 +++++ bookpaint/value_slider.py | 5 + rps.py | 16 +++ rps/__init__.py | 0 rps/rps.py | 146 +++++++++++++++++++++++++ rps/rps_button.py | 107 ++++++++++++++++++ ui/slider.py | 211 ++++++++++++++++++++++++++---------- 11 files changed, 861 insertions(+), 73 deletions(-) create mode 100644 bookpaint/color_circle.py create mode 100644 bookpaint/color_plane.py create mode 100644 bookpaint/utils.py create mode 100644 bookpaint/value_slider.py create mode 100755 rps.py create mode 100644 rps/__init__.py create mode 100644 rps/rps.py create mode 100644 rps/rps_button.py diff --git a/bookpaint/bookpaint_menu.py b/bookpaint/bookpaint_menu.py index 8dd9154..44a364c 100644 --- a/bookpaint/bookpaint_menu.py +++ b/bookpaint/bookpaint_menu.py @@ -19,6 +19,7 @@ from ui import ( from .color_chooser import ColorChooser from .draw_image import InputMethod, StrokeMethod +from .utils import color_to_hex class BookPaintMenu(Modal): @@ -118,6 +119,11 @@ class BookPaintMenu(Modal): self.draw_image.foreground_color, partial(color_chooser.activate_for, "foreground_color"), ) + self.foreground_color_value = CenterLabel( + self, + grid_layout.get_rect((2, 2)), + color_to_hex(self.draw_image.foreground_color), + ) self.width_slider = Slider( self, grid_layout.get_rect((1, 3)).inflate((pad, pad)), diff --git a/bookpaint/color_chooser.py b/bookpaint/color_chooser.py index 0887fc5..a0ce42a 100644 --- a/bookpaint/color_chooser.py +++ b/bookpaint/color_chooser.py @@ -1,15 +1,12 @@ -from colorsys import hsv_to_rgb from functools import partial import pygame -from ui import Modal, Rect +from ui import Button, ColorButton, Label, Modal, Rect, TabBar -from ui import ColorButton - - -def hsv_to_color(hsv): - return pygame.Color(*(int(c * 255) for c in hsv_to_rgb(*(c / 255 for c in hsv)))) +from .color_circle import ColorCircle +from .color_plane import ColorPlane +from .utils import hsv_to_color class ColorChooser(Modal): @@ -26,7 +23,7 @@ class ColorChooser(Modal): for v_range in (range(7, 3, -1), range(3, -1, -1)): for s in range(7, -1, -1): for v in v_range: - for h in range(8): + for h in range(7): color = hsv_to_color((h * 255 / 7, s * 255 / 7, v * 255 / 7)) int_color = (color.r << 16) | (color.g << 8) | color.b if int_color not in colors_seen: @@ -39,12 +36,17 @@ class ColorChooser(Modal): else: yield (pygame.Rect(pos, cell_size), color) x += 1 - pos[0] += cell_size[0] // (2 - bool(x % 8)) - if x < 32: + pos[0] += cell_size[0] + if x % 7 == 0: + pos[0] += cell_size[0] // 2 + if x < 28: continue x, pos[0] = 0, lefttop[0] y += 1 - pos[1] += cell_size[1] // (2 - bool(y % 8)) + if y % 8 == 0: + pos[1] += cell_size[1] // 2 + else: + pos[1] += cell_size[1] for i, gray in enumerate((*grays[1:], grays[0])): yield ( pygame.Rect( @@ -59,7 +61,6 @@ class ColorChooser(Modal): def __init__(self, parent): super().__init__(parent) - # self.rect = rect size = self.surf.get_size() rect = pygame.Rect( (448, 192), @@ -67,9 +68,132 @@ class ColorChooser(Modal): ) Rect(self, rect, "black", "gray34") self.attr = None - self.color = None + self.color = pygame.Color("red") + buttons = [] for r, color in self.get_color_grid((rect.left + 48, rect.top + 192), (64, 64)): - ColorButton(self, r, color, partial(self.set_color, color)) + buttons.append(ColorButton(self, r, color, partial(self.set_color, color))) + self.color_circle = ColorCircle( + self, + pygame.Rect((rect.left + 1024, rect.top + 256), (512, 512)), + self.chooser_set_hsv, + self.color, + ) + self.chooser_button = ColorButton( + self, + pygame.Rect((rect.right - 1024, rect.bottom - 192), (768, 128)), + self.color, + self.deactivate, + ) + #self.hsv_rgb_buttons = ( + # Button( + # self, + # pygame.Rect((rect.left + 1024, rect.top + 160), (96, 96)), + # "H", + # partial(self.set_plane_axis, 0, AxisSetting.HUE), + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1120, rect.top + 160), (96, 96)), + # "S", + # partial(self.set_plane_axis, 0, AxisSetting.SATURATION), + # True, + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1216, rect.top + 160), (96, 96)), + # "V", + # partial(self.set_plane_axis, 0, AxisSetting.VALUE) + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1312, rect.top + 160), (96, 96)), + # "R", + # partial(self.set_plane_axis, 0, AxisSetting.RED) + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1408, rect.top + 160), (96, 96)), + # "G", + # partial(self.set_plane_axis, 0, AxisSetting.GREEN) + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1504, rect.top + 160), (96, 96)), + # "B", + # partial(self.set_plane_axis, 0, AxisSetting.BLUE) + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1792, rect.top + 256), (96, 96)), + # "H", + # partial(self.set_plane_axis, 1, AxisSetting.HUE) + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1792, rect.top + 352), (96, 96)), + # "S", + # partial(self.set_plane_axis, 1, AxisSetting.SATURATION) + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1792, rect.top + 448), (96, 96)), + # "V", + # partial(self.set_plane_axis, 1, AxisSetting.VALUE), + # True, + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1792, rect.top + 544), (96, 96)), + # "R", + # partial(self.set_plane_axis, 1, AxisSetting.RED) + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1792, rect.top + 640), (96, 96)), + # "G", + # partial(self.set_plane_axis, 1, AxisSetting.GREEN) + # ), + # Button( + # self, + # pygame.Rect((rect.left + 1792, rect.top + 736), (96, 96)), + # "B", + # partial(self.set_plane_axis, 1, AxisSetting.BLUE) + # ), + #) + #self.hsv_rgb_slider = Slider( + # self, + # pygame.Rect((rect.left + 928, rect.top + 256), (96, 768)), + # Slider.VERTICAL, + # 0, + # 96, + # self.set_hsv_rgb_slider, + #) + #self.hsv_rgb_slider.value = self.hsv_rgb_slider.extent + #self.hsv_rgb_slider_label = Label( + # self, pygame.Rect((rect.left + 928, rect.top + 160), (96, 96)), "H" + #) + #self.color_plane = ColorPlane( + # self, + # pygame.Rect((rect.left + 1024, rect.top + 256), (768, 768)), + # self.color, + #) + TabBar( + self, + pygame.Rect(rect.topleft, (1024, 128)), + ("Grid", "Chooser"), + ( + buttons, + ( + self.color_circle, + #self.hsv_rgb_slider_label, + #self.hsv_rgb_slider, + #*self.hsv_rgb_buttons, + #self.color_plane, + self.chooser_button, + ), + ), + 1, + ) KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: Modal.deactivate}} @@ -84,3 +208,26 @@ class ColorChooser(Modal): def deactivate(self): super().deactivate() self.parent.set_color(self.attr, self.color) + + def chooser_set_hsv(self, hsv): + self.chooser_button.color = self.color = hsv_to_color(hsv) + self.dirty = True + + def set_plane_axis(self, axis, axis_setting): + if axis == 0: + self.color_plane.set_horizontal(axis_setting) + else: + self.color_plane.set_vertical(axis_setting) + #for button in self.hsv_rgb_buttons: + # if button.callback.args[0] == 0: + # button.highlight = button.callback.args[1] == self.color_plane.horizontal_setting + # else: # button.callback.args[0] == 1 + # button.highlight = button.callback.args[1] == self.color_plane.vertical_setting + + def set_hsv_rgb_slider(self, value): + if value < 0: + value = 0 + elif value > self.hsv_rgb_slider.extent: + value = self.hsv_rgb_slider.extent + self.hsv_rgb_slider.value = value + self.color_plane.set_base_value(value * 255 / self.hsv_rgb_slider.extent) diff --git a/bookpaint/color_circle.py b/bookpaint/color_circle.py new file mode 100644 index 0000000..45334b9 --- /dev/null +++ b/bookpaint/color_circle.py @@ -0,0 +1,169 @@ +from math import atan2, cos, sin, tau +from operator import itemgetter + +import pygame + +from ui import Child + +from .utils import distance, hsv_to_color, color_to_hsv + + +class ColorCircle(Child): + @staticmethod + def get_hue_circle(size): + surf = pygame.Surface(size, pygame.SRCALPHA, 32) + surf.fill(pygame.Color(0, 0, 0, 0)) + center = tuple(x // 2 for x in size) + outer_radius = min(center) + inner_radius = outer_radius * 3 // 4 + inner_radius_squared = inner_radius ** 2 + outer_radius_squared = outer_radius ** 2 + with pygame.PixelArray(surf) as pa: + for x, col in enumerate(pa): + for y in range(len(col)): + pos = (x - center[0]), (y - center[1]) + distance_squared = pos[0] ** 2 + pos[1] ** 2 + if inner_radius_squared <= distance_squared <= outer_radius_squared: + angle = atan2(pos[1], pos[0]) + if angle < 0: + angle += tau + pa[x][y] = hsv_to_color((angle * 255 / tau, 255, 255)) + return inner_radius, surf + + get_alphas = (itemgetter(1), itemgetter(0)) + + @classmethod + def get_overlay_surfs(cls, size): + overlay_surfs = ( + pygame.Surface(size, pygame.SRCALPHA, 32), + pygame.Surface(size, pygame.SRCALPHA, 32), + ) + for surf, get_alpha in zip(overlay_surfs, cls.get_alphas): + 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 - get_alpha((x, y)), + ) + return overlay_surfs + + def __init__(self, parent, rect, callback, color): + super().__init__(parent) + self.rect = rect + min_size = min(rect.size) // 2 + self.inner_radius, self.hue_circle = self.get_hue_circle(rect.size) + self.overlay_surfs = self.get_overlay_surfs((min_size, min_size)) + self.callback = callback + self.hsv = color_to_hsv(color) + self.sv_surf = None + self.pushed = None + + def set_hue(self, rel_pos): + angle = atan2(rel_pos[1], rel_pos[0]) + if angle < 0: + angle += tau + hue = angle * 256 / tau + if hue >= 256: + hue -= 256 + if hue == self.hsv[0]: + return + self.sv_surf = None + self.hsv = (hue, *self.hsv[1:]) + self.callback(self.hsv) + self.dirty = True + + def set_sv(self, rel_pos): + hue_angle = self.hsv[0] * tau / 255 + sv_surf_angle = tau * 9 / 8 - hue_angle + if sv_surf_angle >= tau: + sv_surf_angle -= tau + center = tuple(a // 2 for a in self.overlay_surfs[0].get_size()) + dist = distance(rel_pos, (0, 0)) + angle = atan2(rel_pos[1], rel_pos[0]) + sv_surf_angle + self.hsv = ( + self.hsv[0], + min(max(0, center[0] + cos(angle) * dist), 255), + min(max(0, center[1] + sin(angle) * dist), 255), + ) + self.callback(self.hsv) + self.dirty = True + + def handle_mousebuttondown(self, ev): + if ev.button != 1 or not self.rect.collidepoint(ev.pos): + return + rel_pos = (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery) + self.pushed = distance(rel_pos, (0, 0)) >= self.inner_radius + if self.pushed: + self.set_hue(rel_pos) + else: + self.set_sv(rel_pos) + + def handle_mousemotion(self, ev): + if self.pushed is None: + return + rel_pos = tuple(p - c for p, c in zip(ev.pos, self.rect.center)) + if self.pushed: + self.set_hue(rel_pos) + else: + self.set_sv(rel_pos) + + def handle_mousebuttonup(self, ev): + if self.pushed is None: + return + rel_pos = tuple(p - c for p, c in zip(ev.pos, self.rect.center)) + if self.pushed is True: + self.set_hue(rel_pos) + else: + self.set_sv(rel_pos) + self.pushed = None + + def draw_cursor(self, pos): + pygame.draw.circle(self.surf, "black", pos, 12) + pygame.draw.circle(self.surf, "darkgray", pos, 12, 2) + + def draw_sv_surf(self, hue_angle): + sv_surf_size = self.overlay_surfs[0].get_size() + if self.sv_surf is None: + self.sv_surf = pygame.Surface(sv_surf_size, pygame.SRCALPHA, 32) + self.sv_surf.fill(hsv_to_color((self.hsv[0], 255, 255))) + for overlay_surf in self.overlay_surfs: + self.sv_surf.blit(overlay_surf, (0, 0)) + sv_surf_angle = tau * 9 / 8 - hue_angle + if sv_surf_angle >= tau: + sv_surf_angle -= tau + sv_surf = pygame.transform.rotate(self.sv_surf, sv_surf_angle * 360 / tau) + self.surf.blit( + sv_surf, + tuple( + rc - sc + for rc, sc in zip( + self.rect.center, tuple(a // 2 for a in sv_surf.get_size()) + ) + ) + ) + + center = tuple(a // 2 for a in sv_surf_size) + cursor_distance = distance(self.hsv[1:], center) + cursor_angle = atan2( + self.hsv[2] - center[0], self.hsv[1] - center[1] + ) - sv_surf_angle + while cursor_angle < 0: + cursor_angle += tau + self.draw_cursor( + tuple( + c + f(cursor_angle) * cursor_distance + for c, f in zip(self.rect.center, (cos, sin)) + ) + ) + + def draw(self): + self.surf.blit(self.hue_circle, self.rect.topleft) + hue_angle = self.hsv[0] * tau / 255 + radius = min(x // 2 for x in self.rect.size) * 7 // 8 + self.draw_cursor( + ( + self.rect.centerx + cos(hue_angle) * radius, + self.rect.centery + sin(hue_angle) * radius, + ) + ) + self.draw_sv_surf(hue_angle) diff --git a/bookpaint/color_plane.py b/bookpaint/color_plane.py new file mode 100644 index 0000000..84a26a4 --- /dev/null +++ b/bookpaint/color_plane.py @@ -0,0 +1,72 @@ +from enum import Enum, auto + +import pygame + +from ui import Child +from .utils import color_to_hsv, hsv_to_color + + +class AxisSetting(Enum): + HUE = auto() + SATURATION = auto() + VALUE = auto() + RED = auto() + GREEN = auto() + BLUE = auto() + + +class ColorPlane(Child): + """ + 6 modes at the top R/G/B/H/S/V + 6 modes on the side R/G/B/H/S/V + + Both modes can probably be the same, but they must at all times be + from the same group: R/G/B or H/S/V + + So when you turn on a new group, we can set the other dimension + to the same - or not the same - value. + """ + def get_surface(self): + size = self.rect.size + surf = pygame.Surface(size) + with pygame.PixelArray(surf) as pa: + for x, col in enumerate(pa): + for y in range(size[1]): + col[y] = hsv_to_color( + (int(y * 256 / size[1]), int(x * 256 / size[0]), 255) + ) + return surf + + def __init__(self, parent, rect, color): + super().__init__(parent) + self.rect = rect + self.horizontal_setting = AxisSetting.SATURATION + self.vertical_setting = AxisSetting.VALUE + hsv = color_to_hsv(color) + self.values = hsv[1:] + self.base = hsv[0] + self.surface = self.get_surface() + + def draw(self): + self.surf.blit(self.surface, self.rect.topleft) + + HSV = (AxisSetting.HUE, AxisSetting.SATURATION, AxisSetting.VALUE) + RGB = (AxisSetting.RED, AxisSetting.GREEN, AxisSetting.BLUE) + + def set_horizontal(self, axis_setting): + self.horizontal_setting = axis_setting + group = self.HSV if self.horizontal_setting in self.HSV else self.RGB + if self.vertical_setting not in group: + self.vertical_setting = group[group[0] == self.horizontal_setting] + + def set_vertical(self, axis_setting): + self.vertical_setting = axis_setting + group = self.HSV if self.vertical_setting in self.HSV else self.RGB + if self.horizontal_setting not in group: + self.horizontal_setting = group[group[0] == self.vertical_setting] + + def set_base_value(self, value): + if value == self.base: + return + self.base = value + self.surface = self.get_surface() diff --git a/bookpaint/utils.py b/bookpaint/utils.py new file mode 100644 index 0000000..bd94bc7 --- /dev/null +++ b/bookpaint/utils.py @@ -0,0 +1,27 @@ +from colorsys import hsv_to_rgb, rgb_to_hsv +from math import sqrt + +import pygame + + +def distance(a, b): + return sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) + + +def hsv_to_color(hsv): + return pygame.Color( + *(int(c * 255) for c in hsv_to_rgb(hsv[0] / 256, hsv[1] / 255, hsv[2] / 255)) + ) + + +def color_to_hsv(color): + hsv = rgb_to_hsv(color.r / 255, color.g / 255, color.b / 255) + hue = hsv[0] * 256 + if hue >= 256: + hue -= 256 + return (hue, hsv[1] * 255, hsv[2] * 255) + + +def color_to_hex(color): + assert isinstance(color, pygame.Color) + return str(, 16) diff --git a/bookpaint/value_slider.py b/bookpaint/value_slider.py new file mode 100644 index 0000000..c19d71e --- /dev/null +++ b/bookpaint/value_slider.py @@ -0,0 +1,5 @@ +from ui import Slider + + +class ValueSlider(Slider): + pass diff --git a/rps.py b/rps.py new file mode 100755 index 0000000..a63c65b --- /dev/null +++ b/rps.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import sys +from contextlib import redirect_stdout +from io import StringIO +from pathlib import Path + +sys.path.append(str(Path(__file__).parent)) + +from rps.rps import RockPaperScissors + +with redirect_stdout(StringIO()): + # ruff: noqa: F401 + import pygame # type: ignore + +RockPaperScissors().run() diff --git a/rps/__init__.py b/rps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rps/rps.py b/rps/rps.py new file mode 100644 index 0000000..34edc10 --- /dev/null +++ b/rps/rps.py @@ -0,0 +1,146 @@ +from argparse import ArgumentParser +from functools import partial + +import pygame + +from ui import Button, Root +from .rps_button import RPSButton + + +class FingerButton(Button): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.finger_pushed = {} + + def handle_fingerdown(self, ev): + size = self.surf.get_size() + if self.rect.collidepoint((ev.x * size[0], ev.y * size[1])): + self.finger_pushed[(ev.touch_id, ev.finger_id)] = True + self.dirty = True + + def handle_fingermotion(self, ev): + key = (ev.touch_id, ev.finger_id) + if key not in self.finger_pushed: + return + size = self.surf.get_size() + if not self.rect.collidepoint((ev.x * size[0], ev.y * size[1])): + self.finger_pushed.pop(key) + self.dirty = True + + def handle_fingerup(self, ev): + key = (ev.touch_id, ev.finger_id) + if key not in self.finger_pushed: + return + size = self.surf.get_size() + if self.rect.collidepoint((ev.x * size[0], ev.y * size[1])): + self.callback() + self.finger_pushed.pop(key) + self.dirty = True + + +class RockPaperScissors(Root): + BACKGROUND_COLOR = "black" + LABELS = RPSButton.LABELS + + def __init__(self): + ap = ArgumentParser() + ap.add_argument("--display", type=int, default=0) + args = ap.parse_args() + pygame.init() + num_displays = len(pygame.display.get_desktop_sizes()) + if args.display < -num_displays or args.display >= num_displays: + raise ValueError(f"Invalid display: {args.display}") + if args.display < 0: + args.display += num_displays + super().__init__( + pygame.display.set_mode((0, 0), pygame.FULLSCREEN, display=args.display), + pygame.font.Font(None, size=64), + ) + self.big_font = pygame.font.Font(None, size=256) + self.p1 = 0 + self.p2 = 0 + self.v1 = None + self.v2 = None + size = self.surf.get_size() + self.rps_buttons = ( + RPSButton( + self, + pygame.Rect((0, size[1] // 2), (size[0] // 2, size[1] // 2)).inflate(-256, -256), + 192, + partial(self.finger_push, 0), + ), + RPSButton( + self, + pygame.Rect((size[0] // 2, size[1] // 2), (size[0] // 2, size[1] // 2)).inflate(-256, -256), + 192, + partial(self.finger_push, 1), + ), + ) + self.reset_button = FingerButton( + self, + pygame.Rect( + (size[0] // 2 - 256, size[1] * 3 // 4), (512, 256) + ).inflate(-32, -32), + "Reset", + self.reset, + ) + self.setup_round() + + def setup_round(self): + for button in self.rps_buttons: + button.enabled = True + self.v1 = self.v2 = None + self.reset_button.enabled = False + self.dirty = True + + def draw(self): + super().draw() + size = self.surf.get_size() + center = (size[0] // 2, size[1] // 2) + radius = min(size) // 4 + pygame.draw.circle(self.surf, "yellow", center, radius) + pygame.draw.circle(self.surf, "black", center, radius - 16) + half_radius = radius // 2 + mask = int(self.v1 is not None) | (int(self.v2 is not None) << 1) + fs = self.big_font.render(str(self.p1), True, "yellow") + fs_size = fs.get_size() + self.surf.blit(fs, (center[0] - half_radius - fs_size[0] // 2, center[1] - half_radius - fs_size[1] // 2)) + fs = self.big_font.render(str(self.p2), True, "yellow") + self.surf.blit(fs, (center[0] + half_radius - fs_size[0] // 2, center[1] - half_radius - fs_size[1] // 2)) + if mask & 1: + pygame.draw.circle(self.surf, "cyan", (center[0] - half_radius, center[1] + half_radius), radius // 3) + if mask & (1 << 1): + pygame.draw.circle(self.surf, "cyan", (center[0] + half_radius, center[1] + half_radius), radius // 3) + if mask == 3: + fs = self.font.render(str(self.v1), True, "black") + fs_size = fs.get_size() + self.surf.blit(fs, (center[0] - half_radius - fs_size[0] // 2, center[1] + half_radius - fs_size[1] // 2)) + fs = self.font.render(str(self.v2), True, "black") + fs_size = fs.get_size() + self.surf.blit(fs, (center[0] + half_radius - fs_size[0] // 2, center[1] + half_radius - fs_size[1] // 2)) + + def finger_push(self, player, value): + setattr(self, "v1" if player == 0 else "v2", value) + self.rps_buttons[player].enabled = False + self.dirty = True + if any(button.enabled for button in self.rps_buttons): + return + self.reset_button.enabled = True + if self.v1 == self.LABELS[0]: + if self.v2 == self.LABELS[1]: + self.p2 += 1 + elif self.v2 == self.LABELS[2]: + self.p1 += 1 + elif self.v1 == self.LABELS[1]: + if self.v2 == self.LABELS[0]: + self.p1 += 1 + elif self.v2 == self.LABELS[2]: + self.p2 += 1 + elif self.v1 == self.LABELS[2]: + if self.v2 == self.LABELS[0]: + self.p2 += 1 + elif self.v2 == self.LABELS[1]: + self.p1 += 1 + + def reset(self): + self.setup_round() diff --git a/rps/rps_button.py b/rps/rps_button.py new file mode 100644 index 0000000..e81960c --- /dev/null +++ b/rps/rps_button.py @@ -0,0 +1,107 @@ +from math import atan2, cos, pi, sin, sqrt +from secrets import randbelow + +import pygame + +from ui import Child +from vectors import StrokeCircleSegment + + +def shuffled(iterable, indices): + pool = list(iterable) + result = [] + while len(pool) > 1: + result.append(pool[indices[len(result)]]) + pool.remove(result[-1]) + result.append(pool[0]) + return result + + +class RPSButton(Child): + LABELS = ("Schere", "Stein", "Papier") + + def __init__(self, parent, rect, radius, callback): + super().__init__(parent) + self.rect = rect + self.radius = radius + self.callback = callback + self.pushed = {} + + def handle_fingerdown(self, ev): + size = self.surf.get_size() + pos = (ev.x * size[0], ev.y * size[1]) + if not self.rect.collidepoint(pos): + return + order = randbelow(6) + self.pushed[(ev.touch_id, ev.finger_id)] = { + "pos": (int(pos[0]), int(pos[1])), + "labels": shuffled(self.LABELS, (order // 2, order % 2)), + "flipped": bool(randbelow(2)), + } + self.dirty = True + + def handle_fingermotion(self, ev): + key = (ev.touch_id, ev.finger_id) + if key not in self.pushed: + return + size = self.surf.get_size() + pos = (ev.x * size[0], ev.y * size[1]) + self.pushed[key]["current"] = (int(pos[0]), int(pos[1])) + self.dirty = True + + def handle_fingerup(self, ev): + key = (ev.touch_id, ev.finger_id) + if key not in self.pushed: + return + value = self.pushed.pop(key) + size = self.surf.get_size() + pos = (ev.x * size[0], ev.y * size[1]) + relative = (pos[0] - value["pos"][0], value["pos"][1] - pos[1]) + self.dirty = True + if not sqrt(relative[0] ** 2 + relative[1] ** 2) >= self.radius: + return + angle = atan2(relative[1], relative[0]) + label = None + mod = 2 if value["flipped"] else 0 + if angle <= pi * (3 + mod) / 6 - 0.1 and angle >= pi * (mod - 1) / 6 + 0.1: + label = value["labels"][0] + elif angle <= pi * (mod - 1) / 6 - 0.1 and angle >= pi * (mod - 5) / 6 + 0.1: + label = value["labels"][1] + elif angle <= pi * (mod - 5) / 6 - 0.1 or angle >= pi * (3 + mod) / 6 + 0.1: + label = value["labels"][2] + if label is not None: + self.callback(label) + + def draw(self): + pygame.draw.rect(self.surf, "black", self.rect) + pygame.draw.rect(self.surf, "yellow", self.rect, 1) + for value in self.pushed.values(): + pygame.draw.circle(self.surf, "yellow", value["pos"], self.radius, 1) + mod = 2 if value["flipped"] else 0 + i = -3 + mod + while i < 9: + next_i = i + 4 + StrokeCircleSegment( + value["pos"], + self.radius, + pi * i / 6 + 0.1, + pi * next_i / 6 - 0.1, + 16, + ).draw(self.surf, "darkgray") + i = next_i + rots = (-60, 0, 60, -60)[value["flipped"]:] + for rot, part, label in zip(rots, (1, -3, 5), value["labels"]): + fs = self.font.render(label, True, "white") + if rot != 0: + fs = pygame.transform.rotate(fs, rot) + fs_size = fs.get_size() + angle = pi * (part + mod) / 6 + self.surf.blit( + fs, + ( + value["pos"][0] + cos(angle) * self.radius - fs_size[0] // 2, + value["pos"][1] - sin(angle) * self.radius - fs_size[1] // 2, + ), + ) + if "current" in value: + pygame.draw.circle(self.surf, "red", value["current"], 16) diff --git a/ui/slider.py b/ui/slider.py index 3ca12f9..f45145b 100644 --- a/ui/slider.py +++ b/ui/slider.py @@ -3,94 +3,187 @@ import pygame from .child import Child -class Slider(Child): - HORIZONTAL = 0 - VERTICAL = 1 +class HorizontalSlider(Child): + def _get_extent_base(self): + return self.rect.width - def __init__( - self, parent, rect, direction, value=0, handle_size=None, callback=None - ): + def __init__(self, parent, rect, value=0, handle_size=None, callback=None): super().__init__(parent) self.rect = rect - self.direction = direction - self.extent = self.rect.size[direction] + self.extent = ( + self._get_extent_base() - 1 if handle_size is None else handle_size + ) self.value = value self.handle_size = handle_size - if handle_size is None: - handle_size = 1 - self.extent -= handle_size self.callback = callback - self.pushed = False + self._pushed = None + + def value_from_pos(self, pos): + return pos[0] + + def set_rel_value(self, value): + self.set_value(value - self.rect.left) + + def get_cursor_rect(self): + value, limited = self.map_value() + return pygame.Rect( + (self.rect.left + value, self.rect.top), + (self.handle_size, self.rect.height), + ), limited + + def draw_cursor_line(self): + value, limited = self.map_value() + x = self.rect.left + value + pygame.draw.line( + self.surf, + "dimgray" if limited else "gray", + (x, self.rect.top), + (x, self.rect.bottom), + 8, + ) - def update_value(self, value): - if self.direction == self.HORIZONTAL: - value -= self.rect.left - else: # self.direction == self.VERTICAL - value = self.extent - (value - self.rect.top) + def page_flip(self, cursor_rect, value): + if value < cursor_rect.left: + return max(self.value - cursor_rect.width, 0) + else: + return min(self.value + cursor_rect.width, self.extent) + + def set_value(self, value): + if value == self.value: + return self.value = value if self.callback: self.callback(value) self.dirty = True + @property + def pushed(self): + return self._pushed + + @pushed.setter + def pushed(self, pushed): + if (pushed is None) != (self.pushed is None): + self.dirty = True + self._pushed = pushed + def handle_mousebuttondown(self, ev): if ev.button != 1 or not self.rect.collidepoint(ev.pos): return - self.update_value(ev.pos[self.direction]) - self.pushed = True + value = self.value_from_pos(ev.pos) + if self.handle_size is None: + # small-cursor-grabbing + self.set_rel_value(value) + self.pushed = True + return + cursor_rect = self.get_cursor_rect()[0] + if not cursor_rect.collidepoint(ev.pos): + self.set_value(self.page_flip(cursor_rect, value)) + return + # big-cursor-grabbing + self.pushed = value + + def get_rel(self, value): + return self.pushed - value def handle_mousemotion(self, ev): - if not self.pushed: + if self.pushed is None: return if not ev.buttons[0]: - self.pushed = False - else: - self.update_value(ev.pos[self.direction]) + self.pushed = None + return + value = self.value_from_pos(ev.pos) + if self.handle_size is None: + # small-cursor-grabbing + self.set_rel_value(value) + return + # big-cursor-grabbing + rel = self.get_rel(value) + self.set_value(self.value - rel) + self.pushed = value def handle_mousebuttonup(self, ev): - if ev.button == 1 and self.pushed: - self.update_value(ev.pos[self.direction]) - self.pushed = False + if ev.button == 1 and self.pushed is not None: + value = self.value_from_pos(ev.pos) + if self.handle_size is None: + # small-cursor-grabbing + self.set_rel_value(value) + else: + # big-cursor-grabbing + self.set_value(self.value + value - self.pushed) + self.pushed = None def draw(self): pygame.draw.rect(self.surf, "gray34", self.rect.inflate((8, 8)), 4) pygame.draw.rect(self.surf, "black", self.rect) - ( - self.draw_cursor, self.draw_cursor_line - )[self.handle_size is None](self.surf.subsurface(self.rect)) + (self.draw_cursor, self.draw_cursor_line)[self.handle_size is None]() - def draw_cursor_line(self, subsurf): + def map_value(self): value = self.value - color = "gray" + limited = False if value < 0: - color = "dimgray" - value = -value - if value > self.extent: - value = int(self.extent * (self.extent / value)) - color = "dimgray" - if self.direction == self.HORIZONTAL: - start_pos, end_pos = (value, 0), (value, self.rect.height) - else: # self.direction == self.VERTICAL: - value = self.extent - value - start_pos, end_pos = (0, value), (self.rect.width, value) - pygame.draw.line(subsurf, color, start_pos, end_pos, 8) - - def draw_cursor(self, subsurf): - value = self.value - color = "gray" - if value < 0: - color = "dimgray" value = -value + limited = True if value > self.extent: value = int(self.extent * (self.extent / value)) + limited = True + return value, limited + + def draw_cursor(self): + rect, limited = self.get_cursor_rect() + if self.pushed is not None: + color = "gray34" + elif limited: color = "dimgray" - if self.direction == self.HORIZONTAL: - rect = pygame.Rect( - (value, 0), - (self.handle_size, self.rect.height), - ) - else: # self.direction == self.VERTICAL: - rect = pygame.Rect( - (0, value), - (self.rect.width, self.handle_size), - ) - pygame.draw.rect(subsurf, color, rect) + else: + color = "gray" + pygame.draw.rect(self.surf, color, rect) + + +class VerticalSlider(HorizontalSlider): + def _get_extent_base(self): + return self.rect.height + + def value_from_pos(self, pos): + return pos[1] + + def set_rel_value(self, value): + self.set_value(self.extent - (value - self.rect.top)) + + def get_cursor_rect(self): + value, limited = self.map_value() + return pygame.Rect( + (self.rect.left, self.rect.top + self.extent - value), + (self.rect.width, self.handle_size), + ), limited + + def draw_cursor_line(self): + value, limited = self.map_value() + y = self.rect.top + self.extent - value + pygame.draw.line( + self.surf, + "dimgray" if limited else "gray", + (self.rect.left, y), + (self.rect.right, y), + 8, + ) + + def page_flip(self, cursor_rect, value): + if value < cursor_rect.top: + return min(self.value + cursor_rect.height, self.extent) + else: + return max(self.value - cursor_rect.height, 0) + + def get_rel(self, value): + return value - self.pushed + + +class Slider: + HORIZONTAL = 0 + VERTICAL = 1 + + def __new__(cls, parent, rect, direction, value=0, handle_size=None, callback=None): + if direction == cls.HORIZONTAL: + klass = HorizontalSlider + else: # direction == cls.VERTICAL + klass = VerticalSlider + return klass(parent, rect, value, handle_size, callback) -- 2.51.0