]> git.mar77i.info Git - zenbook_conf/commitdiff
vectors stroking: use less trig. add StrokeSquareLines
authormar77i <mar77i@protonmail.ch>
Fri, 20 Dec 2024 00:44:34 +0000 (01:44 +0100)
committermar77i <mar77i@protonmail.ch>
Fri, 20 Dec 2024 00:44:34 +0000 (01:44 +0100)
vectors.py

index c157f79f0aaaa1c253382ec6f9296c49050083ff..9b10c529f209bb3ece06d84b61cb198420945672 100755 (executable)
@@ -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