997 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			997 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*
 | |
| 	SuperGif
 | |
| 
 | |
| 	Example usage:
 | |
| 
 | |
| 		<img src="./example1_preview.gif" rel:animated_src="./example1.gif" width="360" height="360" rel:auto_play="1" />
 | |
| 
 | |
| 		<script type="text/javascript">
 | |
| 			$$('img').each(function (img_tag) {
 | |
| 				if (/.*\.gif/.test(img_tag.src)) {
 | |
| 					var rub = new SuperGif({ gif: img_tag } );
 | |
| 					rub.load();
 | |
| 				}
 | |
| 			});
 | |
| 		</script>
 | |
| 
 | |
| 	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', { willReadFrequently: true }).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', { willReadFrequently: true });
 | |
| 
 | |
|             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", { willReadFrequently: true }).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', { willReadFrequently: true });
 | |
|             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();
 | |
|                 h.onreadystatechange = function() {
 | |
|                     if (h.readyState == 4) {
 | |
|                         if (h.status == 404) {
 | |
|                             callback(404)
 | |
|                         }
 | |
|                     }
 | |
|                 };
 | |
|                 // 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;
 | |
| }));
 | |
| 
 | |
| 
 |