Skip to content

Commit ca86866

Browse files
committed
initial commit
1 parent c05bb3d commit ca86866

16 files changed

+358
-2
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1-
anim_encoder
2-
============
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

anim_encoder.py

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
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 <organization> 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 <COPYRIGHT HOLDER> 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])

example.html

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<!doctype html>
2+
<html>
3+
4+
<head>
5+
<script type="text/javascript" src="example_anim.js"></script>
6+
<script type="text/javascript">
7+
var delay_scale = 0.7
8+
var timer = null
9+
10+
var animate = function(img, timeline, element)
11+
{
12+
var i = 0
13+
14+
var run_time = 0
15+
for (var j = 0; j < timeline.length - 1; ++j)
16+
run_time += timeline[j].delay
17+
18+
var f = function()
19+
{
20+
var frame = i++ % timeline.length
21+
var delay = timeline[frame].delay * delay_scale
22+
var blits = timeline[frame].blit
23+
24+
var ctx = element.getContext('2d')
25+
26+
for (j = 0; j < blits.length; ++j)
27+
{
28+
var blit = blits[j]
29+
var sx = blit[0]
30+
var sy = blit[1]
31+
var w = blit[2]
32+
var h = blit[3]
33+
var dx = blit[4]
34+
var dy = blit[5]
35+
ctx.drawImage(img, sx, sy, w, h, dx, dy, w, h)
36+
}
37+
38+
timer = window.setTimeout(f, delay)
39+
}
40+
41+
if (timer) window.clearTimeout(timer)
42+
f()
43+
}
44+
45+
var animate_fallback = function(img, timeline, element)
46+
{
47+
var i = 0
48+
49+
var run_time = 0
50+
for (var j = 0; j < timeline.length - 1; ++j)
51+
run_time += timeline[j].delay
52+
53+
var f = function()
54+
{
55+
if (i % timeline.length == 0)
56+
{
57+
while (element.hasChildNodes())
58+
element.removeChild(element.lastChild)
59+
}
60+
61+
var frame = i++ % timeline.length
62+
var delay = timeline[frame].delay * delay_scale
63+
var blits = timeline[frame].blit
64+
65+
for (j = 0; j < blits.length; ++j)
66+
{
67+
var blit = blits[j]
68+
var sx = blit[0]
69+
var sy = blit[1]
70+
var w = blit[2]
71+
var h = blit[3]
72+
var dx = blit[4]
73+
var dy = blit[5]
74+
75+
var d = document.createElement('div')
76+
d.style.position = 'absolute'
77+
d.style.left = dx + "px"
78+
d.style.top = dy + "px"
79+
d.style.width = w + "px"
80+
d.style.height = h + "px"
81+
d.style.backgroundImage = "url('" + img.src + "')"
82+
d.style.backgroundPosition = "-" + sx + "px -" + sy + "px"
83+
84+
element.appendChild(d)
85+
}
86+
87+
timer = window.setTimeout(f, delay)
88+
}
89+
90+
if (timer) window.clearTimeout(timer)
91+
f()
92+
}
93+
94+
function set_animation(img_url, timeline, canvas_id, fallback_id)
95+
{
96+
var img = new Image()
97+
img.onload = function()
98+
{
99+
var canvas = document.getElementById(canvas_id)
100+
if (canvas && canvas.getContext)
101+
animate(img, timeline, canvas)
102+
else
103+
animate_fallback(img, timeline, document.getElementById(fallback_id))
104+
}
105+
img.src = img_url
106+
}
107+
</script>
108+
</head>
109+
110+
<body>
111+
112+
<p>Example Animation. Please ensure you've run anim_encoder.py to generate the required data.
113+
114+
<div><canvas id="anim_target" class="anim_target" width="800" height="450">
115+
<div id="anim_fallback" class="anim_target" style="width: 800px; height: 450px; position: relative;"></div>
116+
<p></canvas></div>
117+
118+
<script>
119+
set_animation("example_packed.png", example_timeline, 'anim_target', 'anim_fallback');
120+
</script>
121+
122+
</body>

example/screen_660305415.png

70.2 KB
Loading

example/screen_660306038.png

70.6 KB
Loading

example/screen_660306220.png

70.6 KB
Loading

example/screen_660306414.png

70.7 KB
Loading

example/screen_660306598.png

70.8 KB
Loading

example/screen_660306790.png

70.9 KB
Loading

example/screen_660307644.png

69.5 KB
Loading

0 commit comments

Comments
 (0)