Source code for reiz._visual.complex

"""
Create complex shapes and murals
................................
"""
from reiz._visual.colors import get_color, ColorType
import pyglet
from pathlib import Path
from reiz._visual._primitives import Polygon as _Polygon
from reiz._visual._primitives import Circle as _Circle
from reiz._visual._primitives import Line as _Line
from typing import Tuple, NewType, List

# : Tuple[float, float], x-y coordinates scaled from -1 to 1 with 0 indicating the center of the screen
XY = NewType("XY", Tuple[float, float])

# %%


class Visual:
    "The base class for all visual cues to inherit from"

    def adapt(self, window):
        pass

    def draw(self, canvas=None):
        if canvas is not None and not canvas.window.has_exit:
            self.adapt(canvas)
        try:
            for v in self.visual:
                v.draw()
        except TypeError:
            self.visual.draw()

    def set_color(self, color: str):
        self.color = get_color(color)

    def __iter__(self):
        yield self

    def __next__(self):
        yield self
        raise StopIteration


# Complex parametric visualisations
# ------------------------------------------------------------------------------


[docs]class Background(Visual): """Fills the whole screen with a single color args ---- color:str the color to fill the screen. see also :data:`reiz.visual.colors.COLORS` """ def __init__(self, color="white"): self.color = tuple([int(c * 255) for c in get_color(color)]) def adapt(self, window): img = pyglet.image.SolidColorImagePattern(color=self.color) img = img.create_image(window.width, window.height) self.visual = pyglet.sprite.Sprite(img=img, x=0, y=0, usage="static")
# ------------------------------------------------------------------------------
[docs]class Mural(Visual): """A text on the screen args ---- text:str the text to show font:str the font-type (needs to be installed on the system) fontsize: float the normalized size of the letters position: XY where on the screen the text should be located. Coordinates mark the center of the text color: ColorType the desired color """ def __init__( self, text: str = "Hello World", font="Times New Roman", fontsize: float = 1, position: Tuple[float, float] = (0, 0), color=(1, 1, 1), anchor_x="center", anchor_y="center", ): self.scale = 0.05 * fontsize self.text = text self.font = font self.pos = position self.set_color(color) self.anchor_x = anchor_x self.anchor_y = anchor_y def adapt(self, canvas): color = tuple(int(c * 255) for c in self.color) x0 = canvas.width // 2 y0 = canvas.height // 2 x = (x0 * self.pos[0]) + x0 y = (y0 * self.pos[1]) + y0 fontsize = int(canvas.width * self.scale) self.visual = pyglet.text.Label( self.text, font_name=self.font, font_size=fontsize, color=color, x=x, y=y, anchor_x=self.anchor_x, anchor_y=self.anchor_y, ) def __repr__(self): return f"Mural('{self.text}')"
# ------------------------------------------------------------------------------
[docs]class Line(Visual): """A line from a to b args ---- a: XY x-y coordinates of the start point b: XY x-y coordinates of the end point color: ColorType the desired color linewidth:float """ def __init__( self, a: XY = (0, 0), b: XY = (0, 0), color: ColorType = "white", linewidth: float = 1, ): self.a = a self.b = b self.color = get_color(color) self.linewidth = linewidth def adapt(self, window): x0 = window.width // 2 y0 = window.height // 2 ax = (x0 * self.a[0]) + x0 ay = (y0 * self.a[1]) + y0 bx = (x0 * self.b[0]) + x0 by = (y0 * self.b[1]) + y0 self.visual = _Line( a=(ax, ay), b=(bx, by), z=0, color=self.color, stroke=self.linewidth, )
[docs]class Polygon(Visual): """An arbitrary shape defined by the coordinates of its nodes args ---- positions: List[XY, ] a list of x-y-coordinates of each node color: ColorType the desired color """ def __init__( self, positions: List[ XY, ] = [(0, 0), (0.1, 0.2), (0.3, 0.1)], color="white", ): self.positions = positions self.color = get_color(color) def adapt(self, window): x0 = window.width // 2 y0 = window.height // 2 v = [] for pos in self.positions: x = (x0 * pos[0]) + x0 y = (y0 * pos[1]) + y0 v.append((x, y)) self.visual = _Polygon( v=v, z=0, color=self.color, stroke=0, rotation=0 )
[docs]class Bar(Visual): """A rectangle starting from the bottom centre of the screen args ---- height:float height in normalized units from 0 to 1 (bottom to top of screen) width:float width in normalized units from 0 to 1 (nothing to whole screen) color: ColorType the desired color """ def __init__( self, height: float = 0.5, width: float = 0.25, color="white" ): self.height = height self.width = width self.color = get_color(color) def adapt(self, window): x0 = window.width // 2 y0 = window.height * 3 / 4 h = self.height w = self.width positions = [(-w, 0), (-w, h), (w, h), (w, 0)] v = [] for pos in positions: x = (x0 * pos[0]) + x0 y = (y0 * pos[1]) + 0 v.append((x, y)) self.visual = _Polygon( v=v, z=0, color=self.color, stroke=0, rotation=0 )
[docs]class Circle(Visual): """A circle, i.e. the outline of a ball args ---- zoom: float size of the circle color: ColorType the desired color position: XY position of the center of the circle opacity: float how opaque the circle is supposed to be from 0 to 1 stroke: float thickness of the outline """ def __init__( self, zoom: float = 1, color: ColorType = "red", position: XY = (0, 0), opacity: float = 1, stroke: float = 0, ): self.pos = position self.color = get_color(color, opacity) self.zoom = zoom self.stroke = stroke def adapt(self, window): x0 = window.width // 2 y0 = window.height // 2 width = int(0.1 * min([window.height, window.width]) * self.zoom) x = (x0 * self.pos[0]) + x0 y = (y0 * self.pos[1]) + y0 self.visual = _Circle( x=x, y=y, z=0, width=width, color=self.color, stroke=self.stroke ) def __repr__(self): return ( f"Circle(zoom={self.zoom}, color={self.color}, " + f"position={self.pos})" )
[docs]class Cylinder(Visual): """A rectangular shape which can be rotated around its center args ---- position: XY position of the center of the circle angle:float rotation angle around its center thickness:float width of the cylinder length:float length of the cylinder color: ColorType the desired color """ def __init__( self, position=(0, 0), angle: float = 0, thickness: float = 0.05, length: float = 0.75, color: ColorType = "brown", ): self.color = get_color(color) self.length = length self.pos = position self.angle = angle self.thickness = thickness def adapt(self, canvas): x0 = canvas.width // 2 y0 = canvas.height // 2 x = (x0 * self.pos[0]) + x0 y = (y0 * self.pos[1]) + y0 thickness = int(canvas.height * self.thickness) length = int(canvas.width * self.length) vertices = ( (x - length // 2, y - thickness // 2), (x - length // 2, y + thickness // 2), (x + length // 2, y + thickness // 2), (x + length // 2, y - thickness // 2), ) self.visual = _Polygon( v=vertices, z=0, color=self.color, stroke=0, rotation=self.angle )
# ------------------------------------------------------------------------------
[docs]class Cross(Visual): """A fixation cross in the center of the screen args ---- zoom: float size of the cross color: ColorType the desired color """ def __init__(self, zoom=1, color="white"): self.color = get_color(color) self.zoom = zoom def adapt(self, window): x0 = window.width // 2 y0 = window.height // 2 armlen = int(self.zoom * 100) armwid = int(self.zoom * 15) y1 = y0 - armlen y2 = y0 - armwid y3 = y0 + armwid y4 = y0 + armlen x1 = x0 - armlen x2 = x0 - armwid x3 = x0 + armwid x4 = x0 + armlen v = [(x1, y2), (x1, y3), (x4, y3), (x4, y2)] self.ho = _Polygon(v=v, z=0, color=self.color, stroke=0, rotation=0) v = [(x2, y1), (x2, y4), (x3, y4), (x3, y1)] self.ve = _Polygon(v=v, z=0, color=self.color, stroke=0, rotation=0) self.visual = [self.ho, self.ve] def __repr__(self): return f"Cross(zoom={self.zoom}, color={self.color[:-1]})"
[docs]class Trapezoid(Visual): """A trapezoid spanned between four nodes args ---- xpos: Tuple[float, float, float, float] a tuple of x coordinates of the nodes ypos: Tuple[float, float] a tuple of y coordinates of the nodes the first entry is the ypos of the first two xpos color: ColorType the desired color """ def __init__( self, xpos=(-0.33, -0.25, 0.25, 0.33), ypos=(-0.25, 0.25), color="white", ): self.xpos = xpos self.ypos = ypos self.color = get_color(color) def adapt(self, window): x0 = window.width // 2 y0 = window.height // 2 # generate vertex coordinates positions = [] for x, y in zip(self.xpos[0:2], self.ypos): positions.append((x, y)) for x, y in zip(self.xpos[2:], reversed(self.ypos)): positions.append((x, y)) # create the trapezoid in the first color (default white) vertex = [] for pos in positions: x = (x0 * pos[0]) + x0 y = (y0 * pos[1]) + y0 vertex.append((x, y)) self.visual = _Polygon( v=vertex, z=0, color=self.color, stroke=0, rotation=0 )
# ------------------------------------------------------------------------------ # %% File-based visualisations
[docs]class Image(Visual): """Show an image loaded from a file args ---- imgpath:str path to the imagefile position:XY x-y-coordinate of the center of the image scale:float size of the figure relative to the screen size """ def __init__( self, imgpath: str, position: Tuple[float, float] = (0, 0), scale: float = 0.5, ): self.imgpath = str(Path(imgpath).expanduser().absolute()) self.img = pyglet.image.load(imgpath) self.scale = scale self.pos = position def adapt(self, window): base_scale = min( window.width / self.img.width, window.height / self.img.height ) scale = self.scale * base_scale self.img.scale = scale x0 = window.width // 2 - (scale * self.img.width // 2) y0 = window.height // 2 - (scale * self.img.height // 2) x = (x0 * self.pos[0]) + x0 y = (y0 * self.pos[1]) + y0 self.visual = pyglet.sprite.Sprite( img=self.img, x=x, y=y, usage="static" ) self.visual.scale = scale def __repr__(self): return f"Image(imgpath='{self.imgpath}')"
# ------------------------------------------------------------------------------