Skip to content

Instantly share code, notes, and snippets.

@cvpe
Created May 7, 2020 15:16
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save cvpe/2a975b157fdebac50161c022b08149a0 to your computer and use it in GitHub Desktop.
Photos Book.py
# - bug: if too much photos (10?), previous at left disappears too quickly
# - bug: rerun sometimes crashes even if Pythonista app restarts too quickly
from objc_util import *
import ui
import math
import photos
import time
import threading
load_framework('SceneKit')
SCNView, SCNScene, SCNPlane, SCNNode, SCNMaterial, SCNCamera, SCNAction, SCNLookAtConstraint = map(ObjCClass, ['SCNView', 'SCNScene', 'SCNPlane' , 'SCNNode', 'SCNMaterial', 'SCNCamera', 'SCNAction', 'SCNLookAtConstraint' ])
class my_thread(threading.Thread):
def __init__(self,wt,node,ui_view):
threading.Thread.__init__(self)
self.wt = wt
self.node = node
self.ui_view = ui_view
def run(self):
time.sleep(self.wt)
self.node.removeFromParentNode()
self.node = None
if self.ui_view:
self.ui_view['play_button'].enabled = True
self.ui_view['pick_button'].enabled = True
class MyView(ui.View):
def __init__(self,w,h):
self.width = w
self.height = h
self.name = 'Photos Books via SceneKit'
self.background_color = 'white'
self.delay_rotate = 2
self.delay_watch = 2
self.assets = None
d_statusbar = 30
d_button = 32
d_between = 10
# End button
end_button = ui.Button(name='end_button')
end_button.title = '❌'
end_button.frame = (d_between, d_statusbar, d_button, d_button)
end_button.font= ('Courier-Bold',20)
end_button.action = self.end_button_action
#end_button.border_width = 1
self.add_subview(end_button)
# Pick button
pick_button = ui.Button(name='pick_button')
pick_button.title = 'pick'
d = 50
pick_button.frame = (self.width-d-d_button, d_statusbar, d, d_button)
pick_button.action = self.pick_button_action
pick_button.enabled = True
self.add_subview(pick_button)
# Play button
play_button = ui.Button(name='play_button')
play_button.title = 'play'
d = 50
play_button.frame = (pick_button.x-d_between-d, d_statusbar, d, d_button)
play_button.action = self.play_button_action
play_button.enabled = False
self.add_subview(play_button)
# slider for delay_rotate
slr = ui.Slider()
d = 100
slr.frame = (play_button.x-d_between-d, d_statusbar+12, d, d_button-12)
slr.value = self.delay_rotate/10
slr.action = self.slider_delay_rotate
self.add_subview(slr)
sllr = ui.Label(name='delay_rotate')
sllr.frame = (slr.x, d_statusbar, slr.width, 12)
sllr.font = ('Arial',12)
sllr.text = 'rotate page in '+str(self.delay_rotate)+'"'
self.add_subview(sllr)
# slider for delay_watch
slw = ui.Slider()
slw.frame = (slr.x-d_between-100, d_statusbar+12, 100, d_button-12)
slw.value = self.delay_watch/10
slw.action = self.slider_delay_watch
self.add_subview(slw)
sllw = ui.Label(name='delay_watch')
sllw.frame = (slw.x, d_statusbar, slw.width, 12)
sllw.font = ('Arial',12)
sllw.text = 'show during '+str(self.delay_rotate)+'"'
self.add_subview(sllw)
# textfield for title
tf = ui.TextField(name='title')
x = end_button.x+end_button.width+d_between
tf.frame = (x,d_statusbar,slw.x-x-d_between,d_button)
tf.text = 'Photos Book'
tf.begin_editing()
self.add_subview(tf)
# horizontal line
line_label = ui.Label()
y = end_button.y+end_button.height+d_between
line_label.frame = (0,y,self.width,1)
line_label.border_width = 1
self.add_subview(line_label)
self.scene_uiview = ui.View(name='scene_uiview')
y = line_label.y+line_label.height
self.scene_uiview.frame = (0,y,self.width,self.height-y)
self.add_subview(self.scene_uiview)
self.self_objc = ObjCInstance(self.scene_uiview)
self.scene_view = SCNView.alloc().initWithFrame_options_(((0, 0),(self.scene_uiview.width, self.scene_uiview.height)), None).autorelease()
self.scene_view.setAutoresizingMask_(18)
self.scene_view.setAllowsCameraControl_(True)
self.self_objc.addSubview_(self.scene_view)
self.scene = SCNScene.scene()
self.scene_view.setScene_(self.scene)
self.root_node = self.scene.rootNode()
self.camera = SCNCamera.camera()
self.camera_node = SCNNode.node()
self.camera_node.setCamera(self.camera)
self.camera_node.setPosition((0,0,2.6))
self.root_node.addChildNode_(self.camera_node)
def slider_delay_rotate(self,sender):
self.delay_rotate = 1 + int(sender.value * 10)
self['delay_rotate'].text = 'rotate page in '+str(self.delay_rotate)+'"'
def slider_delay_watch(self,sender):
self.delay_watch = int(sender.value * 10)
self['delay_watch'].text = 'show during '+str(self.delay_watch)+'"'
def tableview_did_select(self, tableview, section, row):
tableview.selected = row
tableview.close()
def pick_button_action(self,sender):
self['play_button'].enabled = False
all_assets = photos.get_assets()
self.assets = photos.pick_asset(assets=all_assets, title='pick photos of the book', multi=True)
if not self.assets:
# cancel by user
return
self['play_button'].enabled = True
def play_button_action(self,sender):
if self['play_button'].title == 'stop':
# force stop of non_stop thread
self.force_end = True
self['play_button'].title = 'play'
self['pick_button'].enabled = True
return
self['play_button'].enabled = False
self['pick_button'].enabled = False
self['title'].end_editing()
self.title = self['title'].text
ui.delay(self.process_book,0.1)
@on_main_thread
def process_book(self):
self.assets.insert(0,'cover') # front cover
if (len(self.assets) % 2) != 0: # even nbr of photos (odd with cover)
self.assets.append('color') # back cover
self.rotate_action = SCNAction.rotateByAngle_aroundAxis_duration_(-math.pi,(0, 1, 0), self.delay_rotate )
wp = 1
i_photo = 0
for asset in self.assets:
plane_geometry = SCNPlane.planeWithWidth_height_(wp,wp)
Material = SCNMaterial.material()
if type(asset) is str:
#print(asset)
if asset == 'cover':
with ui.ImageContext(100, 100) as ctx:
wt, ht = ui.measure_string(self.title, font= ('Arial Rounded MT Bold',12))
rect = ui.Path.rect(0, 0, 100, 100)
ui.set_color((1,0,0,1))
rect.fill()
img = self.assets[1].get_ui_image()
wi,hi = img.size
wc = 60
hc = wc * hi/wi
if hc > 60:
hc = 60
wc = hc * wi/hi
img.draw((100-wc)/2,(100-hc)/2,wc,hc)
ui.draw_string(self.title,rect=((100-wt)/2,0,0,0), color='white', font= ('Arial Rounded MT Bold',12))
ui_image = ctx.get_image()
Material.contents = ObjCInstance(ui_image)
else: # back cover = "color"
Material.contents = ObjCClass('UIColor').colorWithRed_green_blue_alpha_(1,0,0,1.0)
else:
# center the photo without cropping
ui_image = asset.get_ui_image()
wh = ui_image.size
thumb = self.scene_uiview.width
d = 100
dd = 4
if wh[0] < wh[1]:
# photo is higher than wide
h = thumb
w = int(h * (wh[0]/wh[1]))
x = (thumb-w)/2
y = 0
else:
# photo is larger than high
w = thumb
h = int(w * (wh[1]/wh[0]))
x = 0
y = (thumb-h)/2
with ui.ImageContext(thumb+d,thumb+d) as ctx:
rect_b = ui.Path.rect(0, 0, thumb+d, thumb+d)
ui.set_color('black')
rect_b.fill()
rect = ui.Path.rect(dd, dd, thumb+d-2*dd, thumb+d-2*dd)
ui.set_color('antiquewhite') #(0.9,0.9,0.9,1))
rect.fill()
ui_image.draw(x+d/2,y+d/2,w,h)
hf = (d/2)/3
ui.draw_string(str(i_photo), rect=((thumb+d-hf*2)/2, thumb+d-hf*2, 0, 0), font=('Menlo',hf), color='black', alignment=ui.ALIGN_RIGHT)
ui_image = ctx.get_image()
Material.contents = ObjCInstance(ui_image)
#print(dir(Material.contents()))
Materials = [Material]
plane_geometry.setMaterials_(Materials)
plane_node = SCNNode.nodeWithGeometry_(plane_geometry)
plane_node.hidden = True
#plane_node.setPosition((0,0,-i_photo))
# node pivot is a SCNMatrix4 object (matrix 4x4) not known in Pythonista
# to move the rotation axis from image center to a border,
# we need to make a translation matrix for the pivot
# normally via plane_node.setPivot_(SCNMatrix4MakeTranslation(x,y,z))
# but this SCNMatrix4MakeTranslation is unknown
tx,ty,tz = (-wp/2,0,0)
if (i_photo % 2) != 0:
# odd: rotation of pi around y axis then translation to right
# even photo must be transformed so it is at the back of SCNPlane
x = (-1,0,0,0, 0,1,0,0, 0,0,-1,0, -tx,ty,tz,1)
else:
# even: translation to left
x = (1,0,0,0, 0,1,0,0, 0,0,1,0, tx,ty,tz,1)
plane_node.setPivot_(x)
self.root_node.addChildNode_(plane_node)
# Add a constraint to the camera to keep it pointing to the target node
#constraint = SCNLookAtConstraint.lookAtConstraintWithTarget_(plane_node)
#constraint.gimbalLockEnabled = True
#camera_node.constraints = [constraint]
actions_array = []
if i_photo == 0:
actions_array.append(SCNAction.unhide())
actions_array.append(SCNAction.waitForDuration_(self.delay_watch))
actions_array.append(self.rotate_action)
total_t = self.delay_watch + self.delay_rotate
elif (i_photo % 2) == 0:
t = ((i_photo/2)-1)*(self.delay_watch+self.delay_rotate)+self.delay_watch
actions_array.append(SCNAction.waitForDuration_(t))
actions_array.append(SCNAction.unhide())
actions_array.append(SCNAction.waitForDuration_(self.delay_watch + self.delay_rotate))
actions_array.append(self.rotate_action)
total_t = t + self.delay_watch + self.delay_rotate + self.delay_rotate
else:
t = (i_photo // 2)*(self.delay_watch+self.delay_rotate)
actions_array.append(SCNAction.waitForDuration_(t))
actions_array.append(SCNAction.unhide())
actions_array.append(SCNAction.waitForDuration_(self.delay_watch))
actions_array.append(self.rotate_action)
total_t = t + self.delay_watch + self.delay_rotate
# after rotation, wait open+rotation of next
actions_array.append(SCNAction.waitForDuration_(self.delay_watch + self.delay_rotate))
actions_array.append(SCNAction.hide())
actions = SCNAction.sequence_(actions_array)
plane_node.runAction_(actions)
total_t = total_t + self.delay_watch + self.delay_rotate + 3 # security
# thread: wait time puis delete node
if i_photo < (len(self.assets)-1):
ui_view = None
else:
ui_view = self
t = my_thread(total_t,plane_node,ui_view)
t.start()
i_photo = i_photo + 1
def end_button_action(self,sender):
self.force_end = True
self.close()
def main():
# Main code
w, h = ui.get_screen_size()
# Hide script
back = MyView(w,h)
back.present('fullscreen', hide_title_bar=True)
# Protect against import
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment