# PicoVector PicoVector is an anti-aliased vector graphics implementation built on top of PicoGraphics. PicoVector implements bindings for [Pretty Poly](https://github.com/lowfatcode/pretty-poly) and [Alright Fonts](https://github.com/lowfatcode/alright-fonts). Providing a MicroPython interface for building polygons and shapes, handling transformations and rendering text. While PicoGraphics supports basic primitives like rectangles and circles, PicoVector goes much further: allowing for arbitrary shapes with cut-outs for representing anything from a rounded rectangle to a complex icon. Bouncing DVD logo, anyone? - [Quick Start](#quick-start) - [Drawing Something](#drawing-something) - [Transforms](#transforms) - [A Note About Rotation](#a-note-about-rotation) - [In Depth](#in-depth) - [Anti-aliasing Options](#anti-aliasing-options) - [Text, Fonts and Text Metrics](#text-fonts-and-text-metrics) - [Converting](#converting) - [Loading \& Configuring](#loading--configuring) - [Spacing \& Alignment](#spacing--alignment) - [Measuring Text](#measuring-text) - [Drawing Text](#drawing-text) - [Polygons](#polygons) - [Rectangle](#rectangle) - [Regular](#regular) - [Circle](#circle) - [Arc](#arc) - [Star](#star) - [Line](#line) - [Path](#path) - [Transformations](#transformations) - [T'is A-fine Day For Transformin'](#tis-a-fine-day-for-transformin) - [Translate](#translate) - [Rotate](#rotate) - [Scale](#scale) - [Custom](#custom) ## Quick Start PicoVector wraps PicoGraphics and uses its existing pixel pushing powers to provide pretty polygons. As such, it's super simple to set up: ```python from picographics import PicoGraphics, YOUR_DISPLAY_TYPE from picovector import PicoVector graphics = PicoGraphics(YOUR_DISPLAY_TYPE) vector = PicoVector(graphics) ``` ### Drawing Something The essential drawing primitive for PicoVector is the Polygon. Getting your head round Polygons might be a little more complex. PicoVector uses "Polygon" to mean a collection of one or more polygonal shapes, with subsequent shapes either punching out or filling in areas in an "exclusive or" fashion. For example, three concentric circles added into a "Polygon" will create an outer donut shape from the first two circles, with the third circle appearing inside. eg: ```python from picovector import Polygon bullseye = Polygon() bullseye.circle(120, 120, 100) bullseye.circle(120, 120, 70) bullseye.circle(120, 120, 30) vector.draw(bullseye) ``` ### Transforms Since PicoVector deals in Polygons, which are collections of immutable shapes, we need some other way move them around, scale them or rotate them. Enter Transforms: ```python from picovector import Polygon, Transform bullseye = Polygon() bullseye.circle(120, 120, 100) bullseye.circle(120, 120, 70) bullseye.circle(120, 120, 30) vector.set_transform(Transform().translate(100, 0)) vector.draw(bullseye) ``` ![Three concentric circles forming a bullseye pattern.](polygon-bullseye.png) A Transform is actually a 2d transformation matrix with some convenience functions for producing common, practical transformations. Transformation matrices are weird, and you'll find you need to reverse your steps to get the expected results. For example, say we want to draw a diamond in the middle of our 240x240 pixel screen. Since we want to move a square to the middle of the screen, and then rotate it we might be tempted to do: ```python vector.set_transform(Transform().translate(95, 95).rotate(45, (120, 120))) vector.draw(Polygon().rectangle(0, 0, 50, 50)) ``` Move, and then rotate. That shoul... woah why is the diamond over there!?? ![A black square with a white diamond off to one side](transform-operations-reversed.png) To understand why this doesn't work, removing the `translate()` step: ```python vector.set_transform(Transform().rotate(45, (120, 120))) vector.draw(Polygon().rectangle(0, 0, 50, 50)) ``` puts our diamond at the top of the screen, since we have rotated our 50x50 pixel square 45 degrees around the center of the screen. It's easy to see why translating that by `95, 95` then puts it off to the right. ![A black square with a white diamond in the top middle.](transform-only-rotate.png) The right way is to add the `translate()` last so that it happens first... this is counter-intuitive but you'll get the hang of it after a few mishaps! ```python vector.set_transform(Transform().rotate(45, (120, 120)).translate(95, 95)) vector.draw(Polygon().rectangle(0, 0, 50, 50)) ``` ![A black square with a white diamond, perfectly centered.](transform-operations-corrected.png) Perfect! It's worth reiterating that the same rectangle is used in all the above examples, a plain ol' 50x50 pixel square in the top left corner of the screen. We translate by `95, 95` because this is the screen dimension (240x240) divided by two, minus the square size (50x50) divided by two. Another equally valid but slightly tricky approach would be to use a square like this: ``` Polygon().rectangle(-25, -25, 50, 50) ``` It's the same square, sort of, except it is centered on `0, 0`. #### A Note About Rotation To make rotation a little less confusing, the `rotate()` transform method requires an origin, that's the tuple given as the second argument. It's an X, Y coordinate that describes the point around which the rotation should happen. Rotation then performs three transformations- a translation to shift to your supplied origin, a rotation and a final translation back to its original location. You can supply `None` if you do not want this behaviour and like to live dangerously. ## In Depth ### Anti-aliasing Options Behind the scenes all of PicoVector's drawing is done by PicoGraphics- by setting pixels. Unlike just directly drawing shapes with pixels PicoVector includes anti-aliasing, a smoothing technique that turns diagonal lines into the crisp, blended edges we'd expect from computers today. Available options are: * `ANTIALIAS_NONE` - turns off anti-aliasing for best performance * `ANTIALIAS_FAST` - 4x anti-aliasing, a good balance between speed & quality * `ANTIALIAS_BEST` - 16x anti-aliasing, best quality at the expense of speed ### Text, Fonts and Text Metrics Under the hood PicoVector uses [Alright Fonts](https://github.com/lowfatcode/alright-fonts) a font-format for embedded and low resource platforms. Alright Fonts supports converting TTF and OTF fonts into .af format which can then be displayed using PicoVector. Most of your favourite fonts should work - including silly fonts like [Jokerman](https://en.wikipedia.org/wiki/Jokerman_(typeface)) - but there are some limitations to their complexity. #### Converting Converting from an OTF or TTF font is done with the `afinate` utility. It's a Python script that handles decomposing the font into a simple list of points. Right now you'll need the `port-to-c17` branch: ``` git clone https://github.com/lowfatcode/alright-fonts --branch port-to-c17 ``` And you'll need to set up/activate a virtual env and install some dependencies: ``` cd alright-fonts python3 -m venv .venv source .venv/bin/activate pip install freetype.py simplification ``` And, finally, convert a font with `afinate`: ``` ./afinate --font jokerman.ttf --quality medium jokerman.af ``` This will output two things- a wall of text detailing which characters have been converted and how many points/contours they consist of, and the font file itself. You'll then need to upload the font to your board, this could be via the file explorer in Thonny or your preferred method. #### Loading & Configuring ```python vector.set_font("jokerman.af", 24) ``` #### Spacing & Alignment * `set_font_size()` * `set_font_word_spacing()` * `set_font_letter_spacing()` * `set_font_line_height()` * `set_font_align()` #### Measuring Text * `x, y, w, h = measure_text(text, x=0, y=0, angle=None)` Returns a four tuple with the x, y position, width and height in pixels. #### Drawing Text * `text(text, x, y, angle=None, max_width=0, max_height=0)` When you draw text the x, y position and angle are used to create a new Transform to control the position and rotation of your text. The transform set with `.set_transform()` is also applied. ### Polygons The basis of all drawing operations in PicoVector is the `Polygon` object. A `Polygon` is a collection of one or more paths to be drawn together. This allows for shapes with holes - letters, for example - or more complicated designs - logos or icons - to be scaled, rotated and drawn at once. If paths overlap then the top-most path will "punch" out the one underneath. To use any of the primitives or path drawing methods you must first create a `Polygon`, for example here's a simple triangle: ```python from picovector import Polygon my_shape = Polygon() my_shape.path((10, 0), (0, 10), (20, 10)) ``` #### Rectangle ```python Polygon().rectangle(x, y, w, h, [corners=(tl, tr, bl, br)]) ``` A rectangle is a plain old rectangular shape with optional rounded corners. If `stroke` is greater than zero then the rectangle outline will be produced. If any of the corner radii are greater than zero then that corner will be created. #### Regular ```python Polygon().regular(x, y, radius, sides, [stroke]) ``` Creates a regular polygon with the given radius and number of sides. Needs at least 3 sides (an equilateral triangle) and converges on a circle. If `stroke` is greater than zero then the regular polygon outline will be created. #### Circle ```python Polygon().regular(x, y, radius, [stroke]) ``` Effectively a regular polygon, approximates a circle by automatically picking a number of sides that will look smooth for a given radius. If `stroke` is greater than zero then the circle outline will be created. #### Arc ```python Polygon().arc(x, y, radius, from, to, [stroke]) ``` Create an arc at x, y with radius, from and to angle (degrees). Great for radial graphs. If `stroke` is greater than zero then the arc outline will be created. #### Star ```python Polygon().star(x, y, points, inner_radius, outer_radius, [stroke]) ``` Create a star at x, y with given number of points. The inner and outer radius (in pixels) define where the points start and end. If `stroke` is greater than zero then the arc outline will be created. #### Line ```python Polygon().line(x, y, x2, y2, thickness) ``` #### Path ```python Polygon().path((x, y), (x2, y2), (x3, y3), ...) ``` A path is simply an arbitrary list of points that produce a complete closed shape. It's ideal for drawing complex shapes such as logos or icons. If you have a list of points you can use Python's spread operator to pass it into `path`, eg: ```python my_points = [(10, 0), (0, 10), (20, 10)] my_shape = Polygon() my_shape.path(*my_points) ``` ### Transformations To create a new empty `Transform()` you can just: ```python my_transform = Transform() ``` And tell PicoVector it's the one you want to use with: ```python vector.set_transform(my_transform) ``` #### T'is A-fine Day For Transformin' Since PicoVector supports only 2D transformation matrices you can only perform translations which are "affine." The simplest way to think about the limitations of an affine transformation is to understand that parallel lines will remain parallel. You can, for example, rotate a square into a diamond since its two pairs of parallel sides will remain parallel. You cannot skew a square into a trapezoid, since this would require one pair of sides to not be parallel. #### Translate Totally unrelated to language, a translation describes a displacement along the X and or Y axes - or simply put: how far right and down you want a thing to move. For example our friend Mx. Square from above lives in the top left-hand corner: ![A black square with a white square in the top-left corner.](transform-translate-start.png) Say we translate them by `50, 120`: ```python vector.set_transform(Transform().translate(50, 120)) ``` ![A black square with a white square toward the bottom-left corner.](transform-translate-end.png) Away they go... hey come back we're not done yet! #### Rotate ```python Transform().rotate(angle_degrees, (origin_x, origin_y)) ``` #### Scale ```python Transform().scale(scale_x, scale_y) ``` #### Custom If you want to get fancy, you can supply a custom 2d transformation matrix as a list of nine floating point numbers. Eg: ```python my_custom_transform = Transform().matrix([ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 ]) ``` Okay, this one isn't so fancy- it's a standard "identity" matrix which has no effect whatsoever. What about calculating our own scale matrix? ```python scale_x = 10 scale_y = 10 my_custom_transform = Transform().matrix([ scale_x, 0.0, 0.0, 0.0, scale_y, 0.0, 0.0, 0.0, 1.0 ]) ``` Okay that was surprisingly easy. What about rotation, that must be hard, right? ```python angle = math.pi / 4 # 45 degrees in radians my_custom_transform = Transform().matrix([ math.cos(angle), math.sin(angle), 0.0, -math.sin(angle), math.cos(angle), 0.0, 0.0, 0.0, 1.0 ]) ``` Nope, it's pretty much just scale with bells on.