From cb283c002193c1acb6d7cf1746e2038fe81bb1b4 Mon Sep 17 00:00:00 2001 From: Rahim Packir Saibo Date: Sun, 3 Aug 2014 00:12:15 +0100 Subject: [PATCH 1/4] Added 'snow' pattern - a slightly reworked rain, slower, pastels, interpolation --- patterns/snow.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 patterns/snow.py diff --git a/patterns/snow.py b/patterns/snow.py new file mode 100644 index 0000000..fbe0661 --- /dev/null +++ b/patterns/snow.py @@ -0,0 +1,66 @@ +# 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) +SPAWN_PROBABILITY = 0.5 + +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(0.2, 0.1) + self.color = cubehelper.mix_color(cubehelper.random_color(), WHITE, 0.9) + def tick(self): + self.z -= self.speed + if self.z >= 0: + self.set_pixel_interpolate_float_z(self.x, self.y, self.z, self.color) + # TODO: generalize interpolation, place in cubehelper + # assumes z > 0 + def set_pixel_interpolate_float_z(self, x, y, z, color): + # we're always on a pixel, or between two + if float(int(z)) == z: # on pixel + self.cube.set_pixel((x, y, int(z)), color) + else: # between + z0 = int(math.floor(z)) + z1 = int(math.floor(z+1)) + z1_strength = z - z0 + z0_strength = 1 - z1_strength + + self.cube.set_pixel((x, y, z0), cubehelper.mix_color(BLACK, color, z0_strength)) + self.cube.set_pixel((x, y, z1), cubehelper.mix_color(BLACK, color, z1_strength)) + +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 0.1 + 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) From 5c0b258a54d5ac13e72b27a17b53a2524c822641 Mon Sep 17 00:00:00 2001 From: Rahim Packir Saibo Date: Sun, 3 Aug 2014 00:40:26 +0100 Subject: [PATCH 2/4] Cleaned up snow a little further, extracted frame rate --- patterns/snow.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/patterns/snow.py b/patterns/snow.py index fbe0661..9fa16e1 100644 --- a/patterns/snow.py +++ b/patterns/snow.py @@ -8,7 +8,10 @@ BLACK = (0,0,0) WHITE = (255,255,255) -SPAWN_PROBABILITY = 0.5 + +FRAME_RATE = 25 +FRAME_TIME = 1.0 / FRAME_RATE +SPAWN_PROBABILITY = 5.0 / FRAME_RATE class Drop(object): def __init__(self, cube, x, y): @@ -18,10 +21,10 @@ def __init__(self, cube, x, y): self.z = -1 def reset(self): self.z = self.cube.size - self.speed = random.uniform(0.2, 0.1) + self.speed = random.uniform(1, 2) self.color = cubehelper.mix_color(cubehelper.random_color(), WHITE, 0.9) def tick(self): - self.z -= self.speed + self.z -= self.speed * FRAME_TIME if self.z >= 0: self.set_pixel_interpolate_float_z(self.x, self.y, self.z, self.color) # TODO: generalize interpolation, place in cubehelper @@ -47,7 +50,7 @@ def init(self): 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 0.1 + return FRAME_TIME def spawn(self): d = self.unused.pop(random.randrange(len(self.unused))) d.reset() From 438f1ebaf4fc258f25aeaa6016369d73b4b91ecc Mon Sep 17 00:00:00 2001 From: Rahim Packir Saibo Date: Sun, 3 Aug 2014 12:55:52 +0100 Subject: [PATCH 3/4] Tweaked saturation so that colour differences perceptible on real cube --- patterns/snow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/snow.py b/patterns/snow.py index 9fa16e1..45ab760 100644 --- a/patterns/snow.py +++ b/patterns/snow.py @@ -22,7 +22,7 @@ def __init__(self, cube, x, y): def reset(self): self.z = self.cube.size self.speed = random.uniform(1, 2) - self.color = cubehelper.mix_color(cubehelper.random_color(), WHITE, 0.9) + self.color = cubehelper.mix_color(cubehelper.random_color(), WHITE, 0.7) def tick(self): self.z -= self.speed * FRAME_TIME if self.z >= 0: From 0ab25323accb54228aa426acab94b490ecfe1cd5 Mon Sep 17 00:00:00 2001 From: Rahim Packir Saibo Date: Sun, 3 Aug 2014 18:45:40 +0100 Subject: [PATCH 4/4] Added general interpolation method from single position in floating point space to multiple pixels --- cubehelper.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ patterns/snow.py | 18 ++-------- serialcube.py | 9 +++++ 3 files changed, 98 insertions(+), 15 deletions(-) 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/snow.py b/patterns/snow.py index 45ab760..9187a49 100644 --- a/patterns/snow.py +++ b/patterns/snow.py @@ -26,21 +26,9 @@ def reset(self): def tick(self): self.z -= self.speed * FRAME_TIME if self.z >= 0: - self.set_pixel_interpolate_float_z(self.x, self.y, self.z, self.color) - # TODO: generalize interpolation, place in cubehelper - # assumes z > 0 - def set_pixel_interpolate_float_z(self, x, y, z, color): - # we're always on a pixel, or between two - if float(int(z)) == z: # on pixel - self.cube.set_pixel((x, y, int(z)), color) - else: # between - z0 = int(math.floor(z)) - z1 = int(math.floor(z+1)) - z1_strength = z - z0 - z0_strength = 1 - z1_strength - - self.cube.set_pixel((x, y, z0), cubehelper.mix_color(BLACK, color, z0_strength)) - self.cube.set_pixel((x, y, z1), cubehelper.mix_color(BLACK, color, z1_strength)) + 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): 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()