├── example
├── screen_660305415.png
├── screen_660306038.png
├── screen_660306220.png
├── screen_660306414.png
├── screen_660306598.png
├── screen_660306790.png
├── screen_660307644.png
├── screen_660307810.png
├── screen_660307875.png
├── screen_660308049.png
├── screen_660308235.png
├── screen_660308285.png
└── screen_660309704.png
├── README.md
├── example.html
└── anim_encoder.py
/example/screen_660305415.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660305415.png
--------------------------------------------------------------------------------
/example/screen_660306038.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660306038.png
--------------------------------------------------------------------------------
/example/screen_660306220.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660306220.png
--------------------------------------------------------------------------------
/example/screen_660306414.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660306414.png
--------------------------------------------------------------------------------
/example/screen_660306598.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660306598.png
--------------------------------------------------------------------------------
/example/screen_660306790.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660306790.png
--------------------------------------------------------------------------------
/example/screen_660307644.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660307644.png
--------------------------------------------------------------------------------
/example/screen_660307810.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660307810.png
--------------------------------------------------------------------------------
/example/screen_660307875.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660307875.png
--------------------------------------------------------------------------------
/example/screen_660308049.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660308049.png
--------------------------------------------------------------------------------
/example/screen_660308235.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660308235.png
--------------------------------------------------------------------------------
/example/screen_660308285.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660308285.png
--------------------------------------------------------------------------------
/example/screen_660309704.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icai/anim_encoder/master/example/screen_660309704.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Animation Encoder
2 | ============
3 |
4 | anim_encoder creates small JavaScript+HTML animations from a series on PNG images.
5 |
6 | Details are at http://www.sublimetext.com/~jps/animated_gifs_the_hard_way.html
7 |
--------------------------------------------------------------------------------
/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
108 |
109 |
110 |
111 |
112 | Example Animation. Please ensure you've run anim_encoder.py to generate the required data.
113 |
114 |
117 |
118 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/anim_encoder.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Copyright (c) 2012, Sublime HQ Pty Ltd
3 | # All rights reserved.
4 |
5 | # Redistribution and use in source and binary forms, with or without
6 | # modification, are permitted provided that the following conditions are met:
7 | # * Redistributions of source code must retain the above copyright
8 | # notice, this list of conditions and the following disclaimer.
9 | # * Redistributions in binary form must reproduce the above copyright
10 | # notice, this list of conditions and the following disclaimer in the
11 | # documentation and/or other materials provided with the distribution.
12 | # * Neither the name of the nor the
13 | # names of its contributors may be used to endorse or promote products
14 | # derived from this software without specific prior written permission.
15 |
16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
27 | import scipy.ndimage.measurements as me
28 | import json
29 | import scipy.misc as misc
30 | import re
31 | import sys
32 | import os
33 | import cv2
34 | from numpy import *
35 | from time import time
36 |
37 | # How long to wait before the animation restarts
38 | END_FRAME_PAUSE = 4000
39 |
40 | # How many pixels can be wasted in the name of combining neighbouring changed
41 | # regions.
42 | SIMPLIFICATION_TOLERANCE = 512
43 |
44 | MAX_PACKED_HEIGHT = 10000
45 |
46 | def slice_size(a, b):
47 | return (a.stop - a.start) * (b.stop - b.start)
48 |
49 | def combine_slices(a, b, c, d):
50 | return (slice(min(a.start, c.start), max(a.stop, c.stop)),
51 | slice(min(b.start, d.start), max(b.stop, d.stop)))
52 |
53 | def slices_intersect(a, b, c, d):
54 | if (a.start >= c.stop): return False
55 | if (c.start >= a.stop): return False
56 | if (b.start >= d.stop): return False
57 | if (d.start >= b.stop): return False
58 | return True
59 |
60 | # Combine a large set of rectangles into a smaller set of rectangles,
61 | # minimising the number of additional pixels included in the smaller set of
62 | # rectangles
63 | def simplify(boxes, tol = 0):
64 | out = []
65 | for a,b in boxes:
66 | sz1 = slice_size(a, b)
67 | did_combine = False
68 | for i in xrange(len(out)):
69 | c,d = out[i]
70 | cu, cv = combine_slices(a, b, c, d)
71 | sz2 = slice_size(c, d)
72 | if slices_intersect(a, b, c, d) or (slice_size(cu, cv) <= sz1 + sz2 + tol):
73 | out[i] = (cu, cv)
74 | did_combine = True
75 | break
76 | if not did_combine:
77 | out.append((a,b))
78 |
79 | if tol != 0:
80 | return simplify(out, 0)
81 | else:
82 | return out
83 |
84 | def slice_tuple_size(s):
85 | a, b = s
86 | return (a.stop - a.start) * (b.stop - b.start)
87 |
88 | # Allocates space in the packed image. This does it in a slow, brute force
89 | # manner.
90 | class Allocator2D:
91 | def __init__(self, rows, cols):
92 | self.bitmap = zeros((rows, cols), dtype=uint8)
93 | self.available_space = zeros(rows, dtype=uint32)
94 | self.available_space[:] = cols
95 | self.num_used_rows = 0
96 |
97 | def allocate(self, w, h):
98 | bh, bw = shape(self.bitmap)
99 |
100 | for row in xrange(bh - h + 1):
101 | if self.available_space[row] < w:
102 | continue
103 |
104 | for col in xrange(bw - w + 1):
105 | if self.bitmap[row, col] == 0:
106 | if not self.bitmap[row:row+h,col:col+w].any():
107 | self.bitmap[row:row+h,col:col+w] = 1
108 | self.available_space[row:row+h] -= w
109 | self.num_used_rows = max(self.num_used_rows, row + h)
110 | return row, col
111 | raise RuntimeError()
112 |
113 | def find_matching_rect(bitmap, num_used_rows, packed, src, sx, sy, w, h):
114 | template = src[sy:sy+h, sx:sx+w]
115 | bh, bw = shape(bitmap)
116 | image = packed[0:num_used_rows, 0:bw]
117 |
118 | if num_used_rows < h:
119 | return None
120 |
121 | result = cv2.matchTemplate(image,template,cv2.TM_CCOEFF_NORMED)
122 |
123 | row,col = unravel_index(result.argmax(),result.shape)
124 | if ((packed[row:row+h,col:col+w] == src[sy:sy+h,sx:sx+w]).all()
125 | and (packed[row:row+1,col:col+w,0] == src[sy:sy+1,sx:sx+w,0]).all()):
126 | return row,col
127 | else:
128 | return None
129 |
130 | def generate_animation(anim_name):
131 | frames = []
132 | rex = re.compile("screen_([0-9]+).png")
133 | for f in os.listdir(anim_name):
134 | m = re.search(rex, f)
135 | if m:
136 | frames.append((int(m.group(1)), anim_name + "/" + f))
137 | frames.sort()
138 |
139 | images = [misc.imread(f) for t, f in frames]
140 |
141 | zero = images[0] - images[0]
142 | pairs = zip([zero] + images[:-1], images)
143 | diffs = [sign((b - a).max(2)) for a, b in pairs]
144 |
145 | # Find different objects for each frame
146 | img_areas = [me.find_objects(me.label(d)[0]) for d in diffs]
147 |
148 | # Simplify areas
149 | img_areas = [simplify(x, SIMPLIFICATION_TOLERANCE) for x in img_areas]
150 |
151 | ih, iw, _ = shape(images[0])
152 |
153 | # Generate a packed image
154 | allocator = Allocator2D(MAX_PACKED_HEIGHT, iw)
155 | packed = zeros((MAX_PACKED_HEIGHT, iw, 3), dtype=uint8)
156 |
157 | # Sort the rects to be packed by largest size first, to improve the packing
158 | rects_by_size = []
159 | for i in xrange(len(images)):
160 | src_rects = img_areas[i]
161 |
162 | for j in xrange(len(src_rects)):
163 | rects_by_size.append((slice_tuple_size(src_rects[j]), i, j))
164 |
165 | rects_by_size.sort(reverse = True)
166 |
167 | allocs = [[None] * len(src_rects) for src_rects in img_areas]
168 |
169 | print anim_name,"packing, num rects:",len(rects_by_size),"num frames:",len(images)
170 |
171 | t0 = time()
172 |
173 | for size,i,j in rects_by_size:
174 | src = images[i]
175 | src_rects = img_areas[i]
176 |
177 | a, b = src_rects[j]
178 | sx, sy = b.start, a.start
179 | w, h = b.stop - b.start, a.stop - a.start
180 |
181 | # See if the image data already exists in the packed image. This takes
182 | # a long time, but results in worthwhile space savings (20% in one
183 | # test)
184 | existing = find_matching_rect(allocator.bitmap, allocator.num_used_rows, packed, src, sx, sy, w, h)
185 | if existing:
186 | dy, dx = existing
187 | allocs[i][j] = (dy, dx)
188 | else:
189 | dy, dx = allocator.allocate(w, h)
190 | allocs[i][j] = (dy, dx)
191 |
192 | packed[dy:dy+h, dx:dx+w] = src[sy:sy+h, sx:sx+w]
193 |
194 | print anim_name,"packing finished, took:",time() - t0
195 |
196 | packed = packed[0:allocator.num_used_rows]
197 |
198 | misc.imsave(anim_name + "_packed_tmp.png", packed)
199 | os.system("pngcrush -q " + anim_name + "_packed_tmp.png " + anim_name + "_packed.png")
200 | os.system("rm " + anim_name + "_packed_tmp.png")
201 |
202 | # Generate JSON to represent the data
203 | times = [t for t, f in frames]
204 | delays = (array(times[1:] + [times[-1] + END_FRAME_PAUSE]) - array(times)).tolist()
205 |
206 | timeline = []
207 | for i in xrange(len(images)):
208 | src_rects = img_areas[i]
209 | dst_rects = allocs[i]
210 |
211 | blitlist = []
212 |
213 | for j in xrange(len(src_rects)):
214 | a, b = src_rects[j]
215 | sx, sy = b.start, a.start
216 | w, h = b.stop - b.start, a.stop - a.start
217 | dy, dx = dst_rects[j]
218 |
219 | blitlist.append([dx, dy, w, h, sx, sy])
220 |
221 | timeline.append({'delay': delays[i], 'blit': blitlist})
222 |
223 | f = open(anim_name + '_anim.js', 'wb')
224 | f.write(anim_name + "_timeline = ")
225 | json.dump(timeline, f)
226 | f.close()
227 |
228 |
229 | if __name__ == '__main__':
230 | generate_animation(sys.argv[1])
231 |
--------------------------------------------------------------------------------