Last active
March 4, 2018 19:54
-
-
Save ali1234/81fdc3a52a00e383cfd4cb490399ad41 to your computer and use it in GitHub Desktop.
Unified Graphics Library Yarrrrrrr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import subprocess, time, sys | |
import numpy as np | |
from PIL import Image | |
class GraphicsDeviceBase(object): | |
def __init__(self, buf, depth): | |
self.__buf = buf | |
self.__depth = depth | |
@property | |
def buf(self): | |
return self.__buf | |
@property | |
def width(self): | |
return self.__buf.shape[1] | |
@property | |
def height(self): | |
return self.__buf.shape[0] | |
@property | |
def channels(self): | |
return self.__buf.shape[2] | |
@property | |
def depth(self): | |
return self.__depth | |
# TODO: friendly graphics API goes here | |
class GraphicsDeviceFramebuffer(GraphicsDeviceBase): | |
def __init__(self, width, height, channels, depth): | |
super().__init__(np.zeros((height, width, channels), dtype=np.uint8), depth) | |
class Terminal(GraphicsDeviceFramebuffer): | |
# shows the output on your terminal. no hardware required. | |
def __init__(self, width, height, channels, depth): | |
super().__init__(width, height, channels, depth) | |
sys.stdout.write('\033[2J\033[?25l') | |
def rotation(self, n): | |
pass | |
def show(self): | |
r = 0 | |
g = 1%self.channels | |
b = 2%self.channels | |
if self.depth == 1: | |
outbuf = ((self.buf & self.depth)>0) * 255 | |
else: | |
outbuf = self.buf | |
for n, row in enumerate(outbuf): | |
sys.stdout.write('\033[{};0H'.format(n+2)) | |
s = ' '.join( | |
('\033[38;2;{:d};{:d};{:d}m\u25cf'.format(pixel[r], pixel[g], pixel[b]) for pixel in row) | |
) | |
sys.stdout.write(' ') | |
sys.stdout.write(s) | |
sys.stdout.flush() | |
time.sleep(0.01) | |
def off(self): | |
sys.stdout.write('\033[0m\033[{};0H\033[?25h'.format(self.height+3)) | |
class Ffmpeg(GraphicsDeviceFramebuffer): | |
# records output to a video file | |
def __init__(self, width, height, channels, depth, scale=8): | |
super().__init__(width, height, channels, depth) | |
self.scale = scale | |
self.ffmpeg = subprocess.Popen("ffmpeg -i pipe: -r 30 -pix_fmt yuv420p video.webm".split(), stdin=subprocess.PIPE) | |
def show(): | |
imbuf = np.repeat(np.repeat(self._buf, self.scale, axis=0), self.scale, axis=1) | |
for i in range(0, self.width): | |
imbuf[:,i*scale,:] = 0 | |
for i in range(0, self.height): | |
imbuf[i*scale,:,:] = 0 | |
imbuf = np.pad(imbuf, ((0,1), (0,1), (0,1)), mode='wrap') | |
Image.fromarray(imbuf).save(self.ffmpeg.stdin, format='png') | |
def off(): | |
self.ffmpeg.stdin.close() | |
self.ffmpeg.wait() | |
class Legacy(GraphicsDeviceBase): | |
# TODO: wrapper for legacy drivers | |
# you may need more than one of these since the | |
# drivers are all different :) | |
def __init__(self, legacy, depth): | |
super().__init__(legacy._buf, depth) | |
self._legacy = legacy | |
def rotation(self, n): | |
return self._legacy.rotation(n) | |
def show(self): | |
self._legacy.show() | |
def off(self): | |
self._legacy.off() | |
# TODO: implement brightness etc | |
def AutoEmulator(width, height, channels, depth, driver=None): | |
if driver == 'terminal' or driver == None: | |
return Terminal(width, height, channels, depth) | |
if driver == 'ffmpeg': # don't select this one by default | |
return Ffmpeg(width, height, channels, depth) | |
raise ImportError | |
def UnicornHatHD(driver=None): | |
if driver == 'legacy' or driver == None: | |
try: | |
import unicornhathd | |
return Legacy(unicornhathd, 8) | |
except ImportError: | |
pass | |
return AutoEmulator(16, 16, 3, 0, driver=driver) | |
# i have not tested the rest after this point on real hardware | |
def UnicornHat(driver=None): | |
if driver == 'legacy' or driver == None: | |
try: | |
import unicornhat | |
return Legacy(unicornhat, 8) | |
except ImportError: | |
pass | |
return AutoEmulator(8, 8, 3, 0, driver=driver) | |
def UnicornPhat(driver=None): | |
if driver == 'legacy' or driver == None: | |
try: | |
import unicornhat | |
return Legacy(unicornhat, 8) | |
except ImportError: | |
pass | |
return AutoEmulator(8, 4, 3, 0, driver=driver) | |
def ScrollPhatHD(driver=None): | |
if driver == 'legacy' or driver == None: | |
try: | |
import scrollphathd | |
return Legacy(scrollphathd, 8) | |
except ImportError: | |
pass | |
return AutoEmulator(17, 7, 1, 0, driver=driver) | |
def ScrollPhat(driver=None): | |
if driver == 'legacy' or driver == None: | |
try: | |
import scrollphat | |
return Legacy(scrollphat, 1) | |
except ImportError: | |
pass | |
return AutoEmulator(11, 5, 1, 1, driver=driver) | |
def Blinkt(driver=None): | |
if driver == 'legacy' or driver == None: | |
try: | |
import blinkt | |
return Legacy(blinkt, 8) | |
except ImportError: | |
pass | |
return AutoEmulator(8, 1, 3, 8, driver=driver) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import time, math, random | |
import numpy as np | |
print("""Unicorn HAT HD: demo.py | |
Press Ctrl+C to exit! | |
""") | |
# here just import whichever display you want to use | |
# this could be autodetected if the hardware supports it | |
from devices import UnicornHatHD as Display | |
display = Display(driver=None) # autodetect driver | |
display.rotation(180) | |
# helper functions | |
def distance_from_centre(dx = 0, dy = 0): | |
ox = dx + ((display.width/2) - 0.5) | |
oy = dy + ((display.height/2) - 0.5) | |
# nasty hack warning: the +1 here is to make it work with blinkt which is 1 pixel tall | |
x = np.squeeze(np.arange(0, display.width+1, dtype=np.float)[:, np.newaxis] - ox) | |
y = np.squeeze(np.arange(0, display.height+1, dtype=np.float)[:, np.newaxis] - oy) | |
return x[:-1], y[:-1] | |
def solid(a): | |
s = np.array([[a]], dtype=np.float) | |
def _solid(t): | |
return s | |
return _solid | |
def cycle(t): | |
return np.array([[t*0.2]], dtype=np.float) | |
# RGB effects | |
matrix_buf = np.random.randint(0, 0x10f, (display.height, display.width), dtype=np.int16) | |
matrix_t = 0 | |
def matrix(t): | |
global matrix_buf, matrix_t | |
matrix_buf = matrix_buf + (np.random.randint(-4, 6, (display.height, display.width), dtype=np.int16)) | |
if t - matrix_t > 0.05: | |
fall = matrix_buf > 0xff | |
subs = fall * np.random.randint(0x7f, 0x17f, (display.height, display.width), dtype=np.int16) | |
adds = np.roll(subs, 1, axis=0) * 0.75 | |
adds[0, :] = (np.random.randint(0, 64, (1, display.width), dtype=np.uint8) == 0) * 0xff | |
matrix_buf = matrix_buf + adds - subs | |
matrix_t = t | |
return np.clip(np.stack([matrix_buf-0xff, matrix_buf, matrix_buf-0xff], axis=-1), 0, 0xff) | |
# boolean effects | |
def checker(t): | |
x, y = distance_from_centre(dx = math.sin(t * 1.333) * display.width, dy = math.cos(t * 2.0) * display.width) | |
sc = (math.cos(-t*0.75)) + 2.0 | |
s = math.sin(t); | |
c = math.cos(t); | |
xs = ((x * c - y[:, np.newaxis] * s) - math.sin(t / 2.0) * 0.1) / sc; | |
ys = ((x * s + y[:, np.newaxis] * c) - math.cos(t / 2.0) * 0.1) / sc; | |
return (np.sin(xs) > 0) ^ (np.cos(ys) > 0) | |
def beams(nbeams = None): | |
if nbeams is None: | |
nbeams = random.choice([3, 5, 6]) | |
m = 2 * math.pi / nbeams | |
s = 3 / nbeams | |
def _beams(t): | |
x, y = distance_from_centre(dx = math.sin(t * 3.2) * 3.0, dy = math.cos(t * 1.5) * 3.0) | |
return (np.mod(np.arctan2(x, y[:, np.newaxis]) + (math.pi) + t, m) - s) > 0 | |
return _beams | |
def zoomrings(t): | |
x, y = distance_from_centre(dx = math.sin(t * 3.2) * 3.0, dy = math.cos(t * 1.5) * 3.0) | |
return (np.mod(np.sqrt((x**2) + (y**2)[:, np.newaxis])-(t*10), 10) - 5) > 0 | |
_roni = np.unpackbits(np.array([255, 255, 254, 127, 252, 63, 225, 135, 193, 131, 192, 3, 192, | |
3, 199, 227, 229, 163, 247, 231, 240, 39, 243, 135, 224, 15, | |
240, 31, 253, 127, 255, 255], dtype=np.uint8)).reshape(16, 16) > 0 | |
def roni(t): | |
if display.width == 16 and display.height == 16: | |
return _roni | |
else: | |
return np.array([[True]], dtype=np.bool) | |
def metaballs(num=5): | |
dt = np.random.ranf((1, num))*15.0 | |
sx = np.random.ranf((1, num))*2.0+1.5 | |
sy = np.random.ranf((1, num))*2.0+1.5 | |
def _metaballs(t): | |
x, y = distance_from_centre(dx=np.sin(sx*t+dt)*6, dy=np.cos(sy*t+dt)*6) | |
return np.sum(1 / np.sqrt((x**2) + (y**2)[:, np.newaxis]), axis=-1) > 0.9 | |
return _metaballs | |
life_buf = np.random.randint(0, 255, (display.height+2, display.width), dtype=np.uint8) == 0 | |
life_t = 0 | |
def life(t): | |
global life_buf, life_t | |
if t - life_t > 0.05: | |
life_buf = np.roll(life_buf, -1, axis=0) | |
life_buf[-1] = np.random.randint(0, 3, (1, display.width), dtype=np.uint8) == 0 | |
hneighbours = np.roll(life_buf, 1, axis=0)*1 + life_buf*1 + np.roll(life_buf, -1, axis=0)*1 | |
neighbours = np.roll(hneighbours, 1, axis=1)*1 + hneighbours*1 + np.roll(hneighbours, -1, axis=1)*1 - life_buf | |
life_buf = life_buf & (neighbours > 1) & (neighbours < 4) | |
life_buf = life_buf | (neighbours == 3) | |
life_t = t | |
return life_buf[2:] | |
# grey effects | |
tinylife_buf = np.random.randint(0, 255, ((display.height*4)+2, display.width*4), dtype=np.uint8) == 0 | |
tinylife_t = 0 | |
def tinylife(t): | |
global tinylife_buf, tinylife_t | |
if t - tinylife_t > 0.02: | |
tinylife_buf = np.roll(tinylife_buf, 1, axis=0) | |
tinylife_buf[0] = np.random.randint(0, 3, (1, (display.width*4)), dtype=np.uint8) == 0 | |
hneighbours = np.roll(tinylife_buf, 1, axis=0)*1 + tinylife_buf*1 + np.roll(tinylife_buf, -1, axis=0)*1 | |
neighbours = np.roll(hneighbours, 1, axis=1)*1 + hneighbours*1 + np.roll(hneighbours, -1, axis=1)*1 - tinylife_buf | |
tinylife_buf = tinylife_buf & (neighbours > 1) & (neighbours < 4) | |
tinylife_buf = tinylife_buf | (neighbours == 3) | |
tinylife_t = t | |
return np.sum(np.sum(tinylife_buf[:-2].reshape(display.height, 4, display.width, 4), axis=3), axis=1) / 8 | |
def rings(t): | |
x, y = distance_from_centre(dx = math.sin(t * 2.0) * display.width, dy = math.cos(t * 3.0) * display.width) | |
sc = (math.cos(t * 5.0) * 10.0) + 20.0 | |
return np.mod(np.sqrt((x**2) + (y**2)[:, np.newaxis])/sc, 1) | |
def swirl(t): | |
x, y = distance_from_centre() | |
dist = (np.sqrt((x**2) + (y**2)[:, np.newaxis]) * (0.5 + (0.2*math.sin(t*0.5)))) + (-t * 5) | |
s = np.sin(dist); | |
c = np.cos(dist); | |
xs = x * c - y[:, np.newaxis] * s; | |
ys = x * s + y[:, np.newaxis] * c; | |
return np.mod((np.abs(xs + ys) * 0.05) + (0.1 * t), 1) | |
def tunnel(nbeams=None): | |
b = beams(nbeams) | |
def _tunnel(t): | |
return (zoomrings(t) * 0.45) + (b(t) * 0.45) + 0.1 | |
return _tunnel | |
wind_buf = np.random.randint(0, 6, (display.height, display.width), dtype=np.uint8) == 0 | |
wind_acc_buf = np.zeros((display.height, display.width), dtype=np.float) | |
wind_t = 0 | |
def wind(t): | |
global wind_buf, wind_acc_buf, wind_t | |
if t - wind_t > 0.02: | |
wind_buf = np.roll(wind_buf, 1, axis=1) | |
wind_buf[:,0] = (np.random.randint(0, 6, (display.height,), dtype=np.uint8) == 0) | |
wind_acc_buf = np.clip((wind_acc_buf - 0.1), 0, 1) + wind_buf | |
wind_t = t | |
wind_acc_buf = np.clip(wind_acc_buf, 0.1, 1.0) | |
return wind_acc_buf | |
diamonds_buf = np.random.randint(0, 8, (display.height, display.width), dtype=np.uint16) | |
diamonds_t = 0 | |
def diamonds(t): | |
global diamonds_buf, diamonds_t | |
if t - diamonds_t > 0.05: | |
diamonds_buf = np.amax(np.stack([ | |
diamonds_buf, | |
np.roll(diamonds_buf, 1, axis=0), | |
np.roll(diamonds_buf, -1, axis=0), | |
np.roll(diamonds_buf, 1, axis=1), | |
np.roll(diamonds_buf, -1, axis=1) | |
]), axis=0) | |
diamonds_buf[random.randint(0, display.height-1), random.randint(0, display.width-1)] += 1 | |
if np.all(diamonds_buf > 256): | |
diamonds_buf = diamonds_buf - 256 | |
diamonds_t = t | |
return (diamonds_buf & 0x3) / 3 | |
# colorspace conversions | |
def grey_to_rgb(grey, r=0, g=0.333, b=0.666, x=0.333, y=0.5, z=6): | |
t = grey[:, :, np.newaxis] + np.array([r, g, b])[np.newaxis, np.newaxis, :] | |
return np.clip((x - np.abs(np.mod(t, 1)-y))*z, 0, 1) * 255 | |
def hue(grey_func): | |
def _hue(t): | |
return grey_to_rgb(grey_func(t)) | |
return _hue | |
def trippy(grey_func): | |
def _trippy(t): | |
return grey_to_rgb(grey_func(t), 0, 0.333+math.sin(t), 0.666-math.sin(t)) | |
return _trippy | |
def rgb_bars(grey_func): | |
def _rgb_bars(t): | |
return grey_to_rgb(grey_func(t), z=3) | |
return _rgb_bars | |
# misc | |
def triple_grey_to_rgb(ga, gb, gc, offa=lambda t: t, offb=lambda t: t, offc=lambda t: t): | |
def _triple_grey_to_rgb(t): | |
return np.stack([ga(offa(t)), gb(offb(t)), gc(offc(t))], axis=-1)*255 | |
return _triple_grey_to_rgb | |
def chromatic_aberration(grey): | |
return triple_grey_to_rgb(grey, grey, grey, offa=lambda t: t+(math.sin(t)*0.075), offc=lambda t: t-(math.sin(t)*0.075)) | |
# operators | |
def multiply(a, b): | |
def _multiply(t): | |
return a(t) * b(t)[:,:,np.newaxis] | |
return _multiply | |
def invert(a): | |
def _invert(t): | |
return ~a(t) | |
return _invert | |
def shift(a): | |
def _shift(t): | |
return a(t)+0.5 | |
return _shift | |
def add(a, b): | |
def _add(t): | |
return a(t) + b(t) | |
return _add | |
# choose a random effect | |
def random_ca(): | |
mask = random.choice([checker, zoomrings, beams(), metaballs()]) | |
return chromatic_aberration(mask) | |
def random_mult(): | |
grey = random.choice([swirl, rings]) | |
colr = random.choice([hue, trippy]) | |
mask = random.choice([checker, diamonds, tunnel(), zoomrings, beams(), life, tinylife, wind, metaballs()]) | |
return multiply(colr(grey), mask) | |
def random_invert_mult(): | |
# functions are composable in complex ways but on the small screen | |
# it mostly ends up looking messy if too much stuff is going on | |
grey = random.choice([swirl]) | |
colr = random.choice([hue, trippy]) | |
if random.choice([True, False]): | |
mask = random.choice([zoomrings, beams()]) | |
return add(multiply(colr(grey), mask), multiply(colr(shift(grey)), invert(mask))) | |
else: | |
mask = random.choice([life, metaballs()]) | |
return multiply(colr(grey), invert(mask)) | |
def random_matrix(): | |
if random.randint(0,4) == 0: | |
return multiply(matrix, roni) | |
else: | |
return matrix | |
def random_simple(): | |
grey = random.choice([swirl, rings]) | |
colour = random.choice([hue, trippy]) | |
return colour(grey) | |
def random_effect(): | |
return random.choice([ | |
random_ca, | |
random_mult, | |
random_mult, | |
random_matrix, | |
random_simple | |
])() | |
# playlist | |
effect_time = 10 # seconds | |
effects_count = 0 | |
effects_limit = None | |
effects = [ | |
(multiply(hue(rings), roni), effect_time), | |
(random_effect(), effect_time), | |
] | |
# main loop | |
now = time.monotonic() | |
try: | |
while True: | |
start = now | |
while True: | |
now = time.monotonic() | |
remaining = effects[0][1] - (now - start) | |
if remaining < 0: | |
remaining = 0 | |
if display.channels == 3: | |
display.buf[:] = effects[0][0](now) | |
if remaining < 1: | |
display.buf[:] = display.buf * remaining | |
display.buf[:] = display.buf + (effects[1][0](now) * (1-remaining)) | |
elif display.channels == 1: | |
display.buf[:,:,0] = effects[0][0](now)[:,:,1] | |
if remaining < 1: | |
display.buf[:,:,0] = display.buf[:,:,0] * remaining | |
display.buf[:,:,0] = display.buf[:,:,0] + (effects[1][0](now)[:,:,1] * (1-remaining)) | |
display.show() | |
if remaining == 0: | |
break | |
time.sleep(0.001) | |
effect = effects.pop(0) | |
effects.append((random_effect(), effect_time)) | |
effects_count += 1 | |
if effects_limit is not None and effects_count >= effects_limit: | |
break | |
except KeyboardInterrupt: | |
pass | |
except Exception as e: | |
raise | |
finally: | |
display.off() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment