├── 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 |

115 |
116 |

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 | --------------------------------------------------------------------------------