Skip to content

Instantly share code, notes, and snippets.

@tshirtman
Created January 2, 2022 23:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tshirtman/d5334e3486be6fdeaa1a6b31f2b7c8ed to your computer and use it in GitHub Desktop.
Save tshirtman/d5334e3486be6fdeaa1a6b31f2b7c8ed to your computer and use it in GitHub Desktop.
from kivy.app import App
from kivy.lang.builder import Builder
from kivy.uix.scatterlayout import ScatterLayout
from kivy.uix.image import AsyncImage
from kivy.properties import NumericProperty, ListProperty, StringProperty
from kivy.core.window import Window
from kivy.clock import Clock
from kivy.vector import Vector
from kivy.graphics.transformation import Matrix
from kivy import platform
from math import radians
KV = """
BoxLayout:
ScalableImage:
source: 'image.jpg'
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
<ScalableImage>:
image_size: image.norm_image_size
image_pos: image.pos
AsyncImage:
id: image
source: root.source
Label:
text: f"{image.norm_image_size}, {root.pos}"
"""
class BaseScatterLayout(ScatterLayout):
rotation_transition = NumericProperty(0.02 if platform in ['android', 'ios'] else 0.015)
image_size = ListProperty((0, 0))
image_pos = ListProperty((0, 0))
def transform_with_touch(self, touch):
# just do a simple one finger drag
changed = False
if len(self._touches) == self.translation_touches:
# _last_touch_pos has last pos in correct parent space,
# just like incoming touch
dx = (touch.x - self._last_touch_pos[touch][0]) * self.do_translation_x
dy = (touch.y - self._last_touch_pos[touch][1]) * self.do_translation_y
dx = dx / self.translation_touches
dy = dy / self.translation_touches
# checking if we can move the image
if self.scatter_in_widget(dx, dy):
self.apply_transform(Matrix().translate(dx, dy, 0))
changed = True
if len(self._touches) == 1:
return changed
# we have more than one touch... list of last known pos
points = [Vector(self._last_touch_pos[t]) for t in self._touches if t is not touch]
# add current touch last
points.append(Vector(touch.pos))
# we only want to transform if the touch is part of the two touches
# farthest apart! So first we find anchor, the point to transform
# around as another touch farthest away from current touch's pos
anchor = max(points[:-1], key=lambda p: p.distance(touch.pos))
# now we find the touch farthest away from anchor, if its not the
# same as touch. Touch is not one of the two touches used to transform
farthest = max(points, key=anchor.distance)
if farthest is not points[-1]:
return changed
# ok, so we have touch, and anchor, so we can actually compute the
# transformation
old_line = Vector(*touch.ppos) - anchor
new_line = Vector(*touch.pos) - anchor
if not old_line.length(): # div by zero
return changed
# we don't want to allow rotation when already zoomed in, if we didn't
# rotate first, probably cleaner to use flags though
do_rotation = self.do_rotation and (self.rotation % 90 or self.scale == 1)
if do_rotation:
angle = radians(new_line.angle(old_line))
if angle:
changed = True
if abs(angle) > self.rotation_transition:
self.apply_transform(Matrix().rotate(angle, 0, 0, 1), anchor=self.center)
if self.do_scale:
scale = new_line.length() / old_line.length()
new_scale = scale * self.scale
if new_scale < self.scale_min:
scale = self.scale_min / self.scale
elif new_scale > self.scale_max:
scale = self.scale_max / self.scale
self.apply_transform(Matrix().scale(scale, scale, scale), anchor=anchor)
changed = True
return changed
def scatter_in_widget(self, dx, dy):
# does not take into account corner points
# self.top + dy - swipe from top to bottom
# self.y + dy - swipe from bottom to top
# use the normalized image size to know the maximum translation in each
# direction
img_width, img_height = Vector(self.image_size) * self.scale
img_x = self.center_x - img_width / 2
img_y = self.center_y - img_height / 2
widget_width, widget_height = self.size
widget_x, widget_y = self.to_parent(*self.pos)
widget_right, widget_top = self.to_parent(self.right, self.top)
if (widget_x + dx <= img_x
and widget_y + dy <= img_y
and widget_right + dx >= img_x + img_width
and widget_top + dy >= img_y + img_height
):
return True
return False
class ScalableImage(BaseScatterLayout):
scale_min = NumericProperty(1)
scale_max = NumericProperty(3)
source = StringProperty()
def __init__(self, **kw):
super().__init__(**kw)
self.bind(on_touch_down=self.on_image_touch_down,
on_touch_up=self.on_image_touch_up,
on_touch_move=self.on_image_touch_move)
self.bind(scale=self.update_scatter_img_size, size=self.centering_image)
def update_scatter_img_size(self, *args):
self.image_size = self.size
def on_image_touch_down(self, inst, touch):
pass
def on_image_touch_up(self, inst, touch):
if len(self._touches) <= self.translation_touches:
self.image_rotate(round(self.rotation, 1))
def centering_image(self, *args):
self.center = self.size[0] / 2, self.size[1] / 2
def on_image_touch_move(self, inst, touch):
pass
def image_rotate(self, rotation: float):
print("image rotate")
if 0 <= rotation < 45: # right
rotate_to = 0.0
elif 315 < rotation <= 360: # left
rotate_to = 360.0
elif 225 < rotation <= 315:
rotate_to = 270.0
elif 135 < rotation <= 225:
rotate_to = 180.0
elif 45 <= rotation <= 135:
rotate_to = 90.0
else:
rotate_to = 0.0
if rotate_to != rotation:
self.rotation = rotate_to
self.centering_image()
class TestApp(App):
def build(self):
return Builder.load_string(KV)
TestApp().run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment