From: mar77i Date: Tue, 21 Jan 2025 17:02:12 +0000 (+0100) Subject: import vectors, shapes X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=3c0de253741278f22d73d535127fa0f802ebf54c;p=zenbook_gui import vectors, shapes --- diff --git a/vectors/__init__.py b/vectors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vectors/vectors.py b/vectors/vectors.py new file mode 100644 index 0000000..b3bf98f --- /dev/null +++ b/vectors/vectors.py @@ -0,0 +1,305 @@ +from itertools import chain +from math import acos, atan2, ceil, cos, floor, pi, sin, sqrt, tau + +import pygame + + +class Shape: + def fit(self, pos, unit): + raise NotImplementedError + + def draw(self, surf, color): + pass + + +class Rect(Shape): + def __init__(self, *args): + if len(args) == 2: + args = tuple(chain.from_iterable(args)) + self.left, self.top, self.width, self.height = args + + def fit(self, pos, unit): + klass = type(self) + if klass not in (Rect, Ellipse): + raise NotImplementedError + return klass( + (round(pos[0] + self.left * unit[0]), round(pos[1] + self.top * unit[1])), + (round(self.width * unit[0]), round(self.height * unit[1])), + ) + + def draw(self, surf, color): + pygame.draw.rect( + surf, color, pygame.Rect(self.left, self.top, self.width, self.height) + ) + + +class Ellipse(Rect): + def draw(self, surf, color): + pygame.draw.ellipse( + surf, color, pygame.Rect(self.left, self.top, self.width, self.height) + ) + + +class Polygon(Shape): + def __init__(self, points): + self.points = list(points) + + def fit(self, pos, unit): + if type(self) != Polygon: + raise NotImplementedError + return Polygon( + [ + (round(pos[0] + point[0] * unit[0]), round(pos[1] + point[1] * unit[1])) + for point in self.points + ] + ) + + def draw(self, surf, color): + pygame.draw.polygon(surf, color, self.points) + + +class Circle(Shape): + def __init__(self, center, radius): + self.center = center + self.radius = radius + + def fit(self, pos, unit): + if type(self) != Circle: + raise NotImplementedError + if unit[0] == unit[1]: + return Circle( + ( + pos[0] + self.center[0] * unit[0], + pos[1] + self.center[1] * unit[1], + ), + self.radius * unit[0], + ) + return Ellipse( + ( + round(pos[0] + (self.center[0] - self.radius) * unit[0]), + round(pos[1] + (self.center[1] - self.radius) * unit[1]), + ), + (round(self.radius * 2 * unit[0]), round(self.radius * 2 * unit[1])), + ) + + def draw(self, surf, color): + pygame.draw.circle(surf, color, self.center, self.radius) + + +class StrokeSquareLine(Polygon): + """ + Rotate a rectangle along the direction in which we're drawing. + """ + def __init__(self, p1, p2, width): + self.p1 = p1 + self.p2 = p2 + self.width = width + super().__init__(self.get_points()) + + def get_points(self): + p_rel = (self.p1[0] - self.p2[0], self.p1[1] - self.p2[1]) + twice_distance = 2 * sqrt(p_rel[0] * p_rel[0] + p_rel[1] * p_rel[1]) + if twice_distance: + back = ( + p_rel[0] * self.width / twice_distance, + p_rel[1] * self.width / twice_distance, + ) + back_sum, back_diff = back[0] + back[1], back[0] - back[1] + else: + back_sum, back_diff = -self.width / 2, self.width / 2 + # left is aka: (back[1], -back[0]) + # right is aka: (-back[1], back[0]) + return [ + (self.p1[0] + back_sum, self.p1[1] - back_diff), + (self.p1[0] + back_diff, self.p1[1] + back_sum), + (self.p2[0] - back_sum, self.p2[1] + back_diff), + (self.p2[0] - back_diff, self.p2[1] - back_sum), + ] + + def fit(self, pos, unit): + if type(self) != StrokeSquareLine: + raise NotImplementedError + return StrokeSquareLine( + (pos[0] + self.p1[0] * unit[0], pos[1] + self.p1[1] * unit[1]), + (pos[0] + self.p2[0] * unit[0], pos[1] + self.p2[1] * unit[1]), + self.width * max(unit), + ) + + +class StrokeRoundLine(Polygon): + """ + Enclose a rectangle fitted between two points with a regular, non-rotated n-gon + that should be visually indistinguishable with a half circle + """ + def __init__(self, p1, p2, width): + self.p1 = p1 + self.p2 = p2 + self.width = width + super().__init__(self.get_points()) + + def get_points(self): + radius = self.width / 2 + num_vertices = self.corners_needed_to_appear_round(radius) + delta = (self.p1[0] - self.p2[0], self.p1[1] - self.p2[1]) + distance = sqrt(delta[0] * delta[0] + delta[1] * delta[1]) + points = [] + if distance == 0: + initial_corner = opposing_corner = num_vertices + back = (0, 0) + else: + initial_corner = ceil( + atan2(-delta[0], delta[1]) * num_vertices / tau + ) + opposing_corner = ( + floor(atan2(delta[0], -delta[1]) * num_vertices / tau) - initial_corner + ) % num_vertices + back = (delta[0] * radius / distance, delta[1] * radius / distance) + points.extend( + ( + (self.p2[0] + back[1], self.p2[1] - back[0]), + (self.p1[0] + back[1], self.p1[1] - back[0]), + ) + ) + p = self.p1 + for i in range(num_vertices): + angle = (i + initial_corner) * tau / num_vertices + points.append((p[0] + cos(angle) * radius, p[1] + sin(angle) * radius)) + if i == opposing_corner: + points.extend( + ( + (self.p1[0] - back[1], self.p1[1] + back[0]), + (self.p2[0] - back[1], self.p2[1] + back[0]), + ) + ) + p = self.p2 + return points + + @staticmethod + def corners_needed_to_appear_round(radius, threshold=.5): + return ceil(pi / acos(1 - threshold / radius)) + + def fit(self, pos, unit): + if type(self) != StrokeRoundLine: + raise NotImplementedError + return StrokeRoundLine( + (pos[0] + self.p1[0] * unit[0], pos[1] + self.p1[1] * unit[1]), + (pos[0] + self.p2[0] * unit[0], pos[1] + self.p2[1] * unit[1]), + self.width * max(unit), + ) + + +class Shapes(Shape): + def __init__(self, shapes): + self.shapes = list(shapes) + + def fit(self, pos, unit): + if type(self) != Shapes: + raise NotImplementedError + return Shapes([s.fit(pos, unit) for s in self.shapes]) + + def draw(self, surf, color): + for shape in self.shapes: + shape.draw(surf, color) + + +class StrokePath: + def __init__(self, points, closed, width): + self.points = list(points) + self.closed = bool(closed) + self.width = width + + @property + def shapes(self): + if not self.points: + return + if self.closed: + yield StrokeRoundLine( + self.points[-1], + self.points[0], + self.width, + ) + iter_points = iter(self.points) + old = next(iter_points) + for new in iter_points: + yield StrokeRoundLine(old, new, self.width) + old = new + + def fit(self, pos, unit): + if type(self) != StrokePath: + raise NotImplementedError + return StrokePath( + [ + (pos[0] + point[0] * unit[0], pos[1] + point[1] * unit[1]) + for point in self.points + ], + self.closed, + self.width, + ) + + draw = Shapes.draw + + +class StrokeCircle(StrokePath): + def __init__(self, center, radius, width): + self.center = center + self.radius = radius + super().__init__([], True, width) + self.points.extend(self.get_points()) + + def get_points(self): + num_vertices = StrokeRoundLine.corners_needed_to_appear_round(self.radius) + return [ + ( + self.center[0] + cos(a) * self.radius, + self.center[1] + sin(a) * self.radius, + ) + for a in (tau * i / num_vertices for i in range(num_vertices)) + ] + + def fit(self, pos, unit): + if type(self) != StrokeCircle: + raise NotImplementedError + return StrokeCircle( + (pos[0] + self.center[0] * unit[0], pos[1] + self.center[1] * unit[1]), + self.radius * max(unit), + self.width * max(unit), + ) + + +class StrokeCircleSegment(StrokeCircle): + def __init__(self, center, radius, start_angle, end_angle, width): + self.start_angle = start_angle + self.end_angle = end_angle + super().__init__(center, radius, width) + self.closed = False + + def get_points(self): + return [ + (self.center[0] + cos(a) * self.radius, self.center[1] + sin(a) * self.radius) + for a in self.get_angle_segments( + self.start_angle, + self.end_angle, + StrokeRoundLine.corners_needed_to_appear_round((self.radius + self.width / 2)), + ) + ] + + def get_angle_segments(self, start_angle, end_angle, num_vertices): + start_angle, end_angle = sorted((start_angle, end_angle)) + diff_angle = end_angle - start_angle + for i in range(num_vertices): + a = tau * i / num_vertices + if a >= diff_angle: + yield end_angle + break + yield start_angle + a + + def fit(self, pos, unit): + if type(self) != StrokeCircleSegment: + raise NotImplementedError + return StrokeCircleSegment( + (pos[0] + self.center[0] * unit[0], pos[1] + self.center[1] * unit[1]), + self.radius * max(unit), + self.start_angle, + self.end_angle, + self.width * max(unit), + ) diff --git a/zenbook_conf/shapes.py b/zenbook_conf/shapes.py new file mode 100644 index 0000000..4d7be4b --- /dev/null +++ b/zenbook_conf/shapes.py @@ -0,0 +1,112 @@ +from math import cos, pi, sin, tau + +from vectors import ( + Polygon, Rect, Shapes, StrokeCircleSegment, StrokeRoundLine, StrokeSquareLine +) + + +laptop_single = Shapes( + [ + Rect((2, 3), (16.4, 1)), + Rect((2, 4), (1, 8.1)), + Rect((17.4, 4), (1, 8.1)), + Rect((2, 12.1), (16.4, 1)), + Polygon([(2, 13.1), (18.4, 13.1), (22.4, 18.1), (6, 18.1)]), + ] +) +laptop_double = Shapes( + [ + Rect((2, 3), (16.4, 1)), + Rect((2, 4), (1, 8.1)), + Rect((17.4, 4), (1, 8.1)), + Rect((2, 12.1), (16.4, 1)), + Polygon([(2, 13.1), (3, 13.1), (6.2, 17.1), (5.2, 17.1)]), + Polygon([(17.4, 13.1), (18.4, 13.1), (21.6, 17.1), (20.4, 17.1)]), + Polygon([(5.2, 17.1), (21.6, 17.1), (22.4, 18.1), (6, 18.1)]), + ] +) +laptop_vertical = Shapes( + [ + Rect((0.5, 1), (17.75, 1)), + Rect((0.5, 2), (1, 14.4)), + Rect((9, 2), (1, 14.4)), + Rect((17.25, 2), (1, 14.4)), + Rect((0.5, 16.4), (17.75, 1)), + Polygon([(2, 17.4), (6, 22.4), (20.75, 22.4), (16.75, 17.4)]), + ] +) +FINGER_RADIUS = 1.25 +touchscreen = Shapes( + [ + StrokeCircleSegment((12, 16), 5, 0, tau * 3 / 8, 1), + StrokeRoundLine( + (12 + cos(tau * 3 / 8) * 5, 16 + sin(tau * 3 / 8) * 5), + ( + 4 + cos(tau * 3 / 8) * FINGER_RADIUS, + 13 + sin(tau * 3 / 8) * FINGER_RADIUS, + ), + 1, + ), + StrokeCircleSegment( + (4, 13), FINGER_RADIUS, tau * 3 / 8, tau * 7 / 8, 1, + ), + StrokeRoundLine( + ( + 4 + cos(tau * 7 / 8) * FINGER_RADIUS, + 13 + sin(tau * 7 / 8) * FINGER_RADIUS, + ), + (7, 13.5), + 1, + ), + StrokeRoundLine((7, 13.5), (7, 6), 1), + StrokeCircleSegment((8.25, 6), FINGER_RADIUS, pi, tau, 1), + StrokeRoundLine((9.5, 6), (9.5, 11), 1), + StrokeCircleSegment( + (11, 11.5), FINGER_RADIUS, tau * 5 / 8, tau * 7 / 8, 1, + ), + StrokeCircleSegment( + (13.25, 12), FINGER_RADIUS, tau * 5 / 8, tau * 7 / 8, 1, + ), + StrokeCircleSegment( + (15.5, 12.5), FINGER_RADIUS, tau * 5 / 8, tau, 1, + ), + StrokeRoundLine((16.75, 12.5), (17, 16), 1), + StrokeCircleSegment((8.25, 6), 3, pi, tau, 1), + StrokeCircleSegment((8.25, 6), 5, pi, tau, 1), + ] +) +stylus = Shapes( + [ + StrokeCircleSegment((3, 3), 1.5, tau * 3 / 8, tau * 7 / 8, 1), + StrokeRoundLine( + (3 + cos(tau * 3 / 8) * 1.5, 3 + sin(tau * 3 / 8) * 1.5), + (16 + cos(tau * 3 / 8) * 1.5, 16 + sin(tau * 3 / 8) * 1.5), + 1, + ), + StrokeRoundLine( + (3 + cos(tau * 7 / 8) * 1.5, 3 + sin(tau * 7 / 8) * 1.5), + (16 + cos(tau * 7 / 8) * 1.5, 16 + sin(tau * 7 / 8) * 1.5), + 1, + ), + StrokeRoundLine( + (16 + cos(tau * 3 / 8) * 1.5, 16 + sin(tau * 3 / 8) * 1.5), + (18, 18), + 1, + ), + StrokeRoundLine( + (16 + cos(tau * 7 / 8) * 1.5, 16 + sin(tau * 7 / 8) * 1.5), + (18, 18), + 1, + ), + StrokeRoundLine((11, 11), (12, 12), 1), + ] +) +bluetooth = Shapes( + [ + StrokeSquareLine((10, 2), (15, 6), 1.5), + StrokeSquareLine((15, 6), (5, 14), 1.5), + StrokeSquareLine((10, 2.75), (10, 18.25), 1.5), + StrokeSquareLine((10, 19), (15, 15), 1.5), + StrokeSquareLine((15, 15), (5, 7), 1.5), + ] +)