...
...
@@ -0,0 +1,990 @@
1
+/*
2
+ SuperGif
3
+
4
+ Example usage:
5
+
6
+ <img src="./example1_preview.gif" rel:animated_src="./example1.gif" width="360" height="360" rel:auto_play="1" />
7
+
8
+ <script type="text/javascript">
9
+ $$('img').each(function (img_tag) {
10
+ if (/.*\.gif/.test(img_tag.src)) {
11
+ var rub = new SuperGif({ gif: img_tag } );
12
+ rub.load();
13
+ }
14
+ });
15
+ </script>
16
+
17
+ Image tag attributes:
18
+
19
+ rel:animated_src - If this url is specified, it's loaded into the player instead of src.
20
+ This allows a preview frame to be shown until animated gif data is streamed into the canvas
21
+
22
+ rel:auto_play - Defaults to 1 if not specified. If set to zero, a call to the play() method is needed
23
+
24
+ Constructor options args
25
+
26
+ gif Required. The DOM element of an img tag.
27
+ loop_mode Optional. Setting this to false will force disable looping of the gif.
28
+ auto_play Optional. Same as the rel:auto_play attribute above, this arg overrides the img tag info.
29
+ max_width Optional. Scale images over max_width down to max_width. Helpful with mobile.
30
+ 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.
31
+ loop_delay Optional. The amount of time to pause (in ms) after each single loop (iteration).
32
+ draw_while_loading Optional. Determines whether the gif will be drawn to the canvas whilst it is loaded.
33
+ show_progress_bar Optional. Only applies when draw_while_loading is set to true.
34
+
35
+ Instance methods
36
+
37
+ // loading
38
+ 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
39
+ 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
40
+
41
+ // play controls
42
+ play - Start playing the gif
43
+ pause - Stop playing the gif
44
+ move_to(i) - Move to frame i of the gif
45
+ move_relative(i) - Move i frames ahead (or behind if i < 0)
46
+
47
+ // getters
48
+ get_canvas The canvas element that the gif is playing in. Handy for assigning event handlers to.
49
+ get_playing Whether or not the gif is currently playing
50
+ get_loading Whether or not the gif has finished loading/parsing
51
+ get_auto_play Whether or not the gif is set to play automatically
52
+ get_length The number of frames in the gif
53
+ get_current_frame The index of the currently displayed frame of the gif
54
+
55
+ For additional customization (viewport inside iframe) these params may be passed:
56
+ c_w, c_h - width and height of canvas
57
+ vp_t, vp_l, vp_ w, vp_h - top, left, width and height of the viewport
58
+
59
+ A bonus: few articles to understand what is going on
60
+ http://enthusiasms.org/post/16976438906
61
+ http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
62
+ http://humpy77.deviantart.com/journal/Frame-Delay-Times-for-Animated-GIFs-214150546
63
+
64
+*/
65
+(function (root, factory) {
66
+ if (typeof define === 'function' && define.amd) {
67
+ define([], factory);
68
+ } else if (typeof exports === 'object') {
69
+ module.exports = factory();
70
+ } else {
71
+ root.SuperGif = factory();
72
+ }
73
+}(this, function () {
74
+ // Generic functions
75
+ var bitsToNum = function (ba) {
76
+ return ba.reduce(function (s, n) {
77
+ return s * 2 + n;
78
+ }, 0);
79
+ };
80
+
81
+ var byteToBitArr = function (bite) {
82
+ var a = [];
83
+ for (var i = 7; i >= 0; i--) {
84
+ a.push( !! (bite & (1 << i)));
85
+ }
86
+ return a;
87
+ };
88
+
89
+ // Stream
90
+ /**
91
+ * @constructor
92
+ */
93
+ // Make compiler happy.
94
+ var Stream = function (data) {
95
+ this.data = data;
96
+ this.len = this.data.length;
97
+ this.pos = 0;
98
+
99
+ this.readByte = function () {
100
+ if (this.pos >= this.data.length) {
101
+ throw new Error('Attempted to read past end of stream.');
102
+ }
103
+ if (data instanceof Uint8Array)
104
+ return data[this.pos++];
105
+ else
106
+ return data.charCodeAt(this.pos++) & 0xFF;
107
+ };
108
+
109
+ this.readBytes = function (n) {
110
+ var bytes = [];
111
+ for (var i = 0; i < n; i++) {
112
+ bytes.push(this.readByte());
113
+ }
114
+ return bytes;
115
+ };
116
+
117
+ this.read = function (n) {
118
+ var s = '';
119
+ for (var i = 0; i < n; i++) {
120
+ s += String.fromCharCode(this.readByte());
121
+ }
122
+ return s;
123
+ };
124
+
125
+ this.readUnsigned = function () { // Little-endian.
126
+ var a = this.readBytes(2);
127
+ return (a[1] << 8) + a[0];
128
+ };
129
+ };
130
+
131
+ var lzwDecode = function (minCodeSize, data) {
132
+ // TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String?
133
+ var pos = 0; // Maybe this streaming thing should be merged with the Stream?
134
+ var readCode = function (size) {
135
+ var code = 0;
136
+ for (var i = 0; i < size; i++) {
137
+ if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) {
138
+ code |= 1 << i;
139
+ }
140
+ pos++;
141
+ }
142
+ return code;
143
+ };
144
+
145
+ var output = [];
146
+
147
+ var clearCode = 1 << minCodeSize;
148
+ var eoiCode = clearCode + 1;
149
+
150
+ var codeSize = minCodeSize + 1;
151
+
152
+ var dict = [];
153
+
154
+ var clear = function () {
155
+ dict = [];
156
+ codeSize = minCodeSize + 1;
157
+ for (var i = 0; i < clearCode; i++) {
158
+ dict[i] = [i];
159
+ }
160
+ dict[clearCode] = [];
161
+ dict[eoiCode] = null;
162
+
163
+ };
164
+
165
+ var code;
166
+ var last;
167
+
168
+ while (true) {
169
+ last = code;
170
+ code = readCode(codeSize);
171
+
172
+ if (code === clearCode) {
173
+ clear();
174
+ continue;
175
+ }
176
+ if (code === eoiCode) break;
177
+
178
+ if (code < dict.length) {
179
+ if (last !== clearCode) {
180
+ dict.push(dict[last].concat(dict[code][0]));
181
+ }
182
+ }
183
+ else {
184
+ if (code !== dict.length) throw new Error('Invalid LZW code.');
185
+ dict.push(dict[last].concat(dict[last][0]));
186
+ }
187
+ output.push.apply(output, dict[code]);
188
+
189
+ if (dict.length === (1 << codeSize) && codeSize < 12) {
190
+ // 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.
191
+ codeSize++;
192
+ }
193
+ }
194
+
195
+ // I don't know if this is technically an error, but some GIFs do it.
196
+ //if (Math.ceil(pos / 8) !== data.length) throw new Error('Extraneous LZW bytes.');
197
+ return output;
198
+ };
199
+
200
+
201
+ // The actual parsing; returns an object with properties.
202
+ var parseGIF = function (st, handler) {
203
+ handler || (handler = {});
204
+
205
+ // LZW (GIF-specific)
206
+ var parseCT = function (entries) { // Each entry is 3 bytes, for RGB.
207
+ var ct = [];
208
+ for (var i = 0; i < entries; i++) {
209
+ ct.push(st.readBytes(3));
210
+ }
211
+ return ct;
212
+ };
213
+
214
+ var readSubBlocks = function () {
215
+ var size, data;
216
+ data = '';
217
+ do {
218
+ size = st.readByte();
219
+ data += st.read(size);
220
+ } while (size !== 0);
221
+ return data;
222
+ };
223
+
224
+ var parseHeader = function () {
225
+ var hdr = {};
226
+ hdr.sig = st.read(3);
227
+ hdr.ver = st.read(3);
228
+ if (hdr.sig !== 'GIF') throw new Error('Not a GIF file.'); // XXX: This should probably be handled more nicely.
229
+ hdr.width = st.readUnsigned();
230
+ hdr.height = st.readUnsigned();
231
+
232
+ var bits = byteToBitArr(st.readByte());
233
+ hdr.gctFlag = bits.shift();
234
+ hdr.colorRes = bitsToNum(bits.splice(0, 3));
235
+ hdr.sorted = bits.shift();
236
+ hdr.gctSize = bitsToNum(bits.splice(0, 3));
237
+
238
+ hdr.bgColor = st.readByte();
239
+ hdr.pixelAspectRatio = st.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
240
+ if (hdr.gctFlag) {
241
+ hdr.gct = parseCT(1 << (hdr.gctSize + 1));
242
+ }
243
+ handler.hdr && handler.hdr(hdr);
244
+ };
245
+
246
+ var parseExt = function (block) {
247
+ var parseGCExt = function (block) {
248
+ var blockSize = st.readByte(); // Always 4
249
+ var bits = byteToBitArr(st.readByte());
250
+ block.reserved = bits.splice(0, 3); // Reserved; should be 000.
251
+ block.disposalMethod = bitsToNum(bits.splice(0, 3));
252
+ block.userInput = bits.shift();
253
+ block.transparencyGiven = bits.shift();
254
+
255
+ block.delayTime = st.readUnsigned();
256
+
257
+ block.transparencyIndex = st.readByte();
258
+
259
+ block.terminator = st.readByte();
260
+
261
+ handler.gce && handler.gce(block);
262
+ };
263
+
264
+ var parseComExt = function (block) {
265
+ block.comment = readSubBlocks();
266
+ handler.com && handler.com(block);
267
+ };
268
+
269
+ var parsePTExt = function (block) {
270
+ // No one *ever* uses this. If you use it, deal with parsing it yourself.
271
+ var blockSize = st.readByte(); // Always 12
272
+ block.ptHeader = st.readBytes(12);
273
+ block.ptData = readSubBlocks();
274
+ handler.pte && handler.pte(block);
275
+ };
276
+
277
+ var parseAppExt = function (block) {
278
+ var parseNetscapeExt = function (block) {
279
+ var blockSize = st.readByte(); // Always 3
280
+ block.unknown = st.readByte(); // ??? Always 1? What is this?
281
+ block.iterations = st.readUnsigned();
282
+ block.terminator = st.readByte();
283
+ handler.app && handler.app.NETSCAPE && handler.app.NETSCAPE(block);
284
+ };
285
+
286
+ var parseUnknownAppExt = function (block) {
287
+ block.appData = readSubBlocks();
288
+ // FIXME: This won't work if a handler wants to match on any identifier.
289
+ handler.app && handler.app[block.identifier] && handler.app[block.identifier](block);
290
+ };
291
+
292
+ var blockSize = st.readByte(); // Always 11
293
+ block.identifier = st.read(8);
294
+ block.authCode = st.read(3);
295
+ switch (block.identifier) {
296
+ case 'NETSCAPE':
297
+ parseNetscapeExt(block);
298
+ break;
299
+ default:
300
+ parseUnknownAppExt(block);
301
+ break;
302
+ }
303
+ };
304
+
305
+ var parseUnknownExt = function (block) {
306
+ block.data = readSubBlocks();
307
+ handler.unknown && handler.unknown(block);
308
+ };
309
+
310
+ block.label = st.readByte();
311
+ switch (block.label) {
312
+ case 0xF9:
313
+ block.extType = 'gce';
314
+ parseGCExt(block);
315
+ break;
316
+ case 0xFE:
317
+ block.extType = 'com';
318
+ parseComExt(block);
319
+ break;
320
+ case 0x01:
321
+ block.extType = 'pte';
322
+ parsePTExt(block);
323
+ break;
324
+ case 0xFF:
325
+ block.extType = 'app';
326
+ parseAppExt(block);
327
+ break;
328
+ default:
329
+ block.extType = 'unknown';
330
+ parseUnknownExt(block);
331
+ break;
332
+ }
333
+ };
334
+
335
+ var parseImg = function (img) {
336
+ var deinterlace = function (pixels, width) {
337
+ // Of course this defeats the purpose of interlacing. And it's *probably*
338
+ // the least efficient way it's ever been implemented. But nevertheless...
339
+ var newPixels = new Array(pixels.length);
340
+ var rows = pixels.length / width;
341
+ var cpRow = function (toRow, fromRow) {
342
+ var fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);
343
+ newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));
344
+ };
345
+
346
+ // See appendix E.
347
+ var offsets = [0, 4, 2, 1];
348
+ var steps = [8, 8, 4, 2];
349
+
350
+ var fromRow = 0;
351
+ for (var pass = 0; pass < 4; pass++) {
352
+ for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {
353
+ cpRow(toRow, fromRow)
354
+ fromRow++;
355
+ }
356
+ }
357
+
358
+ return newPixels;
359
+ };
360
+
361
+ img.leftPos = st.readUnsigned();
362
+ img.topPos = st.readUnsigned();
363
+ img.width = st.readUnsigned();
364
+ img.height = st.readUnsigned();
365
+
366
+ var bits = byteToBitArr(st.readByte());
367
+ img.lctFlag = bits.shift();
368
+ img.interlaced = bits.shift();
369
+ img.sorted = bits.shift();
370
+ img.reserved = bits.splice(0, 2);
371
+ img.lctSize = bitsToNum(bits.splice(0, 3));
372
+
373
+ if (img.lctFlag) {
374
+ img.lct = parseCT(1 << (img.lctSize + 1));
375
+ }
376
+
377
+ img.lzwMinCodeSize = st.readByte();
378
+
379
+ var lzwData = readSubBlocks();
380
+
381
+ img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData);
382
+
383
+ if (img.interlaced) { // Move
384
+ img.pixels = deinterlace(img.pixels, img.width);
385
+ }
386
+
387
+ handler.img && handler.img(img);
388
+ };
389
+
390
+ var parseBlock = function () {
391
+ var block = {};
392
+ block.sentinel = st.readByte();
393
+
394
+ switch (String.fromCharCode(block.sentinel)) { // For ease of matching
395
+ case '!':
396
+ block.type = 'ext';
397
+ parseExt(block);
398
+ break;
399
+ case ',':
400
+ block.type = 'img';
401
+ parseImg(block);
402
+ break;
403
+ case ';':
404
+ block.type = 'eof';
405
+ handler.eof && handler.eof(block);
406
+ break;
407
+ default:
408
+ throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0.
409
+ }
410
+
411
+ if (block.type !== 'eof') setTimeout(parseBlock, 0);
412
+ };
413
+
414
+ var parse = function () {
415
+ parseHeader();
416
+ setTimeout(parseBlock, 0);
417
+ };
418
+
419
+ parse();
420
+ };
421
+
422
+ var SuperGif = function ( opts ) {
423
+ var options = {
424
+ //viewport position
425
+ vp_l: 0,
426
+ vp_t: 0,
427
+ vp_w: null,
428
+ vp_h: null,
429
+ //canvas sizes
430
+ c_w: null,
431
+ c_h: null
432
+ };
433
+ for (var i in opts ) { options[i] = opts[i] }
434
+ if (options.vp_w && options.vp_h) options.is_vp = true;
435
+
436
+ var stream;
437
+ var hdr;
438
+
439
+ var loadError = null;
440
+ var loading = false;
441
+
442
+ var transparency = null;
443
+ var delay = null;
444
+ var disposalMethod = null;
445
+ var disposalRestoreFromIdx = null;
446
+ var lastDisposalMethod = null;
447
+ var frame = null;
448
+ var lastImg = null;
449
+
450
+ var playing = true;
451
+ var forward = true;
452
+
453
+ var ctx_scaled = false;
454
+
455
+ var frames = [];
456
+ var frameOffsets = []; // elements have .x and .y properties
457
+
458
+ var gif = options.gif;
459
+ if (typeof options.auto_play == 'undefined')
460
+ options.auto_play = (!gif.getAttribute('rel:auto_play') || gif.getAttribute('rel:auto_play') == '1');
461
+
462
+ var onEndListener = (options.hasOwnProperty('on_end') ? options.on_end : null);
463
+ var loopDelay = (options.hasOwnProperty('loop_delay') ? options.loop_delay : 0);
464
+ var overrideLoopMode = (options.hasOwnProperty('loop_mode') ? options.loop_mode : 'auto');
465
+ var drawWhileLoading = (options.hasOwnProperty('draw_while_loading') ? options.draw_while_loading : true);
466
+ var showProgressBar = drawWhileLoading ? (options.hasOwnProperty('show_progress_bar') ? options.show_progress_bar : true) : false;
467
+ var progressBarHeight = (options.hasOwnProperty('progressbar_height') ? options.progressbar_height : 25);
468
+ var progressBarBackgroundColor = (options.hasOwnProperty('progressbar_background_color') ? options.progressbar_background_color : 'rgba(255,255,255,0.4)');
469
+ var progressBarForegroundColor = (options.hasOwnProperty('progressbar_foreground_color') ? options.progressbar_foreground_color : 'rgba(255,0,22,.8)');
470
+
471
+ var clear = function () {
472
+ transparency = null;
473
+ delay = null;
474
+ lastDisposalMethod = disposalMethod;
475
+ disposalMethod = null;
476
+ frame = null;
477
+ };
478
+
479
+ // XXX: There's probably a better way to handle catching exceptions when
480
+ // callbacks are involved.
481
+ var doParse = function () {
482
+ try {
483
+ parseGIF(stream, handler);
484
+ }
485
+ catch (err) {
486
+ doLoadError('parse');
487
+ }
488
+ };
489
+
490
+ var doText = function (text) {
491
+ toolbar.innerHTML = text; // innerText? Escaping? Whatever.
492
+ toolbar.style.visibility = 'visible';
493
+ };
494
+
495
+ var setSizes = function(w, h) {
496
+ canvas.width = w * get_canvas_scale();
497
+ canvas.height = h * get_canvas_scale();
498
+ toolbar.style.minWidth = ( w * get_canvas_scale() ) + 'px';
499
+
500
+ tmpCanvas.width = w;
501
+ tmpCanvas.height = h;
502
+ tmpCanvas.style.width = w + 'px';
503
+ tmpCanvas.style.height = h + 'px';
504
+ tmpCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
505
+ };
506
+
507
+ var setFrameOffset = function(frame, offset) {
508
+ if (!frameOffsets[frame]) {
509
+ frameOffsets[frame] = offset;
510
+ return;
511
+ }
512
+ if (typeof offset.x !== 'undefined') {
513
+ frameOffsets[frame].x = offset.x;
514
+ }
515
+ if (typeof offset.y !== 'undefined') {
516
+ frameOffsets[frame].y = offset.y;
517
+ }
518
+ };
519
+
520
+ var doShowProgress = function (pos, length, draw) {
521
+ if (draw && showProgressBar) {
522
+ var height = progressBarHeight;
523
+ var left, mid, top, width;
524
+ if (options.is_vp) {
525
+ if (!ctx_scaled) {
526
+ top = (options.vp_t + options.vp_h - height);
527
+ height = height;
528
+ left = options.vp_l;
529
+ mid = left + (pos / length) * options.vp_w;
530
+ width = canvas.width;
531
+ } else {
532
+ top = (options.vp_t + options.vp_h - height) / get_canvas_scale();
533
+ height = height / get_canvas_scale();
534
+ left = (options.vp_l / get_canvas_scale() );
535
+ mid = left + (pos / length) * (options.vp_w / get_canvas_scale());
536
+ width = canvas.width / get_canvas_scale();
537
+ }
538
+ //some debugging, draw rect around viewport
539
+ if (false) {
540
+ if (!ctx_scaled) {
541
+ var l = options.vp_l, t = options.vp_t;
542
+ var w = options.vp_w, h = options.vp_h;
543
+ } else {
544
+ var l = options.vp_l/get_canvas_scale(), t = options.vp_t/get_canvas_scale();
545
+ var w = options.vp_w/get_canvas_scale(), h = options.vp_h/get_canvas_scale();
546
+ }
547
+ ctx.rect(l,t,w,h);
548
+ ctx.stroke();
549
+ }
550
+ }
551
+ else {
552
+ top = (canvas.height - height) / (ctx_scaled ? get_canvas_scale() : 1);
553
+ mid = ((pos / length) * canvas.width) / (ctx_scaled ? get_canvas_scale() : 1);
554
+ width = canvas.width / (ctx_scaled ? get_canvas_scale() : 1 );
555
+ height /= ctx_scaled ? get_canvas_scale() : 1;
556
+ }
557
+
558
+ ctx.fillStyle = progressBarBackgroundColor;
559
+ ctx.fillRect(mid, top, width - mid, height);
560
+
561
+ ctx.fillStyle = progressBarForegroundColor;
562
+ ctx.fillRect(0, top, mid, height);
563
+ }
564
+ };
565
+
566
+ var doLoadError = function (originOfError) {
567
+ var drawError = function () {
568
+ ctx.fillStyle = 'black';
569
+ ctx.fillRect(0, 0, options.c_w ? options.c_w : hdr.width, options.c_h ? options.c_h : hdr.height);
570
+ ctx.strokeStyle = 'red';
571
+ ctx.lineWidth = 3;
572
+ ctx.moveTo(0, 0);
573
+ ctx.lineTo(options.c_w ? options.c_w : hdr.width, options.c_h ? options.c_h : hdr.height);
574
+ ctx.moveTo(0, options.c_h ? options.c_h : hdr.height);
575
+ ctx.lineTo(options.c_w ? options.c_w : hdr.width, 0);
576
+ ctx.stroke();
577
+ };
578
+
579
+ loadError = originOfError;
580
+ hdr = {
581
+ width: gif.width,
582
+ height: gif.height
583
+ }; // Fake header.
584
+ frames = [];
585
+ drawError();
586
+ };
587
+
588
+ var doHdr = function (_hdr) {
589
+ hdr = _hdr;
590
+ setSizes(hdr.width, hdr.height)
591
+ };
592
+
593
+ var doGCE = function (gce) {
594
+ pushFrame();
595
+ clear();
596
+ transparency = gce.transparencyGiven ? gce.transparencyIndex : null;
597
+ delay = gce.delayTime;
598
+ disposalMethod = gce.disposalMethod;
599
+ // We don't have much to do with the rest of GCE.
600
+ };
601
+
602
+ var pushFrame = function () {
603
+ if (!frame) return;
604
+ frames.push({
605
+ data: frame.getImageData(0, 0, hdr.width, hdr.height),
606
+ delay: delay
607
+ });
608
+ frameOffsets.push({ x: 0, y: 0 });
609
+ };
610
+
611
+ var doImg = function (img) {
612
+ if (!frame) frame = tmpCanvas.getContext('2d');
613
+
614
+ var currIdx = frames.length;
615
+
616
+ //ct = color table, gct = global color table
617
+ var ct = img.lctFlag ? img.lct : hdr.gct; // TODO: What if neither exists?
618
+
619
+ /*
620
+ Disposal method indicates the way in which the graphic is to
621
+ be treated after being displayed.
622
+
623
+ Values : 0 - No disposal specified. The decoder is
624
+ not required to take any action.
625
+ 1 - Do not dispose. The graphic is to be left
626
+ in place.
627
+ 2 - Restore to background color. The area used by the
628
+ graphic must be restored to the background color.
629
+ 3 - Restore to previous. The decoder is required to
630
+ restore the area overwritten by the graphic with
631
+ what was there prior to rendering the graphic.
632
+
633
+ Importantly, "previous" means the frame state
634
+ after the last disposal of method 0, 1, or 2.
635
+ */
636
+ if (currIdx > 0) {
637
+ if (lastDisposalMethod === 3) {
638
+ // Restore to previous
639
+ // If we disposed every frame including first frame up to this point, then we have
640
+ // no composited frame to restore to. In this case, restore to background instead.
641
+ if (disposalRestoreFromIdx !== null) {
642
+ frame.putImageData(frames[disposalRestoreFromIdx].data, 0, 0);
643
+ } else {
644
+ frame.clearRect(lastImg.leftPos, lastImg.topPos, lastImg.width, lastImg.height);
645
+ }
646
+ } else {
647
+ disposalRestoreFromIdx = currIdx - 1;
648
+ }
649
+
650
+ if (lastDisposalMethod === 2) {
651
+ // Restore to background color
652
+ // Browser implementations historically restore to transparent; we do the same.
653
+ // http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079
654
+ frame.clearRect(lastImg.leftPos, lastImg.topPos, lastImg.width, lastImg.height);
655
+ }
656
+ }
657
+ // else, Undefined/Do not dispose.
658
+ // frame contains final pixel data from the last frame; do nothing
659
+
660
+ //Get existing pixels for img region after applying disposal method
661
+ var imgData = frame.getImageData(img.leftPos, img.topPos, img.width, img.height);
662
+
663
+ //apply color table colors
664
+ img.pixels.forEach(function (pixel, i) {
665
+ // imgData.data === [R,G,B,A,R,G,B,A,...]
666
+ if (pixel !== transparency) {
667
+ imgData.data[i * 4 + 0] = ct[pixel][0];
668
+ imgData.data[i * 4 + 1] = ct[pixel][1];
669
+ imgData.data[i * 4 + 2] = ct[pixel][2];
670
+ imgData.data[i * 4 + 3] = 255; // Opaque.
671
+ }
672
+ });
673
+
674
+ frame.putImageData(imgData, img.leftPos, img.topPos);
675
+
676
+ if (!ctx_scaled) {
677
+ ctx.scale(get_canvas_scale(),get_canvas_scale());
678
+ ctx_scaled = true;
679
+ }
680
+
681
+ // We could use the on-page canvas directly, except that we draw a progress
682
+ // bar for each image chunk (not just the final image).
683
+ if (drawWhileLoading) {
684
+ ctx.drawImage(tmpCanvas, 0, 0);
685
+ drawWhileLoading = options.auto_play;
686
+ }
687
+
688
+ lastImg = img;
689
+ };
690
+
691
+ var player = (function () {
692
+ var i = -1;
693
+ var iterationCount = 0;
694
+
695
+ var showingInfo = false;
696
+ var pinned = false;
697
+
698
+ /**
699
+ * Gets the index of the frame "up next".
700
+ * @returns {number}
701
+ */
702
+ var getNextFrameNo = function () {
703
+ var delta = (forward ? 1 : -1);
704
+ return (i + delta + frames.length) % frames.length;
705
+ };
706
+
707
+ var stepFrame = function (amount) { // XXX: Name is confusing.
708
+ i = i + amount;
709
+
710
+ putFrame();
711
+ };
712
+
713
+ var step = (function () {
714
+ var stepping = false;
715
+
716
+ var completeLoop = function () {
717
+ if (onEndListener !== null)
718
+ onEndListener(gif);
719
+ iterationCount++;
720
+
721
+ if (overrideLoopMode !== false || iterationCount < 0) {
722
+ doStep();
723
+ } else {
724
+ stepping = false;
725
+ playing = false;
726
+ }
727
+ };
728
+
729
+ var doStep = function () {
730
+ stepping = playing;
731
+ if (!stepping) return;
732
+
733
+ stepFrame(1);
734
+ var delay = frames[i].delay * 10;
735
+ if (!delay) delay = 100; // FIXME: Should this even default at all? What should it be?
736
+
737
+ var nextFrameNo = getNextFrameNo();
738
+ if (nextFrameNo === 0) {
739
+ delay += loopDelay;
740
+ setTimeout(completeLoop, delay);
741
+ } else {
742
+ setTimeout(doStep, delay);
743
+ }
744
+ };
745
+
746
+ return function () {
747
+ if (!stepping) setTimeout(doStep, 0);
748
+ };
749
+ }());
750
+
751
+ var putFrame = function () {
752
+ var offset;
753
+ i = parseInt(i, 10);
754
+
755
+ if (i > frames.length - 1){
756
+ i = 0;
757
+ }
758
+
759
+ if (i < 0){
760
+ i = 0;
761
+ }
762
+
763
+ offset = frameOffsets[i];
764
+
765
+ tmpCanvas.getContext("2d").putImageData(frames[i].data, offset.x, offset.y);
766
+ ctx.globalCompositeOperation = "copy";
767
+ ctx.drawImage(tmpCanvas, 0, 0);
768
+ };
769
+
770
+ var play = function () {
771
+ playing = true;
772
+ step();
773
+ };
774
+
775
+ var pause = function () {
776
+ playing = false;
777
+ };
778
+
779
+
780
+ return {
781
+ init: function () {
782
+ if (loadError) return;
783
+
784
+ if ( ! (options.c_w && options.c_h) ) {
785
+ ctx.scale(get_canvas_scale(),get_canvas_scale());
786
+ }
787
+
788
+ if (options.auto_play) {
789
+ step();
790
+ }
791
+ else {
792
+ i = 0;
793
+ putFrame();
794
+ }
795
+ },
796
+ step: step,
797
+ play: play,
798
+ pause: pause,
799
+ playing: playing,
800
+ move_relative: stepFrame,
801
+ current_frame: function() { return i; },
802
+ length: function() { return frames.length },
803
+ move_to: function ( frame_idx ) {
804
+ i = frame_idx;
805
+ putFrame();
806
+ }
807
+ }
808
+ }());
809
+
810
+ var doDecodeProgress = function (draw) {
811
+ doShowProgress(stream.pos, stream.data.length, draw);
812
+ };
813
+
814
+ var doNothing = function () {};
815
+ /**
816
+ * @param{boolean=} draw Whether to draw progress bar or not; this is not idempotent because of translucency.
817
+ * Note that this means that the text will be unsynchronized with the progress bar on non-frames;
818
+ * but those are typically so small (GCE etc.) that it doesn't really matter. TODO: Do this properly.
819
+ */
820
+ var withProgress = function (fn, draw) {
821
+ return function (block) {
822
+ fn(block);
823
+ doDecodeProgress(draw);
824
+ };
825
+ };
826
+
827
+
828
+ var handler = {
829
+ hdr: withProgress(doHdr),
830
+ gce: withProgress(doGCE),
831
+ com: withProgress(doNothing),
832
+ // I guess that's all for now.
833
+ app: {
834
+ // TODO: Is there much point in actually supporting iterations?
835
+ NETSCAPE: withProgress(doNothing)
836
+ },
837
+ img: withProgress(doImg, true),
838
+ eof: function (block) {
839
+ //toolbar.style.display = '';
840
+ pushFrame();
841
+ doDecodeProgress(false);
842
+ if ( ! (options.c_w && options.c_h) ) {
843
+ canvas.width = hdr.width * get_canvas_scale();
844
+ canvas.height = hdr.height * get_canvas_scale();
845
+ }
846
+ player.init();
847
+ loading = false;
848
+ if (load_callback) {
849
+ load_callback(gif);
850
+ }
851
+
852
+ }
853
+ };
854
+
855
+ var init = function () {
856
+ var parent = gif.parentNode;
857
+
858
+ var div = document.createElement('div');
859
+ canvas = document.createElement('canvas');
860
+ ctx = canvas.getContext('2d');
861
+ toolbar = document.createElement('div');
862
+
863
+ tmpCanvas = document.createElement('canvas');
864
+
865
+ div.width = canvas.width = gif.width;
866
+ div.height = canvas.height = gif.height;
867
+ toolbar.style.minWidth = gif.width + 'px';
868
+
869
+ div.className = 'jsgif';
870
+ toolbar.className = 'jsgif_toolbar';
871
+ div.appendChild(canvas);
872
+ div.appendChild(toolbar);
873
+
874
+ parent.insertBefore(div, gif);
875
+ parent.removeChild(gif);
876
+
877
+ if (options.c_w && options.c_h) setSizes(options.c_w, options.c_h);
878
+ initialized=true;
879
+ };
880
+
881
+ var get_canvas_scale = function() {
882
+ var scale;
883
+ if (options.max_width && hdr && hdr.width > options.max_width) {
884
+ scale = options.max_width / hdr.width;
885
+ }
886
+ else {
887
+ scale = 1;
888
+ }
889
+ return scale;
890
+ }
891
+
892
+ var canvas, ctx, toolbar, tmpCanvas;
893
+ var initialized = false;
894
+ var load_callback = false;
895
+
896
+ var load_setup = function(callback) {
897
+ if (loading) return false;
898
+ if (callback) load_callback = callback;
899
+ else load_callback = false;
900
+
901
+ loading = true;
902
+ frames = [];
903
+ clear();
904
+ disposalRestoreFromIdx = null;
905
+ lastDisposalMethod = null;
906
+ frame = null;
907
+ lastImg = null;
908
+
909
+ return true;
910
+ }
911
+
912
+ return {
913
+ // play controls
914
+ play: player.play,
915
+ pause: player.pause,
916
+ move_relative: player.move_relative,
917
+ move_to: player.move_to,
918
+
919
+ // getters for instance vars
920
+ get_playing : function() { return playing },
921
+ get_canvas : function() { return canvas },
922
+ get_canvas_scale : function() { return get_canvas_scale() },
923
+ get_loading : function() { return loading },
924
+ get_auto_play : function() { return options.auto_play },
925
+ get_length : function() { return player.length() },
926
+ get_current_frame: function() { return player.current_frame() },
927
+ load_url: function(src,callback){
928
+ if (!load_setup(callback)) return;
929
+
930
+ var h = new XMLHttpRequest();
931
+ // new browsers (XMLHttpRequest2-compliant)
932
+ h.open('GET', src, true);
933
+
934
+ if ('overrideMimeType' in h) {
935
+ h.overrideMimeType('text/plain; charset=x-user-defined');
936
+ }
937
+
938
+ // old browsers (XMLHttpRequest-compliant)
939
+ else if ('responseType' in h) {
940
+ h.responseType = 'arraybuffer';
941
+ }
942
+
943
+ // IE9 (Microsoft.XMLHTTP-compliant)
944
+ else {
945
+ h.setRequestHeader('Accept-Charset', 'x-user-defined');
946
+ }
947
+
948
+ h.onloadstart = function() {
949
+ // Wait until connection is opened to replace the gif element with a canvas to avoid a blank img
950
+ if (!initialized) init();
951
+ };
952
+ h.onload = function(e) {
953
+ if (this.status != 200) {
954
+ doLoadError('xhr - response');
955
+ }
956
+ // emulating response field for IE9
957
+ if (!('response' in this)) {
958
+ this.response = new VBArray(this.responseText).toArray().map(String.fromCharCode).join('');
959
+ }
960
+ var data = this.response;
961
+ if (data.toString().indexOf("ArrayBuffer") > 0) {
962
+ data = new Uint8Array(data);
963
+ }
964
+
965
+ stream = new Stream(data);
966
+ setTimeout(doParse, 0);
967
+ };
968
+ h.onprogress = function (e) {
969
+ if (e.lengthComputable) doShowProgress(e.loaded, e.total, true);
970
+ };
971
+ h.onerror = function() { doLoadError('xhr'); };
972
+ h.send();
973
+ },
974
+ load: function (callback) {
975
+ this.load_url(gif.getAttribute('rel:animated_src') || gif.src,callback);
976
+ },
977
+ load_raw: function(arr, callback) {
978
+ if (!load_setup(callback)) return;
979
+ if (!initialized) init();
980
+ stream = new Stream(arr);
981
+ setTimeout(doParse, 0);
982
+ },
983
+ set_frame_offset: setFrameOffset
984
+ };
985
+ };
986
+
987
+ return SuperGif;
988
+}));
989
+
990
+
...
...