UNPKG

gif.js

Version:

JavaScript GIF encoding library

210 lines (171 loc) 5.89 kB
{EventEmitter} = require 'events' browser = require './browser.coffee' class GIF extends EventEmitter defaults = workerScript: 'gif.worker.js' workers: 2 repeat: 0 # repeat forever, -1 = repeat once background: '#fff' quality: 10 # pixel sample interval, lower is better width: null # size derermined from first frame if possible height: null transparent: null debug: false dither: false # see GIFEncoder.js for dithering options frameDefaults = delay: 500 # ms copy: false constructor: (options) -> @running = false @options = {} @frames = [] @freeWorkers = [] @activeWorkers = [] @setOptions options for key, value of defaults @options[key] ?= value setOption: (key, value) -> @options[key] = value if @_canvas? and key in ['width', 'height'] @_canvas[key] = value setOptions: (options) -> @setOption key, value for own key, value of options addFrame: (image, options={}) -> frame = {} frame.transparent = @options.transparent for key of frameDefaults frame[key] = options[key] or frameDefaults[key] # use the images width and height for options unless already set @setOption 'width', image.width unless @options.width? @setOption 'height', image.height unless @options.height? if ImageData? and image instanceof ImageData frame.data = image.data else if (CanvasRenderingContext2D? and image instanceof CanvasRenderingContext2D) or (WebGLRenderingContext? and image instanceof WebGLRenderingContext) if options.copy frame.data = @getContextData image else frame.context = image else if image.childNodes? if options.copy frame.data = @getImageData image else frame.image = image else throw new Error 'Invalid image' @frames.push frame render: -> throw new Error 'Already running' if @running if not @options.width? or not @options.height? throw new Error 'Width and height must be set prior to rendering' @running = true @nextFrame = 0 @finishedFrames = 0 @imageParts = (null for i in [0...@frames.length]) numWorkers = @spawnWorkers() # we need to wait for the palette if @options.globalPalette == true @renderNextFrame() else @renderNextFrame() for i in [0...numWorkers] @emit 'start' @emit 'progress', 0 abort: -> loop worker = @activeWorkers.shift() break unless worker? @log 'killing active worker' worker.terminate() @running = false @emit 'abort' # private spawnWorkers: -> numWorkers = Math.min(@options.workers, @frames.length) [@freeWorkers.length...numWorkers].forEach (i) => @log "spawning worker #{ i }" worker = new Worker @options.workerScript worker.onmessage = (event) => @activeWorkers.splice @activeWorkers.indexOf(worker), 1 @freeWorkers.push worker @frameFinished event.data @freeWorkers.push worker return numWorkers frameFinished: (frame) -> @log "frame #{ frame.index } finished - #{ @activeWorkers.length } active" @finishedFrames++ @emit 'progress', @finishedFrames / @frames.length @imageParts[frame.index] = frame # remember calculated palette, spawn the rest of the workers if @options.globalPalette == true @options.globalPalette = frame.globalPalette @log 'global palette analyzed' @renderNextFrame() for i in [1...@freeWorkers.length] if @frames.length > 2 if null in @imageParts @renderNextFrame() else @finishRendering() finishRendering: -> len = 0 for frame in @imageParts len += (frame.data.length - 1) * frame.pageSize + frame.cursor len += frame.pageSize - frame.cursor @log "rendering finished - filesize #{ Math.round(len / 1000) }kb" data = new Uint8Array len offset = 0 for frame in @imageParts for page, i in frame.data data.set page, offset if i is frame.data.length - 1 offset += frame.cursor else offset += frame.pageSize image = new Blob [data], type: 'image/gif' @emit 'finished', image, data renderNextFrame: -> throw new Error 'No free workers' if @freeWorkers.length is 0 return if @nextFrame >= @frames.length # no new frame to render frame = @frames[@nextFrame++] worker = @freeWorkers.shift() task = @getTask frame @log "starting frame #{ task.index + 1 } of #{ @frames.length }" @activeWorkers.push worker worker.postMessage task#, [task.data.buffer] getContextData: (ctx) -> return ctx.getImageData(0, 0, @options.width, @options.height).data getImageData: (image) -> if not @_canvas? @_canvas = document.createElement 'canvas' @_canvas.width = @options.width @_canvas.height = @options.height ctx = @_canvas.getContext '2d' ctx.setFill = @options.background ctx.fillRect 0, 0, @options.width, @options.height ctx.drawImage image, 0, 0 return @getContextData ctx getTask: (frame) -> index = @frames.indexOf frame task = index: index last: index is (@frames.length - 1) delay: frame.delay transparent: frame.transparent width: @options.width height: @options.height quality: @options.quality dither: @options.dither globalPalette: @options.globalPalette repeat: @options.repeat canTransfer: (browser.name is 'chrome') if frame.data? task.data = frame.data else if frame.context? task.data = @getContextData frame.context else if frame.image? task.data = @getImageData frame.image else throw new Error 'Invalid frame' return task log: (args...) -> return unless @options.debug console.log args... module.exports = GIF