--- /dev/null
+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),
+ )
--- /dev/null
+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),
+ ]
+)