Last active
June 25, 2020 00:14
-
-
Save salt-die/79ae8e64226be9c903444bcdca061ca1 to your computer and use it in GitHub Desktop.
another kivy toy, image reconstructs itself
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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