git @ Cat's Eye Technologies kinoje / 25b8600
Initial import of tool and a few examples. Flesh out README, too. Chris Pressey 6 years ago
7 changed file(s) with 444 addition(s) and 2 deletion(s). Raw diff Collapse all Expand all
0 # Byte-compiled / optimized / DLL files
1 __pycache__/
2 *.py[cod]
3 *$py.class
4
5 # C extensions
6 *.so
7
8 # Distribution / packaging
9 .Python
10 env/
11 build/
12 develop-eggs/
13 dist/
14 downloads/
15 eggs/
16 .eggs/
17 lib/
18 lib64/
19 parts/
20 sdist/
21 var/
22 *.egg-info/
23 .installed.cfg
24 *.egg
25
26 # PyInstaller
27 # Usually these files are written by a python script from a template
28 # before PyInstaller builds the exe, so as to inject date/other infos into it.
29 *.manifest
30 *.spec
31
32 # Installer logs
33 pip-log.txt
34 pip-delete-this-directory.txt
35
36 # Unit test / coverage reports
37 htmlcov/
38 .tox/
39 .coverage
40 .coverage.*
41 .cache
42 nosetests.xml
43 coverage.xml
44 *,cover
45 .hypothesis/
46
47 # Translations
48 *.mo
49 *.pot
50
51 # Django stuff:
52 *.log
53 local_settings.py
54
55 # Flask stuff:
56 instance/
57 .webassets-cache
58
59 # Scrapy stuff:
60 .scrapy
61
62 # Sphinx documentation
63 docs/_build/
64
65 # PyBuilder
66 target/
67
68 # IPython Notebook
69 .ipynb_checkpoints
70
71 # pyenv
72 .python-version
73
74 # celery beat schedule file
75 celerybeat-schedule
76
77 # dotenv
78 .env
79
80 # virtualenv
81 venv/
82 ENV/
83
84 # Spyder project settings
85 .spyderproject
86
87 # Rope project settings
88 .ropeproject
0 # kinoje
1 [WIP] A template-based animation tool
0 kinoje
1 ======
2
3 *Version 0.x, subject to change suddenly and erratically*
4
5 **kinoje** is a templating-based animation tool. A provided template is filled out once for each
6 frame of the animation; the result of the template expansion is used to create a still image; and
7 the resulting sequence of images is compiled into the finished movie.
8
9 The following are required:
10
11 * Python 2.7 — to run the script
12 * PyYAML and Jinja2 — to fill out the templates
13 * POV-Ray or Inkscape — to create the images from the filled-out templates
14 * ffmpeg or ImageMagick — to compile the images into a movie file
15
16 You might also find VLC useful, for viewing the final movie file.
17
18 On Ubuntu 16.04, you can install these with:
19
20 pip install --user Jinja2 PyYAML
21 sudo apt install povray povray-includes inkscape ffmpeg imagemagick vlc
22
23 You can then run the tool from the repository directory like so:
24
25 bin/kinoje eg/moebius.yaml moebius.mp4
26
27 You can also ask it to create a GIF by using that file extension:
28
29 bin/kinoje eg/squares.yaml squares.gif --duration=2.0
30
31 No further documentation on how to use the tool will be given, as it is all very subject to change
32 right now.
0 #!/usr/bin/env python
1
2 from os.path import realpath, dirname, join
3 import sys
4
5 sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src'))
6
7 from kinoje import main
8
9
10 if __name__ == '__main__':
11 main.main()
0 duration: 1.44
1 template: |-
2 global_settings { assumed_gamma 2.2 }
3
4 #include "colors.inc"
5 #include "textures.inc"
6 #include "woods.inc"
7
8 light_source { <-20, 50, 10> color White }
9 light_source { <30, 50, 0> color White }
10
11 camera { location <0, 20, -20> look_at <0, 0, 0> }
12
13 #declare Bar = box { <-4.0, -1.0, -0.5>, <4.0, 1.0, 0.5> }
14
15 {% set offset = tween(t, ((0.0, 1.0), (0.0, 180.0)), ) %}
16 {% for angle in range(0, 360, 15) %}
17 object { Bar
18 transform {
19 rotate <0, 0, {{ offset + angle / 2.0 }}>
20 translate <7, 0, 0>
21 rotate <0, {{ angle }}, 0>
22 }
23 texture {
24 White_Marble
25 }
26 }
27 {% endfor %}
0 type: svg
1 template: |-
2 <?xml version="1.0" standalone="no"?>
3 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4 <svg viewBox="0 0 400 400" version="1.1">
5 <rect width="100%" height="100%" fill="white" />
6 <rect x="25%" y="25%" width="50%" height="50%" fill="black" transform="rotate({{ t * 90.0 }} 200 200)" />
7 <rect x="37.5%" y="37.5%" width="25%" height="25%" fill="white" transform="rotate({{ t * -90.0 }} 200 200)" />
8 </svg>
(New empty file)
0 """\
1 kinoje {options} input-file.yaml output-file.{gif,m4v,mp4}
2
3 Create a movie file from the template and configuration in the given YAML file."""
4
5 # Note: just about everything here is subject to change!
6
7 from datetime import datetime, timedelta
8 from copy import copy
9 import math
10 from optparse import OptionParser
11 import os
12 import re
13 from subprocess import check_call
14 import sys
15 from tempfile import mkdtemp
16
17 from jinja2 import Template
18 import yaml
19 try:
20 from yaml import CLoader as Loader
21 except ImportError:
22 from yaml import Loader
23
24
25 class LoggingExecutor(object):
26 def __init__(self, filename):
27 self.filename = filename
28 self.log = open(filename, 'w')
29
30 def do_it(self, cmd, **kwargs):
31 print cmd
32 try:
33 check_call(cmd, shell=True, stdout=self.log, stderr=self.log, **kwargs)
34 except Exception as e:
35 self.log.close()
36 print str(e)
37 check_call("tail %s" % self.filename, shell=True)
38 sys.exit(1)
39
40 def close(self):
41 self.log.close()
42
43
44 def fmod(n, d):
45 return n - d * int(n / d)
46
47
48 def tween(t, *args):
49 """Format: after t, each arg should be like
50 ((a, b), c)
51 which means: when t >= a and < b, return c,
52 or like
53 ((a, b), (c, d))
54 which means:
55 when t >= a and < b, return a value between c and d which is proportional to the
56 position between a and b that t is,
57 or like
58 ((a, b), (c, d), f)
59 which means the same as case 2, except the function f is applied to the value between c and d
60 before it is returned.
61 """
62 nargs = []
63 for x in args:
64 a = x[0]
65 b = x[1]
66 if not isinstance(x[1], tuple):
67 b = (x[1], x[1])
68 if len(x) == 2:
69 f = lambda z: z
70 else:
71 f = x[2]
72 nargs.append((a, b, f))
73
74 for ((low, hi), (sc_low, sc_hi), f) in nargs:
75 if t >= low and t < hi:
76 pos = (t - low) / (hi - low)
77 sc = sc_low + ((sc_hi - sc_low) * pos)
78 return f(sc)
79 raise ValueError(t)
80
81
82 def main():
83 optparser = OptionParser(__doc__)
84 optparser.add_option("--width", default=320, type=int)
85 optparser.add_option("--height", default=200, type=int)
86
87 optparser.add_option("--tiny", default=False, action='store_true')
88 optparser.add_option("--small", default=False, action='store_true')
89 optparser.add_option("--big", default=False, action='store_true')
90 optparser.add_option("--huge", default=False, action='store_true')
91 optparser.add_option("--giant", default=False, action='store_true')
92 optparser.add_option("--square", default=False, action='store_true')
93
94 optparser.add_option("--start", default=0.0, type=float, metavar='INSTANT',
95 help="t-value at which to start rendering the movie. Default=0.0"
96 )
97 optparser.add_option("--stop", default=1.0, type=float, metavar='INSTANT',
98 help="t-value at which to stop rendering the movie. Default=1.0"
99 )
100 optparser.add_option("--duration", default=None, type=float, metavar='SECONDS',
101 help="Override the duration specified in the configuration."
102 )
103
104 optparser.add_option("--fps", default=25.0, type=float, metavar='FPS',
105 help="The number of frames to render for each second. Note that the "
106 "tool that makes a movie file from images might not honour this value exactly."
107 )
108 optparser.add_option("--still", default=None, type=float)
109 optparser.add_option("--view", default=False, action='store_true')
110 optparser.add_option("--twitter", default=False, action='store_true',
111 help="Make the last frame in a GIF animation delay only half as long."
112 )
113
114 optparser.add_option("--config", default=None, type=str)
115
116 (options, args) = optparser.parse_args(sys.argv[1:])
117
118 if options.tiny:
119 options.width = 160
120 options.height = 100
121 if options.small:
122 options.width = 320
123 options.height = 200
124 if options.big:
125 options.width = 640
126 options.height = 400
127 if options.huge:
128 options.width = 800
129 options.height = 600
130 if options.giant:
131 options.width = 1280
132 options.height = 800
133
134 if options.square:
135 options.height = options.width
136
137 if options.still is not None:
138 options.duration = 1.0
139 options.start = options.still
140
141 infilename = args[0]
142 try:
143 outfilename = args[1]
144 except IndexError:
145 (inbase, inext) = os.path.splitext(infilename)
146 outfilename = inbase + '.mp4'
147 (whatever, outext) = os.path.splitext(outfilename)
148 SUPPORTED_OUTPUT_FORMATS = ('.m4v', '.mp4', '.gif')
149 if outext not in SUPPORTED_OUTPUT_FORMATS:
150 raise ValueError("%s not a supported output format (%r)" % (outext, SUPPORTED_OUTPUT_FORMATS))
151
152 with open(infilename, 'r') as file_:
153 config = yaml.load(file_, Loader=Loader)
154
155 if options.config is not None:
156 settings = {}
157 for setting_string in options.config.split(','):
158 key, value = setting_string.split(':')
159 if re.match(r'^\d*\.?\d*$', value):
160 value = float(value)
161 settings[key] = value
162 config.update(settings)
163
164 template = Template(config['template'])
165
166 fun_context = {}
167 for key, value in config.get('functions', {}).iteritems():
168 fun_context[key] = eval("lambda x: " + value)
169
170 tempdir = mkdtemp()
171
172 frame_fmt = "out%05d.png"
173 framerate = options.fps
174
175 duration = options.duration
176 if duration is None:
177 duration = config['duration']
178
179 start_time = options.start * duration
180 stop_time = options.stop * duration
181 requested_duration = stop_time - start_time
182 num_frames = int(requested_duration * framerate)
183 t_step = 1.0 / (duration * framerate)
184
185 print "Start time: t=%s, %s seconds" % (options.start, start_time)
186 print "Stop time: t=%s, %s seconds" % (options.stop, stop_time)
187 print "Requested duration: %s seconds" % requested_duration
188 print "Frame rate: %s fps" % framerate
189 print "Number of frames: %s (rounded to %s)" % (requested_duration * framerate, num_frames)
190 print "t-Step: %s" % t_step
191
192 exe = LoggingExecutor(os.path.join(tempdir, 'movie.log'))
193 t = options.start
194
195 started_at = datetime.now()
196
197 for frame in xrange(num_frames):
198
199 elapsed = (datetime.now() - started_at).total_seconds()
200 eta = '???'
201 if frame > 0:
202 seconds_per_frame = elapsed / float(frame)
203 eta = started_at + timedelta(seconds=num_frames * seconds_per_frame)
204
205 print "t=%s (%s%% done, eta %s)" % (t, int(((t - options.start) / options.stop) * 100), eta)
206
207 out_pov = os.path.join(tempdir, 'out.pov')
208 context = copy(config)
209 context.update(fun_context)
210 context.update({
211 'width': float(options.width),
212 'height': float(options.height),
213 't': t,
214 'math': math,
215 'tween': tween,
216 'fmod': fmod,
217 })
218 with open(out_pov, 'w') as f:
219 f.write(template.render(context))
220 fn = os.path.join(tempdir, frame_fmt % frame)
221 render_type = config.get('type', 'povray')
222 if render_type == 'povray':
223 cmd_template = "povray -D +I{infile} +O{outfile} +W{width} +H{height} +A"
224 elif render_type == 'svg':
225 cmd_template = "inkscape -z -e {outfile} -w {width} -h {height} {infile}"
226 else:
227 raise NotImplementedError
228 cmd = cmd_template.format(
229 infile=out_pov, outfile=fn, width=options.width, height=options.height
230 )
231 exe.do_it(cmd)
232 t += t_step
233
234 if options.still is not None:
235 exe.do_it("eog %s" % fn)
236 sys.exit(0)
237
238 if outext == '.gif':
239 # TODO: show some warning if this is not an integer delay
240 delay = int(100.0 / framerate)
241
242 filenames = [os.path.join(tempdir, frame_fmt % f) for f in xrange(0, num_frames)]
243 if options.twitter:
244 filespec = ' '.join(filenames[:-1] + ['-delay', str(delay / 2), filenames[-1]])
245 else:
246 filespec = ' '.join(filenames)
247
248 # -strip is there to force convert to process all input files. (if no transformation is given,
249 # it can sometimes stop reading input files. leading to skippy animations. who knows why.)
250 exe.do_it("convert -delay %s -loop 0 %s -strip %s" % (
251 delay, filespec, outfilename
252 ))
253 finished_at = datetime.now()
254 if options.view:
255 exe.do_it("eog %s" % outfilename)
256 elif outext in ('.mp4', '.m4v'):
257 ifmt = os.path.join(tempdir, frame_fmt)
258 # fun fact: even if you say -r 30, it still picks 25 fps
259 cmd = "ffmpeg -i %s -c:v libx264 -profile:v baseline -pix_fmt yuv420p -r %s -y %s" % (
260 ifmt, int(framerate), outfilename
261 )
262 exe.do_it(cmd)
263 finished_at = datetime.now()
264 if options.view:
265 exe.do_it("vlc %s" % outfilename)
266 else:
267 raise NotImplementedError
268
269 exe.close()
270
271 run_duration = finished_at - started_at
272 print "Finished, took %s seconds" % run_duration.total_seconds()