Skip to content

Commit 6c0c45d

Browse files
committed
Cosmic Unicorn: Fast, numpy effect examples.
1 parent f4b0434 commit 6c0c45d

File tree

5 files changed

+561
-0
lines changed

5 files changed

+561
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import time
2+
import gc
3+
import random
4+
from cosmic import CosmicUnicorn
5+
from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN, PEN_P8
6+
from ulab import numpy
7+
8+
"""
9+
Classic fire effect.
10+
Play with the number of spawns, heat, damping factor and colour palette to tweak it.
11+
"""
12+
13+
# MAXIMUM OVERKILL
14+
# machine.freq(250_000_000)
15+
16+
gu = CosmicUnicorn()
17+
gu.set_brightness(0.5)
18+
graphics = PicoGraphics(DISPLAY_COSMIC_UNICORN, pen_type=PEN_P8)
19+
20+
# Number of random fire spawns
21+
FIRE_SPAWNS = 5
22+
23+
# Fire damping
24+
DAMPING_FACTOR = 0.98
25+
26+
# TURN UP THE HEEEAAT
27+
HEAT = 3.0
28+
29+
# Create the fire palette
30+
"""
31+
# Raging Gas Inferno
32+
graphics.create_pen(0, 0, 0)
33+
graphics.create_pen(0, 0, 0)
34+
graphics.create_pen(20, 20, 20)
35+
graphics.create_pen(50, 10, 0)
36+
graphics.create_pen(180, 30, 0)
37+
graphics.create_pen(220, 160, 0)
38+
graphics.create_pen(255, 255, 180)
39+
graphics.create_pen(255, 255, 220)
40+
graphics.create_pen(90, 90, 255)
41+
graphics.create_pen(255, 0, 255)
42+
"""
43+
# Original Colours
44+
graphics.create_pen(0, 0, 0)
45+
graphics.create_pen(20, 20, 20)
46+
graphics.create_pen(180, 30, 0)
47+
graphics.create_pen(220, 160, 0)
48+
graphics.create_pen(255, 255, 180)
49+
50+
PALETTE_SIZE = 5 # Should match the number of colours defined above
51+
52+
53+
def update():
54+
# Clear the bottom two rows (off screen)
55+
heat[height - 1][:] = 0.0
56+
heat[height - 2][:] = 0.0
57+
58+
# Add random fire spawns
59+
for c in range(FIRE_SPAWNS):
60+
x = random.randint(0, width - 4) + 2
61+
heat[height - 1][x - 1:x + 1] = HEAT / 2.0
62+
heat[height - 2][x - 1:x + 1] = HEAT
63+
64+
# Propagate the fire upwards
65+
a = numpy.roll(heat, -1, axis=0) # y + 1, x
66+
b = numpy.roll(heat, -2, axis=0) # y + 2, x
67+
c = numpy.roll(heat, -1, axis=0) # y + 1
68+
d = numpy.roll(c, 1, axis=1) # y + 1, x + 1
69+
e = numpy.roll(c, -1, axis=1) # y + 1, x - 1
70+
71+
# Average over 5 adjacent pixels and apply damping
72+
heat[:] += a + b + d + e
73+
heat[:] *= DAMPING_FACTOR / 5.0
74+
75+
76+
def draw():
77+
# Copy the fire effect to the framebuffer
78+
# Clips the fire to 0.0 to 1.0
79+
# Multiplies it by the number of palette entries (-1) to turn it into a palette index
80+
# Converts to uint8_t datatype to match the framebuffer
81+
memoryview(graphics)[:] = numpy.ndarray(numpy.clip(heat[0:32, 0:32], 0, 1) * (PALETTE_SIZE - 1), dtype=numpy.uint8).tobytes()
82+
gu.update(graphics)
83+
84+
85+
width = CosmicUnicorn.WIDTH
86+
height = CosmicUnicorn.HEIGHT + 4
87+
heat = numpy.zeros((height, width))
88+
89+
t_count = 0
90+
t_total = 0
91+
92+
while True:
93+
gc.collect()
94+
95+
if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_UP):
96+
gu.adjust_brightness(+0.01)
97+
98+
if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_DOWN):
99+
gu.adjust_brightness(-0.01)
100+
101+
tstart = time.ticks_ms()
102+
gc.collect()
103+
update()
104+
draw()
105+
tfinish = time.ticks_ms()
106+
107+
total = tfinish - tstart
108+
t_total += total
109+
t_count += 1
110+
111+
if t_count == 60:
112+
per_frame_avg = t_total / t_count
113+
print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS")
114+
t_count = 0
115+
t_total = 0
116+
117+
# pause for a moment (important or the USB serial device will fail)
118+
# try to pace at 60fps or 30fps
119+
if total > 1000 / 30:
120+
time.sleep(0.0001)
121+
elif total > 1000 / 60:
122+
t = 1000 / 30 - total
123+
time.sleep(t / 1000)
124+
else:
125+
t = 1000 / 60 - total
126+
time.sleep(t / 1000)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import gc
2+
import time
3+
import math
4+
import random
5+
from cosmic import CosmicUnicorn
6+
from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN, PEN_P8
7+
from ulab import numpy
8+
9+
"""
10+
A lava lamp effect, created by blurred, moving particles.
11+
"""
12+
13+
# MAXIMUM OVERKILL
14+
# machine.freq(250_000_000)
15+
16+
gu = CosmicUnicorn()
17+
graphics = PicoGraphics(DISPLAY_COSMIC_UNICORN, pen_type=PEN_P8)
18+
gu.set_brightness(0.5)
19+
20+
width = CosmicUnicorn.WIDTH
21+
height = CosmicUnicorn.HEIGHT
22+
lava = numpy.zeros((height, width))
23+
24+
25+
class Blob():
26+
def __init__(self):
27+
self.x = float(random.randint(0, width - 1))
28+
self.y = float(random.randint(0, height - 1))
29+
self.r = (float(random.randint(0, 40)) / 10.0) + 5.0
30+
self.dx = (float(random.randint(0, 2)) / 20.0) - 0.05
31+
self.dy = (float(random.randint(0, 2)) / 20.0) - 0.025 # positive bias
32+
33+
def move(self):
34+
self.x += self.dx
35+
self.y += self.dy
36+
37+
if self.x < 0.0 or self.x >= float(width):
38+
self.x = max(0.0, self.x)
39+
self.x = min(float(width - 1), self.x)
40+
self.dx = -self.dx
41+
42+
if self.y < 0.0 or self.y >= float(height):
43+
self.y = max(0.0, self.y)
44+
self.y = min(float(width - 1), self.y)
45+
self.dy = -self.dy
46+
47+
48+
blobs = [Blob() for _ in range(10)]
49+
50+
51+
# Fill palette with a steep falloff from bright red to dark blue
52+
PAL_COLS = 9
53+
for x in range(PAL_COLS):
54+
graphics.create_pen_hsv(0.5 + math.log(x + 1, PAL_COLS + 1) / 2.0, 1.0, math.log(x + 1, PAL_COLS + 1))
55+
56+
57+
def update():
58+
# Update the blobs and draw them into the effect
59+
for blob in blobs:
60+
blob.move()
61+
lava[int(blob.y)][int(blob.x)] = blob.r
62+
63+
# Propogate the blobs outwards
64+
a = numpy.roll(lava, 1, axis=0)
65+
b = numpy.roll(lava, -1, axis=0)
66+
d = numpy.roll(lava, 1, axis=1)
67+
e = numpy.roll(lava, -1, axis=1)
68+
69+
# Average over 5 adjacent pixels and apply damping
70+
lava[:] += a + b + d + e
71+
lava[:] *= 0.97 / 5.0
72+
73+
74+
def draw():
75+
# Copy the lava effect to the framebuffer
76+
# Clips to 0.0 - 1.0
77+
# Multiplies by palette entries (-1) to turn it into a palette index
78+
# Converts to uint8_t datatype to match the framebuffer
79+
memoryview(graphics)[:] = numpy.ndarray(numpy.clip(lava, 0.0, 1.0) * (PAL_COLS - 1), dtype=numpy.uint8).tobytes()
80+
gu.update(graphics)
81+
82+
83+
t_count = 0
84+
t_total = 0
85+
86+
while True:
87+
if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_UP):
88+
gu.adjust_brightness(+0.01)
89+
90+
if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_DOWN):
91+
gu.adjust_brightness(-0.01)
92+
93+
tstart = time.ticks_ms()
94+
gc.collect()
95+
update()
96+
draw()
97+
tfinish = time.ticks_ms()
98+
99+
total = tfinish - tstart
100+
t_total += total
101+
t_count += 1
102+
103+
if t_count == 60:
104+
per_frame_avg = t_total / t_count
105+
print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS")
106+
t_count = 0
107+
t_total = 0
108+
109+
# pause for a moment (important or the USB serial device will fail)
110+
# try to pace at 60fps or 30fps
111+
if total > 1000 / 30:
112+
time.sleep(0.0001)
113+
elif total > 1000 / 60:
114+
t = 1000 / 30 - total
115+
time.sleep(t / 1000)
116+
else:
117+
t = 1000 / 60 - total
118+
time.sleep(t / 1000)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import gc
2+
import time
3+
import random
4+
from cosmic import CosmicUnicorn
5+
from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN, PEN_P8
6+
from ulab import numpy
7+
8+
"""
9+
HELLO NEO.
10+
"""
11+
12+
# MAXIMUM OVERKILL
13+
# machine.freq(250_000_000)
14+
15+
gu = CosmicUnicorn()
16+
gu.set_brightness(1.0)
17+
graphics = PicoGraphics(DISPLAY_COSMIC_UNICORN, pen_type=PEN_P8)
18+
19+
20+
# Fill half the palette with GREEEN
21+
for g in range(128):
22+
_ = graphics.create_pen(0, g, 0)
23+
24+
# And half with bright green for white sparkles
25+
for g in range(128):
26+
_ = graphics.create_pen(128, 128 + g, 128)
27+
28+
29+
def update():
30+
trippy[:] *= 0.65
31+
32+
for _ in range(2):
33+
x = random.randint(0, width - 1)
34+
y = random.randint(0, height // 2)
35+
trippy[y][x] = random.randint(128, 255) / 255.0
36+
37+
# Propagate downwards
38+
old = numpy.ndarray(trippy) * 0.5
39+
trippy[:] = numpy.roll(trippy, 1, axis=0)
40+
trippy[:] += old
41+
42+
43+
def draw():
44+
# Copy the effect to the framebuffer
45+
memoryview(graphics)[:] = numpy.ndarray(numpy.clip(trippy, 0, 1) * 254, dtype=numpy.uint8).tobytes()
46+
gu.update(graphics)
47+
48+
49+
width = CosmicUnicorn.WIDTH
50+
height = CosmicUnicorn.HEIGHT
51+
trippy = numpy.zeros((height, width))
52+
53+
t_count = 0
54+
t_total = 0
55+
56+
57+
while True:
58+
if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_UP):
59+
gu.adjust_brightness(+0.01)
60+
61+
if gu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_DOWN):
62+
gu.adjust_brightness(-0.01)
63+
64+
tstart = time.ticks_ms()
65+
gc.collect()
66+
update()
67+
draw()
68+
tfinish = time.ticks_ms()
69+
70+
total = tfinish - tstart
71+
t_total += total
72+
t_count += 1
73+
74+
if t_count == 60:
75+
per_frame_avg = t_total / t_count
76+
print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS")
77+
t_count = 0
78+
t_total = 0
79+
80+
# pause for a moment (important or the USB serial device will fail)
81+
# try to pace at 60fps or 30fps
82+
if total > 1000 / 30:
83+
time.sleep(0.0001)
84+
elif total > 1000 / 60:
85+
t = 1000 / 30 - total
86+
time.sleep(t / 1000)
87+
else:
88+
t = 1000 / 60 - total
89+
time.sleep(t / 1000)

0 commit comments

Comments
 (0)