Skip to content

Instantly share code, notes, and snippets.

@salt-die
Last active June 25, 2020 00:14
Show Gist options
  • Save salt-die/79ae8e64226be9c903444bcdca061ca1 to your computer and use it in GitHub Desktop.
Save salt-die/79ae8e64226be9c903444bcdca061ca1 to your computer and use it in GitHub Desktop.
another kivy toy, image reconstructs itself
import numpy as np
from PIL import Image
from kivy.app import App
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.uix.widget import Widget
from kivy.graphics import Color, Rectangle
FRICTION = .9
MAX_VELOCITY = .1
RECTS = 1e3
IMAGE_SCALE = .75
POKE_POWER = 1e-2
def get_image_and_aspect(file):
"""Return the image and calculate and return the correct rects per row/ rects per col based on the aspect
ratio of the image given the filename.
"""
with Image.open(file) as image:
w, h = image.size
image = np.frombuffer(image.tobytes(), dtype=np.uint8)
image = image.reshape((h, w, 4))
rects_per_row = (RECTS * w / h)**.5
rects_per_column = RECTS / rects_per_row
return image, int(rects_per_row), int(rects_per_column)
PYTHON_LOGO = get_image_and_aspect('python_discord_logo.png')
def rect_setup():
"""Yield the position and color of the rects that make up the image."""
image, rects_per_row, rects_per_column = PYTHON_LOGO
x_scale, y_scale = 1 / rects_per_row, 1 / rects_per_column
x_offset, y_offset = (1 - IMAGE_SCALE) / 2, .1 # Lower-left corner offset of image.
h, w, _ = image.shape
for x in range(rects_per_row):
x = x_scale * x
for y in range(rects_per_column):
y = y_scale * y
sample_loc = int(y * h), int(x * w)
r, g, b, a = image[sample_loc]
if not a:
continue
rect_x = x * IMAGE_SCALE + x_offset
rect_y = (1 - y) * IMAGE_SCALE + y_offset
normalized_color = r / 255, g / 255, b / 255, a / 255
yield rect_x, rect_y, normalized_color
class Rect(Rectangle):
def __init__(self, x, y, screen_width, screen_height, color, *args, **kwargs):
super().__init__(*args, **kwargs)
self.start_x = self.x = x
self.start_y = self.y = y
self.color = Color(*color)
self.screen_width = screen_width
self.screen_height = screen_height
self.vel_x = self.vel_y = 0
def step(self):
"""Apply velocity to position. Reverse velocity if we would move out-of-bounds."""
vx, vy = self.vel_x, self.vel_y
if (mag := (vx**2 + vy**2)**.5) > MAX_VELOCITY:
normal = MAX_VELOCITY / mag
vx *= normal
vy *= normal
self.x += vx
self.y += vy
if not 0 <= self.x <= 1:
vx *= -1
self.x += vx
if not 0 <= self.y <= 1:
vy *= -1
self.y += vy
self.vel_x = vx * FRICTION
self.vel_y = vy * FRICTION
self.pos = self.x * self.screen_width, self.y * self.screen_height
class Dust(Widget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setup_canvas()
self.resize_event = Clock.schedule_once(lambda dt: None, 0)
self.bind(size=self._delayed_resize, pos=self._delayed_resize)
self.update = Clock.schedule_interval(self.step, 0)
def get_rect_size(self):
scaled_w = IMAGE_SCALE * self.width
scaled_h = IMAGE_SCALE * self.height
_, rects_per_row, rects_per_column = PYTHON_LOGO
return scaled_w / rects_per_row, scaled_h / rects_per_column
def setup_canvas(self):
w, h = self.width, self.height
size = self.get_rect_size()
with self.canvas:
self.rects = [Rect(x, y, w, h, color, size=size) for x, y, color in rect_setup()]
def _delayed_resize(self, *args):
self.resize_event.cancel()
self.resize_event = Clock.schedule_once(lambda dt: self.resize(*args), .3)
def resize(self, *args):
w, h = self.width, self.height
size = self.get_rect_size()
for rect in self.rects:
rect.screen_width = w
rect.screen_height = h
rect.size = size
def poke(self, touch):
tx, ty = touch.spos
for rect in self.rects:
dx, dy = rect.x - tx, rect.y - ty
d_d = dx**2 + dy**2
if not d_d:
continue
power = POKE_POWER / d_d
rect.vel_x += power * dx
rect.vel_y += power * dy
def on_touch_down(self, touch):
if touch.is_mouse_scrolling:
self.return_to_start()
else:
self.poke(touch)
return True
def on_touch_move(self, touch):
self.poke(touch)
return True
def step(self, dt):
for rect in self.rects:
rect.step()
def return_to_start(self):
for rect in self.rects:
rect.vel_x = rect.vel_y = 0
Animation(x=rect.start_x, y=rect.start_y, duration=2, t='out_cubic').start(rect)
if __name__ == '__main__':
class DustApp(App):
def build(self):
return Dust()
DustApp().run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment