OLD | NEW |
| (Empty) |
1 {EventEmitter} = require 'events' | |
2 browser = require './browser.coffee' | |
3 | |
4 class GIF extends EventEmitter | |
5 | |
6 defaults = | |
7 workerScript: 'gif.worker.js' | |
8 workers: 2 | |
9 repeat: 0 # repeat forever, -1 = repeat once | |
10 background: '#fff' | |
11 quality: 10 # pixel sample interval, lower is better | |
12 width: null # size derermined from first frame if possible | |
13 height: null | |
14 transparent: null | |
15 | |
16 frameDefaults = | |
17 delay: 500 # ms | |
18 copy: false | |
19 | |
20 constructor: (options) -> | |
21 @running = false | |
22 | |
23 @options = {} | |
24 @frames = [] | |
25 | |
26 @freeWorkers = [] | |
27 @activeWorkers = [] | |
28 | |
29 @setOptions options | |
30 for key, value of defaults | |
31 @options[key] ?= value | |
32 | |
33 setOption: (key, value) -> | |
34 @options[key] = value | |
35 if @_canvas? and key in ['width', 'height'] | |
36 @_canvas[key] = value | |
37 | |
38 setOptions: (options) -> | |
39 @setOption key, value for own key, value of options | |
40 | |
41 addFrame: (image, options={}) -> | |
42 frame = {} | |
43 frame.transparent = @options.transparent | |
44 for key of frameDefaults | |
45 frame[key] = options[key] or frameDefaults[key] | |
46 | |
47 # use the images width and height for options unless already set | |
48 @setOption 'width', image.width unless @options.width? | |
49 @setOption 'height', image.height unless @options.height? | |
50 | |
51 if ImageData? and image instanceof ImageData | |
52 frame.data = image.data | |
53 else if (CanvasRenderingContext2D? and image instanceof CanvasRenderingConte
xt2D) or (WebGLRenderingContext? and image instanceof WebGLRenderingContext) | |
54 if options.copy | |
55 frame.data = @getContextData image | |
56 else | |
57 frame.context = image | |
58 else if image.childNodes? | |
59 if options.copy | |
60 frame.data = @getImageData image | |
61 else | |
62 frame.image = image | |
63 else | |
64 throw new Error 'Invalid image' | |
65 | |
66 @frames.push frame | |
67 | |
68 render: -> | |
69 throw new Error 'Already running' if @running | |
70 | |
71 if not @options.width? or not @options.height? | |
72 throw new Error 'Width and height must be set prior to rendering' | |
73 | |
74 @running = true | |
75 @nextFrame = 0 | |
76 @finishedFrames = 0 | |
77 | |
78 @imageParts = (null for i in [0...@frames.length]) | |
79 numWorkers = @spawnWorkers() | |
80 @renderNextFrame() for i in [0...numWorkers] | |
81 | |
82 @emit 'start' | |
83 @emit 'progress', 0 | |
84 | |
85 abort: -> | |
86 loop | |
87 worker = @activeWorkers.shift() | |
88 break unless worker? | |
89 console.log "killing active worker" | |
90 worker.terminate() | |
91 @running = false | |
92 @emit 'abort' | |
93 | |
94 # private | |
95 | |
96 spawnWorkers: -> | |
97 numWorkers = Math.min(@options.workers, @frames.length) | |
98 [@freeWorkers.length...numWorkers].forEach (i) => | |
99 console.log "spawning worker #{ i }" | |
100 worker = new Worker @options.workerScript | |
101 worker.onmessage = (event) => | |
102 @activeWorkers.splice @activeWorkers.indexOf(worker), 1 | |
103 @freeWorkers.push worker | |
104 @frameFinished event.data | |
105 @freeWorkers.push worker | |
106 return numWorkers | |
107 | |
108 frameFinished: (frame) -> | |
109 console.log "frame #{ frame.index } finished - #{ @activeWorkers.length } ac
tive" | |
110 @finishedFrames++ | |
111 @emit 'progress', @finishedFrames / @frames.length | |
112 @imageParts[frame.index] = frame | |
113 if null in @imageParts | |
114 @renderNextFrame() | |
115 else | |
116 @finishRendering() | |
117 | |
118 finishRendering: -> | |
119 len = 0 | |
120 for frame in @imageParts | |
121 len += (frame.data.length - 1) * frame.pageSize + frame.cursor | |
122 len += frame.pageSize - frame.cursor | |
123 console.log "rendering finished - filesize #{ Math.round(len / 1000) }kb" | |
124 data = new Uint8Array len | |
125 offset = 0 | |
126 for frame in @imageParts | |
127 for page, i in frame.data | |
128 data.set page, offset | |
129 if i is frame.data.length - 1 | |
130 offset += frame.cursor | |
131 else | |
132 offset += frame.pageSize | |
133 | |
134 image = new Blob [data], | |
135 type: 'image/gif' | |
136 | |
137 @emit 'finished', image, data | |
138 | |
139 renderNextFrame: -> | |
140 throw new Error 'No free workers' if @freeWorkers.length is 0 | |
141 return if @nextFrame >= @frames.length # no new frame to render | |
142 | |
143 frame = @frames[@nextFrame++] | |
144 worker = @freeWorkers.shift() | |
145 task = @getTask frame | |
146 | |
147 console.log "starting frame #{ task.index + 1 } of #{ @frames.length }" | |
148 @activeWorkers.push worker | |
149 worker.postMessage task#, [task.data.buffer] | |
150 | |
151 getContextData: (ctx) -> | |
152 return ctx.getImageData(0, 0, @options.width, @options.height).data | |
153 | |
154 getImageData: (image) -> | |
155 if not @_canvas? | |
156 @_canvas = document.createElement 'canvas' | |
157 @_canvas.width = @options.width | |
158 @_canvas.height = @options.height | |
159 | |
160 ctx = @_canvas.getContext '2d' | |
161 ctx.setFill = @options.background | |
162 ctx.fillRect 0, 0, @options.width, @options.height | |
163 ctx.drawImage image, 0, 0 | |
164 | |
165 return @getContextData ctx | |
166 | |
167 getTask: (frame) -> | |
168 index = @frames.indexOf frame | |
169 task = | |
170 index: index | |
171 last: index is (@frames.length - 1) | |
172 delay: frame.delay | |
173 transparent: frame.transparent | |
174 width: @options.width | |
175 height: @options.height | |
176 quality: @options.quality | |
177 repeat: @options.repeat | |
178 canTransfer: (browser.name is 'chrome') | |
179 | |
180 if frame.data? | |
181 task.data = frame.data | |
182 else if frame.context? | |
183 task.data = @getContextData frame.context | |
184 else if frame.image? | |
185 task.data = @getImageData frame.image | |
186 else | |
187 throw new Error 'Invalid frame' | |
188 | |
189 return task | |
190 | |
191 module.exports = GIF | |
OLD | NEW |