Last active
June 21, 2020 19:42
-
-
Save salt-die/c52905e575b6ca8f4323ff3a485fdb32 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. | |
""" | |
from kivy.clock import Clock | |
from kivy.uix.effectwidget import AdvancedEffectBase, EffectWidget | |
from kivy.uix.scatter import Scatter | |
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__ = 'p', 'v', 'a' # position, velocity, acceleration | |
def __init__(self): | |
for attr in self.__slots__: | |
setattr(self, attr, 0j) | |
def move(self, dx, dy): | |
self.p += complex(dx, dy) | |
def step(self): | |
self.v += (self.a - FRICTION * self.v) / MASS | |
self.p += self.v | |
self.a = 0j | |
return abs(self.v) | |
@property | |
def xy(self): | |
return [self.p.real, self.p.imag] | |
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): | |
a = (self.node_2.p - self.node_1.p) * K | |
self.node_1.a += a | |
self.node_2.a -= a | |
class SpringMesh: | |
__slots__ = '_nodes', 'springs' | |
def __init__(self): | |
self._nodes = tuple(WobblyNode() for _ in range(16)) | |
springs = [] | |
for i, j in product(range(4), repeat=2): | |
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): | |
return self._nodes[4 * key[0] + key[1]] | |
def __iter__(self): | |
return iter(self._nodes) | |
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 | |
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.p = 0j # 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