-
-
Save gottadiveintopython/8b3ae65f9592a93058543b2b3d502fe4 to your computer and use it in GitHub Desktop.
Make any widget wobble with wobbly widget!
This file contains 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
"""For this to work, WobblyEffect should be parent to WobblyScatter. | |
Try setting WobblyScatter `size_hint = (None, None)` or `size = self.parent.size` | |
if having size issues. | |
""" | |
from kivy.clock import Clock | |
from kivy.uix.effectwidget import AdvancedEffectBase, EffectWidget | |
from kivy.uix.scatter import Scatter | |
from kivy.uix.widget import Widget | |
from itertools import product | |
FRICTION = .95 | |
K = 8 | |
MASS = 25 | |
EPSILON = .001 | |
class WobblyNode: | |
'''Represents the nodes that springs attach to in a spring mesh. | |
''' | |
__slots__ = 'x', 'y', 'vx', 'vy', 'fx', 'fy' | |
def __init__(self): | |
for attr in self.__slots__: | |
setattr(self, attr, 0) | |
def move(self, dx, dy): | |
self.x += dx | |
self.y += dy | |
def apply_force(self, fx, fy): | |
self.fx += fx | |
self.fy += fy | |
def step(self): | |
fx = self.fx - FRICTION * self.vx | |
fy = self.fy - FRICTION * self.vy | |
self.vx += fx / MASS | |
self.vy += fy / MASS | |
self.x += self.vx | |
self.y += self.vy | |
self.fx = self.fy = 0 | |
total_velocity = abs(self.vx) + abs(self.vy) | |
return total_velocity | |
@property | |
def xy(self): | |
return [self.x, self.y] | |
class Spring: | |
__slots__ = 'node_1', 'node_2' | |
def __init__(self, node_1, node_2): | |
self.node_1 = node_1 | |
self.node_2 = node_2 | |
def step(self): | |
dx = (self.node_2.x - self.node_1.x) * K | |
dy = (self.node_2.y - self.node_1.y) * K | |
self.node_1.apply_force(dx, dy) | |
self.node_2.apply_force(-dx, -dy) | |
class SpringMesh: | |
__slots__ = '_nodes', 'springs' | |
def __init__(self, rows=4, cols=4): | |
self._nodes = tuple(tuple(WobblyNode() for _ in range(cols)) for _ in range(rows)) | |
springs = [] | |
for i, j in product(range(cols), range(rows)): | |
if j < 3: | |
springs.append(Spring(self[i, j], self[i, j + 1])) | |
if i < 3: | |
springs.append(Spring(self[i, j], self[i + 1, j])) | |
self.springs = tuple(springs) | |
def __getitem__(self, key): | |
i, j = key | |
return self._nodes[i][j] | |
def __iter__(self): | |
return (node for row in self._nodes for node in row) | |
BEZIER_PATCH = """ | |
uniform vec2 node_xy[16]; | |
const vec4 bin_coeff = vec4(1.0, 3.0, 3.0, 1.0); // hard-coded binomial coefficients | |
vec2 new_coords; | |
vec4 coeff_u, coeff_v; | |
float bernstein_basis(float u, int deg){ | |
return bin_coeff[deg] * pow(u, float(deg)) * pow(1.0 - u, 3.0 - float(deg));} | |
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords){ | |
for (int i = 0; i < 4; i++){ | |
coeff_u[i] = bernstein_basis(tex_coords.x, i); | |
coeff_v[i] = bernstein_basis(tex_coords.y, i);} | |
new_coords = tex_coords; | |
for (int i = 0; i < 4; i++){ | |
for (int j = 0; j < 4; j++){ | |
new_coords += coeff_u[i] * coeff_v[j] * node_xy[4 * i + j];}} | |
return texture2D(texture, new_coords);} | |
""" | |
class WobblyEffect(EffectWidget): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self.bezier = AdvancedEffectBase(glsl=BEZIER_PATCH, uniforms= {'node_xy': [[0, 0] for _ in range(16)]}) | |
self.effects = [self.bezier] | |
class WobblyScatter(Scatter): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self.spring_mesh = SpringMesh() | |
self.anchor = self.spring_mesh[0, 0] | |
self.update = Clock.schedule_interval(self.step, 0) # run this while wobbling... | |
self.update.cancel() # ...and cancel when wobbling slows | |
def on_touch_down(self, touch): | |
if self.collide_point(touch.x, touch.y): | |
anchor_x = round((touch.x - self.x) / self.width / self.scale * 3) | |
anchor_y = round((touch.y - self.y) / self.height / self.scale * 3) | |
self.anchor = self.spring_mesh[anchor_x, anchor_y] | |
return super().on_touch_down(touch) | |
def on_transform_with_touch(self, touch): | |
dx = touch.sx - touch.psx | |
dy = touch.sy - touch.psy | |
if dx or dy: | |
self.anchor.move(dx, dy) | |
self.update() | |
def step(self, dt=0): | |
mesh = self.spring_mesh | |
for spring in mesh.springs: | |
spring.step() | |
total_velocity = sum(node.step() for node in mesh) | |
if total_velocity < EPSILON: | |
self.update.cancel() | |
for i, j in product(range(4), repeat=2): | |
self.parent.bezier.uniforms['node_xy'][4 * i + j] = mesh[i, j].xy | |
self.anchor.x = self.anchor.y = 0 # Move anchor back to starting position. | |
if __name__ == '__main__': | |
from kivy.app import App | |
from kivy.lang import Builder | |
from textwrap import dedent | |
class WobbleExample(App): | |
def build(self): | |
kv = """ | |
WobblyEffect: | |
WobblyScatter: | |
size_hint: None, None | |
Image: | |
source: 'python_discord_logo.png' | |
""" | |
return Builder.load_string(dedent(kv)) | |
WobbleExample().run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment