// gif.js by @timb :) ;(function(e,t,n,r){function i(r){if(!n[r]){if(!t[r]){if(e)return e(r);throw new Error("Cannot find module '"+r+"'")}var s=n[r]={exports:{}};t[r][0](function(e){var n=t[r][1][e];return i(n?n:e)},s,s.exports)}return n[r].exports}for(var s=0;s 1) { // on("msg", f) var msgMap = {}; msgMap[arguments[0]] = arguments[1]; var opts = arguments[2] || {}; } else { // on({"msg": f, ...}) var msgMap = arguments[0]; var opts = arguments[1] || {}; } for (var string in msgMap){ var msgs = string.split(" "); var fs = msgMap[string]; if (!Array.isArray(fs)) fs = [fs]; for(var i=0, f; f=fs[i]; i++){ if (!f.uid) f.uid = Uid(); } for(var i=0, msg; msg=msgs[i]; i++){ var listeners = (msg.indexOf("*") === -1) ? chan.listeners : chan.globListeners; // todo: this probably wastes a lot of memory? // make a copy of the listener, add to it, and replace the listener // why not just push directly? // send might be iterating over it... and that will fuck up the iteration listeners[msg] = (msg in listeners) ? listeners[msg].concat(fs) : fs.concat(); } } return chan; }; /* off() off("a:b:c") off(f) off("a:b:c", f) off("a:b:c d:e:f") off([f, f2]) off({"a": f, "b": f2}) */ Tube.proto.off = function(){ var chan = this; var listeners, i, msgs, msg; // off() : delete all listeners. but replace, instead of delete if (arguments.length === 0) { chan.listeners = {}; chan.globListeners = {}; return chan; } // off("a:b:c d:e:f") // remove all matching listeners if (arguments.length === 1 && typeof arguments[0] === "string"){ // question... will this fuck up send if we delete in the middle of it dispatching? msgs = arguments[0].split(" "); for (i=0; msg=msgs[i]; i++){ delete chan.listeners[msg]; delete chan.globListeners[msg]; } return chan; } // off(f) or off([f, f2]) // remove all matching functions if (typeof arguments[0] === "function" || Array.isArray(arguments[0])) { var fs = (typeof arguments[0] === "function") ? [arguments[0]] : arguments[0]; // TODO return chan; } // off("a:b:c", f) or off({"a": f, "b": f2}) if (arguments.length > 1) { // off("msg", f) var msgMap = {}; msgMap[arguments[0]] = arguments[1]; } else { // off({"msg": f, ...}) var msgMap = arguments[0]; } for (var string in msgMap){ msgs = string.split(" "); var fs = msgMap[string]; if (typeof fs === "function") fs = [fs]; for(var i=0; msg=msgs[i]; i++){ if (msg in chan.listeners) listeners = chan.listeners; else if (msg in chan.globListeners) listeners = chan.globListeners; else continue; // gotta do this carefully in case we are still iterating through the listener in send // build a new array and assign it to the property, instead of mutating it. // console.log(" length of listeners[" + msg + "]: " + listeners[msg].length) // console.log(listeners[msg].join(",")); // console.log(fs.join(",")); listeners[msg] = listeners[msg].filter( function(f){ return fs.indexOf(f) === -1 } ); // console.log(" length of listeners[" + msg + "]: " + listeners[msg].length) } } return chan; }; /* c = Tube() c.on("foo", fn) c("foo", "bar", []) will call fn("bar", [], "foo") */ Tube.proto.send = function(msgString /*, data... */){ // todo: don't do this? if (!Tube.total[msgString]) Tube.total[msgString] = 0 Tube.total[msgString]+=1; var listener, listeners = this.listeners, globListeners = this.globListeners, //args = Array.prototype.splice.call(arguments, 1), msgs = tokenize(msgString), msg, f; if (arguments.length) { var args = Array.prototype.splice.call(arguments, 1); args.push(msgString); } else { var args = []; } for (var m=0; msg=msgs[m]; m++){ var fsToRun = []; var uidKeyFnValue = {}; var uidKeyMsgStringValue = {}; // note this will die on errors // todo: implement http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/ // exact matches if (listener = listeners[msg]) { for (var i=0; f=listener[i]; i++){ // fsToRun.push([f, msg]); uidKeyFnValue[f.uid] = f; uidKeyMsgStringValue[f.uid] = msg; } } // glob matches var msgSplit = msg.split(":"); for (var pattern in globListeners){ if (pattern !== "*") { // * always matches var patternSplit = globcache[pattern] || (globcache[pattern] = pattern.split(":")); if (!globber(patternSplit, msgSplit)) continue; } listener = globListeners[pattern]; for (var i=0; f=listener[i]; i++){ //f.apply(window, args); // hm possibly pass the actual message to the func // fsToRun.push([f, msg]); uidKeyFnValue[f.uid] = f; uidKeyMsgStringValue[f.uid] = msg; } } var fns = []; for (var f in uidKeyFnValue) fns.push(uidKeyFnValue[f]); for (var i=0, f; f=fns[i]; i++) f.apply(f, args); } return this; }; module.exports = Tube; },{"./object/setproto":7,"./string/tokenize":8,"./string/globber":9,"./uid":10,"./nexttick":11}],7:[function(require,module,exports){// TODO: replace all uses of // setproto(foo, proto) // with // foo.__proto__ = proto // when IE is no longer shit var setproto = function(obj, proto){ if (obj.__proto__) obj.__proto__ = proto; else for (var key in proto) obj[key] = proto[key]; }; module.exports = setproto; },{}],8:[function(require,module,exports){// trimmed string into array of strings var tokenize = function(str, splitOn){ return str .trim() .split(splitOn || tokenize.default); }; tokenize.default = /\s+/g; module.exports = tokenize; },{}],9:[function(require,module,exports){// can use * to match 0 or more : separated msgs // globber("*".split(":"), "a:b:c".split(":")) => true // globber("*:c".split(":"), "a:b:c".split(":")) => true // globber("a:*".split(":"), "a:b:c".split(":")) => true // globber("a:*:c".split(":"), "a:b:c".split(":")) => true // based on codegolf.stackexchange.com/questions/467/implement-glob-matcher var globber = function(patterns, strings) { // console.log("globber called with: " + patterns.join(":"), strings.join(":")) var first = patterns[0], rest = patterns.slice(1), len = strings.length, matchFound; if(first === '*') { for(var i = 0; i <= len; ++i) { // console.log("* " + i + " trying " + rest.join(":") + " with " + strings.slice(i).join(":")) if(globber(rest, strings.slice(i))) return true; } return false; } else { matchFound = (first === strings[0]); // console.log ("literal matching " + first + " " + strings[0] + " " + !!matched) } return matchFound && ((!rest.length && !len) || globber(rest, strings.slice(1))); }; module.exports = globber; },{}],10:[function(require,module,exports){var Uid = function(){ return (Uid.counter++ + ""); } Uid.counter = 1; module.exports = Uid; },{}],11:[function(require,module,exports){// based on https://github.com/timoxley/next-tick/blob/master/index.js // postMessage behaves badly on IE8 if (window.ActiveXObject || !window.postMessage) { var nextTick = function(fn) { setTimeout(fn, 0); } } else { // based on setZeroTimeout by David Baron // - http://dbaron.org/log/20100309-faster-timeouts var timeouts = [] , name = 'next-tick-zero-timeout' window.addEventListener('message', function(e){ if (e.source == window && e.data == name) { if (e.stopPropagation) e.stopPropagation(); if (timeouts.length) timeouts.shift()(); } }, true); var nextTick = function(fn){ timeouts.push(fn); window.postMessage(name, '*'); } } module.exports = nextTick; },{}],4:[function(require,module,exports){var Benchmark = require('./benchmark'); var setproto = require('./object/setproto'); var extend = require('./object/extend'); var Tube = require('./tube'); // usage: Buffer(url || File || ArrayBuffer) var BufferLoader = function(src, opts){ var loader = Tube(); setproto(loader, BufferLoader.proto); if (opts && opts.benchmark) loader.benchmark = opts.benchmark; //buf.remember("load error") // loader.on("load", function(abuf){ setupBuffer(loader, abuf) }); // <-- hm, needs ref to buf loader.src = src; return loader; } BufferLoader.proto = {}; extend(BufferLoader.proto, Tube.proto); BufferLoader.proto.load = function(){ var loader = this; var src = loader.src; if (typeof src === "string") loader.loadFromUrl(src) else if (src instanceof File) loader.loadFromFile(src); else if (src instanceof ArrayBuffer) loader("load", src); } BufferLoader.proto.loadFromFile = function(file){ var loader = this; var r = new FileReader(); r.addEventListener('load', function(e){ if (loader.benchmark) loader.benchmark.stop("fetch-from-disk"); loader('load', r.result, e); // console.log(r.file.name + " 100%: " + r.result.byteLength + " bytes") }); r.addEventListener('error', function(e){ // console.log(arguments); loader('error', e, r) }); r.addEventListener('progress', function(e){ //console.log(r.file.name + " " + (e.loaded / e.total * 100) + "%: " + e.loaded + " bytes") loader('progress', e) }); if (loader.benchmark) loader.benchmark.start("fetch-from-disk"); r.readAsArrayBuffer(file); }; BufferLoader.proto.loadFromUrl = function(url){ var loader=this; var xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = "arraybuffer"; xhr.addEventListener('load', function(e){ if (loader.benchmark) loader.benchmark.stop("fetch-from-network"); // console.log(r.file.name + " 100%: " + r.result.byteLength + " bytes") loader('load', xhr.response, e); }); xhr.addEventListener('error', function(e){ // console.log(arguments); loader('error', e, xhr); }); xhr.addEventListener('progress', function(e){ //console.log(r.file.name + " " + (e.loaded / e.total * 100) + "%: " + e.loaded + " bytes") loader('progress', e) }); if (loader.benchmark) loader.benchmark.start("fetch-from-network"); // xhr.open("GET", url); xhr.send(); }; module.exports = BufferLoader; },{"./benchmark":2,"./object/setproto":7,"./object/extend":12,"./tube":3}],12:[function(require,module,exports){module.exports = function(destination, source) { for (var property in source) destination[property] = source[property]; return destination; }; },{}],5:[function(require,module,exports){(function(){var blockSigs = require('./spec').blockSigs; var extSigs = require('./spec').extSigs; var palette = require('./palette'); var makeCurrentFrame = require('./animate').makeCurrentFrame; var BinarySpecReader = require('../../binaryspec'); var spec = require('./spec').spec; var specReader = BinarySpecReader(spec); var decode = function(gif, opts){ decodeNextPart(gif); }; // this gets called when more binary data is ready to process // it gets set as BufferedReader's onload function /* var moreDataInBufferEvent = function(gif){ if (gif.waitingForFileBuffer) decodeNextPart(gif); }; */ /* loads the next part of the gif, eventually calling the gif's onload function when every part has been loaded this function gets called when a previous part has finished loading, and when there's more data in the buffered reader if the next part has a fixed size according to spec, we know whether we have to wait on the buffer to fill or not if the next part has a variable size, call the howToDecode function which will try to load it */ var decodeNextPart = function(gif, nextPart){ //gif.waitingForFileBuffer = false; nextPart = nextPart || "header"; var buf = gif.buf; while (nextPart !== "done" && nextPart !== "error"){ if (nextPart in howToDecode && typeof howToDecode[nextPart] === "function") { // dont know size nextPart = howToDecode[nextPart](gif); } else { // we know exact size of next part var partSize = specReader.parts[nextPart].byteSize; if (buf.abuf.byteLength < partSize + buf.cursor){ gif.waitingForFileBuffer = true; return; } else { var fields = specReader.decodeBinaryFieldsToJSON(nextPart, buf.cursor, buf); buf.cursor += specReader.parts[nextPart].byteSize; nextPart = handleLoadedPart[nextPart](gif, fields); } } // todo: maybe do this when and return unknown from dataBlock read // if (nextPart === "unknown") nextPart = "dataBlock" } if (nextPart === "done"){ // create palette from all frames' palettes if (gif.benchmark) gif.benchmark.start("palette"); gif.paletteTotal = palette.create(gif); if (gif.benchmark) gif.benchmark.stop("palette"); // todo: move elsewhere makeCurrentFrame.bind(gif)(); // GIF.decode.doneDecodingCleanup(gif); //gif.tube("progress", "done"); gif.decoded = true; gif.tube("decoded"); return; } }; var howToDecode = { "globalPalette": function(gif){ var buf = gif.buf; var paletteSize = gif.paletteSize * 3; // r,g,b bytes if (buf.abuf.byteLength < buf.cursor + paletteSize) { gif.waitingForFileBuffer = true; return; } //gif.palette = GIF.palette.binary2rgba(gif.reader.buffer.slice(gif.cursor, gif.cursor + paletteSize)); gif.palette = palette.binary2rgba(new Uint8Array(buf.abuf, buf.cursor, paletteSize)); buf.cursor += paletteSize; return "dataBlock"; }, "localPalette": function(gif){ var buf = gif.buf; var paletteSize = gif.frames[gif.frames.length - 1].paletteSize * 3; // r,g,b bytes if (buf.abuf.byteLength < buf.cursor + paletteSize) { gif.waitingForFileBuffer = true; return; } gif.frames[gif.frames.length - 1].palette = palette.binary2rgba(new Uint8Array(buf.abuf, buf.cursor, paletteSize)); buf.cursor += paletteSize; return "imageData"; }, "dataBlock": function(gif){ var buf = gif.buf; if (buf.abuf.byteLength < buf.cursor + 1) { gif.waitingForFileBuffer = true; return; } var blockType = "unknown", extType = "unknown", nextPart; var blockSig = buf.u8[buf.cursor]; if (blockSig in blockSigs) { blockType = blockSigs[blockSig]; } else { //console.log("unknown data block type ("+ Number(blockSig).toString(16) +")!"); nextPart = "done"; } if (blockType === "extension"){ // we need to determine what kind of extension the block is // need next two bytes to find out what kind of extension the data block is if (buf.abuf.byteLength < buf.cursor + 2) { gif.waitingForFileBuffer = true; return; console.log("ran out of buffer") } var extSig = buf.u8[buf.cursor+1]; if (extSig in extSigs) { extType = extSigs[extSig]; nextPart = extType; } else { //console.log("unknown extension type ("+ Number(extSig).toString(16) +")") nextPart = "done"; } } else { if (blockType === "unknown") { blockType = "dataBlock"; buf.cursor += 1 } nextPart = blockType; } return nextPart; }, "trailer": function(gif){ return "done"; } }; var handleLoadedPart = { "header": function(gif, fields){ if (fields.signature != "GIF"){ gif.tube("error", "file doesn't seem to be a gif (file signature: '"+fields.signature+"')"); return "error"; } // gif.version = fields.version return "screenDesc"; }, "screenDesc": function(gif, fields){ for (var field in fields) gif[field] = fields[field]; // todo: make nicer // this should be a u3 not this bit[3] bit-shifted horseshit gif.paletteSize = (gif.paletteSize[0] << 2) + (gif.paletteSize[1] << 1) + (gif.paletteSize[2]); gif.paletteSize = Math.pow(2, gif.paletteSize + 1) if (gif.paletteExists) return "globalPalette"; else return "dataBlock"; }, "imageDesc": function(gif, fields){ // make a blank image frame if none exists or the last frame already has image data // we don't know if a blank frame has already been created because a graphic control block might // have come before this block and made one if (!gif.frames.length || ("w" in gif.frames[gif.frames.length - 1])) gif.frames.push({}) var frame = gif.frames[gif.frames.length - 1] for (var field in fields) frame[field] = fields[field]; frame.paletteSize = (frame.paletteSize[0] << 2) + (frame.paletteSize[1] << 1) + (frame.paletteSize[2]); frame.paletteSize = Math.pow(2, frame.paletteSize + 1) if (frame.paletteExists) return "localPalette"; else return "imageData"; }, // if sublocks was able to read: // increase cursor from subblocks // increase cursor from fields // else // set waiting // subtract 2 from cursor (for the 2 that was read to get the data block) // subtract part.bytesize from cursor (not needed in this case?) "applicationExtension": function(gif, fields){ var blockinfo = readSubBlocks(gif); if (blockinfo === false) { gif.waitingForFileBuffer = true gif.buf.cursor -= specReader.parts.applicationExtension.byteSize return; } else { var extension = {"data": blockinfo}; for(var field in fields) extension[field] = fields[field] gif.extensions.push(extension) return "dataBlock"; } }, "comment": function(gif, fields){ var blockinfo = readSubBlocks(gif); if (blockinfo === false) { gif.waitingForFileBuffer = true; gif.buf.cursor -= specReader.parts.comment.byteSize; return; } else { var extension = {"comment": blockinfo}; for(var field in fields) extension[field] = fields[field]; gif.extensions.push(extension); return "dataBlock"; } }, "plainText": function(gif, fields){ var blockinfo = readSubBlocks(gif); if (blockinfo === false) { gif.waitingForFileBuffer = true; gif.buf.cursor -= specReader.parts.plainText.byteSize; return; } else { var extension = {"plainText": blockinfo}; for(var field in fields) extension[field] = fields[field]; gif.extensions.push(extension); return "dataBlock"; } }, "graphicControl": function(gif, fields){ var dm = (fields.disposalMethod[0] << 2) + (fields.disposalMethod[1] << 1) + (fields.disposalMethod[2]); gif.frames.push({"delay": fields.delay, "transparentIndex": fields.transparentColor ? fields.transparentIndex : -1, "disposalMethod": dm}) return "dataBlock"; }, "imageData": function(gif, fields){ var blockinfo = readSubBlocks(gif); if (blockinfo === false) { gif.waitingForFileBuffer = true gif.buf.cursor -= specReader.parts.imageData.byteSize console.log("fucked") return; } else { // TODO: ENABLE THIS! // gif.tube("progress", "found " + gif.frames.length + " frames"); var frame = gif.frames[gif.frames.length - 1]; //var palette = ("palette" in frame) ? frame.palette : gif.palette // local palette otherwise global palette frame.lzwCodeSize = fields.lzwCodeSize; frame.blockinfo = blockinfo; // var transparentIndex = ("transparentIndex" in frame) ? frame.transparentIndex : -1 return "dataBlock"; } } }; // read subblocks out of a gif's buffer... // reads block sizes and returns an object with a start cursor and an array of block ends // returns false if there's not enough data var readSubBlocks = function(gif){ if (gif.benchmark) gif.benchmark.start("read-subblocks"); var blockEnds = []; var buf = gif.buf, u8 = buf.u8, byteLength = u8.byteLength; // gif.cursor is for whole file... pos is a cursor for just this blob var startBlockCursor = buf.cursor; var pos = buf.cursor; startBlockCursor += 1; var byteSize = 0; var outOfData = false // only actually advance cursor if we can read in all the sub blocks from the buffer var cursorTemp = 0; while(!outOfData){ // get block size if (byteLength < pos + 1) { outOfData = true; break;} byteSize = u8[pos]; pos += 1; // a sub block with size 0 indicates end of sub blocks if (byteSize === 0) { cursorTemp += 1; break;} // read block if (byteLength < pos + byteSize) { outOfData = true; break;} blockEnds.push(pos + byteSize); pos += byteSize cursorTemp += byteSize + 1 // gif.subBlocksRead += 1 } // TODO? CLEAN UP! if (outOfData) { // gif.bufferMisses += 1 // gif.benchmark.wasted += (Date.now() - start) / 1000 console.log("out of data") return false } else { // end of sub blocks happened buf.cursor += cursorTemp //if ("onprogress" in gif) gif.onprogress(gif, Math.floor(gif.cursor / gif.file.size * 100)) // gif.benchmark.subblocks += (Date.now() - start) / 1000; if (gif.benchmark) gif.benchmark.stop("read-subblocks"); return {start: startBlockCursor, blockEnds: blockEnds}; } }; module.exports = decode; })() },{"./spec":13,"./palette":14,"./animate":15,"../../binaryspec":16}],13:[function(require,module,exports){var blockSigs = { 0x21: "extension", 0x2c: "imageDesc", 0x3b: "trailer" }; exports.blockSigs = blockSigs; var extSigs = { 0xf9: "graphicControl", 0xfe: "comment", 0x01: "plainText", 0xff: "applicationExtension" }; exports.extSigs = extSigs; /* GIF.getGeneralBlockType = function(sig){ if (sig == 0x3b) return "trailer" else if (sig < 0x7F) return "graphic rendering block" else if (sig < 0xF9) return "control block" else return "special purpose block" } */ var spec = { "header": [ "str[3] signature", "str[3] version" ], "screenDesc": [ "u16 w", "u16 h", "bit paletteExists", "bit[3] resolution ignore", "bit sortFlag ignore", "bit[3] paletteSize", "u8 bgColorIndex", "u8 aspectRatio ignore" ], "imageDesc": [ "u8 sig ignore", "u16 x", "u16 y", "u16 w", "u16 h", "bit paletteExists", "bit interlaced", "bit sortFlag", "bit[2] reserved ignore", "bit[3] paletteSize" ], "applicationExtension": [ "u8 sig ignore", "u8 extSig ignore", "u8 blockSize ignore", "str[8] identifier", "str[3] authCode ignore" ], "graphicControl": [ "u8 sig ignore", "u8 extSig ignore", "u8 blockSize ignore", "bit[3] reserved ignore", "bit[3] disposalMethod", "bit userInput ignore", "bit transparentColor", "u16 delay", "u8 transparentIndex", "u8 blockTerminator ignore" ], "comment": [ "u8 sig ignore", "u8 extSig ignore" ], "plainText": [ "u8 sig ignore", "u8 extSig ignore", "u8 blockSize", "u16 textGridLeft", "u16 textGridTop", "u16 textGridWidth", "u16 textGridHeight", "u8 charCellWidth", "u8 charCellHeight", "u8 fgColorIndex", "u8 bgColorIndex" ], "imageData": [ "u8 lzwCodeSize" ] }; exports.spec = spec; },{}],14:[function(require,module,exports){(function(){var rgba2css = require('../../color/rgba2css'); var create2d = require('../../create/2d'); var createImageData = require('../../create/imagedata'); var palette = {}; // flat typed array of r g b a values from binary GIF palette palette.binary2rgba = function(abuf /*, transparentIndex */){ var table = new Uint8Array(abuf.byteLength/3*4); var counter = 0 for(var i = 0, length = abuf.byteLength/3*4; i 0) ? frame.delay * 10 : defaultDelay; totalDelay += delay; delays.push(totalDelay); } this.currentFrame = makeCurrentFrameFunction(delays); }; var makeCurrentFrameFunction = function(delays){ var totalTime = delays[delays.length - 1]; return function(timestamp){ var r = (timestamp || Date.now()) % totalTime; for(var i=0; i 1); // char(2) is 16 bits. uint(16) is 16 bits var bitSize = bitSizes[fieldType] * fieldLength; parsedSpec.fields.push({"name": instruction[1], "type": fieldType, "ignore": ignore, "bitSize": bitSize, "isArray": isArray}); size += bitSize; } parsedSpec.bitSize = size; parsedSpec.byteSize = size / 8; return parsedSpec; }; // decodes a chunk according to data types in gif.spec.js // todo: rewrite the binary decoding stuff to not be so shit BinarySpecReader.proto.decodeBinaryFieldsToJSON = function(partName, cursor, buf){ var reader = this; var part = reader.parts[partName]; var fields = {}, numFields = part.fields.length, bitPos = 0; for(var i = 0; i < numFields; i++){ var field = part.fields[i]; if (!field.ignore) { var bitOffset = bitPos % 8; var decodeByteStart = Math.floor((bitPos - bitOffset) / 8); var decodeByteEnd = decodeByteStart + Math.ceil(field.bitSize / 8); switch(field.type){ case "u8": fields[field.name] = buf.u8[cursor + decodeByteStart]; break; case "i8": fields[field.name] = buf.dv.getInt8(cursor + decodeByteStart); break; case "u16": fields[field.name] = buf.dv.getUint16(cursor + decodeByteStart, true); break; case "i16": fields[field.name] = buf.dv.getInt16(cursor + decodeByteStart, true); break; case "u32": fields[field.name] = buf.dv.getUint32(cursor + decodeByteStart, true); break; case "i32": fields[field.name] = buf.dv.getInt32(cursor + decodeByteStart, true); break; case "str": fields[field.name] = abuf2str(buf.abuf, cursor + decodeByteStart, field.bitSize >> 3); break; case "bit": if (!field.isArray) { fields[field.name] = new BitView(buf.abuf, cursor + decodeByteStart).getBit(bitOffset); } else { var bv = new BitView(buf.abuf, cursor + decodeByteStart); var bits = []; for (var bb=bitOffset; bb> 3]; var off = idx & 0x7; return (v >> (7-off)) & 1; }; module.exports = BitView; },{}],6:[function(require,module,exports){var create2d = require('../../create/2d'); var createImageData = require('../../create/imagedata'); //import queueOrNextTick from 'lib/nexttick4'; var nextTick = require('../../nexttick'); var lzwImageData = require('./decode-lzw'); /* this has two rendering types... canvas and webgl both build gif image frames from binary data that was decoded when the gif loaded canvas method will build full-frame canvas objects and place them into each frame of the gif. eg, render.canvas(gif) will create gif.frames[0].ctx and so on webgl method builds imagedata textures and tries to pack multiple frames efficiently into channels if it can. in gifs, each frame stored might just be a rectangle that changed from the previous frame. these are referred to as "raw frames" in this code */ var render = function(gif, config){ config = config || {}; var bench = gif.benchmark || false; var frameNum = config.frameNum || 0; if (frameNum === 0){ // preallocate all frames for (var i=0; i= gif.frames.length) { // done making frames //gif.tube("progress", "done") gif.rendered = true; gif.tube("rendered"); return } var frame = gif.frames[frameNum]; var pixeldata = gif.buf.pixeldata; // gif.percentLoaded = frameNum / gif.frames.length //gif.tube("decompressing frame " + (frameNum+1)) // lzw if (bench) bench.start("decompress-lzw"); // var pixeldata = new Uint8Array(frame.w * frame.h); //pixeldata.area = frame.w * frame.h; lzwImageData(frame.blockinfo, gif.buf.u8, frame.lzwCodeSize, frame.w, frame.h, pixeldata); if (bench) bench.stop("decompress-lzw"); // deinterlace if (frame.interlaced) { if (bench) bench.start("deinterlace"); pixeldata = deinterlacePixels(pixeldata, frame.w, frame.h) if (bench) bench.stop("deinterlace"); } // canvas-ize if (bench) bench.start("pixeldata-to-canvas"); makeFullFrame(pixeldata, gif, frameNum); if (bench) bench.stop("pixeldata-to-canvas"); // todo: queue this better var func = render.bind(undefined, gif, {"frameNum": frameNum+1}); // queueOrNextTick(func); nextTick(func); // setZeroTimeout(func); //setTimeout(func, 1) // otherwise progress won't show in chrome }; var makeFullFrame = function(pixeldata, gif, frameNum){ var frame = gif.frames[frameNum], ctx = frame.ctx; if (frameNum === 0){ // don't need previous frame info to do disposal if it's the first frame ctx.putImageData(pixelDataToImageData(pixeldata, gif, frame), frame.x, frame.y, 0,0,frame.w,frame.h); return; } var prevFrameNum = frameNum-1, prevFrame = gif.frames[prevFrameNum], prevCanvas = prevFrame.ctx.canvas, rawCtx; // disposal method is 0 (unspecified) or 1 (do not dispose) // do nothing, paste new frame image over old one if (prevFrame.disposalMethod === 0 || prevFrame.disposalMethod === 1){ rawCtx = makeRawFrameAsContext(gif, frameNum, pixeldata); ctx.drawImage(prevCanvas, 0, 0) ctx.drawImage(rawCtx.canvas, 0,0,frame.w,frame.h, frame.x,frame.y,frame.w,frame.h) } // disposal method is 2 (restore to background color) // but everyone just restores to transparency // see notes on http://www.imagemagick.org/Usage/anim_basics/#background if (prevFrame.disposalMethod === 2){ // fast path... whole frame cleared if (prevFrame.x === 0 && prevFrame.y === 0 && prevFrame.w === gif.w && prevFrame.h === gif.h) { // var rawContext = makeRawFrameAsContext(gif, frameNum) // frame.context.drawImage(rawContext.canvas, frame.x, frame.y); ctx.putImageData(makeRawFrameAsImageData(gif, frameNum, pixeldata), frame.x, frame.y, 0,0,frame.w,frame.h); } else { // draw the edges of the previous frame and then draw the current frame overtop /* .__________. |__________| | | | | | | | | |_|______|_| |__________| */ //top if (prevFrame.y > 0) ctx.drawImage(prevCanvas, 0,0, gif.w,prevFrame.y, 0,0, gif.w,prevFrame.y); //left if (prevFrame.x > 0) ctx.drawImage(prevCanvas, 0,prevFrame.y, prevFrame.x,prevFrame.h, 0,prevFrame.y, prevFrame.x,prevFrame.h); // right if (prevFrame.x+prevFrame.w < gif.w) ctx.drawImage(prevCanvas, prevFrame.x+prevFrame.w, prevFrame.y, (gif.w-prevFrame.x-prevFrame.w),prevFrame.h, prevFrame.x+prevFrame.w, prevFrame.y, (gif.w-prevFrame.x-prevFrame.w),prevFrame.h); // bottom if (prevFrame.y+prevFrame.h < gif.h) ctx.drawImage(prevCanvas, 0,prevFrame.y+prevFrame.h, gif.w,(gif.h-prevFrame.y-prevFrame.h), 0,prevFrame.y+prevFrame.h, gif.w,(gif.h-prevFrame.y-prevFrame.h)) rawCtx = makeRawFrameAsContext(gif, frameNum, pixeldata); ctx.drawImage(rawCtx.canvas, 0,0,frame.w,frame.h, frame.x,frame.y,frame.w,frame.h); } } // disposal method is 3 (restore to previous) if (prevFrame.disposalMethod === 3){ // look for last previous frame that doesn't have "previous" disposal method while(prevFrameNum > 0 && gif.frames[prevFrameNum].disposalMethod === 3) prevFrameNum -= 1; prevFrame = gif.frames[prevFrameNum] // console.log(prevFrameNum) if (prevFrame.disposalMethod != 3) ctx.drawImage(prevFrame.ctx.canvas, 0, 0) rawCtx = makeRawFrameAsContext(gif, frameNum, pixeldata); ctx.drawImage(rawCtx.canvas, 0,0,frame.w,frame.h, frame.x, frame.y,frame.w,frame.h) } } var makeRawFrameAsContext = function(gif, frameNum, pixeldata){ // cache a context to reuse if (makeRawFrameAsContext.ctx && makeRawFrameAsContext.ctx.canvas.width === gif.w && makeRawFrameAsContext.ctx.canvas.height === gif.h) { var ctx = makeRawFrameAsContext.ctx; } else { var ctx = makeRawFrameAsContext.ctx = create2d(gif.w, gif.h); } var frame = gif.frames[frameNum]; pixeldata = pixeldata || frame.pixelData; var palette = ("palette" in frame) ? frame.palette : gif.palette; var transparentIndex = ("transparentIndex" in frame) ? frame.transparentIndex : -1; if (transparentIndex > -1) palette[(transparentIndex*4)+3] = 0; var rawImageData = pixelData2imageData(gif, palette, pixeldata, frame.w, frame.h, transparentIndex); ctx.putImageData(rawImageData, 0, 0, 0,0,frame.w,frame.h) return ctx // return imageData2contextDirty(rawImageData, 0,0,frame.w,frame.h); }; var makeRawFrameAsImageData = function(gif, frameNum, pixeldata){ var frame = gif.frames[frameNum]; pixeldata = pixeldata || frame.pixelData; var palette = ("palette" in frame) ? frame.palette : gif.palette; var transparentIndex = ("transparentIndex" in frame) ? frame.transparentIndex : -1; if (transparentIndex > -1) palette[(transparentIndex*4)+3] = 0; return pixelData2imageData(gif, palette, pixeldata, frame.w, frame.h, transparentIndex) }; var pixelDataToImageData = function(pixeldata, gif, frame){ // var frame = gif.frames[frameNum]; var palette = ("palette" in frame) ? frame.palette : gif.palette; var transparentIndex = ("transparentIndex" in frame) ? frame.transparentIndex : -1; if (transparentIndex > -1) palette[(transparentIndex*4)+3] = 0; return pixelData2imageData(gif, palette, pixeldata, frame.w, frame.h, transparentIndex) }; var imageData2context = function(imageData){ var ctx = create2d(imageData.width, imageData.height) ctx.putImageData(imageData, 0, 0) return ctx }; var imageData2contextDirty = function(imageData, dx,dy,dw,dh){ var ctx = create2d(imageData.width, imageData.height) ctx.putImageData(imageData, 0, 0, dx,dy,dw,dh) return ctx }; var pixelData2imageData = function(gif, palette, pixeldata, w, h, transparentIndex){ if (pixelData2imageData.imagedata && pixelData2imageData.imagedata.width === gif.w && pixelData2imageData.imagedata.height === gif.h) { var imagedata = pixelData2imageData.imagedata; } else { var imagedata = pixelData2imageData.imagedata = createImageData(gif.w, gif.h); } var data = imagedata.data // var i = pixeldata.length; var i = 0; for (var y=0; y>= code_size; bits -= code_size; if (code > available) { console.log(":("); // console.log(num2bin(code)); // break; } // if we get here something bad happened ;( if (code === end_of_information) { console.log("fuck"); break}; if (code === clear) { code_size = data_size + 1; code_mask = (1 << code_size) - 1; available = clear + 2; old_code = NullCode; continue; } if (old_code === NullCode){ pixelStack[top++] = suffix[code]; old_code = code; first = code; continue; } in_code = code; if (code === available){ pixelStack[top++] = first; code = old_code; } while (code > clear){ pixelStack[top++] = suffix[code]; code = prefix[code]; } first = (suffix[code]) // & 0xff; // ^^ timb: not needed? // Add a new string to the string table, if (available >= MaxStack) { /*console.log("maxstack!");*/ /*break;*/ } pixelStack[top++] = first; prefix[available] = old_code; suffix[available] = first; available++; /* why does == have higher precedence than & ? i just looked it up and i think it is because javascript has basically the same precedence as C and "once upon a time, C didn't have the logical operators && and ||, and the bitwise operators & and | did double duty." */ if ((available & code_mask) === 0 && available < MaxStack){ code_size++; code_mask += available; } old_code = in_code; } top--; if (paletteRemap) pixels[pi++] = paletteRemap[pixelStack[top]]; else pixels[pi++] = pixelStack[top]; i++; } // not needed: typed arrays init'd to 0 already // for (i = pi, len=pixels.length; i < len; i++) // pixels[i] = 0; // clear missing pixels return pixels }; var MaxStack = 4096; // lzwImageData.prefix = new Uint16Array(MaxStack*2) // lzwImageData.suffix = new Uint8Array(MaxStack) module.exports = lzwImageData; },{}]},{},[1]);