from .color_chooser import ColorChooser
from .draw_image import InputMethod, StrokeMethod
+from .utils import color_to_hex
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)),
-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):
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:
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(
def __init__(self, parent):
super().__init__(parent)
- # self.rect = rect
size = self.surf.get_size()
rect = pygame.Rect(
(448, 192),
)
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}}
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)
--- /dev/null
+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)
--- /dev/null
+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()
--- /dev/null
+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)
--- /dev/null
+from ui import Slider
+
+
+class ValueSlider(Slider):
+ pass
--- /dev/null
+#!/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()
--- /dev/null
+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()
--- /dev/null
+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)
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)