Created
May 7, 2020 15:16
Star
You must be signed in to star a gist
Photos Book.py
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
# - 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