Package toon :: Module core
[hide private]
[frames] | no frames]

Source Code for Module toon.core

   1  #!/usr/bin/env python 
   2  # 
   3  # Toonloop for Python 
   4  # 
   5  # Copyright 2008 Alexandre Quessy & Tristan Matthews 
   6  # <alexandre@quessy.net> & <le.businessman@gmail.com> 
   7  # http://www.toonloop.com 
   8  # 
   9  # Original idea by Alexandre Quessy 
  10  # http://alexandre.quessy.net 
  11  # 
  12  # Toonloop is free software: you can redistribute it and/or modify 
  13  # it under the terms of the GNU General Public License as published by 
  14  # the Free Software Foundation, either version 3 of the License, or 
  15  # (at your option) any later version. 
  16  # 
  17  # Toonloop is distributed in the hope that it will be useful, 
  18  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  19  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  20  # GNU General Public License for more details. 
  21  # 
  22  # You should have received a copy of the gnu general public license 
  23  # along with Toonloop.  If not, see <http://www.gnu.org/licenses/>. 
  24  # 
  25  """ 
  26  Toonloop is a live stop motion performance tool.  
  27   
  28  The idea is to spread its use for teaching new medias to children and to  
  29  give a professional tool for movie creators. 
  30   
  31  In the left window, you can see what is seen by the live camera. 
  32  In the right window, it is the result of the stop motion loop. 
  33   
  34  INSTALLATION NOTES :  
  35  The camera module for pygame is available from pygame's svn revision  
  36  1744 or greater 
  37  svn co svn://seul.org/svn/pygame/trunk 
  38   
  39  The startup file to execute is toonloop 
  40  """ 
  41  # keep this in sync with man_toonloop.txt 
  42  INTERACTIVE_HELP = """Toonloop interactive keyboard controls : 
  43   - Press the SPACE bar to grab a frame. 
  44   - Press DELETE or BACKSPACE to delete the last frame. 
  45   - Press 'r' to reset and start the current sequence.  
  46     (and remove all its frames) 
  47   - Press 's' to save the current sequence as a Motion-JPEG movie. 
  48   - Press 'i' to print current loop frame number, number of frames in loop  
  49     and global framerate. 
  50   - Press 'h' to print a help message. 
  51   - Press UP to increase frame rate. 
  52   - Press DOWN to decrease frame rate. 
  53   - Press 'a' to toggle on/off the auto recording. 
  54     (it records one frame on every frame) It is an intervalometer. 
  55     Best used to create timelapse clips automatically. 
  56   - Press 'k' or 'j' to increase or decrease the auto recording rate. 
  57   - Press 'f' or ESCAPE to toggle fullscreen mode. 
  58   - Press SHIFT-'q' to quit. 
  59   - Press '.' to change the graphical theme. 
  60   - Press TAB to change the playback direction. 
  61   - Press a number from 0 to 9 to switch to a different clip number. 
  62   - Press 'p' to pause playback. 
  63   - Press 'o' to toggle the onion skinning on/off. 
  64   - Press 'b' to take a snapshot as the new background image. 
  65   - Press 'n' to select the next effect available.""" 
  66  import gc  
  67  import sys 
  68  from time import strftime 
  69  import os 
  70  import glob 
  71  import pprint 
  72   
  73  from twisted.internet import reactor 
  74  from twisted.internet import defer 
  75   
  76  from rats import render 
  77  from rats import sig 
  78  try: 
  79      from toon import opensoundcontrol 
  80      OSC_LOADED = True 
  81  except ImportError, e: 
  82      print("For OSC support, please install pyliblo.") 
  83      OSC_LOADED = False 
  84  from toon import mencoder 
  85  from toon import draw 
  86  from toon import puredata 
  87  from toon import midi 
  88  from toon import save 
  89  from toon import sampler 
  90  from toon import data 
  91  from toon import fx 
  92  try: 
  93      from toon import web 
  94      WEB_LOADED = True 
  95  except ImportError, e: 
  96      print("For web support, please install the python-nevow package.") 
  97      print(e.message) 
  98      WEB_LOADED = False 
  99  try: 
 100      from rats import statesaving 
 101      STATESAVING_LOADED = True 
 102  except ImportError, e: 
 103      print("For state saving support, please install the python-simplejson package.") 
 104      print(e.message) 
 105      STATESAVING_LOADED = False 
 106   
 107  import pygame 
 108  import pygame.camera 
 109  import pygame.locals as PYGM # no namespace contamination 
 110  from pygame import time 
 111  from OpenGL import GL # no namespace contamination 
 112   
 113  PACKAGE_DATA_PATH = os.path.dirname(data.__file__) 
 114  DIRECTION_FORWARD = "forward" 
 115  DIRECTION_BACKWARD = "backward" 
 116  DIRECTION_YOYO = "yoyo" # back and forth 
 117  THEME_SPLIT_SCREEN = "split_screen" 
 118  THEME_PICTURE_IN_PICTURE = "picture_in_picture" 
 119  THEME_PLAYBACK_PICTURE_IN_PICTURE = "playback_picture_in_picture" 
 120   
121 -class ToonloopError(Exception):
122 """ 123 Any error Toonloop might encounter 124 """ 125 pass
126
127 -class Configuration(object): #Serializable):
128 """ 129 Configuration options. 130 131 Default values are defined here. 132 Overriden as **argd arguments to Toonloop(**argd) 133 134 Python allows commas at the end of lists and tuples 135 """
136 - def __init__(self, **argd):
137 # basics 138 self.verbose = True 139 self.video_device = 0 140 141 # project 142 self.toonloop_home = os.path.expanduser("~/Documents/toonloop") 143 self.config_file = os.path.expanduser("~/.toonloop.json") 144 self.project_name = "new_project" # name of the folder 145 #self.project_file = 'project.txt' 146 self.max_num_clips = 10 147 self.delete_jpeg = False 148 149 # framerate 150 self.framerate_min = 1 151 self.framerate_max = 30 152 # TODO: framerate value 153 154 # image size 155 self.image_width = 320 # 640 156 self.image_height = 240 # 480 157 self.image_flip_horizontal = False 158 #self.playback_opacity = 0.3 159 #self.max_num_frames = 1000 160 161 # window 162 self.display_width = self.image_width * 2 # 640 , was 1024 163 self.display_height = self.image_height * 2 # 480 , was 768 164 self.display_fullscreen = False 165 self.display_theme = THEME_SPLIT_SCREEN 166 167 # web services 168 self.web_server_port = 8000 169 self.web_enabled = False 170 171 # fudi puredata interface 172 self.fudi_enabled = False 173 self.fudi_receive_port = 15555 174 self.fudi_send_port = 17777 175 self.fudi_send_host = 'localhost' 176 177 # osc 178 self.osc_enabled = False 179 self.osc_send_port = 7770 180 self.osc_send_host = 'localhost' 181 self.osc_listen_port = 7772 182 self.osc_verbose = False 183 self.osc_sampler_enabled = False 184 self.osc_sampler_num_sounds = 500 185 #self.osc_receive_hosts = '' 186 187 # onionskin 188 self.onionskin_enabled = True 189 self.onionskin_on = False 190 self.onionskin_opacity = 0.3 191 192 # background 193 self.bgimage_enabled = False 194 self.bgimage = os.path.join(PACKAGE_DATA_PATH, 'bgimage_02.jpg') 195 #self.bgcolor_b = 0.2 #TODO: not used right now 196 #self.bgcolor_g = 0.8 197 #self.bgcolor_r = 1.0 198 self.bgimage_glob_enabled = False # list of glob JPG files that can be browsed using +/- iteration 199 self.bgimage_glob = '' # os.path.join(self.toonloop_home, self.project_name, 'data') # defaults to the images from the current project ! 200 201 # white flash 202 self.fx_white_flash = True 203 self.fx_white_flash_alpha = 0.5 204 # todo:duration 205 # effects 206 self.effect_name = "None" 207 208 # intervalometer 209 self.intervalometer_on = False 210 self.intervalometer_enabled = True 211 self.intervalometer_rate_seconds = 10.0 # in seconds 212 # TODO: clean intervalometer on/enabled stuff 213 214 # autosave 215 self.autosave_on = False 216 self.autosave_enabled = True 217 self.autosave_rate_seconds = 600.0 # in seconds 218 219 # midi 220 self.midi_enabled = False 221 self.midi_verbose = True 222 self.midi_input_id = -1 # means default 223 self.midi_pedal_control_id = 64 224 self.midi_note_record = 41 225 self.midi_note_clear = 43 226 227 # overrides some attributes whose defaults and names are below. 228 self.__dict__.update(**argd) 229 # this one is special : it needs other values to set itself. Let's find a way to prevent this 230
231 - def save(self):
232 """ 233 Save to JSON config file. 234 235 Uses self.config_file as a file to save to. 236 """ 237 exclude_list = ["toonloop_home", "config_file", "bgimage"] 238 if STATESAVING_LOADED: 239 data = {} 240 for key in sorted(self.__dict__): # FIXME: does not work! 241 value = self.__dict__[key] 242 if key in exclude_list: 243 if self.verbose: 244 print("Excluding %s since it is in the exclude list." % (key)) 245 else: 246 data[key] = value 247 if self.verbose: 248 print("Saving config to %s" % (self.config_file)) 249 try: 250 statesaving.save(self.config_file, data) 251 except statesaving.StateSavingError, e: 252 if self.verbose: 253 print(e.message) 254 else: 255 print("Saved config file to %s." % (self.config_file)) 256 else: 257 if self.verbose: 258 print("Could not save config. Json is not loaded.")
259
260 - def load(self):
261 """ 262 Load from JSON config file. 263 264 Uses self.config_file as a file to load from. 265 """ 266 # TODO: fix the bugs it can create. (by saving/loading only vars that are safe) 267 # FIXME: not used now. 268 if STATESAVING_LOADED: # if found json module 269 try: 270 data = statesaving.load(self.config_file) 271 except statesaving.StateSavingError, e: 272 if self.verbose: 273 print("Could not load configuration file \"%s\". %s" % (self.config_file, e.message)) 274 else: 275 print("Loading configuration values from \"%s\"." % (self.config_file)) 276 self.__dict__.update(data) 277 else: 278 if self.verbose: 279 print("Could not load config. Json is not loaded.")
280
281 - def print_values(self):
282 for k in sorted(self.__dict__): 283 v = self.__dict__[k] 284 print(" -o %s %s" % (k, v))
285
286 - def set(self, name, value):
287 """ 288 Casts to its type and sets the value. 289 290 Intended to be used even from ASCII string values. (FUDI, etc.) 291 292 A bool value can be set using 'True' or 1 and 'False' or 0 293 """ 294 # try: 295 kind = type(self.__dict__[name]) 296 if kind is bool: 297 if value == 'True': 298 casted_value = True 299 elif value == 'False': 300 casted_value = False 301 else: 302 casted_value = bool(int(value)) 303 else: 304 casted_value = kind(value) 305 self.__dict__[name] = casted_value 306 if self.verbose: 307 print('Setting config option \"%s\" to \"%s\" (%s)' % (name, self.__dict__[name], kind.__name__)) 308 return kind
309 # except Exception, e: 310 # print e.message 311
312 -class ToonClip(object): #Serializable):
313 """ 314 Toonloop clip. 315 A clip is a serie of frames. 316 """
317 - def __init__(self, id, **argd):
318 """ 319 :param id: int 320 """ 321 self.id = id 322 self.playhead = 0 # between 0 and n - 1 323 self.playhead_previous = 0 # previous position of the playhead 324 self.playhead_iterate_every = 1 325 self.direction = DIRECTION_FORWARD 326 # Ratio that decides of the framerate 327 # self.framerate = 12 328 self.__dict__.update(argd) 329 self.images = [] 330 self.writehead = len(self.images) # index of the next image to be filled up. Between 0 and n.
331
332 -class SplitScreenTheme(object):
333 """ 334 Theme allow to customize the graphical attributes of the rendering. 335 This is the base theme with a split screen. 336 """
337 - def __init__(self):
338 self.name = THEME_SPLIT_SCREEN 339 self.render_play_first = True 340 self.play_pos = (2.0, 0.0, 0.0) 341 self.play_scale = (2.0, 1.5, 1.0) 342 self.edit_pos = (-2.0, 0.0, 0.0) 343 self.edit_scale = (2.0, 1.5, 1.0) 344 # saving progress bar 345 self.progress_foreground_color = (1.0, 1.0, 1.0, 0.5) 346 self.progress_background_color = (0.7, 0.7, 0.7, 0.5) 347 self.progress_line_color = (1.0, 1.0, 1.0, 0.6) 348 self.progress_pos = (0.0, -2.0, 0.0) 349 self.progress_scale = (3.0, 0.05, 1.0)
350 #self.flash_color = (1.0, 1.0, 1.0, 1.0) 351 #TODO: add a theme not displaying the edit "viewport". 352
353 -class PictureInPictureTheme(SplitScreenTheme):
354 """ 355 This theme is a picture in picture theme. 356 """
357 - def __init__(self):
358 SplitScreenTheme.__init__(self) # inherit some attributes from the base theme. 359 self.render_play_first = False 360 self.name = THEME_PICTURE_IN_PICTURE 361 self.play_pos = (0.0, 0.0, 0.0) 362 self.play_scale = (4.0, 3.0, 1.0) 363 self.edit_pos = (2.0, 1.5, 0.0) 364 self.edit_scale = (1.0, 0.75, 1.0)
365
366 -class PlaybackPictureInPictureTheme(SplitScreenTheme):
367 """ 368 This theme is a picture in picture theme as well, but the edit is big. 369 """
370 - def __init__(self):
371 SplitScreenTheme.__init__(self) # inherit some attributes from the base theme. 372 self.render_play_first = True 373 self.name = THEME_PLAYBACK_PICTURE_IN_PICTURE 374 self.edit_pos = (0.0, 0.0, 0.0) 375 self.edit_scale = (4.0, 3.0, 1.0) 376 self.play_pos = (2.0, 1.5, 0.0) 377 self.play_scale = (1.0, 0.75, 1.0)
378
379 -class Toonloop(render.Game):
380 """ 381 Toonloop is a realtime stop motion tool. 382 383 For 1 GB of RAM, one can expect to stock about 3000 images at 640x480, if having much swap memory. 384 385 Private methods starts with _. 386 The draw method is a special one inherited from rats.render.Game. The process_events one as well. 387 388 Onion skin effect and chroma key shader are mutually exclusives. 389 390 There are a lot of services/features that are disabled by default. (web, fudi, osc, midi, etc.) 391 """ 392 # OpenGL textures indices in the list 393 TEXTURE_MOST_RECENT = 0 394 TEXTURE_PLAYBACK = 1 395 TEXTURE_ONION = 2 396 TEXTURE_BACKGROUND = 3 397
398 - def __init__(self, config):
399 """ 400 Startup poutine. 401 402 Reads config. 403 Starts pygame, the camera, the clips list. 404 Creates the window. Hides the mouse. 405 Start services (OSC, web and FUDI) 406 Enables the effects or the onion peal. 407 """ 408 self.config = config 409 # size of the rendering window 410 self._display_size = (self.config.display_width, self.config.display_height) 411 self.running = True 412 self.pd = None # fudi send and receive 413 self.midi_manager = None 414 self.paused = False 415 self.image_size = (self.config.image_width, self.config.image_height) 416 self.clock = pygame.time.Clock() 417 self.fps = 0 # for statistics 418 self.clip_id = 1 # Currently selected clip 419 self.clip = None # current ToonClip instance 420 self.clips = [] # ToonClip instances 421 self._saver_progress = None # ClipSaver progress bar ratio. float from 0 to 1 422 self._init_clips() 423 self.renderer = None # Renderer instance that owns it. 424 self.themes = { 425 THEME_SPLIT_SCREEN: SplitScreenTheme(), 426 THEME_PICTURE_IN_PICTURE: PictureInPictureTheme(), 427 THEME_PLAYBACK_PICTURE_IN_PICTURE: PlaybackPictureInPictureTheme(), 428 } 429 if self.config.display_theme not in self.themes.keys(): 430 print("Error: not such theme: %s. using default") 431 self.config.display_theme = THEME_SPLIT_SCREEN 432 self.theme = self.themes[self.config.display_theme] 433 434 # the icon 435 try: 436 icon = pygame.image.load(os.path.join(PACKAGE_DATA_PATH, "icon.png")) 437 pygame.display.set_icon(icon) # a 32 x 32 surface 438 except pygame.error, e: 439 print("ERROR : Could not load icon : %s" % (e.message)) 440 # the pygame window 441 window_flags = PYGM.OPENGL | PYGM.DOUBLEBUF | PYGM.HWSURFACE 442 if self.config.display_fullscreen: 443 window_flags |= PYGM.FULLSCREEN 444 self._display_size = (0, 0) # Automatically detected ! 445 self.display = pygame.display.set_mode(self._display_size, window_flags) 446 else: 447 window_flags |= PYGM.RESIZABLE 448 self.display = pygame.display.set_mode(self._display_size, window_flags) 449 pygame.display.set_caption("Toonloop") 450 pygame.mouse.set_visible(False) 451 # the images 452 self.most_recent_image = pygame.surface.Surface(self.image_size) # , 0, self.display) 453 454 self.osc = None # sender and receiver. 455 self.camera = None # pygame camera 456 self.is_mac = False # is on Mac OS X or not. (linux) For the camera. 457 self.textures = [0, 0, 0, 0] # list of OpenGL texture objects id 458 self._setup_camera() 459 self._setup_window() 460 self.effects = {} 461 self.optgroups = {} 462 self._setup_effects() 463 self.background_image = None 464 self._setup_background() 465 self._clear_playback_view() 466 self._clear_onion_peal() 467 self._playhead_iterator = 0 468 # intervalometer 469 self._intervalometer_delayed_id = None 470 self.intervalometer_on = self.config.intervalometer_on 471 self._bgimage_glob_index = 0 472 if config.intervalometer_on: 473 self.intervalometer_toggle(True) 474 # autosave 475 self._autosave_delayed_id = None 476 if config.autosave_on: 477 self.autosave_toggle(True) 478 # copy conf elements 479 if self.config.verbose: 480 pprint.pprint(self.config.__dict__) 481 self._has_just_added_frame = False 482 self.clip_saver = None 483 self.sampler = None # toon.sampler.Sampler 484 # signal / slots. Each is documented with its arg, or no arg. 485 self.signal_playhead = sig.Signal() # int index 486 self.signal_writehead = sig.Signal() # int index 487 self.signal_framerate = sig.Signal() # int fps 488 self.signal_clip = sig.Signal() # int clip id 489 self.signal_frame_add = sig.Signal() # no arg 490 self.signal_frame_remove = sig.Signal() # no arg 491 self.signal_sampler_record = sig.Signal() # bool start/stop 492 self.signal_sampler_clear = sig.Signal() # bool start/stop 493 494 # start services 495 reactor.callLater(0, self._start_services)
496
497 - def _setup_effects(self):
498 """ 499 Set all effects up. 500 """ 501 self.effects = fx.load_effects() 502 # current effect name is "None" 503 for effect in self.effects.itervalues(): 504 self.optgroups[effect.name] = effect.options
505
506 - def effect_next(self):
507 """ 508 Selects the next effect. 509 """ 510 previous = self.config.effect_name 511 all = self.effects.keys() 512 if len(all) == 0: 513 print("No effect loaded.") 514 else: 515 index_of_previous = all.index(previous) 516 if index_of_previous >= len(all) - 1: 517 new = all[0] 518 else: 519 new = all[index_of_previous + 1] 520 if self.config.verbose: 521 print("Using effect %s" % (new)) 522 self.config.effect_name = new
523
524 - def _get_current_effect(self):
525 return self.effects[self.config.effect_name]
526
527 - def _start_services(self):
528 """ 529 Starts the Toonloop network services. 530 531 Called once the Twisted reactor has been started. 532 533 Implemented services : 534 * web server with Media RSS and Restructured text 535 * FUDI protocol with PureData 536 * MIDI input (not quite a service, but we start it here) 537 """ 538 # OSC 539 if self.config.osc_enabled and OSC_LOADED: 540 self.osc = opensoundcontrol.ToonOsc( 541 self, 542 listen_port=self.config.osc_listen_port, 543 send_port=self.config.osc_send_port, 544 send_host=self.config.osc_send_host, 545 ) 546 if self.config.osc_sampler_enabled: 547 self.sampler = sampler.Sampler(self, self.osc) 548 549 #index_file_path = os.path.join(os.curdir, 'toon', 'index.rst') 550 # WEB 551 if WEB_LOADED and self.config.web_enabled: 552 try: 553 self.web = web.start( 554 self, 555 self.config.web_server_port, 556 static_files_path=self.config.toonloop_home) 557 #index_file_path=index_file_path) 558 except: 559 print("Error loading web UI :") 560 print(sys.exc_info()) 561 # FUDI 562 if self.config.fudi_enabled: 563 try: 564 # TODO: subscription push mecanism 565 app = self 566 fudi_recv = self.config.fudi_receive_port 567 fudi_send = self.config.fudi_send_port 568 fudi_send_host = self.config.fudi_send_host 569 570 self.pd = puredata.start(app=app, receive_port=fudi_recv, send_port=fudi_send, send_host=fudi_send_host) 571 except: 572 print("Error loading puredata: %s" % (sys.exc_info())) 573 #raise 574 # MIDI 575 if self.config.midi_enabled: 576 self.midi_manager = midi.SimpleMidiInput(self.config.midi_input_id, self.config.midi_verbose) 577 self.midi_manager.register_callback(self._cb_midi_event) 578 try: 579 self.midi_manager.start() 580 except midi.NotConnectedError, e: 581 print("Could not setup MIDI device %d" % (self.config.midi_input_id)) 582 self.signal_clip(0) # default clip id 583 self.signal_writehead(0)
584
585 - def theme_change(self):
586 """ 587 Pick next theme 588 """ 589 all = self.themes.keys() 590 current_index = all.index(self.theme.name) 591 current_index = (current_index + 1) % len(all) 592 self.theme = self.themes[all[current_index]] 593 if self.config.verbose: 594 print("Switching to the %s theme." % (self.theme.name)) 595 self.config.display_theme = self.theme.name
596
597 - def sampler_record(self, start=True):
598 """ 599 Starts of stops recording a sampler. 600 The sounds sampler handles this. 601 """ 602 self.signal_sampler_record(start)
603
604 - def sampler_clear(self):
605 """ 606 Clear the sound in current frame 607 The sounds sampler handles this. 608 """ 609 self.signal_sampler_clear()
610
611 - def _cb_midi_event(self, event):
612 """ 613 Called when a MIDI event happens. 614 If MIDI is enabled, of course. 615 """ 616 MIDI_NOTE = 144 617 MIDI_CTRL = 176 618 if event.status == MIDI_NOTE: # MIDI note 619 note = event.data1 620 velocity = event.data2 621 on = event.data2 >= 1 # bool 622 if self.config.midi_verbose: 623 print("MIDI note: Pitch: %s Velocity: %s" % (note, velocity)) 624 if note == self.config.midi_note_record: 625 if on: 626 print("start recording sample") 627 self.sampler_record(True) 628 else: 629 print("stop recording sample") 630 self.sampler_record(False) 631 if note == self.config.midi_note_clear: 632 if on: 633 print("clear a sample") 634 self.sampler_clear() 635 elif event.status == MIDI_CTRL: # MIDI control 636 ctrl_id = event.data1 637 val = event.data2 638 if self.config.midi_verbose: 639 print("MIDI control: ID: %s Value: %s" % (ctrl_id, val)) 640 if ctrl_id == self.config.midi_pedal_control_id and val >= 1: 641 self.frame_add()
642
643 - def _setup_background(self):
644 """ 645 Loads initial background image. 646 """ 647 if self.config.bgimage_enabled: 648 self.bgimage_load(self.config.bgimage)
649
650 - def bgimage_load(self, path):
651 """ 652 Replace the background image by a new image file path. 653 """ 654 if self.config.bgimage_enabled: 655 self.config.bgimage = path 656 if self.config.verbose: 657 print('setup background %s' % (path)) 658 try: 659 self.background_image = pygame.image.load(path) 660 except Exception, e: # FIXME: more specific 661 self.config.bgimage_enabled = False 662 print("Error with background image \"%s\": %s" % (path, e.message)) 663 else: 664 # Create an OpenGL texture 665 draw.texture_from_image(self.textures[self.TEXTURE_BACKGROUND], self.background_image)
666
667 - def bgimage_snap(self):
668 """ 669 Makes the current image from the camera to be the background image. 670 """ 671 try: 672 if self.is_mac: 673 self.background_image = self.most_recent_image.copy() 674 else: 675 self.background_image = self.most_recent_image 676 except MemoryError, e: 677 print("CRITICAL ERROR : No more memory !!! %s" % (e.message)) 678 else: 679 self.config.bgimage_enabled = True 680 # Create an OpenGL texture 681 draw.texture_from_image(self.textures[self.TEXTURE_BACKGROUND], self.background_image)
682 683
684 - def bgimage_glob_next(self, increment=1):
685 """ 686 Loads the next image from the list of JPG images in a directory. 687 688 lowercase .jpg extension. 689 """ 690 if self.config.bgimage_glob_enabled: 691 if self.config.bgimage_glob == '': 692 self.config.bgimage_glob = os.path.join(self.config.toonloop_home, self.config.project_name, 'data') 693 dir = self.config.bgimage_glob 694 ext = '.jpg' 695 pattern = '%s/*%s' % (dir, ext) 696 files = glob.glob(pattern) 697 if self.config.verbose: 698 print("bgimage_glob_next pattern :" % (pattern)) 699 print("bgimage_glob_next len(files) :" % (len(files))) 700 701 if len(files) > 0: 702 old_val = self._bgimage_glob_index 703 new_val = (self._bgimage_glob_index + increment) % len(files) 704 print('bgimage_glob_next new_val :' % (new_val)) 705 if old_val == new_val: 706 if self.config.verbose: 707 print('bgimage_glob_next same val. Not changing:' % (old_val)) 708 else: 709 file_path = sorted(files)[new_val] 710 self._bgimage_glob_index = new_val 711 self.bgimage_load(file_path)
712
713 - def print_stats(self):
714 """ 715 Print statistics 716 """ 717 print(">>>>>>> Toonloop Statistics >>>>>>>>") 718 try: 719 self.config.print_values() 720 print('pygame.display.Info(): %s' % (pygame.display.Info())) 721 total_imgs = 0 722 for clip_num in range(len(self.clips)): 723 num_images = len(self.clips[clip_num].images) 724 print(' * Clip #%d has %d images.' % (clip_num, num_images)) 725 total_imgs += num_images 726 print('TOTAL: %d images.' % (total_imgs)) 727 print("Current playhead: " + str(self.clip.playhead)) 728 print("Num images: " + str(len(self.clip.images))) 729 print("FPS: %d" % (self.fps)) 730 print("Playhead frequency ratio: 30 / %d" % (self.clip.playhead_iterate_every)) 731 self.print_optgroups() 732 print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<") 733 except AttributeError, e: 734 print sys.exc_info()
735
736 - def set_option_in_group(self, group, key, value):
737 """ 738 Set an option that is in a OptionsGroup. 739 See toon.optgroup.OptionsGroup. 740 741 Might raise ToonloopError of OptionGroupError 742 :param group: Name of the group. 743 :param key: Name of the option. 744 :param value: str with the value. 745 """ 746 if group not in self.optgroups.keys(): 747 raise ToonloopError("No option group named %s." % (group)) 748 else: 749 obj = self.optgroups[group] 750 obj.set_value(key, value) 751 if self.config.verbose: 752 print("Set %s in group %s to %s" % (key, group, value))
753
754 - def print_optgroups(self):
755 for name, group in self.optgroups.iteritems(): 756 print("Options in group %s:" % (name)) 757 for key, value in group.__dict__.iteritems(): 758 print(" -x %s %s %s" % (name, key, value))
759
760 - def print_help(self):
761 """ 762 Prints help for live keyboard controls. 763 """ 764 print(INTERACTIVE_HELP)
765
766 - def _draw_hud(self):
767 """ 768 Draws the head-up-display. Numbers, text, etc. overlayed on top of images. 769 """ 770 #TODO 771 pass
772
773 - def _init_clips(self):
774 """ 775 Creates a new project. 776 """ 777 for i in range(self.config.max_num_clips): 778 self.clips.append(ToonClip(i)) 779 try: 780 self.clip = self.clips[self.clip_id] 781 except IndexError: 782 if self.config.max_num_clips > 1: 783 self.clip_id = 1 # default clip is 1, for a better usability 784 else: 785 self.clip_id = 0 786 self.clip = self.clips[self.clip_id]
787
788 - def clip_select(self, index=0):
789 """ 790 Selects an other clip 791 """ 792 self.clip_id = index 793 self.clip = self.clips[index] 794 if self.config.verbose: 795 print("Clip #%s" % (self.clip_id)) 796 if len(self.clip.images) == 0: 797 self._clear_playback_view() 798 self.signal_clip(self.clip_id)
799
800 - def _setup_window(self):
801 """ 802 OpenGL setup. 803 """ 804 # create OpenGL texture objects 805 # window is 1280 x 960 806 self._resize_window()#self._display_size) # arg totally useless 807 GL.glEnable(GL.GL_TEXTURE_RECTANGLE_ARB) # 2D) 808 GL.glEnable(GL.GL_BLEND) 809 GL.glShadeModel(GL.GL_SMOOTH) 810 GL.glClearColor(0.0, 0.0, 0.0, 0.0) # black background 811 GL.glColor4f(1.0, 1.0, 1.0, 1.0) # self.config.playback_opacity) # for now we use it for all 812 GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA) 813 for i in range(len(self.textures)): 814 self.textures[i] = GL.glGenTextures(1)
815 # print "texture names : ", self.textures 816 817
818 - def _resize_window(self):
819 #, (width, height)): 820 """ 821 Called when we resize the window. 822 (fullscreen on/off) 823 824 The OpenGL coordinates go from -4 to 4 horizontally 825 and -3 to 3 vertically. 826 (ratio is 4:3) 827 """ 828 #print("resize", width, height) 829 #if height == 0: 830 # height = 1 831 GL.glMatrixMode(GL.GL_PROJECTION) 832 GL.glLoadIdentity() 833 GL.glOrtho(-4.0, 4.0, -3.0, 3.0, -1.0, 1.0) 834 GL.glMatrixMode(GL.GL_MODELVIEW) 835 GL.glLoadIdentity()
836
837 - def _setup_camera(self):
838 """ 839 Starts using the video camera. 840 """ 841 if os.uname()[0] == 'Darwin': 842 self.is_mac = True 843 else: 844 self.is_mac = False 845 size = (self.config.image_width, self.config.image_height) 846 try: 847 pygame.camera.init() 848 # except AttributeError, e: 849 # print "Sometimes the camera module need it's init()", 850 # print " to be called and sometimes not." 851 except AttributeError, e: 852 print('ERROR: pygame.camera has no method init() ! %s' % (e.message)) 853 except Exception, e: 854 print("error calling pygame.camera.init(): %s" % (e.message)) 855 print(sys.exc_info()) 856 raise ToonloopError("Error initializing the video camera. %s" % (e.message)) 857 try: 858 print("cameras : %s" % (pygame.camera.list_cameras())) 859 if self.is_mac: 860 print("Using camera %s" % (self.config.video_device)) 861 self.camera = pygame.camera.Camera(str(self.config.video_device), size) 862 else: 863 print("Using camera /dev/video%d" % (self.config.video_device)) 864 self.camera = pygame.camera.Camera("/dev/video%d" % (self.config.video_device), size) 865 self.camera.start() 866 except SystemError, e: 867 print(sys.exc_info()) 868 raise ToonloopError("Invalid camera. %s" % (str(e.message))) 869 except Exception, e: 870 print(sys.exc_info()) 871 raise ToonloopError("Invalid camera. %s" % (str(e.message)))
872
873 - def frame_add(self):
874 """ 875 Copies the last grabbed frame to the list of images. 876 """ 877 index = self.clip.writehead 878 try: 879 if self.is_mac: 880 #self.clip.images.append(self.most_recent_image.copy()) 881 self.clip.images.insert(index, self.most_recent_image.copy()) 882 else: 883 #self.clip.images.append(self.most_recent_image) 884 self.clip.images.insert(index, self.most_recent_image) 885 except MemoryError, e: 886 print("CRITICAL ERROR : No more memory !!! %s" % (e.message)) 887 else: 888 self.clip.writehead += 1 889 # Creates an OpenGL texture 890 draw.texture_from_image(self.textures[self.TEXTURE_ONION], self.most_recent_image) 891 self._has_just_added_frame = True 892 if self.config.verbose: 893 pass 894 #print("Added frame at index %d" % (index)) 895 #print('num frames: %s' % (len(self.clip.images))) 896 self.signal_writehead(len(self.clip.images)) 897 self.signal_frame_add()
898
899 - def writehead_move(self, steps):
900 """ 901 Moves the writehead of the current clip. 902 :param steps: How many frames in which direction. 903 To move to the left, give a steps value of 1. 904 To move to the right, give a steps value of -1. 905 """ 906 index = self.clip.writehead + steps 907 if index == -1: 908 if self.config.verbose: 909 print("Already at the first frame. Type RETURN to go to last.") 910 else: 911 self.writehead_goto(index)
912
913 - def writehead_goto(self, index):
914 """ 915 Moves the writehead of the current clip to the given index. 916 :param index: int 917 -1 for the end. 918 0 for the beginning 919 """ 920 last = len(self.clip.images) 921 if index == -1: 922 index = last 923 elif index < 0: # if negative 924 index = last - index 925 if index < -last: # if more negative than it can be... 926 index = 0 927 elif index > last: 928 print("writehead_goto: Frame index %d is too big. Using %d instead." % (index, last)) 929 index = last 930 if self.config.verbose: 931 print("writehead_goto %d" % (index)) 932 if len(self.clip.images) == 0: 933 index = 0 934 else: 935 # copying the onion skinning texture 936 try: 937 draw.texture_from_image(self.textures[self.TEXTURE_ONION], self.clip.images[index]) 938 except IndexError, e: 939 index = len(self.clip.images) - 1 940 draw.texture_from_image(self.textures[self.TEXTURE_ONION], self.clip.images[index]) 941 self.clip.writehead = index 942 self.signal_writehead(index) # FIXME : is this ok to call it ?
943
944 - def draw(self):
945 """ 946 Renders one frame. 947 Called from the event loop. (twisted) 948 """ 949 GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) 950 if not self.paused: 951 self._playhead_iterator = (self._playhead_iterator + 1) % self.clip.playhead_iterate_every 952 if self._playhead_iterator == 0: 953 self._playhead_iterate() 954 if len(self.clip.images) > 0: 955 # send osc message /toon/playhead 956 self.signal_playhead(self.clip.playhead) 957 # 30/3 = 10 FPS 958 self._camera_grab_frame() # grab a frame 959 960 # now, let's draw something 961 GL.glEnable(GL.GL_TEXTURE_RECTANGLE_ARB) 962 if self.theme.render_play_first: 963 self._draw_edit_background() 964 self._draw_edit_view() 965 self._draw_onion_skin() 966 self._draw_white_flash() 967 self._draw_play_background() 968 self._draw_playback_view() 969 else: 970 self._draw_play_background() 971 self._draw_playback_view() 972 self._draw_edit_background() 973 self._draw_edit_view() 974 self._draw_onion_skin() 975 self._draw_white_flash() 976 GL.glDisable(GL.GL_TEXTURE_RECTANGLE_ARB) # important not to draw a big pixel ! 977 # ----------- saving progress bar 978 self._draw_saving_progress_bar() 979 # ----------- done drawing. 980 self.clock.tick() 981 self.fps = self.clock.get_fps() 982 pygame.display.flip() 983 # old : pygame.display.update() 984 gc.collect() # force garbage collection on every frame
985 # TODO: on every frame is a bit much. Maybe every second ? 986 # otherwise, python slows down when it is time to collect garbage. 987
988 - def _draw_saving_progress_bar(self):
989 """ 990 Draws the progress bar of saving of the clip, if in progress. 991 """ 992 if self._saver_progress is not None: 993 GL.glPushMatrix() 994 GL.glTranslatef(*self.theme.progress_pos) 995 GL.glScalef(*self.theme.progress_scale) 996 draw.draw_horizontal_progress_bar( 997 background_color=self.theme.progress_background_color, 998 foreground_color=self.theme.progress_foreground_color, 999 line_color=self.theme.progress_line_color, 1000 progress=self._saver_progress) 1001 GL.glPopMatrix()
1002
1003 - def _draw_white_flash(self):
1004 if self._has_just_added_frame: 1005 self._has_just_added_frame = False 1006 if self.config.fx_white_flash: 1007 # TODO: use time.time() to create tween. 1008 # left view 1009 a = self.config.fx_white_flash_alpha 1010 GL.glColor4f(1.0, 1.0, 1.0, a) 1011 GL.glPushMatrix() 1012 GL.glTranslatef(*self.theme.edit_pos) 1013 GL.glScalef(*self.theme.edit_scale) 1014 draw.draw_square() 1015 GL.glPopMatrix()
1016
1017 - def _draw_edit_background(self):
1018 """ 1019 Renders the background 1020 """ 1021 # r = self.config.bgcolor_r 1022 # g = self.config.bgcolor_g 1023 # b = self.config.bgcolor_b 1024 # glColor(r, g, b, 1.0) 1025 # glPushMatrix() 1026 # glScalef(4.0, 3, 1.0) 1027 # draw_square() 1028 # glPopMatrix() 1029 # glColor(1.0, 1.0, 1.0, 1.0) 1030 if self.config.bgimage_enabled: 1031 GL.glColor4f(1.0, 1.0, 1.0, 1.0) 1032 # playback view 1033 GL.glPushMatrix() 1034 GL.glTranslatef(*self.theme.play_pos) 1035 GL.glScalef(*self.theme.play_scale) 1036 GL.glBindTexture(GL.GL_TEXTURE_RECTANGLE_ARB, self.textures[self.TEXTURE_BACKGROUND]) 1037 if self.config.image_flip_horizontal: # FIXME? 1038 GL.glRotatef(180., 0., 1., 0.) 1039 draw.draw_textured_square(self.config.image_width, self.config.image_height) 1040 GL.glPopMatrix()
1041
1042 - def _draw_play_background(self):
1043 if self.config.bgimage_enabled: 1044 GL.glColor4f(1.0, 1.0, 1.0, 1.0) 1045 # edit view 1046 GL.glPushMatrix() 1047 GL.glTranslatef(*self.theme.edit_pos) 1048 GL.glScalef(*self.theme.edit_scale) 1049 GL.glBindTexture(GL.GL_TEXTURE_RECTANGLE_ARB, self.textures[self.TEXTURE_BACKGROUND]) 1050 if self.config.image_flip_horizontal: # FIXME? 1051 GL.glRotatef(180., 0., 1., 0.) 1052 draw.draw_textured_square(self.config.image_width, self.config.image_height) 1053 GL.glPopMatrix()
1054
1055 - def _camera_grab_frame(self):
1056 """ 1057 Image capture from the video camera. 1058 """ 1059 if self.is_mac: 1060 self.camera.get_image(self.most_recent_image) 1061 else: 1062 self.most_recent_image = self.camera.get_image() 1063 # Create an OpenGL texture 1064 draw.texture_from_image(self.textures[self.TEXTURE_MOST_RECENT], self.most_recent_image)
1065
1066 - def _draw_edit_view(self):
1067 """ 1068 Renders edit view (the live camera + onion peal) 1069 """ 1070 self._get_current_effect().pre_draw() 1071 GL.glColor4f(1.0, 1.0, 1.0, 1.0) 1072 GL.glPushMatrix() 1073 GL.glTranslatef(*self.theme.edit_pos) #-2.0, 0.0, 0.0) 1074 GL.glScalef(*self.theme.edit_scale) #2.0, 1.5, 1.0) 1075 # most recent grabbed : 1076 GL.glBindTexture(GL.GL_TEXTURE_RECTANGLE_ARB, self.textures[self.TEXTURE_MOST_RECENT]) 1077 if self.config.image_flip_horizontal: # FIXME? 1078 GL.glRotatef(180., 0., 1., 0.) 1079 draw.draw_textured_square(self.config.image_width, self.config.image_height) 1080 # self.display_width = 1024 1081 GL.glPopMatrix() 1082 # old: self.display.blit(self.most_recent_image, (0, 0)) 1083 self._get_current_effect().post_draw()
1084
1085 - def _draw_onion_skin(self):
1086 if self.config.onionskin_enabled and self.config.onionskin_on: 1087 if len(self.clip.images) > 0: 1088 # Onion skin over dit view: 1089 self._get_current_effect().pre_draw() 1090 GL.glPushMatrix() 1091 GL.glTranslatef(*self.theme.edit_pos)#-2.0, 0.0, 0.0) 1092 GL.glScalef(*self.theme.edit_scale)#2.0, 1.5, 1.0) 1093 GL.glColor4f(1.0, 1.0, 1.0, self.config.onionskin_opacity) 1094 GL.glBindTexture(GL.GL_TEXTURE_RECTANGLE_ARB, self.textures[self.TEXTURE_ONION]) 1095 if self.config.image_flip_horizontal: # FIXME? 1096 GL.glRotatef(180., 0., 1., 0.) 1097 draw.draw_textured_square(self.config.image_width, self.config.image_height) 1098 GL.glColor4f(1.0, 1.0, 1.0, 1.0) # self.config.playback_opacity) 1099 GL.glPopMatrix() 1100 self._get_current_effect().post_draw()
1101
1102 - def _draw_playback_view(self):
1103 """ 1104 Renders the playback view. 1105 """ 1106 self._get_current_effect().pre_draw() 1107 GL.glColor4f(1.0, 1.0, 1.0, 1.0) 1108 GL.glPushMatrix() 1109 GL.glTranslatef(*self.theme.play_pos)#2.0, 0.0, 0.0) 1110 GL.glScalef(*self.theme.play_scale)#2.0, 1.5, 1.0) 1111 GL.glBindTexture(GL.GL_TEXTURE_RECTANGLE_ARB, self.textures[self.TEXTURE_PLAYBACK]) 1112 if self.config.image_flip_horizontal: # FIXME? 1113 GL.glRotatef(180., 0., 1., 0.) 1114 draw.draw_textured_square(self.config.image_width, self.config.image_height) 1115 GL.glPopMatrix() 1116 self._get_current_effect().post_draw()
1117
1118 - def _playhead_iterate(self):
1119 """ 1120 Increments the playhead position of one frame 1121 """ 1122 if len(self.clip.images) == 0: 1123 self.clip.playhead_previous = 0 1124 # nothing to copy to VRAM 1125 else: 1126 # check things up 1127 if self.clip.direction == DIRECTION_FORWARD: 1128 direction = DIRECTION_FORWARD 1129 elif self.clip.direction == DIRECTION_BACKWARD: 1130 direction = DIRECTION_BACKWARD 1131 elif self.clip.direction == DIRECTION_YOYO: 1132 # FIXME: this should be simpler. Maybe use a table lookup? 1133 if self.clip.playhead > self.clip.playhead_previous: 1134 # if was going forward 1135 if self.clip.playhead >= len(self.clip.images) - 1: 1136 direction = DIRECTION_BACKWARD 1137 else: 1138 direction = DIRECTION_FORWARD 1139 else: 1140 # if was going backwards 1141 if self.clip.playhead == 0: 1142 direction = DIRECTION_FORWARD 1143 else: 1144 direction = DIRECTION_BACKWARD 1145 self.clip.playhead_previous = self.clip.playhead 1146 # actually do the incrementation 1147 if direction == DIRECTION_FORWARD: 1148 if self.clip.playhead < len(self.clip.images) - 1: 1149 self.clip.playhead += 1 1150 else: 1151 self.clip.playhead = 0 1152 elif direction == DIRECTION_BACKWARD: 1153 if self.clip.playhead > 0: 1154 self.clip.playhead -= 1 1155 else: 1156 self.clip.playhead = len(self.clip.images) - 1 1157 # now, let's copy it to VRAM 1158 try: 1159 image = self.clip.images[self.clip.playhead] 1160 except IndexError, e: 1161 print("ERROR: No frame %s in clip %s." % (self.clip.playhead, self.clip)) 1162 image = self.clip.images[0] 1163 self.clip.playhead = 0 1164 draw.texture_from_image(self.textures[self.TEXTURE_PLAYBACK], image)
1165
1166 - def _clear_playback_view(self):
1167 """ 1168 Sets all pixels in the playback view as black. 1169 """ 1170 blank_surface = pygame.Surface((self.config.image_width, self.config.image_height)) 1171 draw.texture_from_image(self.textures[self.TEXTURE_PLAYBACK], blank_surface)
1172
1173 - def _clear_onion_peal(self):
1174 """ 1175 Sets all pixels in the onion peal as black. 1176 """ 1177 blank_surface = pygame.Surface((self.config.image_width, self.config.image_height)) 1178 draw.texture_from_image(self.textures[self.TEXTURE_ONION], blank_surface)
1179
1180 - def pause(self, val=None):
1181 """ 1182 Toggles on/off the pause 1183 """ 1184 if val is not None: 1185 self.paused = val 1186 else: 1187 self.paused = not self.paused
1188
1189 - def clip_reset(self):
1190 """ 1191 Deletes all frames from the current animation 1192 """ 1193 self.clip.images = [] 1194 self._clear_playback_view()
1195
1196 - def clip_save(self):
1197 """ 1198 Saves all images as jpeg and encodes them to a MJPEG movie. 1199 1200 See _write_next_image 1201 """ 1202 if self.config.verbose: 1203 print("clip_save") 1204 # TODO : in a thread 1205 if len(self.clip.images) > 1: 1206 if self.clip_saver is not None: 1207 if self.clip_saver.is_busy: 1208 print("There is already one clip being saved. Try again later.") 1209 return # TODO: return failed deferred 1210 else: 1211 self.clip_saver = None 1212 dir_path = os.path.join(self.config.toonloop_home, self.config.project_name) 1213 file_prefix = strftime("%Y-%m-%d_%Hh%Mm%S") # without an extension. 1214 file_prefix += '_%s' % (self.clip_id) 1215 if self.config.verbose: 1216 print("Will save images %s %s" % (dir_path, file_prefix)) 1217 core = self 1218 self.clip_saver = save.ClipSaver(core, dir_path, file_prefix, self.clip_id) 1219 self.clip_saver.signal_progress.connect(self._slot_saver_progress) 1220 self.clip_saver.signal_done.connect(self._slot_saver_done) 1221 return self.clip_saver.save() # returns a deferred
1222
1223 - def _slot_saver_progress(self, progress_ratio):
1224 """ 1225 :param progress_ratio: float from 0 to 1 1226 """ 1227 self._saver_progress = progress_ratio
1228
1229 - def _slot_saver_done(self, success):
1230 """ 1231 :param success: boolean 1232 """ 1233 self._saver_progress = None
1234
1235 - def frame_remove(self):
1236 """ 1237 Deletes the last frame from the current list of images. 1238 """ 1239 if self.clip.images != []: 1240 index = self.clip.writehead - 1 1241 try: 1242 self.clip.images.pop(index) 1243 except IndexError, e: 1244 print("ERROR: Could not remove frame '%s' at writehead - 1 in clip %s." % (index, self.clip)) 1245 index = len(self.clip.images) - 1 1246 self.clip.images.pop(index) 1247 self.clip.writehead = max(0, index - 1) 1248 if self.clip.writehead != 0: 1249 self.clip.writehead -= 1 # FIXME Is this ok? 1250 # would it be better to also delete it ? calling del 1251 if self.clip.images == []: 1252 self._clear_playback_view() 1253 else: 1254 pass 1255 if self.clip.writehead > 0: 1256 try: 1257 draw.texture_from_image(self.textures[self.TEXTURE_ONION], self.clip.images[self.clip.writehead - 1]) 1258 except IndexError, e: 1259 print("frame_remove:onion skin texture : %s" % (e.message)) 1260 self.signal_writehead(self.clip.writehead) 1261 self.signal_frame_remove()
1262
1263 - def effect_select(self, name_or_index=None):
1264 """ 1265 :param name_or_index: Either a int from 0 to n-1 or a string 1266 """ 1267 if self.config.verbose: 1268 print("effect_select(%s)" % (name_or_index)) 1269 all = self.effects.keys() 1270 curr = self.config.effect_name 1271 1272 if name_or_index is None: 1273 try: 1274 noeffect = all.index("None") 1275 except ValueError, e: 1276 print(e.message) 1277 else: 1278 self.config.effect_name = "None" 1279 elif type(name_or_index) == int: 1280 try: 1281 name = all[name_or_index] 1282 except IndexError, e: 1283 print(e.message) 1284 else: 1285 self.config.effect_name = name 1286 else: 1287 try: 1288 _tmp = self.effects[name_or_index] 1289 except IndexError, e: 1290 print(e.message) 1291 else: 1292 self.config.effect_name = name_or_index
1293
1294 - def onionskin_toggle(self, val=None):
1295 """ 1296 Toggles on/off the onion skin. 1297 (see most recent frame grabbed in transparency) 1298 """ 1299 if val is not None: 1300 self.config.onionskin_on = val is True # set to val 1301 else: 1302 self.config.onionskin_on = not self.config.onionskin_on # toggle 1303 print('config.onionskin_on = %s' % (self.config.onionskin_on))
1304
1305 - def framerate_increase(self, dir=1):
1306 """ 1307 Increase or decreases the FPS 1308 :param dir: by how much increment it. 1309 """ 1310 # TODO: for the user, it should be in FPS, not some weird ratio 1311 # does not alter the FPS of the renderer, but rather only the playback rate. 1312 # if self.renderer is not None: 1313 # # accesses the Renderer instance that owns this. 1314 # will_be = self.renderer.desired_fps + dir 1315 # if will_be > 0 and will_be <= 60: 1316 # self.renderer.desired_fps = will_be 1317 # print "FPS:", will_be 1318 will_be = self.clip.playhead_iterate_every - dir 1319 if will_be > self.config.framerate_min and will_be <= self.config.framerate_max: 1320 self.clip.playhead_iterate_every = will_be 1321 if self.config.verbose: 1322 print("Playhead frequency ratio: 30 / %d" % (will_be)) 1323 self.signal_framerate(will_be)
1324
1325 - def toggle_fullscreen(self):
1326 """ 1327 Toggles from window to fullscreen view. 1328 """ 1329 self.config.display_fullscreen != self.config.display_fullscreen 1330 pygame.display.toggle_fullscreen()
1331
1332 - def process_events(self, events):
1333 """ 1334 Processes pygame events. 1335 :param events: got them using pygame.event.get() 1336 """ 1337 for e in events: 1338 if e.type == PYGM.QUIT: 1339 self.running = False 1340 # TODO : catch window new size when resized. 1341 elif e.type == pygame.MOUSEBUTTONDOWN: 1342 if e.button == 1: 1343 self.frame_add() # left mouse button 1344 elif e.button == 3: 1345 self.frame_remove() # right mouse button 1346 elif e.type == pygame.VIDEORESIZE: 1347 print("VIDEORESIZE %s" % (e)) 1348 elif e.type == PYGM.KEYDOWN: 1349 modifiers = pygame.key.get_mods() 1350 if modifiers & PYGM.KMOD_LSHIFT != 0: 1351 if self.config.verbose: 1352 #print("Left shit is being pressed.") 1353 pass 1354 try: 1355 if self.config.verbose: 1356 if e.key < 255 and not e.key == PYGM.K_ESCAPE: 1357 c = chr(e.key) 1358 #print("key down: %s (\"%s\")" % (e.key, c)) 1359 if e.key == PYGM.K_k: # K 1360 self.intervalometer_rate_increase(1) 1361 elif e.key == PYGM.K_j: # J 1362 self.intervalometer_rate_increase(-1) 1363 elif e.key == PYGM.K_f: # F Fullscreen 1364 self.toggle_fullscreen() 1365 elif e.key == PYGM.K_i: # I Info 1366 self.print_stats() 1367 elif e.key == PYGM.K_p: # P Pause 1368 self.pause() 1369 elif e.key == PYGM.K_r: # R Reset 1370 self.clip_reset() 1371 elif e.key == PYGM.K_h: # H Help 1372 self.print_help() 1373 elif e.key == PYGM.K_s: # S Save 1374 self.clip_save() 1375 elif e.key == PYGM.K_x: # X : config save 1376 self.config_save() 1377 elif e.key == PYGM.K_n: # N : next effect 1378 self.effect_next() 1379 elif e.key == PYGM.K_o: # O Onion 1380 self.onionskin_toggle() 1381 elif e.key == PYGM.K_b: # B Background image snapshot 1382 self.bgimage_snap() 1383 elif e.key == PYGM.K_a: # A Auto 1384 print("toggle intervalometer") 1385 self.intervalometer_toggle() 1386 elif e.key == PYGM.K_q: # q Start recording sample 1387 if modifiers & PYGM.KMOD_LSHIFT != 0: # if left key is being pressed 1388 self.quit() 1389 else: 1390 self.sampler_record(True) 1391 elif e.key == PYGM.K_w: # Clear the sound in current frame 1392 self.sampler_clear() 1393 elif e.key == PYGM.K_0: # [0, 9] Clip selection 1394 self.clip_select(0) 1395 elif e.key == PYGM.K_1: 1396 self.clip_select(1) 1397 elif e.key == PYGM.K_2: 1398 self.clip_select(2) 1399 elif e.key == PYGM.K_3: 1400 self.clip_select(3) 1401 elif e.key == PYGM.K_4: 1402 self.clip_select(4) 1403 elif e.key == PYGM.K_5: 1404 self.clip_select(5) 1405 elif e.key == PYGM.K_6: 1406 self.clip_select(6) 1407 elif e.key == PYGM.K_7: 1408 self.clip_select(7) 1409 elif e.key == PYGM.K_8: 1410 self.clip_select(8) 1411 elif e.key == PYGM.K_9: 1412 self.clip_select(9) 1413 elif e.key == PYGM.K_UP: # UP Speed Increase 1414 self.framerate_increase(1) 1415 elif e.key == PYGM.K_DOWN: # DOWN Speed Decrease 1416 self.framerate_increase(-1) 1417 elif e.key == PYGM.K_RIGHT: # previous frame 1418 self.writehead_move(1) 1419 elif e.key == PYGM.K_LEFT: # next frame 1420 self.writehead_move(-1) 1421 elif e.key == PYGM.K_RETURN: # RETURN: goes to last frame 1422 self.writehead_goto(-1) 1423 elif e.key == PYGM.K_SPACE: # SPACE Add frame 1424 self.frame_add() 1425 elif e.key == PYGM.K_BACKSPACE: # BACKSPACE Remove frame 1426 self.frame_remove() 1427 elif e.key == PYGM.K_TAB: # TAB changes direction 1428 self.direction_change() 1429 elif e.key == PYGM.K_PERIOD: # PERIOD changes theme 1430 self.theme_change() 1431 elif e.key == PYGM.K_MINUS: 1432 pass # TODO 1433 elif e.key == PYGM.K_PLUS: 1434 pass # TODO 1435 elif e.key == PYGM.K_ESCAPE: 1436 self.toggle_fullscreen() 1437 except ValueError, e : 1438 if self.config.verbose: 1439 print("Key event error : %s" % (e.message))
1440
1441 - def quit(self):
1442 """ 1443 Quits the application in a short while. 1444 """ 1445 reactor.callLater(0.1, self._quit)
1446
1447 - def _quit(self):
1448 self.running = False
1449 # self.cleanup will be called. 1450
1451 - def direction_change(self, direction=None):
1452 """ 1453 Changes the direction of the current clip's playback. 1454 1455 If direction param is None, will cycle through the available directions. 1456 :param direction: int with the value of the constant DIRECTION_FORWARD or DIRECTION_BACKWARD 1457 """ 1458 if direction is None: 1459 if self.clip.direction == DIRECTION_BACKWARD: 1460 direction = DIRECTION_FORWARD 1461 elif self.clip.direction == DIRECTION_FORWARD: 1462 direction = DIRECTION_YOYO 1463 else: 1464 direction = DIRECTION_BACKWARD 1465 #if direction == DIRECTION_YOYO: 1466 # print("Playhead direction YOYO is not yet implemented.") # TODO 1467 #else: 1468 if self.config.verbose: 1469 print("Changing playback direction to %s" % (direction)) 1470 self.clip.direction = direction
1471
1472 - def autosave_toggle(self, val=None):
1473 """ 1474 Toggles on/off the autosave 1475 """ 1476 if self.config.autosave_enabled: 1477 if val is not None: 1478 self.config.autosave_on = val 1479 else: 1480 self.config.autosave_on = not self.config.autosave_on 1481 if self.config.autosave_on: 1482 self._autosave_delayed_id = reactor.callLater(0, self._autosave) 1483 if self.config.verbose: 1484 print("autosave ON") 1485 else: 1486 if self._autosave_delayed_id is not None: 1487 if self._autosave_delayed_id.active(): 1488 self._autosave_delayed_id.cancel() 1489 if self.config.verbose: 1490 print("autosave OFF")
1491
1492 - def intervalometer_toggle(self, val=None):
1493 """ 1494 Toggles on/off the intervalometer / timelapse / auto mode. 1495 """ 1496 if self.config.intervalometer_enabled: 1497 if val is not None: 1498 self.intervalometer_on = val 1499 else: 1500 self.intervalometer_on = not self.intervalometer_on 1501 if self.intervalometer_on: 1502 self._intervalometer_delayed_id = reactor.callLater(0, self._intervalometer_frame_add) 1503 if self.config.verbose: 1504 print("intervalometer ON") 1505 else: 1506 if self._intervalometer_delayed_id is not None: 1507 if self._intervalometer_delayed_id.active(): 1508 self._intervalometer_delayed_id.cancel() 1509 if self.config.verbose: 1510 print("intervalometer OFF")
1511
1512 - def intervalometer_rate_increase(self, dir=1):
1513 """ 1514 Increase or decreases the intervalometer rate. (in seconds) 1515 :param dir: by how much increment it. 1516 """ 1517 if self.config.intervalometer_enabled: 1518 will_be = self.config.intervalometer_rate_seconds + dir 1519 if will_be > 0 and will_be <= 60: 1520 self.config.intervalometer_rate_seconds = will_be 1521 print("auto rate: %s" % (will_be)) 1522 if self.config.verbose: 1523 print("Cleaning up before exiting.")
1524
1525 - def _intervalometer_frame_add(self):
1526 """ 1527 Called when it is time to automatically grab an image. 1528 1529 The auto mode is like an intervalometer to create timelapse animations. 1530 """ 1531 self.frame_add() 1532 if self.config.verbose: 1533 print("intervalometer auto grab %d" % (len(self.clip.images))) 1534 sys.stdout.flush() 1535 if self.intervalometer_on: 1536 self._intervalometer_delayed_id = reactor.callLater(self.config.intervalometer_rate_seconds, self._intervalometer_frame_add)
1537
1538 - def _autosave(self):
1539 """ 1540 Called when it is time to automatically save a movie 1541 """ 1542 # self.frame_add() 1543 if self.config.verbose: 1544 print("autosave") 1545 self.clip_save() 1546 if self.config.autosave_on: 1547 self._autosave_delayed_id = reactor.callLater(self.config.autosave_rate_seconds, self._autosave)
1548
1549 - def cleanup(self):
1550 """ 1551 Called by the rats.render.Renderer before quitting the application. 1552 """ 1553 if self.config.verbose: 1554 print("Cleaning up.") 1555 if self.config.osc_enabled: 1556 if self.config.verbose: 1557 print("Deleting OSC sender/receiver.") 1558 del self.osc
1559 # glDeleteTextures(3, self.textures) 1560
1561 - def config_set(name, value):
1562 """ 1563 Changes a configuration option. 1564 """ 1565 self.config.set(name, value)
1566
1567 - def config_save(self):
1568 """ 1569 Saves the current config to a file. 1570 """ 1571 if self.config.verbose: 1572 print("Save config to %s" % (self.config.config_file)) 1573 self.config.save()
1574