#!/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
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):
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)
],
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):
]
)
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