From 989dcd57de215c3f3dac8515e0efbe35e52aa760 Mon Sep 17 00:00:00 2001 From: sirx Date: Sun, 6 Nov 2016 03:14:18 +0100 Subject: [PATCH] animiertes eygon --- s/anim.js | 26 ++ s/index.tpl.html | 2 + s/libgif.js | 990 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1018 insertions(+) create mode 100644 s/anim.js create mode 100644 s/libgif.js diff --git a/s/anim.js b/s/anim.js new file mode 100644 index 0000000..691a671 --- /dev/null +++ b/s/anim.js @@ -0,0 +1,26 @@ +var $parent = document.createElement("div") + $gif = document.createElement("img") + ,$favicon = document.createElement("link") + +$gif.crossOrigin = "anonymous" + +$gif.src = "./s/favicon.gif" + +$favicon.rel = "icon" + +window.parent.document.head.appendChild($favicon) + +$parent.appendChild($gif) + +var supergif = new SuperGif({gif: $gif}) + ,$canvas + +supergif.load(()=> { + $canvas = supergif.get_canvas() + updateFavicon() +}) + +function updateFavicon() { + $favicon.href = $canvas.toDataURL() + window.requestAnimationFrame(updateFavicon) +} diff --git a/s/index.tpl.html b/s/index.tpl.html index 4f4ac55..29ee593 100644 --- a/s/index.tpl.html +++ b/s/index.tpl.html @@ -15,5 +15,7 @@ + + diff --git a/s/libgif.js b/s/libgif.js new file mode 100644 index 0000000..b14e0e9 --- /dev/null +++ b/s/libgif.js @@ -0,0 +1,990 @@ +/* + SuperGif + + Example usage: + + + + + + Image tag attributes: + + rel:animated_src - If this url is specified, it's loaded into the player instead of src. + This allows a preview frame to be shown until animated gif data is streamed into the canvas + + rel:auto_play - Defaults to 1 if not specified. If set to zero, a call to the play() method is needed + + Constructor options args + + gif Required. The DOM element of an img tag. + loop_mode Optional. Setting this to false will force disable looping of the gif. + auto_play Optional. Same as the rel:auto_play attribute above, this arg overrides the img tag info. + max_width Optional. Scale images over max_width down to max_width. Helpful with mobile. + on_end Optional. Add a callback for when the gif reaches the end of a single loop (one iteration). The first argument passed will be the gif HTMLElement. + loop_delay Optional. The amount of time to pause (in ms) after each single loop (iteration). + draw_while_loading Optional. Determines whether the gif will be drawn to the canvas whilst it is loaded. + show_progress_bar Optional. Only applies when draw_while_loading is set to true. + + Instance methods + + // loading + load( callback ) Loads the gif specified by the src or rel:animated_src sttributie of the img tag into a canvas element and then calls callback if one is passed + load_url( src, callback ) Loads the gif file specified in the src argument into a canvas element and then calls callback if one is passed + + // play controls + play - Start playing the gif + pause - Stop playing the gif + move_to(i) - Move to frame i of the gif + move_relative(i) - Move i frames ahead (or behind if i < 0) + + // getters + get_canvas The canvas element that the gif is playing in. Handy for assigning event handlers to. + get_playing Whether or not the gif is currently playing + get_loading Whether or not the gif has finished loading/parsing + get_auto_play Whether or not the gif is set to play automatically + get_length The number of frames in the gif + get_current_frame The index of the currently displayed frame of the gif + + For additional customization (viewport inside iframe) these params may be passed: + c_w, c_h - width and height of canvas + vp_t, vp_l, vp_ w, vp_h - top, left, width and height of the viewport + + A bonus: few articles to understand what is going on + http://enthusiasms.org/post/16976438906 + http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp + http://humpy77.deviantart.com/journal/Frame-Delay-Times-for-Animated-GIFs-214150546 + +*/ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + root.SuperGif = factory(); + } +}(this, function () { + // Generic functions + var bitsToNum = function (ba) { + return ba.reduce(function (s, n) { + return s * 2 + n; + }, 0); + }; + + var byteToBitArr = function (bite) { + var a = []; + for (var i = 7; i >= 0; i--) { + a.push( !! (bite & (1 << i))); + } + return a; + }; + + // Stream + /** + * @constructor + */ + // Make compiler happy. + var Stream = function (data) { + this.data = data; + this.len = this.data.length; + this.pos = 0; + + this.readByte = function () { + if (this.pos >= this.data.length) { + throw new Error('Attempted to read past end of stream.'); + } + if (data instanceof Uint8Array) + return data[this.pos++]; + else + return data.charCodeAt(this.pos++) & 0xFF; + }; + + this.readBytes = function (n) { + var bytes = []; + for (var i = 0; i < n; i++) { + bytes.push(this.readByte()); + } + return bytes; + }; + + this.read = function (n) { + var s = ''; + for (var i = 0; i < n; i++) { + s += String.fromCharCode(this.readByte()); + } + return s; + }; + + this.readUnsigned = function () { // Little-endian. + var a = this.readBytes(2); + return (a[1] << 8) + a[0]; + }; + }; + + var lzwDecode = function (minCodeSize, data) { + // TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String? + var pos = 0; // Maybe this streaming thing should be merged with the Stream? + var readCode = function (size) { + var code = 0; + for (var i = 0; i < size; i++) { + if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) { + code |= 1 << i; + } + pos++; + } + return code; + }; + + var output = []; + + var clearCode = 1 << minCodeSize; + var eoiCode = clearCode + 1; + + var codeSize = minCodeSize + 1; + + var dict = []; + + var clear = function () { + dict = []; + codeSize = minCodeSize + 1; + for (var i = 0; i < clearCode; i++) { + dict[i] = [i]; + } + dict[clearCode] = []; + dict[eoiCode] = null; + + }; + + var code; + var last; + + while (true) { + last = code; + code = readCode(codeSize); + + if (code === clearCode) { + clear(); + continue; + } + if (code === eoiCode) break; + + if (code < dict.length) { + if (last !== clearCode) { + dict.push(dict[last].concat(dict[code][0])); + } + } + else { + if (code !== dict.length) throw new Error('Invalid LZW code.'); + dict.push(dict[last].concat(dict[last][0])); + } + output.push.apply(output, dict[code]); + + if (dict.length === (1 << codeSize) && codeSize < 12) { + // If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long. + codeSize++; + } + } + + // I don't know if this is technically an error, but some GIFs do it. + //if (Math.ceil(pos / 8) !== data.length) throw new Error('Extraneous LZW bytes.'); + return output; + }; + + + // The actual parsing; returns an object with properties. + var parseGIF = function (st, handler) { + handler || (handler = {}); + + // LZW (GIF-specific) + var parseCT = function (entries) { // Each entry is 3 bytes, for RGB. + var ct = []; + for (var i = 0; i < entries; i++) { + ct.push(st.readBytes(3)); + } + return ct; + }; + + var readSubBlocks = function () { + var size, data; + data = ''; + do { + size = st.readByte(); + data += st.read(size); + } while (size !== 0); + return data; + }; + + var parseHeader = function () { + var hdr = {}; + hdr.sig = st.read(3); + hdr.ver = st.read(3); + if (hdr.sig !== 'GIF') throw new Error('Not a GIF file.'); // XXX: This should probably be handled more nicely. + hdr.width = st.readUnsigned(); + hdr.height = st.readUnsigned(); + + var bits = byteToBitArr(st.readByte()); + hdr.gctFlag = bits.shift(); + hdr.colorRes = bitsToNum(bits.splice(0, 3)); + hdr.sorted = bits.shift(); + hdr.gctSize = bitsToNum(bits.splice(0, 3)); + + hdr.bgColor = st.readByte(); + hdr.pixelAspectRatio = st.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64 + if (hdr.gctFlag) { + hdr.gct = parseCT(1 << (hdr.gctSize + 1)); + } + handler.hdr && handler.hdr(hdr); + }; + + var parseExt = function (block) { + var parseGCExt = function (block) { + var blockSize = st.readByte(); // Always 4 + var bits = byteToBitArr(st.readByte()); + block.reserved = bits.splice(0, 3); // Reserved; should be 000. + block.disposalMethod = bitsToNum(bits.splice(0, 3)); + block.userInput = bits.shift(); + block.transparencyGiven = bits.shift(); + + block.delayTime = st.readUnsigned(); + + block.transparencyIndex = st.readByte(); + + block.terminator = st.readByte(); + + handler.gce && handler.gce(block); + }; + + var parseComExt = function (block) { + block.comment = readSubBlocks(); + handler.com && handler.com(block); + }; + + var parsePTExt = function (block) { + // No one *ever* uses this. If you use it, deal with parsing it yourself. + var blockSize = st.readByte(); // Always 12 + block.ptHeader = st.readBytes(12); + block.ptData = readSubBlocks(); + handler.pte && handler.pte(block); + }; + + var parseAppExt = function (block) { + var parseNetscapeExt = function (block) { + var blockSize = st.readByte(); // Always 3 + block.unknown = st.readByte(); // ??? Always 1? What is this? + block.iterations = st.readUnsigned(); + block.terminator = st.readByte(); + handler.app && handler.app.NETSCAPE && handler.app.NETSCAPE(block); + }; + + var parseUnknownAppExt = function (block) { + block.appData = readSubBlocks(); + // FIXME: This won't work if a handler wants to match on any identifier. + handler.app && handler.app[block.identifier] && handler.app[block.identifier](block); + }; + + var blockSize = st.readByte(); // Always 11 + block.identifier = st.read(8); + block.authCode = st.read(3); + switch (block.identifier) { + case 'NETSCAPE': + parseNetscapeExt(block); + break; + default: + parseUnknownAppExt(block); + break; + } + }; + + var parseUnknownExt = function (block) { + block.data = readSubBlocks(); + handler.unknown && handler.unknown(block); + }; + + block.label = st.readByte(); + switch (block.label) { + case 0xF9: + block.extType = 'gce'; + parseGCExt(block); + break; + case 0xFE: + block.extType = 'com'; + parseComExt(block); + break; + case 0x01: + block.extType = 'pte'; + parsePTExt(block); + break; + case 0xFF: + block.extType = 'app'; + parseAppExt(block); + break; + default: + block.extType = 'unknown'; + parseUnknownExt(block); + break; + } + }; + + var parseImg = function (img) { + var deinterlace = function (pixels, width) { + // Of course this defeats the purpose of interlacing. And it's *probably* + // the least efficient way it's ever been implemented. But nevertheless... + var newPixels = new Array(pixels.length); + var rows = pixels.length / width; + var cpRow = function (toRow, fromRow) { + var fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width); + newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels)); + }; + + // See appendix E. + var offsets = [0, 4, 2, 1]; + var steps = [8, 8, 4, 2]; + + var fromRow = 0; + for (var pass = 0; pass < 4; pass++) { + for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) { + cpRow(toRow, fromRow) + fromRow++; + } + } + + return newPixels; + }; + + img.leftPos = st.readUnsigned(); + img.topPos = st.readUnsigned(); + img.width = st.readUnsigned(); + img.height = st.readUnsigned(); + + var bits = byteToBitArr(st.readByte()); + img.lctFlag = bits.shift(); + img.interlaced = bits.shift(); + img.sorted = bits.shift(); + img.reserved = bits.splice(0, 2); + img.lctSize = bitsToNum(bits.splice(0, 3)); + + if (img.lctFlag) { + img.lct = parseCT(1 << (img.lctSize + 1)); + } + + img.lzwMinCodeSize = st.readByte(); + + var lzwData = readSubBlocks(); + + img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData); + + if (img.interlaced) { // Move + img.pixels = deinterlace(img.pixels, img.width); + } + + handler.img && handler.img(img); + }; + + var parseBlock = function () { + var block = {}; + block.sentinel = st.readByte(); + + switch (String.fromCharCode(block.sentinel)) { // For ease of matching + case '!': + block.type = 'ext'; + parseExt(block); + break; + case ',': + block.type = 'img'; + parseImg(block); + break; + case ';': + block.type = 'eof'; + handler.eof && handler.eof(block); + break; + default: + throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0. + } + + if (block.type !== 'eof') setTimeout(parseBlock, 0); + }; + + var parse = function () { + parseHeader(); + setTimeout(parseBlock, 0); + }; + + parse(); + }; + + var SuperGif = function ( opts ) { + var options = { + //viewport position + vp_l: 0, + vp_t: 0, + vp_w: null, + vp_h: null, + //canvas sizes + c_w: null, + c_h: null + }; + for (var i in opts ) { options[i] = opts[i] } + if (options.vp_w && options.vp_h) options.is_vp = true; + + var stream; + var hdr; + + var loadError = null; + var loading = false; + + var transparency = null; + var delay = null; + var disposalMethod = null; + var disposalRestoreFromIdx = null; + var lastDisposalMethod = null; + var frame = null; + var lastImg = null; + + var playing = true; + var forward = true; + + var ctx_scaled = false; + + var frames = []; + var frameOffsets = []; // elements have .x and .y properties + + var gif = options.gif; + if (typeof options.auto_play == 'undefined') + options.auto_play = (!gif.getAttribute('rel:auto_play') || gif.getAttribute('rel:auto_play') == '1'); + + var onEndListener = (options.hasOwnProperty('on_end') ? options.on_end : null); + var loopDelay = (options.hasOwnProperty('loop_delay') ? options.loop_delay : 0); + var overrideLoopMode = (options.hasOwnProperty('loop_mode') ? options.loop_mode : 'auto'); + var drawWhileLoading = (options.hasOwnProperty('draw_while_loading') ? options.draw_while_loading : true); + var showProgressBar = drawWhileLoading ? (options.hasOwnProperty('show_progress_bar') ? options.show_progress_bar : true) : false; + var progressBarHeight = (options.hasOwnProperty('progressbar_height') ? options.progressbar_height : 25); + var progressBarBackgroundColor = (options.hasOwnProperty('progressbar_background_color') ? options.progressbar_background_color : 'rgba(255,255,255,0.4)'); + var progressBarForegroundColor = (options.hasOwnProperty('progressbar_foreground_color') ? options.progressbar_foreground_color : 'rgba(255,0,22,.8)'); + + var clear = function () { + transparency = null; + delay = null; + lastDisposalMethod = disposalMethod; + disposalMethod = null; + frame = null; + }; + + // XXX: There's probably a better way to handle catching exceptions when + // callbacks are involved. + var doParse = function () { + try { + parseGIF(stream, handler); + } + catch (err) { + doLoadError('parse'); + } + }; + + var doText = function (text) { + toolbar.innerHTML = text; // innerText? Escaping? Whatever. + toolbar.style.visibility = 'visible'; + }; + + var setSizes = function(w, h) { + canvas.width = w * get_canvas_scale(); + canvas.height = h * get_canvas_scale(); + toolbar.style.minWidth = ( w * get_canvas_scale() ) + 'px'; + + tmpCanvas.width = w; + tmpCanvas.height = h; + tmpCanvas.style.width = w + 'px'; + tmpCanvas.style.height = h + 'px'; + tmpCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0); + }; + + var setFrameOffset = function(frame, offset) { + if (!frameOffsets[frame]) { + frameOffsets[frame] = offset; + return; + } + if (typeof offset.x !== 'undefined') { + frameOffsets[frame].x = offset.x; + } + if (typeof offset.y !== 'undefined') { + frameOffsets[frame].y = offset.y; + } + }; + + var doShowProgress = function (pos, length, draw) { + if (draw && showProgressBar) { + var height = progressBarHeight; + var left, mid, top, width; + if (options.is_vp) { + if (!ctx_scaled) { + top = (options.vp_t + options.vp_h - height); + height = height; + left = options.vp_l; + mid = left + (pos / length) * options.vp_w; + width = canvas.width; + } else { + top = (options.vp_t + options.vp_h - height) / get_canvas_scale(); + height = height / get_canvas_scale(); + left = (options.vp_l / get_canvas_scale() ); + mid = left + (pos / length) * (options.vp_w / get_canvas_scale()); + width = canvas.width / get_canvas_scale(); + } + //some debugging, draw rect around viewport + if (false) { + if (!ctx_scaled) { + var l = options.vp_l, t = options.vp_t; + var w = options.vp_w, h = options.vp_h; + } else { + var l = options.vp_l/get_canvas_scale(), t = options.vp_t/get_canvas_scale(); + var w = options.vp_w/get_canvas_scale(), h = options.vp_h/get_canvas_scale(); + } + ctx.rect(l,t,w,h); + ctx.stroke(); + } + } + else { + top = (canvas.height - height) / (ctx_scaled ? get_canvas_scale() : 1); + mid = ((pos / length) * canvas.width) / (ctx_scaled ? get_canvas_scale() : 1); + width = canvas.width / (ctx_scaled ? get_canvas_scale() : 1 ); + height /= ctx_scaled ? get_canvas_scale() : 1; + } + + ctx.fillStyle = progressBarBackgroundColor; + ctx.fillRect(mid, top, width - mid, height); + + ctx.fillStyle = progressBarForegroundColor; + ctx.fillRect(0, top, mid, height); + } + }; + + var doLoadError = function (originOfError) { + var drawError = function () { + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, options.c_w ? options.c_w : hdr.width, options.c_h ? options.c_h : hdr.height); + ctx.strokeStyle = 'red'; + ctx.lineWidth = 3; + ctx.moveTo(0, 0); + ctx.lineTo(options.c_w ? options.c_w : hdr.width, options.c_h ? options.c_h : hdr.height); + ctx.moveTo(0, options.c_h ? options.c_h : hdr.height); + ctx.lineTo(options.c_w ? options.c_w : hdr.width, 0); + ctx.stroke(); + }; + + loadError = originOfError; + hdr = { + width: gif.width, + height: gif.height + }; // Fake header. + frames = []; + drawError(); + }; + + var doHdr = function (_hdr) { + hdr = _hdr; + setSizes(hdr.width, hdr.height) + }; + + var doGCE = function (gce) { + pushFrame(); + clear(); + transparency = gce.transparencyGiven ? gce.transparencyIndex : null; + delay = gce.delayTime; + disposalMethod = gce.disposalMethod; + // We don't have much to do with the rest of GCE. + }; + + var pushFrame = function () { + if (!frame) return; + frames.push({ + data: frame.getImageData(0, 0, hdr.width, hdr.height), + delay: delay + }); + frameOffsets.push({ x: 0, y: 0 }); + }; + + var doImg = function (img) { + if (!frame) frame = tmpCanvas.getContext('2d'); + + var currIdx = frames.length; + + //ct = color table, gct = global color table + var ct = img.lctFlag ? img.lct : hdr.gct; // TODO: What if neither exists? + + /* + Disposal method indicates the way in which the graphic is to + be treated after being displayed. + + Values : 0 - No disposal specified. The decoder is + not required to take any action. + 1 - Do not dispose. The graphic is to be left + in place. + 2 - Restore to background color. The area used by the + graphic must be restored to the background color. + 3 - Restore to previous. The decoder is required to + restore the area overwritten by the graphic with + what was there prior to rendering the graphic. + + Importantly, "previous" means the frame state + after the last disposal of method 0, 1, or 2. + */ + if (currIdx > 0) { + if (lastDisposalMethod === 3) { + // Restore to previous + // If we disposed every frame including first frame up to this point, then we have + // no composited frame to restore to. In this case, restore to background instead. + if (disposalRestoreFromIdx !== null) { + frame.putImageData(frames[disposalRestoreFromIdx].data, 0, 0); + } else { + frame.clearRect(lastImg.leftPos, lastImg.topPos, lastImg.width, lastImg.height); + } + } else { + disposalRestoreFromIdx = currIdx - 1; + } + + if (lastDisposalMethod === 2) { + // Restore to background color + // Browser implementations historically restore to transparent; we do the same. + // http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079 + frame.clearRect(lastImg.leftPos, lastImg.topPos, lastImg.width, lastImg.height); + } + } + // else, Undefined/Do not dispose. + // frame contains final pixel data from the last frame; do nothing + + //Get existing pixels for img region after applying disposal method + var imgData = frame.getImageData(img.leftPos, img.topPos, img.width, img.height); + + //apply color table colors + img.pixels.forEach(function (pixel, i) { + // imgData.data === [R,G,B,A,R,G,B,A,...] + if (pixel !== transparency) { + imgData.data[i * 4 + 0] = ct[pixel][0]; + imgData.data[i * 4 + 1] = ct[pixel][1]; + imgData.data[i * 4 + 2] = ct[pixel][2]; + imgData.data[i * 4 + 3] = 255; // Opaque. + } + }); + + frame.putImageData(imgData, img.leftPos, img.topPos); + + if (!ctx_scaled) { + ctx.scale(get_canvas_scale(),get_canvas_scale()); + ctx_scaled = true; + } + + // We could use the on-page canvas directly, except that we draw a progress + // bar for each image chunk (not just the final image). + if (drawWhileLoading) { + ctx.drawImage(tmpCanvas, 0, 0); + drawWhileLoading = options.auto_play; + } + + lastImg = img; + }; + + var player = (function () { + var i = -1; + var iterationCount = 0; + + var showingInfo = false; + var pinned = false; + + /** + * Gets the index of the frame "up next". + * @returns {number} + */ + var getNextFrameNo = function () { + var delta = (forward ? 1 : -1); + return (i + delta + frames.length) % frames.length; + }; + + var stepFrame = function (amount) { // XXX: Name is confusing. + i = i + amount; + + putFrame(); + }; + + var step = (function () { + var stepping = false; + + var completeLoop = function () { + if (onEndListener !== null) + onEndListener(gif); + iterationCount++; + + if (overrideLoopMode !== false || iterationCount < 0) { + doStep(); + } else { + stepping = false; + playing = false; + } + }; + + var doStep = function () { + stepping = playing; + if (!stepping) return; + + stepFrame(1); + var delay = frames[i].delay * 10; + if (!delay) delay = 100; // FIXME: Should this even default at all? What should it be? + + var nextFrameNo = getNextFrameNo(); + if (nextFrameNo === 0) { + delay += loopDelay; + setTimeout(completeLoop, delay); + } else { + setTimeout(doStep, delay); + } + }; + + return function () { + if (!stepping) setTimeout(doStep, 0); + }; + }()); + + var putFrame = function () { + var offset; + i = parseInt(i, 10); + + if (i > frames.length - 1){ + i = 0; + } + + if (i < 0){ + i = 0; + } + + offset = frameOffsets[i]; + + tmpCanvas.getContext("2d").putImageData(frames[i].data, offset.x, offset.y); + ctx.globalCompositeOperation = "copy"; + ctx.drawImage(tmpCanvas, 0, 0); + }; + + var play = function () { + playing = true; + step(); + }; + + var pause = function () { + playing = false; + }; + + + return { + init: function () { + if (loadError) return; + + if ( ! (options.c_w && options.c_h) ) { + ctx.scale(get_canvas_scale(),get_canvas_scale()); + } + + if (options.auto_play) { + step(); + } + else { + i = 0; + putFrame(); + } + }, + step: step, + play: play, + pause: pause, + playing: playing, + move_relative: stepFrame, + current_frame: function() { return i; }, + length: function() { return frames.length }, + move_to: function ( frame_idx ) { + i = frame_idx; + putFrame(); + } + } + }()); + + var doDecodeProgress = function (draw) { + doShowProgress(stream.pos, stream.data.length, draw); + }; + + var doNothing = function () {}; + /** + * @param{boolean=} draw Whether to draw progress bar or not; this is not idempotent because of translucency. + * Note that this means that the text will be unsynchronized with the progress bar on non-frames; + * but those are typically so small (GCE etc.) that it doesn't really matter. TODO: Do this properly. + */ + var withProgress = function (fn, draw) { + return function (block) { + fn(block); + doDecodeProgress(draw); + }; + }; + + + var handler = { + hdr: withProgress(doHdr), + gce: withProgress(doGCE), + com: withProgress(doNothing), + // I guess that's all for now. + app: { + // TODO: Is there much point in actually supporting iterations? + NETSCAPE: withProgress(doNothing) + }, + img: withProgress(doImg, true), + eof: function (block) { + //toolbar.style.display = ''; + pushFrame(); + doDecodeProgress(false); + if ( ! (options.c_w && options.c_h) ) { + canvas.width = hdr.width * get_canvas_scale(); + canvas.height = hdr.height * get_canvas_scale(); + } + player.init(); + loading = false; + if (load_callback) { + load_callback(gif); + } + + } + }; + + var init = function () { + var parent = gif.parentNode; + + var div = document.createElement('div'); + canvas = document.createElement('canvas'); + ctx = canvas.getContext('2d'); + toolbar = document.createElement('div'); + + tmpCanvas = document.createElement('canvas'); + + div.width = canvas.width = gif.width; + div.height = canvas.height = gif.height; + toolbar.style.minWidth = gif.width + 'px'; + + div.className = 'jsgif'; + toolbar.className = 'jsgif_toolbar'; + div.appendChild(canvas); + div.appendChild(toolbar); + + parent.insertBefore(div, gif); + parent.removeChild(gif); + + if (options.c_w && options.c_h) setSizes(options.c_w, options.c_h); + initialized=true; + }; + + var get_canvas_scale = function() { + var scale; + if (options.max_width && hdr && hdr.width > options.max_width) { + scale = options.max_width / hdr.width; + } + else { + scale = 1; + } + return scale; + } + + var canvas, ctx, toolbar, tmpCanvas; + var initialized = false; + var load_callback = false; + + var load_setup = function(callback) { + if (loading) return false; + if (callback) load_callback = callback; + else load_callback = false; + + loading = true; + frames = []; + clear(); + disposalRestoreFromIdx = null; + lastDisposalMethod = null; + frame = null; + lastImg = null; + + return true; + } + + return { + // play controls + play: player.play, + pause: player.pause, + move_relative: player.move_relative, + move_to: player.move_to, + + // getters for instance vars + get_playing : function() { return playing }, + get_canvas : function() { return canvas }, + get_canvas_scale : function() { return get_canvas_scale() }, + get_loading : function() { return loading }, + get_auto_play : function() { return options.auto_play }, + get_length : function() { return player.length() }, + get_current_frame: function() { return player.current_frame() }, + load_url: function(src,callback){ + if (!load_setup(callback)) return; + + var h = new XMLHttpRequest(); + // new browsers (XMLHttpRequest2-compliant) + h.open('GET', src, true); + + if ('overrideMimeType' in h) { + h.overrideMimeType('text/plain; charset=x-user-defined'); + } + + // old browsers (XMLHttpRequest-compliant) + else if ('responseType' in h) { + h.responseType = 'arraybuffer'; + } + + // IE9 (Microsoft.XMLHTTP-compliant) + else { + h.setRequestHeader('Accept-Charset', 'x-user-defined'); + } + + h.onloadstart = function() { + // Wait until connection is opened to replace the gif element with a canvas to avoid a blank img + if (!initialized) init(); + }; + h.onload = function(e) { + if (this.status != 200) { + doLoadError('xhr - response'); + } + // emulating response field for IE9 + if (!('response' in this)) { + this.response = new VBArray(this.responseText).toArray().map(String.fromCharCode).join(''); + } + var data = this.response; + if (data.toString().indexOf("ArrayBuffer") > 0) { + data = new Uint8Array(data); + } + + stream = new Stream(data); + setTimeout(doParse, 0); + }; + h.onprogress = function (e) { + if (e.lengthComputable) doShowProgress(e.loaded, e.total, true); + }; + h.onerror = function() { doLoadError('xhr'); }; + h.send(); + }, + load: function (callback) { + this.load_url(gif.getAttribute('rel:animated_src') || gif.src,callback); + }, + load_raw: function(arr, callback) { + if (!load_setup(callback)) return; + if (!initialized) init(); + stream = new Stream(arr); + setTimeout(doParse, 0); + }, + set_frame_offset: setFrameOffset + }; + }; + + return SuperGif; +})); + +