From: mar77i Date: Fri, 20 Dec 2024 00:44:34 +0000 (+0100) Subject: vectors stroking: use less trig. add StrokeSquareLines X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=af32946f15f8b9354f2c9d036bf3161d4adcd7f2;p=zenbook_conf vectors stroking: use less trig. add StrokeSquareLines --- diff --git a/vectors.py b/vectors.py index c157f79..9b10c52 100755 --- a/vectors.py +++ b/vectors.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from itertools import chain -from math import atan2, ceil, cos, floor, pi, sin +from math import atan2, ceil, cos, floor, pi, sin, sqrt import pygame @@ -61,38 +61,89 @@ class Circle(Shape): pygame.draw.ellipse(surf, color, self.fit(pos, unit)) -class StrokeLine(Polygon): - num_vertices = 32 - +class StrokeSquareLine(Polygon): + """ + Rotate a rectangle along the direction in which we're drawing. + """ def __init__(self, start, end, width): - super().__init__( - list(self.make_line_shape(start, end, self.num_vertices, width / 2)) + super().__init__(list(self.make_line_shape(start, end, width / 2))) + + def make_line_shape(self, p1, p2, shape_r): + p_rel = (p1[0] - p2[0], p1[1] - p2[1]) + distance = sqrt(p_rel[0] * p_rel[0] + p_rel[1] * p_rel[1]) + if distance: + back = (p_rel[0] * shape_r / distance, p_rel[1] * shape_r / distance) + back_sum, back_diff = back[0] + back[1], back[0] - back[1] + else: + back_sum, back_diff = -shape_r, shape_r + # left is aka: (back[1], -back[0]) + # right is aka: (-back[1], back[0]) + yield from ( + (p1[0] + back_sum, p1[1] - back_diff), + (p1[0] + back_diff, p1[1] + back_sum), + (p2[0] - back_sum, p2[1] + back_diff), + (p2[0] - back_diff, p2[1] - back_sum), ) - @staticmethod - def make_line_shape(p1, p2, n, shape_r): - angle = atan2(p1[1] - p2[1], p1[0] - p2[0]) + +class StrokeRoundLine(StrokeSquareLine): + """ + 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, start, end, width, num_vertices): + self.num_vertices = num_vertices + super().__init__(start, end, width) + + def make_line_shape(self, p1, p2, shape_r): + p_rel = (p1[0] - p2[0], p1[1] - p2[1]) + distance = sqrt(p_rel[0] * p_rel[0] + p_rel[1] * p_rel[1]) + if distance == 0: + yield from ( + (p1[0] + cos(a) * shape_r, p1[1] + sin(a) * shape_r) + for a in ( + (i) * tau / self.num_vertices + for i in range(self.num_vertices) + ) + ) + return + angle = atan2(p_rel[1], p_rel[0]) if angle < 0: angle += tau # 0 <= angle < tau - initial_corner = ceil((angle + tau * 3 / 4) * n / tau) % n - opposing_corner = (floor((angle + tau / 4) * n / tau) - initial_corner) % n + initial_corner = ceil( + atan2(-p_rel[0], p_rel[1]) * self.num_vertices / tau + ) % self.num_vertices + opposing_corner = ( + floor(atan2(p_rel[0], -p_rel[1]) * self.num_vertices / tau) - initial_corner + ) % self.num_vertices shape = [ (cos(a) * shape_r, sin(a) * shape_r) - for a in ((i + initial_corner) * tau / n for i in range(n)) + for a in ( + (i + initial_corner) * tau / self.num_vertices + for i in range(self.num_vertices) + ) ] - left = (cos(angle + tau * 3 / 4) * shape_r, sin(angle + tau * 3 / 4) * shape_r) - right = (cos(angle + tau / 4) * shape_r, sin(angle + tau / 4) * shape_r) - yield (p2[0] + left[0], p2[1] + left[1]) - yield (p1[0] + left[0], p1[1] + left[1]) + if distance == 0: + for vertex in shape: + yield (p1[0] + vertex[0], p1[1] + vertex[1]) + return + # left: (cos(angle + tau * 3 / 4) * shape_r, sin(angle + tau * 3 / 4) * shape_r) + # right: (cos(angle + tau / 4) * shape_r, sin(angle + tau / 4) * shape_r) + # back = (cos(angle) * shape_r, sin(angle) * shape_r) + # no need for trig here, though, because we only go ±90°: + back = (p_rel[0] * shape_r / distance, p_rel[1] * shape_r / distance) + # left is aka: (back[1], -back[0]) + # right is aka: (-back[1], back[0]) + yield (p2[0] + back[1], p2[1] - back[0]) + yield (p1[0] + back[1], p1[1] - back[0]) p = p1 - for i in range(n): - s = shape[i] - yield (p[0] + s[0], p[1] + s[1]) - if i == opposing_corner and p == p1 and p1 != p2: - yield (p[0] + right[0], p[1] + right[1]) + for i, vertex in enumerate(shape): + yield (p[0] + vertex[0], p[1] + vertex[1]) + if i == opposing_corner and p == p1: + yield (p[0] - back[1], p[1] + back[0]) p = p2 - yield (p[0] + right[0], p[1] + right[1]) + yield (p[0] - back[1], p[1] + back[0]) class Shapes(Shape): @@ -105,37 +156,37 @@ class Shapes(Shape): class StrokePath(Shapes): - def __init__(self, points, closed, width): + def __init__(self, points, closed, width, num_vertices): self._points = list(points) self.closed = bool(closed) self.width = width - super().__init__(self.get_shapes()) + self.num_vertices = num_vertices + super().__init__([]) - def get_shapes(self): + @property + def shapes(self): + if not self._points: + return if self.closed: - yield StrokeLine(self._points[-1], self._points[0], self.width) + yield StrokeRoundLine( + self._points[-1], + self._points[0], + self.width, + self.num_vertices, + ) iter_points = iter(self._points) old = next(iter_points) for new in iter_points: - yield StrokeLine(old, new, self.width) + yield StrokeRoundLine(old, new, self.width, self.num_vertices) old = new - @property - def points(self): - return self._points[:] - - @points.setter - def points(self, points): - self._points.clear() - self._points.extend(points) - self._points = points - self.shapes.clear() - self.shapes.extend(self.get_shapes()) + @shapes.setter + def shapes(self, value): + pass class StrokeCircle(StrokePath): - def __init__(self, center, radius, width): - num_vertices = StrokeLine.num_vertices + def __init__(self, center, radius, width, num_vertices): super().__init__( [ (center[0] + cos(a) * radius, center[1] + sin(a) * radius) @@ -143,23 +194,23 @@ class StrokeCircle(StrokePath): ], True, width, + num_vertices, ) class StrokeCircleSegment(StrokePath): - def __init__(self, center, radius, start_angle, end_angle, width): + def __init__(self, center, radius, start_angle, end_angle, width, num_vertices): super().__init__( [ (center[0] + cos(a) * radius, center[1] + sin(a) * radius) - for a in self.get_angle_segments(start_angle, end_angle) + for a in self.get_angle_segments(start_angle, end_angle, num_vertices) ], False, width, + num_vertices, ) - @classmethod - def get_angle_segments(cls, start_angle, end_angle): - num_vertices = StrokeLine.num_vertices + 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): @@ -201,101 +252,114 @@ laptop_vertical = Shapes( ] ) FINGER_RADIUS = 1.25 +NUM_VERTICES = 32 touchscreen = Shapes( [ - StrokeCircleSegment((12, 16), 5, 0, tau * 3 / 8, 1), - StrokeLine( + StrokeCircleSegment((12, 16), 5, 0, tau * 3 / 8, 1, NUM_VERTICES), + 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, + NUM_VERTICES, ), - StrokeCircleSegment((4, 13), FINGER_RADIUS, tau * 3 / 8, tau * 7 / 8, 1), - StrokeLine( + StrokeCircleSegment((4, 13), FINGER_RADIUS, tau * 3 / 8, tau * 7 / 8, 1, NUM_VERTICES), + StrokeRoundLine( ( 4 + cos(tau * 7 / 8) * FINGER_RADIUS, 13 + sin(tau * 7 / 8) * FINGER_RADIUS, ), (7, 13.5), 1, + NUM_VERTICES, + ), + StrokeRoundLine((7, 13.5), (7, 6), 1, NUM_VERTICES), + StrokeCircleSegment((8.25, 6), FINGER_RADIUS, pi, tau, 1, NUM_VERTICES), + StrokeRoundLine((9.5, 6), (9.5, 11), 1, NUM_VERTICES), + StrokeCircleSegment( + (11, 11.5), FINGER_RADIUS, tau * 5 / 8, tau * 7 / 8, 1, NUM_VERTICES + ), + StrokeCircleSegment( + (13.25, 12), FINGER_RADIUS, tau * 5 / 8, tau * 7 / 8, 1, NUM_VERTICES ), - StrokeLine((7, 13.5), (7, 6), 1), - StrokeCircleSegment((8.25, 6), FINGER_RADIUS, pi, tau, 1), - StrokeLine((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), - StrokeLine((16.75, 12.5), (17, 16), 1), - StrokeCircleSegment((8.25, 6), 3, pi, tau, 1), - StrokeCircleSegment((8.25, 6), 5, pi, tau, 1), + StrokeCircleSegment( + (15.5, 12.5), FINGER_RADIUS, tau * 5 / 8, tau, 1, NUM_VERTICES + ), + StrokeRoundLine((16.75, 12.5), (17, 16), 1, NUM_VERTICES), + StrokeCircleSegment((8.25, 6), 3, pi, tau, 1, NUM_VERTICES), + StrokeCircleSegment((8.25, 6), 5, pi, tau, 1, NUM_VERTICES), ] ) stylus = Shapes( [ - StrokeCircleSegment((3, 3), 1.5, tau * 3 / 8, tau * 7 / 8, 1), - StrokeLine( + StrokeCircleSegment((3, 3), 1.5, tau * 3 / 8, tau * 7 / 8, 1, NUM_VERTICES), + 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, + NUM_VERTICES, ), - StrokeLine( + 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, + NUM_VERTICES, ), - StrokeLine( + StrokeRoundLine( (16 + cos(tau * 3 / 8) * 1.5, 16 + sin(tau * 3 / 8) * 1.5), (18, 18), 1, + NUM_VERTICES, ), - StrokeLine( + StrokeRoundLine( (16 + cos(tau * 7 / 8) * 1.5, 16 + sin(tau * 7 / 8) * 1.5), (18, 18), 1, + NUM_VERTICES, ), - StrokeLine((11, 11), (12, 12), 1), + StrokeRoundLine((11, 11), (12, 12), 1, NUM_VERTICES), ] ) bluetooth = Shapes( [ - StrokeLine((10, 2), (15, 6), 1.5), - StrokeLine((15, 6), (5, 14), 1.5), - StrokeLine((10, 2), (10, 19), 1.5), - StrokeLine((10, 19), (15, 15), 1.5), - StrokeLine((15, 15), (5, 7), 1.5), + 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), ] ) -shapes = [ - { - "shape": laptop_single, - "pos": (100, 100), - }, - { - "shape": laptop_double, - "pos": (600, 100), - }, - { - "shape": laptop_vertical, - "pos": (1100, 100), - }, - { - "shape": touchscreen, - "pos": (100, 600), - }, - { - "shape": stylus, - "pos": (600, 600), - }, - { - "shape": bluetooth, - "pos": (1100, 600), - }, -] def main(): + shapes = [ + { + "shape": laptop_single, + "pos": (100, 100), + }, + { + "shape": laptop_double, + "pos": (600, 100), + }, + { + "shape": laptop_vertical, + "pos": (1100, 100), + }, + { + "shape": touchscreen, + "pos": (100, 600), + }, + { + "shape": stylus, + "pos": (600, 600), + }, + { + "shape": bluetooth, + "pos": (1100, 600), + }, + ] surf = pygame.display.set_mode((2000, 1600)) running = True dirty = False