Skip to content

Commit

Permalink
Cosmic Unicorn: Fast, numpy effect examples.
Browse files Browse the repository at this point in the history
  • Loading branch information
Gadgetoid committed Feb 13, 2023
1 parent da92ee1 commit f4015f1
Show file tree
Hide file tree
Showing 5 changed files with 561 additions and 0 deletions.
126 changes: 126 additions & 0 deletions micropython/examples/cosmic_unicorn/numpy/fire_effect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import time
import gc
import random
from cosmic import CosmicUnicorn
from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN, PEN_P8
from ulab import numpy

"""
Classic fire effect.
Play with the number of spawns, heat, damping factor and colour palette to tweak it.
"""

# MAXIMUM OVERKILL
# machine.freq(250_000_000)

gu = CosmicUnicorn()
gu.set_brightness(0.5)
graphics = PicoGraphics(DISPLAY_COSMIC_UNICORN, pen_type=PEN_P8)

# Number of random fire spawns
FIRE_SPAWNS = 5

# Fire damping
DAMPING_FACTOR = 0.98

# TURN UP THE HEEEAAT
HEAT = 3.0

# Create the fire palette
"""
# Raging Gas Inferno
graphics.create_pen(0, 0, 0)
graphics.create_pen(0, 0, 0)
graphics.create_pen(20, 20, 20)
graphics.create_pen(50, 10, 0)
graphics.create_pen(180, 30, 0)
graphics.create_pen(220, 160, 0)
graphics.create_pen(255, 255, 180)
graphics.create_pen(255, 255, 220)
graphics.create_pen(90, 90, 255)
graphics.create_pen(255, 0, 255)
"""
# Original Colours
graphics.create_pen(0, 0, 0)
graphics.create_pen(20, 20, 20)
graphics.create_pen(180, 30, 0)
graphics.create_pen(220, 160, 0)
graphics.create_pen(255, 255, 180)

PALETTE_SIZE = 5 # Should match the number of colours defined above


def update():
# Clear the bottom two rows (off screen)
heat[height - 1][:] = 0.0
heat[height - 2][:] = 0.0

# Add random fire spawns
for c in range(FIRE_SPAWNS):
x = random.randint(0, width - 4) + 2
heat[height - 1][x - 1:x + 1] = HEAT / 2.0
heat[height - 2][x - 1:x + 1] = HEAT

# Propagate the fire upwards
a = numpy.roll(heat, -1, axis=0) # y + 1, x
b = numpy.roll(heat, -2, axis=0) # y + 2, x
c = numpy.roll(heat, -1, axis=0) # y + 1
d = numpy.roll(c, 1, axis=1) # y + 1, x + 1
e = numpy.roll(c, -1, axis=1) # y + 1, x - 1

# Average over 5 adjacent pixels and apply damping
heat[:] += a + b + d + e
heat[:] *= DAMPING_FACTOR / 5.0


def draw():
# Copy the fire effect to the framebuffer
# Clips the fire to 0.0 to 1.0
# Multiplies it by the number of palette entries (-1) to turn it into a palette index
# Converts to uint8_t datatype to match the framebuffer
memoryview(graphics)[:] = numpy.ndarray(numpy.clip(heat[0:32, 0:32], 0, 1) * (PALETTE_SIZE - 1), dtype=numpy.uint8).tobytes()
gu.update(graphics)


width = CosmicUnicorn.WIDTH
height = CosmicUnicorn.HEIGHT + 4
heat = numpy.zeros((height, width))

t_count = 0
t_total = 0

while True:
gc.collect()

if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_UP):
gu.adjust_brightness(+0.01)

if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_DOWN):
gu.adjust_brightness(-0.01)

tstart = time.ticks_ms()
gc.collect()
update()
draw()
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)
118 changes: 118 additions & 0 deletions micropython/examples/cosmic_unicorn/numpy/lava_lamp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import gc
import time
import math
import random
from cosmic import CosmicUnicorn
from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN, PEN_P8
from ulab import numpy

"""
A lava lamp effect, created by blurred, moving particles.
"""

# MAXIMUM OVERKILL
# machine.freq(250_000_000)

gu = CosmicUnicorn()
graphics = PicoGraphics(DISPLAY_COSMIC_UNICORN, pen_type=PEN_P8)
gu.set_brightness(0.5)

width = CosmicUnicorn.WIDTH
height = CosmicUnicorn.HEIGHT
lava = numpy.zeros((height, width))


class Blob():
def __init__(self):
self.x = float(random.randint(0, width - 1))
self.y = float(random.randint(0, height - 1))
self.r = (float(random.randint(0, 40)) / 10.0) + 5.0
self.dx = (float(random.randint(0, 2)) / 20.0) - 0.05
self.dy = (float(random.randint(0, 2)) / 20.0) - 0.025 # positive bias

def move(self):
self.x += self.dx
self.y += self.dy

if self.x < 0.0 or self.x >= float(width):
self.x = max(0.0, self.x)
self.x = min(float(width - 1), self.x)
self.dx = -self.dx

if self.y < 0.0 or self.y >= float(height):
self.y = max(0.0, self.y)
self.y = min(float(width - 1), self.y)
self.dy = -self.dy


blobs = [Blob() for _ in range(10)]


# Fill palette with a steep falloff from bright red to dark blue
PAL_COLS = 9
for x in range(PAL_COLS):
graphics.create_pen_hsv(0.5 + math.log(x + 1, PAL_COLS + 1) / 2.0, 1.0, math.log(x + 1, PAL_COLS + 1))


def update():
# Update the blobs and draw them into the effect
for blob in blobs:
blob.move()
lava[int(blob.y)][int(blob.x)] = blob.r

# Propogate the blobs outwards
a = numpy.roll(lava, 1, axis=0)
b = numpy.roll(lava, -1, axis=0)
d = numpy.roll(lava, 1, axis=1)
e = numpy.roll(lava, -1, axis=1)

# Average over 5 adjacent pixels and apply damping
lava[:] += a + b + d + e
lava[:] *= 0.97 / 5.0


def draw():
# Copy the lava effect to the framebuffer
# Clips to 0.0 - 1.0
# Multiplies by palette entries (-1) to turn it into a palette index
# Converts to uint8_t datatype to match the framebuffer
memoryview(graphics)[:] = numpy.ndarray(numpy.clip(lava, 0.0, 1.0) * (PAL_COLS - 1), dtype=numpy.uint8).tobytes()
gu.update(graphics)


t_count = 0
t_total = 0

while True:
if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_UP):
gu.adjust_brightness(+0.01)

if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_DOWN):
gu.adjust_brightness(-0.01)

tstart = time.ticks_ms()
gc.collect()
update()
draw()
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)
89 changes: 89 additions & 0 deletions micropython/examples/cosmic_unicorn/numpy/the_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import gc
import time
import random
from cosmic import CosmicUnicorn
from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN, PEN_P8
from ulab import numpy

"""
HELLO NEO.
"""

# MAXIMUM OVERKILL
# machine.freq(250_000_000)

gu = CosmicUnicorn()
gu.set_brightness(1.0)
graphics = PicoGraphics(DISPLAY_COSMIC_UNICORN, pen_type=PEN_P8)


# Fill half the palette with GREEEN
for g in range(128):
_ = graphics.create_pen(0, g, 0)

# And half with bright green for white sparkles
for g in range(128):
_ = graphics.create_pen(128, 128 + g, 128)


def update():
trippy[:] *= 0.65

for _ in range(2):
x = random.randint(0, width - 1)
y = random.randint(0, height // 2)
trippy[y][x] = random.randint(128, 255) / 255.0

# Propagate downwards
old = numpy.ndarray(trippy) * 0.5
trippy[:] = numpy.roll(trippy, 1, axis=0)
trippy[:] += old


def draw():
# Copy the effect to the framebuffer
memoryview(graphics)[:] = numpy.ndarray(numpy.clip(trippy, 0, 1) * 254, dtype=numpy.uint8).tobytes()
gu.update(graphics)


width = CosmicUnicorn.WIDTH
height = CosmicUnicorn.HEIGHT
trippy = numpy.zeros((height, width))

t_count = 0
t_total = 0


while True:
if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_UP):
gu.adjust_brightness(+0.01)

if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_DOWN):
gu.adjust_brightness(-0.01)

tstart = time.ticks_ms()
gc.collect()
update()
draw()
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)
Loading

0 comments on commit f4015f1

Please sign in to comment.