Making things more... fun?

Basics of Drawing

I went to the docs to copy-paste a few examples for later reference:

from ipycanvas import Canvas

canvas = Canvas(width=200, height=150)

canvas.fill_rect(25, 25, 100, 100)
canvas.clear_rect(45, 45, 60, 60)
canvas.stroke_rect(50, 50, 50, 50)

canvas

Apparently it's fast... Let's play with some perlin noise from 05.

import days_of_code
from days_of_code.perlin import *
import numpy as np

n_particles = 5_000
steps = 50
delta = 1

x = np.array(np.random.rayleigh(250, n_particles))
y = np.array(np.random.rayleigh(250, n_particles))
size = np.random.randint(1, 3, n_particles)

canvas = Canvas(width=800, height=500, sync_image_data=True)

for i in range(steps):
    # Draw
    canvas.fill_style = 'green'
    canvas.fill_rects(x, y, size)
    
    # Compute new locations
    angles = days_of_code.perlin.perlin(x/100, y/100) * 3
    x += np.sin(angles)*delta
    y += np.cos(angles)*delta

canvas
canvas.to_file('outputs/perlin_ipycanvas1.png') # Saving to file requires sync_image_data=True when making the canvas
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-4-dbfb50efe5a2> in <module>
      1 #skiptest
----> 2 canvas.to_file('outputs/perlin_ipycanvas1.png') # Saving to file requires sync_image_data=True when making the canvas

~/anaconda3/lib/python3.7/site-packages/ipycanvas/canvas.py in to_file(self, filename)
    195         """
    196         if self.image_data is None:
--> 197             raise RuntimeError('No image data to save, please be sure that ``sync_image_data`` is set to True')
    198         if not filename.endswith('.png') and not filename.endswith('.PNG'):
    199             raise RuntimeError('Can only save to a PNG file')

RuntimeError: No image data to save, please be sure that ``sync_image_data`` is set to True

Nice! But the real fun is in making things move

Animations

The idea here is to set things up, animate in an infinite loop with sleep to slow things down, and then have events listening for mouse and keyboard input to change state and such.

from time import sleep
from ipycanvas import Canvas, hold_canvas

canvas = Canvas(width=800, height=500)
display(canvas)

x = np.array(np.random.rayleigh(250, n_particles))
y = np.array(np.random.rayleigh(250, n_particles))
size = np.random.randint(1, 3, n_particles)

for i in range(100): # while(True) for infinite
    with hold_canvas(canvas):
        # Clear the old animation step
        canvas.clear()
        
        canvas.fill_style = 'green'
        canvas.fill_rects(x, y, size)
    
        # Compute new locations
        angles = days_of_code.perlin.perlin(x/100, y/100) * 3
        x += np.sin(angles)*delta
        y += np.cos(angles)*delta
        
    # Animation frequency ~50Hz = 1./50. seconds
    sleep(0.02)

Interaction

I tried to add some interaction during animation with both ipycanvas' mouse events and ipevents - neither worked. It turns out using time.sleep() to run animations blocks everything and the events only get fired once the animation stops. To fix this we can use the threading library to handle running the animation loop in a separate thread.

class RepeatedTimer[source]

RepeatedTimer(interval, function, *args, **kwargs)

from ipywidgets import Label, HTML
from copy import deepcopy

# Setting up the canvas and event handling
canvas = Canvas(width=800, height=500)  
h = HTML('Event info')

x = np.array(np.random.rayleigh(250, n_particles))
y = np.array(np.random.rayleigh(250, n_particles))
size = np.random.randint(1, 3, n_particles)


def update_canvas():
    with hold_canvas(canvas):
        canvas.clear() # Clear the old animation step       
        canvas.fill_style = 'green' 
        canvas.fill_rects(x, y, size) # Draw the new frame 

def handle_mouse_down(xpos, ypos):
    global x, y
    x_new = np.array(np.random.rayleigh(30, 100))+xpos-15
    y_new = np.array(np.random.rayleigh(30, 100))+ypos-15
    x = np.concatenate([x[-4000:], x_new])
    y = np.concatenate([y[-4000:], y_new])
    size = np.random.randint(1, 3, len(x))
    update_canvas()
    
    
    
#     x = np.array(np.random.rayleigh(250, n_particles))
#     y = np.array(np.random.rayleigh(250, n_particles))
#     size = np.random.randint(1, 3, n_particles)
    
    h.value = str(xpos) + '_' + str(x.shape) + '_' + str(x.shape)
#     x = np.array(np.random.rayleigh(250, n_particles))
#     y = np.array(np.random.rayleigh(250, n_particles))
#     size = np.random.randint(1, 3, n_particles)

canvas.on_mouse_down(handle_mouse_down)
display(canvas, h)
def update():
    global x, y
    update_canvas()
    # Compute new locations
    angles = days_of_code.perlin.perlin(x/100, y/100) * 3
    x += np.sin(angles)*delta
    y += np.cos(angles)*delta

interval = 0.02
rt = RepeatedTimer(interval, update) # it auto-starts, no need of rt.start()    
rt.stop()