]> git.mar77i.info Git - zenbook_gui/commitdiff
yuuge update. new game, fixed slider, slow progress on bookpaint
authormar77i <mar77i@protonmail.ch>
Sat, 15 Mar 2025 01:07:33 +0000 (02:07 +0100)
committermar77i <mar77i@protonmail.ch>
Sat, 15 Mar 2025 01:07:33 +0000 (02:07 +0100)
bookpaint/bookpaint_menu.py
bookpaint/color_chooser.py
bookpaint/color_circle.py [new file with mode: 0644]
bookpaint/color_plane.py [new file with mode: 0644]
bookpaint/utils.py [new file with mode: 0644]
bookpaint/value_slider.py [new file with mode: 0644]
rps.py [new file with mode: 0755]
rps/__init__.py [new file with mode: 0644]
rps/rps.py [new file with mode: 0644]
rps/rps_button.py [new file with mode: 0644]
ui/slider.py

index 8dd9154d805071901a136480749823a8c136361a..44a364ca1177263c5eae4ae634d45fcf9e7bee9c 100644 (file)
@@ -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)),
index 0887fc5823443d3d0a5cac772c92b44cb29062f6..a0ce42af4a11102b5a87659bd32591adb050c142 100644 (file)
@@ -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 (file)
index 0000000..45334b9
--- /dev/null
@@ -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 (file)
index 0000000..84a26a4
--- /dev/null
@@ -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 (file)
index 0000000..bd94bc7
--- /dev/null
@@ -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 (file)
index 0000000..c19d71e
--- /dev/null
@@ -0,0 +1,5 @@
+from ui import Slider
+
+
+class ValueSlider(Slider):
+    pass
diff --git a/rps.py b/rps.py
new file mode 100755 (executable)
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 (file)
index 0000000..e69de29
diff --git a/rps/rps.py b/rps/rps.py
new file mode 100644 (file)
index 0000000..34edc10
--- /dev/null
@@ -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 (file)
index 0000000..e81960c
--- /dev/null
@@ -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)
index 3ca12f90fb42adf30e38906740ff8c8c6450860f..f45145b0bb62bf12eb58da4aaa38f1b3ff591607 100644 (file)
@@ -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)