diff --git a/cubehelper.py b/cubehelper.py index 31403d5..af2fa43 100644 --- a/cubehelper.py +++ b/cubehelper.py @@ -99,6 +99,9 @@ def color_to_int(color): b = int(b * 256.0 - 0.5) return (r, g, b) +# WARN: rounding behaviour isn't perfect, eg +# >>> cubehelper.color_to_float((0,0,0)) +# (0.001953125, 0.001953125, 0.001953125) def color_to_float(color): if isinstance(color, numbers.Integral): r = color >> 16 @@ -111,3 +114,86 @@ def color_to_float(color): g = (g + 0.5) / 256.0 b = (b + 0.5) / 256.0 return (r, g, b) + +# in 3d space any floating coord lies between 8 pixels (or exactly on/between) +# return a list of the surrounding 8 pixels with the color scaled proportionately +# ie list of 8x ((x,y,z), (r,g,b)) +# Due to the rounding issues in color_to_float we end up with values where +# we'd expect 0 (which would allow us to clean up black pixels) +def interpolated_pixels_from_point(xyz, color): + (x, y, z) = xyz + + x0 = int(x) + y0 = int(y) + z0 = int(z) + + x1 = x0 + 1 + y1 = y0 + 1 + z1 = z0 + 1 + + # weightings on each axis + x1w = x - x0 + y1w = y - y0 + z1w = z - z0 + + x0w = 1 - x1w + y0w = 1 - y1w + z0w = 1 - z1w + + # weighting for each of the pixels + c000w = x0w * y0w * z0w + c001w = x0w * y0w * z1w + c010w = x0w * y1w * z0w + c011w = x0w * y1w * z1w + c100w = x1w * y0w * z0w + c101w = x1w * y0w * z1w + c110w = x1w * y1w * z0w + c111w = x1w * y1w * z1w + + black = (0,0,0) + + c000 = mix_color(black, color, c000w) + c001 = mix_color(black, color, c001w) + c010 = mix_color(black, color, c010w) + c011 = mix_color(black, color, c011w) + c100 = mix_color(black, color, c100w) + c101 = mix_color(black, color, c101w) + c110 = mix_color(black, color, c110w) + c111 = mix_color(black, color, c111w) + + return [ + ((x0, y0, z0), c000), + ((x0, y0, z1), c001), + ((x0, y1, z0), c010), + ((x0, y1, z1), c011), + ((x1, y0, z0), c100), + ((x1, y0, z1), c101), + ((x1, y1, z0), c110), + ((x1, y1, z1), c111) + ] + +# This doesn't check for negative coords +def restrict_pixels_to_cube_bounds(pixels, cube_size): + return [ + pixel for pixel in pixels if ( + pixel[0][0] < cube_size and + pixel[0][1] < cube_size and + pixel[0][2] < cube_size ) + ] + +def restrict_pixels_to_non_black(pixels): + threshold = 0.002 # this is mostly to work round the rounding issue above + return [ + pixel for pixel in pixels if ( + pixel[1][0] > threshold and + pixel[1][1] > threshold and + pixel[1][2] > threshold ) + ] + +# provide xyz as float coords, returns a list of between one and 8 pixels +def sanitized_interpolated(xyz, color, cube_size): + return restrict_pixels_to_non_black( + restrict_pixels_to_cube_bounds( + interpolated_pixels_from_point(xyz, color), + cube_size + )) \ No newline at end of file diff --git a/patterns/gravity.py b/patterns/gravity.py new file mode 100644 index 0000000..903e5de --- /dev/null +++ b/patterns/gravity.py @@ -0,0 +1,69 @@ +# Fork of rain - pastel "snow flakes" slowly falling (interpolated) +# Copyright (C) Paul Brook +# Released under the terms of the GNU General Public License version 3 + +import random +import math +import cubehelper + +BLACK = (0,0,0) +WHITE = (255,255,255) + +FRAME_RATE = 25 +FRAME_TIME = 1.0 / FRAME_RATE +SPAWN_PROBABILITY = 5.0 / FRAME_RATE + +GRAVITY = 10.0 +COEFFICIENT_OF_RESTITUTION = 0.9 + +class Drop(object): + def __init__(self, cube, x, y): + self.cube = cube + self.x = x + self.y = y + self.z = -10 + def reset(self): + self.z = self.cube.size + self.speed = 0; #random.uniform(1, 2) + self.color = cubehelper.mix_color(cubehelper.random_color(), WHITE, 0.4) + def tick(self): + self.speed += GRAVITY * FRAME_TIME + self.z -= (self.speed) * FRAME_TIME + # apply bounce + # make sure there's still enough energy left not to sit on bottom + # if there's not gravity will suck them down to be cleaned up + if self.z < 0 and self.speed > GRAVITY: + self.speed = -self.speed * COEFFICIENT_OF_RESTITUTION + + position_float = (self.x,self.y,self.z) + pixels = cubehelper.sanitized_interpolated(position_float, self.color, self.cube.size) + self.cube.set_pixels(pixels) + +class Pattern(object): + def init(self): + self.double_buffer = True + self.drops = [] + self.unused = [] + for x in range(0, self.cube.size): + for y in range(0, self.cube.size): + self.unused.append(Drop(self.cube, x, y)) + return FRAME_TIME + def spawn(self): + try: + d = self.unused.pop(random.randrange(len(self.unused))) + d.reset() + self.drops.append(d) + except ValueError: + pass + def tick(self): + if random.random() < SPAWN_PROBABILITY: + self.spawn() + drops = self.drops; + self.drops = [] + self.cube.clear() + for d in drops: + d.tick() + if d.z < -5: + self.unused.append(d) + else: + self.drops.append(d) diff --git a/patterns/snow.py b/patterns/snow.py new file mode 100644 index 0000000..9187a49 --- /dev/null +++ b/patterns/snow.py @@ -0,0 +1,57 @@ +# Fork of rain - pastel "snow flakes" slowly falling (interpolated) +# Copyright (C) Paul Brook +# Released under the terms of the GNU General Public License version 3 + +import random +import math +import cubehelper + +BLACK = (0,0,0) +WHITE = (255,255,255) + +FRAME_RATE = 25 +FRAME_TIME = 1.0 / FRAME_RATE +SPAWN_PROBABILITY = 5.0 / FRAME_RATE + +class Drop(object): + def __init__(self, cube, x, y): + self.cube = cube + self.x = x + self.y = y + self.z = -1 + def reset(self): + self.z = self.cube.size + self.speed = random.uniform(1, 2) + self.color = cubehelper.mix_color(cubehelper.random_color(), WHITE, 0.7) + def tick(self): + self.z -= self.speed * FRAME_TIME + if self.z >= 0: + position_float = (self.x,self.y,self.z) + pixels = cubehelper.sanitized_interpolated(position_float, self.color, self.cube.size) + self.cube.set_pixels(pixels) + +class Pattern(object): + def init(self): + self.double_buffer = True + self.drops = [] + self.unused = [] + for x in range(0, self.cube.size): + for y in range(0, self.cube.size): + self.unused.append(Drop(self.cube, x, y)) + return FRAME_TIME + def spawn(self): + d = self.unused.pop(random.randrange(len(self.unused))) + d.reset() + self.drops.append(d) + def tick(self): + if random.random() < SPAWN_PROBABILITY: + self.spawn() + drops = self.drops; + self.drops = [] + self.cube.clear() + for d in drops: + d.tick() + if d.z < 0: + self.unused.append(d) + else: + self.drops.append(d) diff --git a/serialcube.py b/serialcube.py index e750cad..d4e86fd 100644 --- a/serialcube.py +++ b/serialcube.py @@ -172,6 +172,15 @@ def set_pixel(self, xyz, rgb): self.select_board(board) self.do_cmd(offset, r, g, b) + # This doesn't do anything smart if the same pixel is specified multiple + # times + def set_pixels(self, pixels): + # Potential optimisation: sort the pixels by y + # (minimises board switches) + for pixel in pixels: + (xyz, rgb) = pixel + self.set_pixel(xyz, rgb) + def render(self): self.bus_reset() self._flush_data()