Created
January 2, 2022 23:43
-
-
Save tshirtman/d5334e3486be6fdeaa1a6b31f2b7c8ed to your computer and use it in GitHub Desktop.
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
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