1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
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
110 from pygame import time
111 from OpenGL import GL
112
113 PACKAGE_DATA_PATH = os.path.dirname(data.__file__)
114 DIRECTION_FORWARD = "forward"
115 DIRECTION_BACKWARD = "backward"
116 DIRECTION_YOYO = "yoyo"
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
122 """
123 Any error Toonloop might encounter
124 """
125 pass
126
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 """
137
138 self.verbose = True
139 self.video_device = 0
140
141
142 self.toonloop_home = os.path.expanduser("~/Documents/toonloop")
143 self.config_file = os.path.expanduser("~/.toonloop.json")
144 self.project_name = "new_project"
145
146 self.max_num_clips = 10
147 self.delete_jpeg = False
148
149
150 self.framerate_min = 1
151 self.framerate_max = 30
152
153
154
155 self.image_width = 320
156 self.image_height = 240
157 self.image_flip_horizontal = False
158
159
160
161
162 self.display_width = self.image_width * 2
163 self.display_height = self.image_height * 2
164 self.display_fullscreen = False
165 self.display_theme = THEME_SPLIT_SCREEN
166
167
168 self.web_server_port = 8000
169 self.web_enabled = False
170
171
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
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
186
187
188 self.onionskin_enabled = True
189 self.onionskin_on = False
190 self.onionskin_opacity = 0.3
191
192
193 self.bgimage_enabled = False
194 self.bgimage = os.path.join(PACKAGE_DATA_PATH, 'bgimage_02.jpg')
195
196
197
198 self.bgimage_glob_enabled = False
199 self.bgimage_glob = ''
200
201
202 self.fx_white_flash = True
203 self.fx_white_flash_alpha = 0.5
204
205
206 self.effect_name = "None"
207
208
209 self.intervalometer_on = False
210 self.intervalometer_enabled = True
211 self.intervalometer_rate_seconds = 10.0
212
213
214
215 self.autosave_on = False
216 self.autosave_enabled = True
217 self.autosave_rate_seconds = 600.0
218
219
220 self.midi_enabled = False
221 self.midi_verbose = True
222 self.midi_input_id = -1
223 self.midi_pedal_control_id = 64
224 self.midi_note_record = 41
225 self.midi_note_clear = 43
226
227
228 self.__dict__.update(**argd)
229
230
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__):
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
261 """
262 Load from JSON config file.
263
264 Uses self.config_file as a file to load from.
265 """
266
267
268 if STATESAVING_LOADED:
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
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
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
310
311
313 """
314 Toonloop clip.
315 A clip is a serie of frames.
316 """
318 """
319 :param id: int
320 """
321 self.id = id
322 self.playhead = 0
323 self.playhead_previous = 0
324 self.playhead_iterate_every = 1
325 self.direction = DIRECTION_FORWARD
326
327
328 self.__dict__.update(argd)
329 self.images = []
330 self.writehead = len(self.images)
331
333 """
334 Theme allow to customize the graphical attributes of the rendering.
335 This is the base theme with a split screen.
336 """
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
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
351
352
354 """
355 This theme is a picture in picture theme.
356 """
358 SplitScreenTheme.__init__(self)
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
367 """
368 This theme is a picture in picture theme as well, but the edit is big.
369 """
378
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
393 TEXTURE_MOST_RECENT = 0
394 TEXTURE_PLAYBACK = 1
395 TEXTURE_ONION = 2
396 TEXTURE_BACKGROUND = 3
397
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
410 self._display_size = (self.config.display_width, self.config.display_height)
411 self.running = True
412 self.pd = None
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
418 self.clip_id = 1
419 self.clip = None
420 self.clips = []
421 self._saver_progress = None
422 self._init_clips()
423 self.renderer = None
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
435 try:
436 icon = pygame.image.load(os.path.join(PACKAGE_DATA_PATH, "icon.png"))
437 pygame.display.set_icon(icon)
438 except pygame.error, e:
439 print("ERROR : Could not load icon : %s" % (e.message))
440
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)
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
452 self.most_recent_image = pygame.surface.Surface(self.image_size)
453
454 self.osc = None
455 self.camera = None
456 self.is_mac = False
457 self.textures = [0, 0, 0, 0]
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
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
475 self._autosave_delayed_id = None
476 if config.autosave_on:
477 self.autosave_toggle(True)
478
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
484
485 self.signal_playhead = sig.Signal()
486 self.signal_writehead = sig.Signal()
487 self.signal_framerate = sig.Signal()
488 self.signal_clip = sig.Signal()
489 self.signal_frame_add = sig.Signal()
490 self.signal_frame_remove = sig.Signal()
491 self.signal_sampler_record = sig.Signal()
492 self.signal_sampler_clear = sig.Signal()
493
494
495 reactor.callLater(0, self._start_services)
496
505
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
526
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
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
550
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
558 except:
559 print("Error loading web UI :")
560 print(sys.exc_info())
561
562 if self.config.fudi_enabled:
563 try:
564
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
574
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)
583 self.signal_writehead(0)
584
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
598 """
599 Starts of stops recording a sampler.
600 The sounds sampler handles this.
601 """
602 self.signal_sampler_record(start)
603
605 """
606 Clear the sound in current frame
607 The sounds sampler handles this.
608 """
609 self.signal_sampler_clear()
610
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:
619 note = event.data1
620 velocity = event.data2
621 on = event.data2 >= 1
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:
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
644 """
645 Loads initial background image.
646 """
647 if self.config.bgimage_enabled:
648 self.bgimage_load(self.config.bgimage)
649
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:
661 self.config.bgimage_enabled = False
662 print("Error with background image \"%s\": %s" % (path, e.message))
663 else:
664
665 draw.texture_from_image(self.textures[self.TEXTURE_BACKGROUND], self.background_image)
666
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
681 draw.texture_from_image(self.textures[self.TEXTURE_BACKGROUND], self.background_image)
682
683
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
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
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
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
761 """
762 Prints help for live keyboard controls.
763 """
764 print(INTERACTIVE_HELP)
765
767 """
768 Draws the head-up-display. Numbers, text, etc. overlayed on top of images.
769 """
770
771 pass
772
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
784 else:
785 self.clip_id = 0
786 self.clip = self.clips[self.clip_id]
787
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
815
816
817
819
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
829
830
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
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
849
850
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
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
881 self.clip.images.insert(index, self.most_recent_image.copy())
882 else:
883
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
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
895
896 self.signal_writehead(len(self.clip.images))
897 self.signal_frame_add()
898
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
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:
924 index = last - index
925 if index < -last:
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
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)
943
985
986
987
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
1016
1041
1054
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
1064 draw.texture_from_image(self.textures[self.TEXTURE_MOST_RECENT], self.most_recent_image)
1065
1084
1086 if self.config.onionskin_enabled and self.config.onionskin_on:
1087 if len(self.clip.images) > 0:
1088
1089 self._get_current_effect().pre_draw()
1090 GL.glPushMatrix()
1091 GL.glTranslatef(*self.theme.edit_pos)
1092 GL.glScalef(*self.theme.edit_scale)
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:
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)
1099 GL.glPopMatrix()
1100 self._get_current_effect().post_draw()
1101
1117
1119 """
1120 Increments the playhead position of one frame
1121 """
1122 if len(self.clip.images) == 0:
1123 self.clip.playhead_previous = 0
1124
1125 else:
1126
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
1133 if self.clip.playhead > self.clip.playhead_previous:
1134
1135 if self.clip.playhead >= len(self.clip.images) - 1:
1136 direction = DIRECTION_BACKWARD
1137 else:
1138 direction = DIRECTION_FORWARD
1139 else:
1140
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
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
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
1172
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
1190 """
1191 Deletes all frames from the current animation
1192 """
1193 self.clip.images = []
1194 self._clear_playback_view()
1195
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
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
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")
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()
1222
1224 """
1225 :param progress_ratio: float from 0 to 1
1226 """
1227 self._saver_progress = progress_ratio
1228
1230 """
1231 :param success: boolean
1232 """
1233 self._saver_progress = None
1234
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
1250
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
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
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
1301 else:
1302 self.config.onionskin_on = not self.config.onionskin_on
1303 print('config.onionskin_on = %s' % (self.config.onionskin_on))
1304
1306 """
1307 Increase or decreases the FPS
1308 :param dir: by how much increment it.
1309 """
1310
1311
1312
1313
1314
1315
1316
1317
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
1326 """
1327 Toggles from window to fullscreen view.
1328 """
1329 self.config.display_fullscreen != self.config.display_fullscreen
1330 pygame.display.toggle_fullscreen()
1331
1440
1442 """
1443 Quits the application in a short while.
1444 """
1445 reactor.callLater(0.1, self._quit)
1446
1448 self.running = False
1449
1450
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
1466
1467
1468 if self.config.verbose:
1469 print("Changing playback direction to %s" % (direction))
1470 self.clip.direction = direction
1471
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
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
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
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
1539 """
1540 Called when it is time to automatically save a movie
1541 """
1542
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
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
1560
1562 """
1563 Changes a configuration option.
1564 """
1565 self.config.set(name, value)
1566
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