Permalink
Browse files

initial commit

  • Loading branch information...
jskinner committed Sep 17, 2012
1 parent c05bb3d commit ca86866ef5eab6cd6e7c40d39e039af0311c01dd
View
@@ -1,2 +1,6 @@
-anim_encoder
-============
+Animation Encoder
+============
+
+anim_encoder creates small JavaScript+HTML animations from a series on PNG images.
+
+Details are at http://www.sublimetext.com/~jps/animated_gifs_the_hard_way.html
View
@@ -0,0 +1,230 @@
+#!/usr/bin/python
+# Copyright (c) 2012, Sublime HQ Pty Ltd
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the <organization> nor the
+# names of its contributors may be used to endorse or promote products
+# derived from this software without specific prior written permission.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import scipy.ndimage.measurements as me
+import json
+import scipy.misc as misc
+import re
+import sys
+import os
+import cv2
+from numpy import *
+from time import time
+
+# How long to wait before the animation restarts
+END_FRAME_PAUSE = 4000
+
+# How many pixels can be wasted in the name of combining neighbouring changed
+# regions.
+SIMPLIFICATION_TOLERANCE = 512
+
+MAX_PACKED_HEIGHT = 10000
+
+def slice_size(a, b):
+ return (a.stop - a.start) * (b.stop - b.start)
+
+def combine_slices(a, b, c, d):
+ return (slice(min(a.start, c.start), max(a.stop, c.stop)),
+ slice(min(b.start, d.start), max(b.stop, d.stop)))
+
+def slices_intersect(a, b, c, d):
+ if (a.start >= c.stop): return False
+ if (c.start >= a.stop): return False
+ if (b.start >= d.stop): return False
+ if (d.start >= b.stop): return False
+ return True
+
+# Combine a large set of rectangles into a smaller set of rectangles,
+# minimising the number of additional pixels included in the smaller set of
+# rectangles
+def simplify(boxes, tol = 0):
+ out = []
+ for a,b in boxes:
+ sz1 = slice_size(a, b)
+ did_combine = False
+ for i in xrange(len(out)):
+ c,d = out[i]
+ cu, cv = combine_slices(a, b, c, d)
+ sz2 = slice_size(c, d)
+ if slices_intersect(a, b, c, d) or (slice_size(cu, cv) <= sz1 + sz2 + tol):
+ out[i] = (cu, cv)
+ did_combine = True
+ break
+ if not did_combine:
+ out.append((a,b))
+
+ if tol != 0:
+ return simplify(out, 0)
+ else:
+ return out
+
+def slice_tuple_size(s):
+ a, b = s
+ return (a.stop - a.start) * (b.stop - b.start)
+
+# Allocates space in the packed image. This does it in a slow, brute force
+# manner.
+class Allocator2D:
+ def __init__(self, rows, cols):
+ self.bitmap = zeros((rows, cols), dtype=uint8)
+ self.available_space = zeros(rows, dtype=uint32)
+ self.available_space[:] = cols
+ self.num_used_rows = 0
+
+ def allocate(self, w, h):
+ bh, bw = shape(self.bitmap)
+
+ for row in xrange(bh - h + 1):
+ if self.available_space[row] < w:
+ continue
+
+ for col in xrange(bw - w + 1):
+ if self.bitmap[row, col] == 0:
+ if not self.bitmap[row:row+h,col:col+w].any():
+ self.bitmap[row:row+h,col:col+w] = 1
+ self.available_space[row:row+h] -= w
+ self.num_used_rows = max(self.num_used_rows, row + h)
+ return row, col
+ raise RuntimeError()
+
+def find_matching_rect(bitmap, num_used_rows, packed, src, sx, sy, w, h):
+ template = src[sy:sy+h, sx:sx+w]
+ bh, bw = shape(bitmap)
+ image = packed[0:num_used_rows, 0:bw]
+
+ if num_used_rows < h:
+ return None
+
+ result = cv2.matchTemplate(image,template,cv2.TM_CCOEFF_NORMED)
+
+ row,col = unravel_index(result.argmax(),result.shape)
+ if ((packed[row:row+h,col:col+w] == src[sy:sy+h,sx:sx+w]).all()
+ and (packed[row:row+1,col:col+w,0] == src[sy:sy+1,sx:sx+w,0]).all()):
+ return row,col
+ else:
+ return None
+
+def generate_animation(anim_name):
+ frames = []
+ rex = re.compile("screen_([0-9]+).png")
+ for f in os.listdir(anim_name):
+ m = re.search(rex, f)
+ if m:
+ frames.append((int(m.group(1)), anim_name + "/" + f))
+ frames.sort()
+
+ images = [misc.imread(f) for t, f in frames]
+
+ zero = images[0] - images[0]
+ pairs = zip([zero] + images[:-1], images)
+ diffs = [sign((b - a).max(2)) for a, b in pairs]
+
+ # Find different objects for each frame
+ img_areas = [me.find_objects(me.label(d)[0]) for d in diffs]
+
+ # Simplify areas
+ img_areas = [simplify(x, SIMPLIFICATION_TOLERANCE) for x in img_areas]
+
+ ih, iw, _ = shape(images[0])
+
+ # Generate a packed image
+ allocator = Allocator2D(MAX_PACKED_HEIGHT, iw)
+ packed = zeros((MAX_PACKED_HEIGHT, iw, 3), dtype=uint8)
+
+ # Sort the rects to be packed by largest size first, to improve the packing
+ rects_by_size = []
+ for i in xrange(len(images)):
+ src_rects = img_areas[i]
+
+ for j in xrange(len(src_rects)):
+ rects_by_size.append((slice_tuple_size(src_rects[j]), i, j))
+
+ rects_by_size.sort(reverse = True)
+
+ allocs = [[None] * len(src_rects) for src_rects in img_areas]
+
+ print anim_name,"packing, num rects:",len(rects_by_size),"num frames:",len(images)
+
+ t0 = time()
+
+ for size,i,j in rects_by_size:
+ src = images[i]
+ src_rects = img_areas[i]
+
+ a, b = src_rects[j]
+ sx, sy = b.start, a.start
+ w, h = b.stop - b.start, a.stop - a.start
+
+ # See if the image data already exists in the packed image. This takes
+ # a long time, but results in worthwhile space savings (20% in one
+ # test)
+ existing = find_matching_rect(allocator.bitmap, allocator.num_used_rows, packed, src, sx, sy, w, h)
+ if existing:
+ dy, dx = existing
+ allocs[i][j] = (dy, dx)
+ else:
+ dy, dx = allocator.allocate(w, h)
+ allocs[i][j] = (dy, dx)
+
+ packed[dy:dy+h, dx:dx+w] = src[sy:sy+h, sx:sx+w]
+
+ print anim_name,"packing finished, took:",time() - t0
+
+ packed = packed[0:allocator.num_used_rows]
+
+ misc.imsave(anim_name + "_packed_tmp.png", packed)
+ os.system("pngcrush -q " + anim_name + "_packed_tmp.png " + anim_name + "_packed.png")
+ os.system("rm " + anim_name + "_packed_tmp.png")
+
+ # Generate JSON to represent the data
+ times = [t for t, f in frames]
+ delays = (array(times[1:] + [times[-1] + END_FRAME_PAUSE]) - array(times)).tolist()
+
+ timeline = []
+ for i in xrange(len(images)):
+ src_rects = img_areas[i]
+ dst_rects = allocs[i]
+
+ blitlist = []
+
+ for j in xrange(len(src_rects)):
+ a, b = src_rects[j]
+ sx, sy = b.start, a.start
+ w, h = b.stop - b.start, a.stop - a.start
+ dy, dx = dst_rects[j]
+
+ blitlist.append([dx, dy, w, h, sx, sy])
+
+ timeline.append({'delay': delays[i], 'blit': blitlist})
+
+ f = open(anim_name + '_anim.js', 'wb')
+ f.write(anim_name + "_timeline = ")
+ json.dump(timeline, f)
+ f.close()
+
+
+if __name__ == '__main__':
+ generate_animation(sys.argv[1])
View
@@ -0,0 +1,122 @@
+<!doctype html>
+<html>
+
+<head>
+<script type="text/javascript" src="example_anim.js"></script>
+<script type="text/javascript">
+var delay_scale = 0.7
+var timer = null
+
+var animate = function(img, timeline, element)
+{
+ var i = 0
+
+ var run_time = 0
+ for (var j = 0; j < timeline.length - 1; ++j)
+ run_time += timeline[j].delay
+
+ var f = function()
+ {
+ var frame = i++ % timeline.length
+ var delay = timeline[frame].delay * delay_scale
+ var blits = timeline[frame].blit
+
+ var ctx = element.getContext('2d')
+
+ for (j = 0; j < blits.length; ++j)
+ {
+ var blit = blits[j]
+ var sx = blit[0]
+ var sy = blit[1]
+ var w = blit[2]
+ var h = blit[3]
+ var dx = blit[4]
+ var dy = blit[5]
+ ctx.drawImage(img, sx, sy, w, h, dx, dy, w, h)
+ }
+
+ timer = window.setTimeout(f, delay)
+ }
+
+ if (timer) window.clearTimeout(timer)
+ f()
+}
+
+var animate_fallback = function(img, timeline, element)
+{
+ var i = 0
+
+ var run_time = 0
+ for (var j = 0; j < timeline.length - 1; ++j)
+ run_time += timeline[j].delay
+
+ var f = function()
+ {
+ if (i % timeline.length == 0)
+ {
+ while (element.hasChildNodes())
+ element.removeChild(element.lastChild)
+ }
+
+ var frame = i++ % timeline.length
+ var delay = timeline[frame].delay * delay_scale
+ var blits = timeline[frame].blit
+
+ for (j = 0; j < blits.length; ++j)
+ {
+ var blit = blits[j]
+ var sx = blit[0]
+ var sy = blit[1]
+ var w = blit[2]
+ var h = blit[3]
+ var dx = blit[4]
+ var dy = blit[5]
+
+ var d = document.createElement('div')
+ d.style.position = 'absolute'
+ d.style.left = dx + "px"
+ d.style.top = dy + "px"
+ d.style.width = w + "px"
+ d.style.height = h + "px"
+ d.style.backgroundImage = "url('" + img.src + "')"
+ d.style.backgroundPosition = "-" + sx + "px -" + sy + "px"
+
+ element.appendChild(d)
+ }
+
+ timer = window.setTimeout(f, delay)
+ }
+
+ if (timer) window.clearTimeout(timer)
+ f()
+}
+
+function set_animation(img_url, timeline, canvas_id, fallback_id)
+{
+ var img = new Image()
+ img.onload = function()
+ {
+ var canvas = document.getElementById(canvas_id)
+ if (canvas && canvas.getContext)
+ animate(img, timeline, canvas)
+ else
+ animate_fallback(img, timeline, document.getElementById(fallback_id))
+ }
+ img.src = img_url
+}
+</script>
+</head>
+
+<body>
+
+<p>Example Animation. Please ensure you've run anim_encoder.py to generate the required data.
+
+<div><canvas id="anim_target" class="anim_target" width="800" height="450">
+<div id="anim_fallback" class="anim_target" style="width: 800px; height: 450px; position: relative;"></div>
+<p></canvas></div>
+
+<script>
+set_animation("example_packed.png", example_timeline, 'anim_target', 'anim_fallback');
+</script>
+
+</body>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit ca86866

Please sign in to comment.