Skip to content

Instantly share code, notes, and snippets.

@gottadiveintopython
Forked from salt-die/wobblywidget.py
Created May 14, 2020 17:27
Show Gist options
  • Save gottadiveintopython/8b3ae65f9592a93058543b2b3d502fe4 to your computer and use it in GitHub Desktop.
Save gottadiveintopython/8b3ae65f9592a93058543b2b3d502fe4 to your computer and use it in GitHub Desktop.
Make any widget wobble with wobbly widget!
"""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