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