4
"""This module provides a Layer class and several derivatives. A Layer
5
is a graphical overlay that may be composited onto an image canvas.
7
Run this script standalone for a demonstration:
9
$ python libtovid/layer.py
11
Layer subclasses may combine graphical elements, including other Layers,
12
into a single interface for drawing and customizing those elements. Layers may
13
exhibit animation, through the use of keyframed drawing commands, or through
14
use of the Effect class (and its subclasses, as defined in libtovid/effect.py).
16
Each Layer subclass provides (at least) an __init__ function, and a draw
17
function. For more on how to use Layers, see the Layer class definition and
18
template example below.
41
from libtovid.utils import get_file_type
42
from libtovid.render.drawing import Drawing, save_image
43
from libtovid.render.effect import Effect
44
from libtovid.render.animation import Keyframe, Tween
45
from libtovid.media import MediaFile
46
from libtovid.transcode import rip
47
from libtovid import log
52
"""A visual element, or a composition of visual elements. Conceptually
53
similar to a layer in the GIMP or Photoshop, with support for animation
54
effects and sub-Layers.
57
"""Initialize the layer. Extend this in derived classes to accept
58
configuration settings for drawing the layer; call this function
59
from any derived __init__ functions."""
62
self._parent_flipbook = None
63
self._parent_layer = None
66
### Child-parent initialization
68
def _init_childs(self):
69
"""Give access to all descendant layers and effects to their parents.
71
In layers, you can access your parent layer (if sublayed) with:
73
and to the top Flipbook object with:
74
layer._parent_flipbook
76
for x in range(0, len(self.effects)):
77
self.effects[x]._init_parent_flipbook(self._parent_flipbook)
78
self.effects[x]._init_parent_layer(self)
79
for x in range(0, len(self.sublayers)):
80
self.sublayers[x][0]._init_parent_flipbook(self._parent_flipbook)
81
self.sublayers[x][0]._init_parent_layer(self)
82
self.sublayers[x][0]._init_childs(self)
84
def _init_parent_flipbook(self, flipbook):
85
self._parent_flipbook = flipbook
87
def _init_parent_layer(self, layer):
88
self._parent_layer = layer
92
### Derived-class interface
95
def draw(self, drawing, frame):
96
"""Draw the layer and all sublayers onto the given Drawing. Override
97
this function in derived layers."""
98
assert isinstance(drawing, Drawing)
101
### Sublayer and effect interface
104
def add_sublayer(self, layer, position=(0, 0)):
105
"""Add the given Layer as a sublayer of this one, at the given position.
106
Sublayers are drawn in the order they are added; each sublayer may have
107
its own effects, but the parent Layer's effects apply to all sublayers.
109
assert isinstance(layer, Layer)
110
self.sublayers.append((layer, position))
112
def draw_sublayers(self, drawing, frame):
113
"""Draw all sublayers onto the given Drawing for the given frame."""
114
assert isinstance(drawing, Drawing)
115
for sublayer, position in self.sublayers:
117
drawing.translate(position)
118
sublayer.draw(drawing, frame)
121
def add_effect(self, effect):
122
"""Add the given Effect to this Layer. A Layer may have multiple effects
123
applied to it; all effects apply to the current layer, and all sublayers.
125
assert isinstance(effect, Effect)
126
self.effects.append(effect)
128
def draw_with_effects(self, drawing, frame):
129
"""Render the entire layer, with all effects applied.
131
drawing: A Drawing object to draw the Layer on
132
frame: The frame number that is being drawn
135
# Do preliminary effect rendering
136
for effect in self.effects:
137
effect.pre_draw(drawing, frame)
138
# Draw the layer and sublayers
139
self.draw(drawing, frame)
140
# Close out effect rendering, in reverse (nested) order
141
for effect in reversed(self.effects):
142
effect.post_draw(drawing, frame)
145
# ============================================================================
147
# ============================================================================
148
# Copy and paste the following code to create your own Layer.
150
# Layer subclasses should define two things:
152
# __init__(): How to initialize the layer with parameters
153
# draw(): How do draw the layer on a Drawing
155
# First, declare the layer class name. Include (Layer) to indicate that your
157
class MyLayer (Layer):
158
"""Overlapping semitransparent rectangles.
159
(Modify this documentation string to describe what's in your layer)"""
161
# Here's the class initialization function, __init__. Define here any
162
# parameters that might be used to configure your layer's behavior or
163
# appearance in some way (along with default values, if you like). Here,
164
# we're allowing configuration of the fill and stroke colors, with default
165
# values of 'blue' and 'black', respectively:
167
def __init__(self, fill_color='blue', stroke_color='black'):
168
"""Create a MyLayer with the given fill and stroke colors."""
169
# Initialize the base Layer class. Always do this.
171
# Store the given colors, for later use
172
self.fill_color = fill_color
173
self.stroke_color = stroke_color
175
# The draw() function is responsible for rendering the contents of the
176
# layer onto a Drawing. It will use the configuration given to __init__
177
# (in this case, fill and stroke colors) to render something onto a Drawing
178
# associated with a particular frame number:
180
def draw(self, drawing, frame):
181
"""Draw MyLayer contents onto the given drawing, at the given frame
184
# For safety's sake, make sure you really have a Drawing object:
185
assert isinstance(drawing, Drawing)
187
# Save the drawing context. This prevents any upcoming effects or
188
# style changes from messing up surrounding layers in the Drawing.
191
# Get a Cairo pattern source for the fill and stroke colors
192
# (TODO: Make this easier, or use a simpler example)
193
fc = drawing.create_source(self.fill_color, 0.6)
194
sc = drawing.create_source(self.stroke_color)
196
# And a stroke width of 1, say:
197
drawing.stroke_width(1)
199
# Now, draw something. Here, a couple of pretty semitransparent
200
# rectangles, using the fill and stroke color patterns created earlier:
201
drawing.rectangle(0, 0, 50, 20)
204
drawing.rectangle(15, 12, 45, 28)
208
# Be sure to restore the drawing context afterwards:
211
# That's it! Your layer is ready to use. See the Demo section at the end of
212
# this file for examples on how to create and render Layers using Python.
214
# ============================================================================
215
# End of Layer template
216
# ============================================================================
219
class Background (Layer):
220
"""A background that fills the frame with a solid color, or an image."""
221
def __init__(self, color='black', filename=''):
224
self.filename = filename
226
def draw(self, drawing, frame):
227
assert isinstance(drawing, Drawing)
228
log.debug("Drawing Background")
229
width, height = drawing.size
231
# Fill drawing with an image
232
if self.filename is not '':
233
drawing.image(0, 0, width, height, self.filename)
234
# Fill drawing with a solid color
236
drawing.rectangle(0, 0, width, height)
237
drawing.fill(self.color)
241
### --------------------------------------------------------------------
244
"""A rectangular image, scaled to the given size.
246
image_source -- can be anything Drawing::image() can accept.
247
See documentation in render/drawing.py.
249
def __init__(self, image_source, (x, y), (width, height)):
251
self.size = (width, height)
252
self.image_source = image_source
253
self.position = (x, y)
256
def draw(self, drawing, frame=1):
257
assert isinstance(drawing, Drawing)
258
log.debug("Drawing Image")
260
# Save the source for future calls to draw, so no further
261
# processing will be necessary. And other effects can be done
262
# without interferring with the original source.
263
self.image_source = drawing.image(self.position[0], self.position[1],
264
self.size[0], self.size[1],
269
### --------------------------------------------------------------------
271
class VideoClip (Layer):
272
"""A rectangular video clip, scaled to the given size.
274
TODO: num_frames should accept a range [first, end], an int (1-INT) and
275
rip frames accordingly. For now, it only accepts an INT for the range 1-INT
277
def __init__(self, filename, (width, height), position=(0,0), num_frames=120):
279
self.filename = filename
280
self.mediafile = MediaFile(filename)
281
self.size = (width, height)
282
# List of filenames of individual frames
283
self.frame_files = []
284
# TODO: be able to change hardcoded default values
285
self.rip_frames(1, num_frames)
287
assert(isinstance(position, tuple))
288
self.position = position
290
def rip_frames(self, start, end):
291
"""Rip frames from the video file, from start to end frames."""
292
log.info("VideoClip: Ripping frames %s to %s" % (start, end))
293
outdir = '/tmp/%s_frames' % self.filename
294
self.frame_files = rip.rip_frames(self.mediafile, outdir,
297
def draw(self, drawing, frame=1):
298
"""Draw ripped video frames to the given drawing. For now, it's
299
necessary to call rip_frames() before calling this function.
303
assert isinstance(drawing, Drawing)
304
log.debug("Drawing VideoClip")
305
if len(self.frame_files) == 0:
306
log.error("VideoClip: need to call rip_frames() before drawing.")
309
# Loop frames (modular arithmetic)
310
if frame >= len(self.frame_files):
311
frame = frame % len(self.frame_files)
312
filename = self.frame_files[frame-1]
313
drawing.image(self.position, self.size, filename)
317
### --------------------------------------------------------------------
320
"""A simple text string, with size, color and font.
322
text -- UTF8 encoded string.
324
def __init__(self, text, position=(0, 0), color='white', fontsize=20, \
325
font='Helvetica', align='left'):
329
self.fontsize = fontsize
333
self.position = position
335
# TODO: This is gonna be pretty broken...
336
def extents(self, drawing):
337
"""Return the extents of the text as a (x0, y0, x1, y1) tuple."""
338
assert isinstance(drawing, Drawing)
340
drawing.font(self.font)
341
drawing.font_size(self.fontsize)
342
x_bearing, y_bearing, width, height, x_adv, y_adv = \
343
drawing.text_extents(self.text)
345
# Add current layer's position to the (x,y) bearing of extents
346
x0 = int(self.position[0] + x_bearing)
347
y0 = int(self.position[1] + y_bearing)
349
y1 = int(y0 + height)
350
return (x0, y0, x1, y1)
352
def draw(self, drawing, frame=1):
353
assert isinstance(drawing, Drawing)
354
log.debug("Drawing Text")
357
drawing.font(self.font)
358
drawing.font_size(self.fontsize)
359
if self.color is not None:
360
drawing.set_source(self.color)
361
# TODO: DO something with the align !!
362
drawing.text(self.text, self.position[0], self.position[1], self.align)
366
### --------------------------------------------------------------------
368
class ShadedText (Layer):
369
"""A simple text string, with size, color and font.
371
text -- UTF8 encoded string.
373
def __init__(self, text, position=(0, 0), offset=(5, 5),
374
color='white', shade_color='gray', fontsize=20,
375
font='Nimbus Sans', align='left'):
377
shade_position = (position[0] + offset[0],
378
position[1] + offset[1])
379
self.under = Text(text, shade_position, shade_color,
380
fontsize, font, align)
381
self.over = Text(text, position, color, fontsize, font, align)
383
def draw(self, drawing, frame=1):
384
assert isinstance(drawing, Drawing)
385
log.debug("Drawing Text")
387
self.under.draw(drawing, frame)
388
self.over.draw(drawing, frame)
392
### --------------------------------------------------------------------
395
"""A text string with a rectangular background.
397
You can access Text's extents() function from within here too."""
398
def __init__(self, text, position=(0,0), color='white', bgcolor='#555',
399
fontsize=20, font='NimbusSans'):
400
Text.__init__(self, text, position, color, fontsize, font)
401
self.bgcolor = bgcolor
403
assert(isinstance(position, tuple))
404
self.position = position
406
def draw(self, drawing, frame=1):
407
assert isinstance(drawing, Drawing)
408
log.debug("Drawing Label")
409
#(dx, dy, w, h, ax, ay) = self.extents(drawing)
410
(x0, y0, x1, y1) = self.extents(drawing)
414
# Calculate rectangle dimensions from text size/length
417
# Padding to use around text
418
pad = self.fontsize / 3
419
# Calculate start and end points of background rectangle
420
start = (-pad, -height - pad)
421
end = (width + pad, pad)
423
# Draw a stroked round rectangle
425
drawing.stroke_width(1)
426
drawing.roundrectangle(start[0], start[1], end[0], end[1], pad, pad)
427
drawing.stroke('black')
428
drawing.fill(self.bgcolor, 0.3)
431
# Call base Text class to draw the text
432
Text.draw(self, drawing, frame)
438
### --------------------------------------------------------------------
441
"""A thumbnail image or video."""
442
def __init__(self, filename, (width, height), position=(0,0), title=''):
444
self.filename = filename
445
self.size = (width, height)
446
self.title = title or os.path.basename(filename)
448
assert(isinstance(position, tuple))
449
self.position = position
451
# Determine whether file is a video or image, and create the
452
# appropriate sublayer
453
filetype = get_file_type(filename)
454
if filetype == 'video':
455
self.add_sublayer(VideoClip(filename, self.size, self.position))
456
elif filetype == 'image':
457
self.add_sublayer(Image(filename, self.size, self.position))
458
self.lbl = Label(self.title, fontsize=15)
459
self.add_sublayer(self.lbl, self.position)
461
def draw(self, drawing, frame=1):
462
assert isinstance(drawing, Drawing)
463
log.debug("Drawing Thumb")
465
(x0, y0, x1, y1) = self.lbl.extents(drawing)
466
drawing.translate(0, x1-x0)
467
self.draw_sublayers(drawing, frame)
471
### --------------------------------------------------------------------
473
class ThumbGrid (Layer):
474
"""A rectangular array of thumbnail images or videos."""
475
def __init__(self, files, titles=None, (width, height)=(600, 400),
476
(columns, rows)=(0, 0), aspect=(4,3)):
477
"""Create a grid of thumbnail images or videos from a list of files,
478
fitting in a space no larger than the given size, with the given number
479
of columns and rows. Use 0 to auto-layout columns or rows, or both
484
assert len(files) == len(titles)
488
self.size = (width, height)
489
# Auto-dimension (using desired rows/columns, if given)
490
self.columns, self.rows = \
491
self._fit_items(len(files), columns, rows, aspect)
492
# Calculate thumbnail size, keeping aspect
493
w = (width - self.columns * 16) / self.columns
494
h = w * aspect[1] / aspect[0]
497
# Calculate thumbnail positions
499
for row in range(self.rows):
500
for column in range(self.columns):
501
x = column * (width / self.columns)
502
y = row * (height / self.rows)
503
positions.append((x, y))
505
# Add Thumb sublayers
506
for file, title, position in zip(files, titles, positions):
507
title = os.path.basename(file)
508
self.add_sublayer(Thumb(file, thumbsize, (0, 0), title), position)
510
def _fit_items(self, num_items, columns, rows, aspect=(4, 3)):
511
# Both fixed, nothing to calculate
512
if columns > 0 and rows > 0:
513
# Make sure num_items will fit (columns, rows)
514
if num_items < columns * rows:
515
return (columns, rows)
516
# Not enough room; auto-dimension both
518
log.warning("ThumbGrid: Can't fit %s items" % num_items +\
519
" in (%s, %s) grid;" % (columns, rows) +\
520
" doing auto-dimensioning instead.")
522
# Auto-dimension to fit num_items
523
if columns == 0 and rows == 0:
524
# TODO: Take aspect ratio into consideration to find an optimal fit
525
root = int(math.floor(math.sqrt(num_items)))
526
return ((1 + num_items / root), root)
527
# Rows fixed; use enough columns to fit num_items
528
if columns == 0 and rows > 0:
529
return ((1 + num_items / rows), rows)
530
# Columns fixed; use enough rows to fit num_items
531
if rows == 0 and columns > 0:
532
return (columns, (1 + num_items / columns))
534
def draw(self, drawing, frame=1):
535
assert isinstance(drawing, Drawing)
536
log.debug("Drawing ThumbGrid")
538
self.draw_sublayers(drawing, frame)
542
### --------------------------------------------------------------------
544
class SafeArea (Layer):
545
"""Render a safe area box at a given percentage.
547
def __init__(self, percent, color):
548
self.percent = percent
551
def draw(self, drawing, frame=1):
552
assert isinstance(drawing, Drawing)
553
log.debug("Drawing SafeArea")
554
# Calculate rectangle dimensions
555
scale = float(self.percent) / 100.0
556
width, height = drawing.size
557
topleft = ((1.0 - scale) * width / 2,
558
(1.0 - scale) * height / 2)
561
drawing.translate(topleft[0], topleft[1])
563
drawing.stroke_width(3)
564
drawing.rectangle(0, 0, width * scale, height * scale)
565
drawing.stroke(self.color)
567
drawing.font_size(18)
568
drawing.set_source(self.color)
569
drawing.text(u"%s%%" % self.percent, 10, 20)
574
### --------------------------------------------------------------------
576
class Scatterplot (Layer):
577
"""A 2D scatterplot of data.
579
Untested since MVG move.
581
def __init__(self, xy_dict, width=240, height=80, x_label='', y_label=''):
582
"""Create a scatterplot using data in xy_dict, a dictionary of
583
lists of y-values, indexed by x-value."""
584
self.xy_dict = xy_dict
585
self.width, self.height = (width, height)
586
self.x_label = x_label
587
self.y_label = y_label
589
def draw(self, drawing, frame):
590
"""Draw the scatterplot."""
591
assert isinstance(drawing, Drawing)
592
log.debug("Drawing Scatterplot")
593
width, height = (self.width, self.height)
594
x_vals = self.xy_dict.keys()
597
largest = max(self.xy_dict[x] or [0])
600
# For numeric x, scale by maximum x-value
601
x_is_num = isinstance(x_vals[0], int) or isinstance(x_vals[0], float)
603
x_scale = float(width) / max(x_vals)
604
# For string x, scale by number of x-values
606
x_scale = float(width) / len(x_vals)
607
# Scale y according to maximum value
608
y_scale = float(height) / max_y
612
drawing.rectangle(0, 0, width, height)
613
drawing.fill('white', 0.75)
616
#->comment("Axes of scatterplot")
618
drawing.stroke_width(2)
619
drawing.line(0, 0, 0, height)
620
drawing.stroke('black')
621
drawing.line(0, height, width, height)
622
drawing.stroke('black')
627
drawing.set_source('blue')
630
for i, x in enumerate(x_vals):
633
drawing.translate(x * x_scale, height + 15)
635
drawing.translate(i * x_scale, height + 15)
637
drawing.text(x, 0, 0)
640
drawing.font_size(20)
641
drawing.text(self.x_label, width/2, height+40)
645
drawing.text(max_y, -30, 0)
646
drawing.translate(-25, height/2)
648
drawing.text(self.y_label, 0, 0)
653
# Plot all y-values for each x (as small circles)
654
#->comment("Scatterplot data")
656
for i, x in enumerate(x_vals):
658
x_coord = x * x_scale
660
x_coord = i * x_scale
661
# Shift x over slightly
663
# Plot all y-values for this x
664
for y in self.xy_dict[x]:
665
y_coord = height - y * y_scale
666
drawing.circle(x_coord, y_coord, 3)
667
drawing.fill('red', 0.2)
674
### --------------------------------------------------------------------
676
class InterpolationGraph (Layer):
677
# TODO: Support graphing of tuple data
678
"""A graph of an interpolation curve, defined by a list of Keyframes and
679
an interpolation method."""
680
def __init__(self, keyframes, size=(240, 80), method='linear'):
681
"""Create an interpolation graph of the given keyframes, at the given
682
size, using the given interpolation method."""
685
self.keyframes = keyframes
688
# Interpolate keyframes
689
self.tween = Tween(keyframes, method)
691
def draw(self, drawing, frame):
692
"""Draw the interpolation graph, including frame/value axes,
693
keyframes, and the interpolation curve."""
694
assert isinstance(drawing, Drawing)
695
log.debug("Drawing InterpolationGraph")
696
data = self.tween.data
697
# Calculate maximum extents of the graph
698
width, height = self.size
699
x_scale = float(width) / len(data)
700
y_scale = float(height) / max(data)
702
#->drawing.comment("InterpolationGraph Layer")
708
#->drawing.comment("Axes of graph")
710
drawing.stroke_width(3)
711
drawing.polyline([(0, 0), (0, height), (width, height)], False)
712
drawing.stroke('#ccc')
715
# Create a list of (x, y) points to be graphed
718
while x <= len(self.tween.data):
719
# y increases downwards; subtract from height to give a standard
720
# Cartesian-oriented graph (so y increases upwards)
721
point = (int(x * x_scale), int(height - data[x-1] * y_scale))
726
drawing.stroke_width(2)
727
drawing.polyline(curve, False)
728
drawing.stroke('blue')
731
# Draw Keyframes as dotted vertical lines
733
# Vertical dotted lines
734
drawing.set_source('red')
735
drawing.stroke_width(2)
736
for key in self.keyframes:
737
x = int(key.frame * x_scale)
738
drawing.line(x, 0, x, height)
739
drawing.stroke('red')
741
# Draw Keyframe labels
742
drawing.set_source('white')
743
for key in self.keyframes:
744
x = int(key.frame * x_scale)
745
y = int(height - key.data * y_scale - 3)
746
drawing.text(u"(%s,%s)" % (key.frame, key.data), x, y)
750
# Draw a yellow dot for current frame
751
#->drawing.comment("Current frame marker")
753
pos = (frame * x_scale, height - data[frame-1] * y_scale)
754
drawing.circle(pos[0], pos[1], 2)
755
drawing.fill('yellow')
762
### --------------------------------------------------------------------
764
class ColorBars (Layer):
765
"""Standard SMPTE color bars
766
(http://en.wikipedia.org/wiki/SMPTE_color_bars)
768
def __init__(self, size, position=(0,0)):
769
"""Create color bars in a region of the given size and position.
774
assert(isinstance(position, tuple))
775
self.position = position
777
def draw(self, drawing, frame=1):
778
assert isinstance(drawing, Drawing)
779
log.debug("Drawing ColorBars")
781
width, height = self.size
784
# Get to the right place and size
785
drawing.translate(x, y)
786
drawing.scale(width, height)
787
# Video-black background
788
drawing.rectangle(0, 0, 1, 1)
789
drawing.fill('rgb(16, 16, 16)')
791
# Top 67% of picture: Color bars at 75% amplitude
795
size = (seventh, bottom)
796
bars = [(0, top, seventh, bottom, 'rgb(191, 191, 191)'),
797
(seventh, top, seventh, bottom, 'rgb(191, 191, 0)'),
798
(2*seventh, top, seventh, bottom, 'rgb(0, 191, 191)'),
799
(3*seventh, top, seventh, bottom, 'rgb(0, 191, 0)'),
800
(4*seventh, top, seventh, bottom, 'rgb(191, 0, 191)'),
801
(5*seventh, top, seventh, bottom, 'rgb(191, 0, 0)'),
802
(6*seventh, top, seventh, bottom, 'rgb(0, 0, 191)')]
804
# Next 8% of picture: Reverse blue bars
807
height = bottom - top
808
bars.extend([(0, top, seventh, height, 'rgb(0, 0, 191)'),
809
(seventh, top, seventh, height, 'rgb(16, 16, 16)'),
810
(2*seventh, top, seventh, height, 'rgb(191, 0, 191)'),
811
(3*seventh, top, seventh, height, 'rgb(16, 16, 16)'),
812
(4*seventh, top, seventh, height, 'rgb(0, 191, 191)'),
813
(5*seventh, top, seventh, height, 'rgb(16, 16, 16)'),
814
(6*seventh, top, seventh, height, 'rgb(191, 191, 191)')])
816
# Lower 25%: Pluge signal
820
height = bottom - top
821
bars.extend([(0, top, 1.0, height, 'rgb(16, 16, 16)'),
822
(0, top, sixth, height, 'rgb(0, 29, 66)'),
823
(sixth, top, sixth, height, 'rgb(255, 255, 255)'),
824
(2*sixth, top, sixth, height, 'rgb(44, 0, 92)'),
825
# Sub- and super- black narrow bars
826
(4*sixth, top, 0.33*sixth, height, 'rgb(7, 7, 7)'),
827
(4.33*sixth, top, 0.33*sixth, height,'rgb(16, 16, 16)'),
828
(4.66*sixth, top, 0.33*sixth, height, 'rgb(24, 24, 24)')])
830
# Draw and fill all bars
831
for x, y, width, height, color in bars:
832
drawing.rectangle(x, y, width, height)
839
if __name__ == '__main__':
841
# Get arguments, if any
842
if len(sys.argv) > 1:
843
# Use all args as image filenames to ThumbGrid
844
images = sys.argv[1:]
846
# A Drawing to render Layer demos to
847
drawing = Drawing(800, 600)
849
# Draw a background layer
850
bgd = Background(color='#7080A0')
854
bars = ColorBars((320, 240), (400, 100))
855
bars.draw(drawing, 1)
859
drawing.translate(460, 200)
860
label = Label("tovid loves Linux")
861
label.draw(drawing, 1)
864
# Draw a text layer, with position.
865
text = Text("Jackdaws love my big sphinx of quartz",
867
text.draw(drawing, 1)
871
drawing.translate(80, 60)
872
text = Text("Jackdaws love my big sphinx of quartz")
873
text.draw(drawing, 1)
876
# Draw a template layer (overlapping semitransparent rectangles)
877
template = MyLayer('white', 'darkblue')
878
# Scale and translate the layer before drawing it
880
drawing.translate(50, 100)
881
drawing.scale(3.0, 3.0)
882
template.draw(drawing, 1)
885
# Draw a safe area test (experimental)
886
safe = SafeArea(93, 'yellow')
887
safe.draw(drawing, 1)
889
# Draw a thumbnail grid (if images were provided)
892
drawing.translate(350, 300)
893
thumbs = ThumbGrid(images, (320, 250))
894
thumbs.draw(drawing, 1)
897
# Draw an interpolation graph
899
drawing.translate(60, 400)
900
# Some random keyframes to graph
901
keys = [Keyframe(1, 25), Keyframe(10, 5), Keyframe(30, 35),
902
Keyframe(40, 35), Keyframe(45, 20), Keyframe(60, 40)]
903
interp = InterpolationGraph(keys, (400, 120), method="cosine")
904
interp.draw(drawing, 25)
914
drawing.translate(550, 350)
915
#drawing.scale(200, 200)
916
plot = Scatterplot(xy_data, 200, 200, "Spam", "Eggs")
917
plot.draw(drawing, 1)
920
log.info("Output to /tmp/my.png")
921
save_image(drawing, '/tmp/my.png', 800, 600)