├── .gitignore
├── LICENSE
├── README.md
├── index.js
├── lib
└── lib-gif.js
├── package.json
└── src
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Matthew Conlen
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any
4 | purpose with or without fee is hereby granted, provided that the above
5 | copyright notice and this permission notice appear in all copies.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scrolly-gif
2 | Animate a gif as the user scrolls the page
3 |
4 | This is mostly a wrapper around https://github.com/buzzfeed/libgif-js
5 |
6 | Examples: http://mathisonian.github.io/scrolly-gif/
7 |
8 | ## install
9 |
10 | ```
11 | npm install scrolly-gif
12 | ```
13 |
14 | ## usage
15 |
16 | ```js
17 | var scrollyGif = require('scrolly-gif');
18 |
19 | // simple:
20 | scrollyGif(document.getElementById('my-gif-element'));
21 |
22 | // or with jquery:
23 | $('img.scrolly-gif').each(function() {
24 | scrollyGif(this);
25 | });
26 | ```
27 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = require('./src');
3 |
--------------------------------------------------------------------------------
/lib/lib-gif.js:
--------------------------------------------------------------------------------
1 | /*
2 | SuperGif
3 |
4 | Example usage:
5 |
6 |
7 |
8 |
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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scrolly-gif",
3 | "version": "1.0.1",
4 | "description": "Animate a gif based on user scroll",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mathisonian/scrolly-gif.git"
12 | },
13 | "keywords": [
14 | "gif",
15 | "scroll",
16 | "animate",
17 | "scrolly"
18 | ],
19 | "author": "Matthew Conlen",
20 | "license": "ISC",
21 | "bugs": {
22 | "url": "https://github.com/mathisonian/scrolly-gif/issues"
23 | },
24 | "homepage": "https://github.com/mathisonian/scrolly-gif#readme"
25 | }
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | var SuperGif = require('../lib/lib-gif');
2 |
3 | module.exports = function(elt) {
4 |
5 | var rect = elt.getBoundingClientRect();
6 | var gif = SuperGif({ gif: elt, auto_play: false });
7 |
8 | var height = rect.bottom - rect.top;
9 |
10 | var absoluteTop = rect.top + window.scrollY;
11 | var absoluteBottom = rect.bottom + window.scrollY;
12 | var absolutePosition = rect.top + window.scrollY + height / 2;
13 |
14 | gif.load(function() {
15 | gif.move_to(0);
16 | var gifLength = gif.get_length();
17 |
18 | window.addEventListener('scroll', function(e) {
19 | // calculate current position based on scroll position
20 | var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
21 | var scrollY = window.scrollY;
22 |
23 | if((absoluteBottom - scrollY) >= windowHeight || (absoluteBottom - scrollY) <= 0) {
24 | return;
25 | } else {
26 | var position = (windowHeight - height) - (absoluteTop - scrollY);
27 | position /= (windowHeight - height);
28 | var mappedIndex = position * gifLength;
29 | mappedIndex = Math.min(Math.round(mappedIndex), gifLength - 1);
30 | gif.move_to(mappedIndex);
31 | }
32 | });
33 | });
34 | }
35 |
--------------------------------------------------------------------------------