Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PicoVector. #783

Merged
merged 20 commits into from
Sep 6, 2023
Merged

PicoVector. #783

merged 20 commits into from
Sep 6, 2023

Conversation

Gadgetoid
Copy link
Member

@Gadgetoid Gadgetoid commented May 25, 2023

PicoVector, based upon Pretty Poly, lays the groundwork for Alright Fonts, a vector-based font library that will eventually replace our Hershey font implementation. More details here: https://github.com/lowfatcode/alright-fonts

It also gives us a nice(er) polygon renderer, which couldn't hurt.

The aim here is to make PicoVector a library - like PNG or JPEG - which takes a PicoGraphics instance and offers some additional functionality. In the case of PicoVector that will be a set of vector primitives, helpers and so forth.

todo

  1. Primitives - do we need explicit circle, square, triangle, etc? should these be direct functions, or return a set of contours
  2. Transforms? - the ability to take a set of contours and translate/rotate/scale it could be useful?
  3. Fix memory allocation issues - currently polygon rendering will glitch periodically as some data or another is garbage collected. Using a fixed period gc.collect() appears to stop this. Need to figure out exactly where some memory is allocated, in-use, but not reachable when GC walks the pointers.

@Gadgetoid Gadgetoid changed the title PicoGraphics: Experimental Pretty Poly bring-up. PicoVector. Aug 11, 2023
@Gadgetoid Gadgetoid force-pushed the feature/ppaf branch 2 times, most recently from 5249a0e to ddc3e41 Compare August 15, 2023 14:53
@Gadgetoid
Copy link
Member Author

Gadgetoid commented Aug 16, 2023

Some rather rough and ready code to render a radial 6-channel spectrometer graph.

import time
import math
from pimoroni_i2c import PimoroniI2C
from breakout_as7262 import BreakoutAS7262
from picographics import PicoGraphics, DISPLAY_PICO_W_EXPLORER, PEN_RGB332
from picovector import PicoVector, Polygon, RegularPolygon, ANTIALIAS_NONE, ANTIALIAS_X4, ANTIALIAS_X16

PINS_BREAKOUT_GARDEN = {"sda": 4, "scl": 5}
PINS_PICO_EXPLORER = {"sda": 20, "scl": 21}
i2c = PimoroniI2C(**PINS_PICO_EXPLORER)

as7262 = BreakoutAS7262(i2c)

dev_type = as7262.device_type()
hw_version = as7262.hardware_version()
fw_version = as7262.firmware_version()

as7262.set_gain(BreakoutAS7262.X16)
as7262.set_measurement_mode(BreakoutAS7262.CONT_ROYGBR)
as7262.set_illumination_current(BreakoutAS7262.MA12)
as7262.set_indicator_current(BreakoutAS7262.MA4)
as7262.set_leds(True, True)

print("Device: ", dev_type, "HW: ", hw_version, sep="", end=", ")
print("FW: ", fw_version[0], ".", fw_version[1], ".", fw_version[2])

display = PicoGraphics(DISPLAY_PICO_W_EXPLORER, pen_type=PEN_RGB332)
display.set_backlight(0.8)

vector = PicoVector(display)
vector.set_antialiasing(ANTIALIAS_X4)

result = vector.set_font("/AdvRe.af", 30)

WIDTH, HEIGHT = display.get_bounds()

RADIUS = 90
SPIN_DEGREES = 0
angle = 0

RED = display.create_pen(255, 0, 0)
ORANGE = display.create_pen(255, 128, 0)
YELLOW = display.create_pen(255, 255, 0)
GREEN = display.create_pen(0, 255, 0)
BLUE = display.create_pen(0, 0, 255)
VIOLET = display.create_pen(255, 0, 255)

BLACK = display.create_pen(0, 0, 0)
GREY = display.create_pen(128, 128, 128)
WHITE = display.create_pen(255, 255, 255)

cols = [RED, ORANGE, YELLOW, GREEN, BLUE, VIOLET]


def regular_polygon(o_x, o_y, radius, rotation):
    sides = 6
    angle = math.radians(360 / sides)
    rotation = math.radians(rotation)

    points = []

    for side in range(sides):
        current_angle = side * angle + rotation
        x = math.cos(current_angle) * radius[side]
        y = math.sin(current_angle) * radius[side]
        points.append((int(x) + o_x, int(y) + o_y))

    return Polygon(*points)


labels = [
    "R",
    "O",
    "Y",
    "G",
    "B",
    "V"
]

angle = 0

while True:
    display.set_pen(BLACK)
    display.clear()
    reading = list(as7262.read())
    print("R:", reading[0], end=" ")
    print("O:", reading[1], end=" ")
    print("Y:", reading[2], end=" ")
    print("G:", reading[3], end=" ")
    print("B:", reading[4], end=" ")
    print("V:", reading[5])

    for i in range(6):
        reading[i] = int(reading[i] / 3.0)
        reading[i] = min(reading[i], RADIUS)

    poly = regular_polygon(int(WIDTH / 2), int(HEIGHT / 2), reading, 0)
    lines = RegularPolygon(int(WIDTH / 2), int(HEIGHT / 2), 6, RADIUS)
    label_points = RegularPolygon(int(WIDTH / 2), int(HEIGHT / 2), 6, RADIUS * 0.7)

    vector.rotate(poly, angle, int(WIDTH / 2), int(HEIGHT / 2))
    vector.rotate(lines, angle, int(WIDTH / 2), int(HEIGHT / 2))
    vector.rotate(label_points, angle - (360 / 12), int(WIDTH / 2), int(HEIGHT / 2))


    display.set_pen(GREY)
    for point in lines:
        x, y = point
        display.line(int(WIDTH / 2), int(HEIGHT / 2), int(x), int(y))


    points = list(poly)
    a = points[-1]
    c = (int(WIDTH / 2), int(HEIGHT / 2))
    for i in range(6):
        display.set_pen(cols[i])
        b = points[i]
        vector.draw(Polygon(a, b, c))
        a = b

    i = 0
    for p in label_points:
        l = labels[i]
        x, y = p
        display.set_pen(cols[i])
        vector.text(l, int(x) - 5, int(y) - 20)
        i += 1

    display.set_pen(WHITE)
    
    vector.text("Spectrograph", 5, -5)
    
    angle += SPIN_DEGREES

    display.update()

@Gadgetoid
Copy link
Member Author

And a clock example, because it was inevitable -

import time
import math
import random
from picographics import PicoGraphics, DISPLAY_PICO_W_EXPLORER, PEN_RGB332
from picovector import PicoVector, Polygon, RegularPolygon, Rectangle, ANTIALIAS_NONE, ANTIALIAS_X4, ANTIALIAS_X16


display = PicoGraphics(DISPLAY_PICO_W_EXPLORER, pen_type=PEN_RGB332)
display.set_backlight(0.8)

vector = PicoVector(display)
vector.set_antialiasing(ANTIALIAS_X4)


RED = display.create_pen(255, 0, 0)
ORANGE = display.create_pen(255, 128, 0)
YELLOW = display.create_pen(255, 255, 0)
GREEN = display.create_pen(0, 255, 0)
BLUE = display.create_pen(0, 0, 255)
VIOLET = display.create_pen(255, 0, 255)

BLACK = display.create_pen(0, 0, 0)
GREY = display.create_pen(128, 128, 128)
WHITE = display.create_pen(255, 255, 255)
result = vector.set_font("/AdvRe.af", 30)

WIDTH, HEIGHT = display.get_bounds()

def random_polygon(length, x, y, w, h):
    for i in range(length):
        yield random.randint(x, x + w), random.randint(y, y + h)


hub = RegularPolygon(int(WIDTH / 2), int(HEIGHT / 2), 36, 5)
hub2 = RegularPolygon(int(WIDTH / 2), int(HEIGHT / 2), 36, 10)

#p = RegularPolygon(0, 0, 6, 100)
a = 0

print(time.localtime())

while True:
    year, month, day, hour, minute, second, _, _ = time.localtime()

    #p = Polygon(*random_polygon(10, 0, 0, WIDTH, HEIGHT))
    display.set_pen(BLACK)
    display.clear()
    display.set_pen(GREY)

    tick_mark = Rectangle(int(WIDTH / 2) - 1, 0, 2, 10)
    for _ in range(12):
        vector.rotate(tick_mark, 360 / 12.0, int(WIDTH / 2), int(HEIGHT / 2))
        vector.draw(tick_mark)
        

    angle_second = second * 6
    second_hand = Rectangle(-1, -100, 2, 100)
    vector.rotate(second_hand, angle_second, 0, 0)
    vector.translate(second_hand, int(WIDTH / 2), int(HEIGHT / 2))

    angle_minute = minute * 6
    angle_minute += second / 10.0
    minute_hand = Rectangle(-2, -60, 4, 60)
    vector.rotate(minute_hand, angle_minute, 0, 0)
    vector.translate(minute_hand, int(WIDTH / 2), int(HEIGHT / 2))

    angle_hour = (hour % 12) * 30
    angle_hour += minute / 2
    hour_hand = Rectangle(-3, -30, 6, 30)
    vector.rotate(hour_hand, angle_hour, 0, 0)
    vector.translate(hour_hand, int(WIDTH / 2), int(HEIGHT / 2))

    display.set_pen(WHITE)

    vector.draw(second_hand, minute_hand, hour_hand)
    display.set_pen(BLACK)
    vector.draw(hub2)
    display.set_pen(WHITE)
    vector.draw(hub)
    display.update()
    time.sleep(1.0 / 30)
    a += 1

@Gadgetoid
Copy link
Member Author

Rotated text test for benchmarking:

import time
import math
import random
import machine
from picographics import PicoGraphics, DISPLAY_PICO_W_EXPLORER, PEN_RGB332
from picovector import PicoVector, Polygon, RegularPolygon, Rectangle, ANTIALIAS_NONE, ANTIALIAS_X4, ANTIALIAS_X16

machine.freq(250_000_000)

display = PicoGraphics(DISPLAY_PICO_W_EXPLORER, pen_type=PEN_RGB332)
display.set_backlight(0.8)

vector = PicoVector(display)
vector.set_antialiasing(ANTIALIAS_NONE)


RED = display.create_pen(255, 0, 0)
ORANGE = display.create_pen(255, 128, 0)
YELLOW = display.create_pen(255, 255, 0)
GREEN = display.create_pen(0, 255, 0)
BLUE = display.create_pen(0, 0, 255)
VIOLET = display.create_pen(255, 0, 255)

BLACK = display.create_pen(0, 0, 0)
GREY = display.create_pen(128, 128, 128)
WHITE = display.create_pen(255, 255, 255)
result = vector.set_font("/Jokerman-Regular.af", 30)

WIDTH, HEIGHT = display.get_bounds()

X = int(WIDTH / 2)
Y = int(HEIGHT / 2)

a = 0

frames = 0

t_count = 0
t_total = 0

while True:
    tstart = time.ticks_ms()
    display.set_pen(BLACK)
    display.clear()
    for x in range(8):
        c = 31 + (x * 32)
        display.set_pen(display.create_pen(255-c, 0, c))
        vector.text("Hello World\nHello World", X, Y, angle=x * 45 + a)
    display.update()
    a += 1

    
    tfinish = time.ticks_ms()

    total = tfinish - tstart
    t_total += total
    t_count += 1

    if t_count == 60:
        per_frame_avg = t_total / t_count
        print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS")
        t_count = 0
        t_total = 0

    # pause for a moment (important or the USB serial device will fail)
    # try to pace at 60fps or 30fps
    if total > 1000 / 30:
        time.sleep(0.0001)
    elif total > 1000 / 60:
        t = 1000 / 30 - total
        time.sleep(t / 1000)
    else:
        t = 1000 / 60 - total
        time.sleep(t / 1000)

@Gadgetoid
Copy link
Member Author

Example to bake the points for two concentric circles and then animate it as if it were a radial loading graph by selecting groups of four points to display. Resolutions over 45 points will give a slightly smoother circle, but the RGB332 colourspace runs out of distinct colours to represent the segments:

import time
import math
import random
from picographics import PicoGraphics, DISPLAY_PICO_W_EXPLORER, PEN_RGB332
from picovector import PicoVector, Polygon, RegularPolygon, Rectangle, ANTIALIAS_NONE, ANTIALIAS_X4, ANTIALIAS_X16


display = PicoGraphics(DISPLAY_PICO_W_EXPLORER, pen_type=PEN_RGB332)
display.set_backlight(0.5)

vector = PicoVector(display)
vector.set_antialiasing(ANTIALIAS_X4)

BLACK = display.create_pen(0, 0, 0)
WHITE = display.create_pen(255, 255, 255)

WIDTH, HEIGHT = display.get_bounds()


def bake_radial_graph(ox, oy, radius, thickness, resolution=45):
    outer_points = []
    inner_points = []

    angle_per_point = math.radians(360.0 / resolution)
    
    outer = radius
    inner = radius - thickness
    
    for x in range(resolution):
        a = angle_per_point * x

        s = math.cos(a)
        c = math.sin(a)

        in_x = int(c * inner) + ox
        out_x = int(c * outer) + ox

        in_y = int(s * inner) + oy
        out_y = int(s * outer) + oy

        outer_points.append((out_x, out_y))
        inner_points.insert(0, (in_x, in_y))
    
    return outer_points, inner_points


a = 2
d = 1

outer_points, inner_points = bake_radial_graph(int(WIDTH / 2), int(HEIGHT / 2), 120, 20)
num_points = len(outer_points)

angle = 0

while True:
    display.set_pen(BLACK)
    display.clear()

    display.set_pen(WHITE)

    inner_start = inner_points[0]
    outer_start = outer_points[-1]

    for hue in range(1, a):
        points = [inner_start, inner_points[hue], outer_points[-(hue + 1)], outer_start]
        inner_start = inner_points[hue]
        outer_start = outer_points[-(hue + 1)]
        display.set_pen(display.create_pen_hsv(hue / num_points, 1.0, 1.0))
        p = Polygon(*points)
        vector.rotate(p, angle, int(WIDTH / 2), int(HEIGHT / 2))
        vector.draw(p)

    display.update()
    a += d
    if a >= num_points:
        d = -1
    if a <= 2:
        d = 1
    angle += 8

@Gadgetoid
Copy link
Member Author

I'm grappling with a very strange bug relating to rotating polygons immediately after drawing some unrelated polygons. This seems to be fixed by either using the spread operator to unpack a tuple of polygons to draw, or by inserting a small (1ms) delay before rotation. It feels like something is clobbering the matrix transform for rotation but it's being stubborn to pin down-

import gc
import time
from picographics import PicoGraphics, DISPLAY_PICO_W_EXPLORER, PEN_RGB332
from picovector import PicoVector, Polygon, Rectangle, ANTIALIAS_NONE

display = PicoGraphics(DISPLAY_PICO_W_EXPLORER, pen_type=PEN_RGB332)
display.set_backlight(0.5)

vector = PicoVector(display)
vector.set_antialiasing(ANTIALIAS_NONE)

BLACK = display.create_pen(0, 0, 0)
WHITE = display.create_pen(255, 255, 255)

WIDTH, HEIGHT = display.get_bounds()

poly_list = [Rectangle(10, 85, 60, 60), Rectangle(250, 85, 60, 60)]


while True:
    display.set_pen(BLACK)
    vector.draw(Rectangle(0, 0, 320, 240), Rectangle(60, 60, 200, 120))  # Breaks
    #vector.draw(*(Rectangle(0, 0, 320, 240), Rectangle(60, 60, 200, 120))) # Works

    #time.sleep_ms(1)  # Works if I add a 1ms sleep here

    for p in poly_list:
        vector.rotate(p, 8, int(WIDTH / 2), int(HEIGHT / 2))

    display.set_pen(WHITE)
    vector.draw(*poly_list)
    
    display.update()

@Gadgetoid Gadgetoid merged commit a334899 into main Sep 6, 2023
30 checks passed
@Gadgetoid Gadgetoid mentioned this pull request Sep 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant