├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── dist
├── aframe-gif-component.js
├── aframe-gif-component.min.js
├── aframe-gif-shader.js
└── aframe-gif-shader.min.js
├── example.gif
├── examples
├── basic
│ ├── banana.gif
│ ├── index.html
│ ├── nyancat.gif
│ └── pusheen.gif
├── common.css
├── index.html
└── main.js
├── index.js
├── lib
└── gifsparser.js
├── package.json
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | { "presets": ["es2015"] }
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .sw[ponm]
2 | examples/build.js
3 | examples/node_modules/
4 | gh-pages
5 | node_modules/
6 | npm-debug.log
7 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Mayo Tobita
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AFrame GIF Shader
2 |
3 | **A gif shader for [A-Frame](https://aframe.io) VR. Inspired by [@gtk2k](https://github.com/gtk2k)'s [awesome sample](https://github.com/gtk2k/gtk2k.github.io/tree/master/animation_gif).**
4 |
5 | **To control, please use [`aframe-gif-component`](https://github.com/mayognaise/aframe-gif-component).**
6 |
7 | **[DEMO](https://mayognaise.github.io/aframe-gif-shader/basic/index.html)**
8 |
9 | 
10 |
11 | **Now transparent gif are supported!** 🎉🎊
12 |
13 | ## Properties
14 |
15 | - Basic material's properties are supported.
16 | - The property is pretty much same as `flat` shader.
17 |
18 | | Property | Description | Default Value |
19 | | -------- | ----------- | ------------- |
20 | |src|image url. @see [Textures](https://aframe.io/docs/components/material.html#Textures)|null|
21 | |autoplay|play automatecally once it's ready|true|
22 |
23 | For refference, please check the following links:
24 |
25 | - [Material](https://aframe.io/docs/master/components/material.html)
26 | - [Textures](https://aframe.io/docs/master/components/material.html#textures)
27 | - [Flat Shading Model](https://aframe.io/docs/core/shaders.html#Flat-Shading-Model)
28 |
29 | ## Usage
30 |
31 | ### Browser Installation
32 |
33 | Install and use by directly including the [browser files](dist):
34 |
35 | ```html
36 |
37 | My A-Frame Scene
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | ```
48 |
49 | ### NPM Installation
50 |
51 | Install via NPM:
52 |
53 | ```bash
54 | npm i -D aframe-gif-shader
55 | ```
56 |
57 | Then register and use.
58 |
59 | ```js
60 | import 'aframe'
61 | import 'aframe-gif-shader'
62 | ```
63 |
64 | ### Contributors
65 |
66 | Thank you so much 🙏
67 |
68 | - [@UXVirtual](https://github.com/UXVirtual)
69 | - [@urish](https://github.com/urish)
70 | - [@pablodiegoss](https://github.com/pablodiegoss)
71 | - [@margauxdivernois](https://github.com/margauxdivernois)
72 | - [@Danpollak](https://github.com/Danpollak)
73 |
--------------------------------------------------------------------------------
/dist/aframe-gif-component.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 |
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 |
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId])
10 | /******/ return installedModules[moduleId].exports;
11 |
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ exports: {},
15 | /******/ id: moduleId,
16 | /******/ loaded: false
17 | /******/ };
18 |
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 |
22 | /******/ // Flag the module as loaded
23 | /******/ module.loaded = true;
24 |
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 |
29 |
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 |
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 |
36 | /******/ // __webpack_public_path__
37 | /******/ __webpack_require__.p = "";
38 |
39 | /******/ // Load entry module and return exports
40 | /******/ return __webpack_require__(0);
41 | /******/ })
42 | /************************************************************************/
43 | /******/ ([
44 | /* 0 */
45 | /***/ (function(module, exports, __webpack_require__) {
46 |
47 | 'use strict';
48 |
49 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
50 |
51 | var _gifsparser = __webpack_require__(1);
52 |
53 | if (typeof AFRAME === 'undefined') {
54 | throw 'Component attempted to register before AFRAME was available.';
55 | }
56 |
57 | /* get util from AFRAME */
58 | var parseUrl = AFRAME.utils.srcLoader.parseUrl;
59 | var debug = AFRAME.utils.debug;
60 | // debug.enable('shader:gif:*')
61 |
62 | debug.enable('shader:gif:warn');
63 | var warn = debug('shader:gif:warn');
64 | var log = debug('shader:gif:debug');
65 |
66 | /* store data so that you won't load same data */
67 | var gifData = {};
68 |
69 | /* create error message */
70 | function createError(err, src) {
71 | return { status: 'error', src: src, message: err, timestamp: Date.now() };
72 | }
73 |
74 | AFRAME.registerShader('gif', {
75 |
76 | /**
77 | * For material component:
78 | * @see https://github.com/aframevr/aframe/blob/60d198ef8e2bfbc57a13511ae5fca7b62e01691b/src/components/material.js
79 | * For example of `registerShader`:
80 | * @see https://github.com/aframevr/aframe/blob/41a50cd5ac65e462120ecc2e5091f5daefe3bd1e/src/shaders/flat.js
81 | * For MeshBasicMaterial
82 | * @see http://threejs.org/docs/#Reference/Materials/MeshBasicMaterial
83 | */
84 | schema: {
85 |
86 | /* For material */
87 | color: { type: 'color' },
88 | fog: { default: true },
89 |
90 | /* For texuture */
91 | src: { default: null },
92 | autoplay: { default: true }
93 |
94 | },
95 |
96 | /**
97 | * Initialize material. Called once.
98 | * @protected
99 | */
100 | init: function init(data) {
101 | log('init', data);
102 | log(this.el.components);
103 | this.__cnv = document.createElement('canvas');
104 | this.__cnv.width = 2;
105 | this.__cnv.height = 2;
106 | this.__ctx = this.__cnv.getContext('2d');
107 | this.__texture = new THREE.Texture(this.__cnv); //renders straight from a canvas
108 | if (data.repeat) {
109 | this.__texture.wrapS = THREE.RepeatWrapping;
110 | this.__texture.wrapT = THREE.RepeatWrapping;
111 | this.__texture.repeat.set(data.repeat.x, data.repeat.y);
112 | }
113 | this.__material = {};
114 | this.__reset();
115 | this.material = new THREE.MeshBasicMaterial({ map: this.__texture });
116 | this.el.sceneEl.addBehavior(this);
117 | return this.material;
118 | },
119 |
120 |
121 | /**
122 | * Update or create material.
123 | * @param {object|null} oldData
124 | */
125 | update: function update(oldData) {
126 | log('update', oldData);
127 | this.__updateMaterial(oldData);
128 | this.__updateTexture(oldData);
129 | return this.material;
130 | },
131 |
132 |
133 | /**
134 | * Called on each scene tick.
135 | * @protected
136 | */
137 | tick: function tick(t) {
138 | if (!this.__frames || this.paused()) return;
139 | if (Date.now() - this.__startTime >= this.__nextFrameTime) {
140 | this.nextFrame();
141 | }
142 | },
143 |
144 |
145 | /*================================
146 | = material =
147 | ================================*/
148 |
149 | /**
150 | * Updating existing material.
151 | * @param {object} data - Material component data.
152 | */
153 | __updateMaterial: function __updateMaterial(data) {
154 | var material = this.material;
155 |
156 | var newData = this.__getMaterialData(data);
157 | Object.keys(newData).forEach(function (key) {
158 | material[key] = newData[key];
159 | });
160 | },
161 |
162 |
163 | /**
164 | * Builds and normalize material data, normalizing stuff along the way.
165 | * @param {Object} data - Material data.
166 | * @return {Object} data - Processed material data.
167 | */
168 | __getMaterialData: function __getMaterialData(data) {
169 | return {
170 | fog: data.fog,
171 | color: new THREE.Color(data.color)
172 | };
173 | },
174 |
175 |
176 | /*==============================
177 | = texure =
178 | ==============================*/
179 |
180 | /**
181 | * set texure
182 | * @private
183 | * @param {Object} data
184 | * @property {string} status - success / error
185 | * @property {string} src - src url
186 | * @property {array} times - array of time length of each image
187 | * @property {number} cnt - total counts of gif images
188 | * @property {array} frames - array of each image
189 | * @property {Date} timestamp - created at the texure
190 | */
191 |
192 | __setTexure: function __setTexure(data) {
193 | log('__setTexure', data);
194 | if (data.status === 'error') {
195 | warn('Error: ' + data.message + '\nsrc: ' + data.src);
196 | this.__reset();
197 | } else if (data.status === 'success' && data.src !== this.__textureSrc) {
198 | this.__reset();
199 | /* Texture added or changed */
200 | this.__ready(data);
201 | }
202 | },
203 |
204 |
205 | /**
206 | * Update or create texure.
207 | * @param {Object} data - Material component data.
208 | */
209 | __updateTexture: function __updateTexture(data) {
210 | var src = data.src,
211 | autoplay = data.autoplay;
212 |
213 | /* autoplay */
214 |
215 | if (typeof autoplay === 'boolean') {
216 | this.__autoplay = autoplay;
217 | } else if (typeof autoplay === 'undefined') {
218 | this.__autoplay = true;
219 | }
220 | if (this.__autoplay && this.__frames) {
221 | this.play();
222 | }
223 |
224 | /* src */
225 | if (src) {
226 | this.__validateSrc(src, this.__setTexure.bind(this));
227 | } else {
228 | /* Texture removed */
229 | this.__reset();
230 | }
231 | },
232 |
233 |
234 | /*=============================================
235 | = varidation for texure =
236 | =============================================*/
237 |
238 | __validateSrc: function __validateSrc(src, cb) {
239 |
240 | /* check if src is a url */
241 | var url = parseUrl(src);
242 | if (url) {
243 | this.__getImageSrc(url, cb);
244 | return;
245 | }
246 |
247 | var message = void 0;
248 |
249 | /* check if src is a query selector */
250 | var el = this.__validateAndGetQuerySelector(src);
251 | if (!el || (typeof el === 'undefined' ? 'undefined' : _typeof(el)) !== 'object') {
252 | return;
253 | }
254 | if (el.error) {
255 | message = el.error;
256 | } else {
257 | var tagName = el.tagName.toLowerCase();
258 | if (tagName === 'video') {
259 | src = el.src;
260 | message = 'For video, please use `aframe-video-shader`';
261 | } else if (tagName === 'img') {
262 | this.__getImageSrc(el.src, cb);
263 | return;
264 | } else {
265 | message = 'For <' + tagName + '> element, please use `aframe-html-shader`';
266 | }
267 | }
268 |
269 | /* if there is message, create error data */
270 | if (message) {
271 | var srcData = gifData[src];
272 | var errData = createError(message, src);
273 | /* callbacks */
274 | if (srcData && srcData.callbacks) {
275 | srcData.callbacks.forEach(function (cb) {
276 | return cb(errData);
277 | });
278 | } else {
279 | cb(errData);
280 | }
281 | /* overwrite */
282 | gifData[src] = errData;
283 | }
284 | },
285 |
286 |
287 | /**
288 | * Validate src is a valid image url
289 | * @param {string} src - url that will be tested
290 | * @param {function} cb - callback with the test result
291 | */
292 | __getImageSrc: function __getImageSrc(src, cb) {
293 | var _this = this;
294 |
295 | /* if src is same as previous, ignore this */
296 | if (src === this.__textureSrc) {
297 | return;
298 | }
299 |
300 | /* check if we already get the srcData */
301 | var srcData = gifData[src];
302 | if (!srcData || !srcData.callbacks) {
303 | /* create callback */
304 | srcData = gifData[src] = { callbacks: [] };
305 | srcData.callbacks.push(cb);
306 | } else if (srcData.src) {
307 | cb(srcData);
308 | return;
309 | } else if (srcData.callbacks) {
310 | /* add callback */
311 | srcData.callbacks.push(cb);
312 | return;
313 | }
314 | var tester = new Image();
315 | tester.crossOrigin = 'Anonymous';
316 | tester.addEventListener('load', function (e) {
317 | /* check if it is gif */
318 | _this.__getUnit8Array(src, function (arr) {
319 | if (!arr) {
320 | onError('This is not gif. Please use `shader:flat` instead');
321 | return;
322 | }
323 | /* parse data */
324 | (0, _gifsparser.parseGIF)(arr, function (times, cnt, frames) {
325 | /* store data */
326 | var newData = { status: 'success', src: src, times: times, cnt: cnt, frames: frames, timestamp: Date.now()
327 | /* callbacks */
328 | };if (srcData.callbacks) {
329 | srcData.callbacks.forEach(function (cb) {
330 | return cb(newData);
331 | });
332 | /* overwrite */
333 | gifData[src] = newData;
334 | }
335 | }, function (err) {
336 | return onError(err);
337 | });
338 | });
339 | });
340 | tester.addEventListener('error', function (e) {
341 | return onError('Could be the following issue\n - Not Image\n - Not Found\n - Server Error\n - Cross-Origin Issue');
342 | });
343 | function onError(message) {
344 | /* create error data */
345 | var errData = createError(message, src);
346 | /* callbacks */
347 | if (srcData.callbacks) {
348 | srcData.callbacks.forEach(function (cb) {
349 | return cb(errData);
350 | });
351 | /* overwrite */
352 | gifData[src] = errData;
353 | }
354 | }
355 | tester.src = src;
356 | },
357 |
358 |
359 | /**
360 | *
361 | * get mine type
362 | *
363 | */
364 | __getUnit8Array: function __getUnit8Array(src, cb) {
365 | if (typeof cb !== 'function') {
366 | return;
367 | }
368 |
369 | var xhr = new XMLHttpRequest();
370 | xhr.open('GET', src);
371 | xhr.responseType = 'arraybuffer';
372 | xhr.addEventListener('load', function (e) {
373 | var uint8Array = new Uint8Array(e.target.response);
374 | var arr = uint8Array.subarray(0, 4);
375 | // const header = arr.map(value => value.toString(16)).join('')
376 | var header = '';
377 | for (var i = 0; i < arr.length; i++) {
378 | header += arr[i].toString(16);
379 | }
380 | if (header === '47494638') {
381 | cb(uint8Array);
382 | } else {
383 | cb();
384 | }
385 | });
386 | xhr.addEventListener('error', function (e) {
387 | log(e);
388 | cb();
389 | });
390 | xhr.send();
391 | },
392 |
393 |
394 | /**
395 | * Query and validate a query selector,
396 | *
397 | * @param {string} selector - DOM selector.
398 | * @return {object} Selected DOM element | error message object.
399 | */
400 | __validateAndGetQuerySelector: function __validateAndGetQuerySelector(selector) {
401 | try {
402 | var el = document.querySelector(selector);
403 | if (!el) {
404 | return { error: 'No element was found matching the selector' };
405 | }
406 | return el;
407 | } catch (e) {
408 | // Capture exception if it's not a valid selector.
409 | return { error: 'no valid selector' };
410 | }
411 | },
412 |
413 |
414 | /*================================
415 | = playback =
416 | ================================*/
417 |
418 | /**
419 | * Pause gif
420 | * @public
421 | */
422 | pause: function pause() {
423 | log('pause');
424 | this.__paused = true;
425 | },
426 |
427 |
428 | /**
429 | * Play gif
430 | * @public
431 | */
432 | play: function play() {
433 | log('play');
434 | this.__paused = false;
435 | },
436 |
437 |
438 | /**
439 | * Toggle playback. play if paused and pause if played.
440 | * @public
441 | */
442 |
443 | togglePlayback: function togglePlayback() {
444 |
445 | if (this.paused()) {
446 | this.play();
447 | } else {
448 | this.pause();
449 | }
450 | },
451 |
452 |
453 | /**
454 | * Return if the playback is paused.
455 | * @public
456 | * @return {boolean}
457 | */
458 | paused: function paused() {
459 | return this.__paused;
460 | },
461 |
462 |
463 | /**
464 | * Go to next frame
465 | * @public
466 | */
467 | nextFrame: function nextFrame() {
468 | this.__draw();
469 |
470 | /* update next frame time */
471 | while (Date.now() - this.__startTime >= this.__nextFrameTime) {
472 |
473 | this.__nextFrameTime += this.__delayTimes[this.__frameIdx++];
474 | if ((this.__infinity || this.__loopCnt) && this.__frameCnt <= this.__frameIdx) {
475 | /* go back to the first */
476 | this.__frameIdx = 0;
477 | }
478 | }
479 | },
480 |
481 |
482 | /*==============================
483 | = canvas =
484 | ==============================*/
485 |
486 | /**
487 | * clear canvas
488 | * @private
489 | */
490 | __clearCanvas: function __clearCanvas() {
491 | this.__ctx.clearRect(0, 0, this.__width, this.__height);
492 | this.__texture.needsUpdate = true;
493 | },
494 |
495 |
496 | /**
497 | * draw
498 | * @private
499 | */
500 | __draw: function __draw() {
501 | if (this.__frameIdx != 0) {
502 | var lastFrame = this.__frames[this.__frameIdx - 1];
503 | // Disposal method indicates if you should clear or not the background.
504 | // This flag is represented in binary and is a packed field which can also represent transparency.
505 | // http://matthewflickinger.com/lab/whatsinagif/animation_and_transparency.asp
506 | if (lastFrame.disposalMethod == 8 || lastFrame.disposalMethod == 9) {
507 | this.__clearCanvas();
508 | }
509 | } else {
510 | this.__clearCanvas();
511 | }
512 | var actualFrame = this.__frames[this.__frameIdx];
513 | if (typeof actualFrame !== 'undefined') {
514 | this.__ctx.drawImage(actualFrame, 0, 0, this.__width, this.__height);
515 | this.__texture.needsUpdate = true;
516 | }
517 | },
518 |
519 |
520 | /*============================
521 | = ready =
522 | ============================*/
523 |
524 | /**
525 | * setup gif animation and play if autoplay is true
526 | * @private
527 | * @property {string} src - src url
528 | * @param {array} times - array of time length of each image
529 | * @param {number} cnt - total counts of gif images
530 | * @param {array} frames - array of each image
531 | */
532 | __ready: function __ready(_ref) {
533 | var src = _ref.src,
534 | times = _ref.times,
535 | cnt = _ref.cnt,
536 | frames = _ref.frames;
537 |
538 | log('__ready');
539 | this.__textureSrc = src;
540 | this.__delayTimes = times;
541 | cnt ? this.__loopCnt = cnt : this.__infinity = true;
542 | this.__frames = frames;
543 | this.__frameCnt = times.length;
544 | this.__startTime = Date.now();
545 | this.__width = THREE.Math.floorPowerOfTwo(frames[0].width);
546 | this.__height = THREE.Math.floorPowerOfTwo(frames[0].height);
547 | this.__cnv.width = this.__width;
548 | this.__cnv.height = this.__height;
549 | this.__draw();
550 | if (this.__autoplay) {
551 | this.play();
552 | } else {
553 | this.pause();
554 | }
555 | },
556 |
557 |
558 | /*=============================
559 | = reset =
560 | =============================*/
561 | /**
562 | * @private
563 | */
564 | __reset: function __reset() {
565 | this.pause();
566 | this.__clearCanvas();
567 | this.__startTime = 0;
568 | this.__nextFrameTime = 0;
569 | this.__frameIdx = 0;
570 | this.__frameCnt = 0;
571 | this.__delayTimes = null;
572 | this.__infinity = false;
573 | this.__loopCnt = 0;
574 | this.__frames = null;
575 | this.__textureSrc = null;
576 | }
577 | });
578 |
579 | /***/ }),
580 | /* 1 */
581 | /***/ (function(module, exports) {
582 |
583 | 'use strict';
584 |
585 | /**
586 | *
587 | * Gif parser by @gtk2k
588 | * https://github.com/gtk2k/gtk2k.github.io/tree/master/animation_gif
589 | *
590 | */
591 |
592 | exports.parseGIF = function (gif, successCB, errorCB) {
593 |
594 | var pos = 0;
595 | var delayTimes = [];
596 | var loadCnt = 0;
597 | var graphicControl = null;
598 | var imageData = null;
599 | var frames = [];
600 | var loopCnt = 0;
601 | if (gif[0] === 0x47 && gif[1] === 0x49 && gif[2] === 0x46 && // 'GIF'
602 | gif[3] === 0x38 && (gif[4] === 0x39 || gif[4] === 0x37) && gif[5] === 0x61) {
603 | // '89a'
604 | pos += 13 + +!!(gif[10] & 0x80) * Math.pow(2, (gif[10] & 0x07) + 1) * 3;
605 | var gifHeader = gif.subarray(0, pos);
606 | while (gif[pos] && gif[pos] !== 0x3b) {
607 | var offset = pos,
608 | blockId = gif[pos];
609 | if (blockId === 0x21) {
610 | var label = gif[++pos];
611 | if ([0x01, 0xfe, 0xf9, 0xff].indexOf(label) !== -1) {
612 | label === 0xf9 && delayTimes.push((gif[pos + 3] + (gif[pos + 4] << 8)) * 10);
613 | label === 0xff && (loopCnt = gif[pos + 15] + (gif[pos + 16] << 8));
614 | while (gif[++pos]) {
615 | pos += gif[pos];
616 | }label === 0xf9 && (graphicControl = gif.subarray(offset, pos + 1));
617 | } else {
618 | errorCB && errorCB('parseGIF: unknown label');break;
619 | }
620 | } else if (blockId === 0x2c) {
621 | pos += 9;
622 | pos += 1 + +!!(gif[pos] & 0x80) * (Math.pow(2, (gif[pos] & 0x07) + 1) * 3);
623 | while (gif[++pos]) {
624 | pos += gif[pos];
625 | }var imageData = gif.subarray(offset, pos + 1);
626 | // Each frame should have an image and a flag to indicate how to dispose it.
627 | var frame = {
628 | // http://matthewflickinger.com/lab/whatsinagif/animation_and_transparency.asp
629 | // Disposal method is a flag stored in the 3rd byte of the graphics control
630 | // This byte is packed and stores more information, only 3 bits of it represent the disposal
631 | disposalMethod: graphicControl[3],
632 | blob: URL.createObjectURL(new Blob([gifHeader, graphicControl, imageData]))
633 | };
634 | frames.push(frame);
635 | } else {
636 | errorCB && errorCB('parseGIF: unknown blockId');break;
637 | }
638 | pos++;
639 | }
640 | } else {
641 | errorCB && errorCB('parseGIF: no GIF89a');
642 | }
643 | if (frames.length) {
644 |
645 | var cnv = document.createElement('canvas');
646 | var loadImg = function loadImg() {
647 | for (var i = 0; i < frames.length; i++) {
648 | var img = new Image();
649 | img.onload = function (e, i) {
650 | if (i === 0) {
651 | cnv.width = img.width;
652 | cnv.height = img.height;
653 | }
654 | loadCnt++;
655 | frames[i] = this;
656 | if (loadCnt === frames.length) {
657 | loadCnt = 0;
658 | imageFix(1);
659 | }
660 | }.bind(img, null, i);
661 | // Link html image tag with the extracted GIF Frame
662 | img.src = frames[i].blob;
663 | img.disposalMethod = frames[i].disposalMethod;
664 | }
665 | };
666 | var imageFix = function imageFix(i) {
667 | var img = new Image();
668 | img.onload = function (e, i) {
669 | loadCnt++;
670 | frames[i] = this;
671 | if (loadCnt === frames.length) {
672 | cnv = null;
673 | successCB && successCB(delayTimes, loopCnt, frames);
674 | } else {
675 | imageFix(++i);
676 | }
677 | }.bind(img);
678 | img.src = cnv.toDataURL('image/gif');
679 | };
680 | loadImg();
681 | }
682 | };
683 |
684 | /***/ })
685 | /******/ ]);
--------------------------------------------------------------------------------
/dist/aframe-gif-component.min.js:
--------------------------------------------------------------------------------
1 | !function(t){function e(a){if(r[a])return r[a].exports;var i=r[a]={exports:{},id:a,loaded:!1};return t[a].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){"use strict";function a(t,e){return{status:"error",src:e,message:t,timestamp:Date.now()}}var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},s=r(1);if("undefined"==typeof AFRAME)throw"Component attempted to register before AFRAME was available.";var n=AFRAME.utils.srcLoader.parseUrl,o=AFRAME.utils.debug;o.enable("shader:gif:warn");var _=o("shader:gif:warn"),h=o("shader:gif:debug"),u={};AFRAME.registerShader("gif",{schema:{color:{type:"color"},fog:{default:!0},src:{default:null},autoplay:{default:!0}},init:function(t){return h("init",t),h(this.el.components),this.__cnv=document.createElement("canvas"),this.__cnv.width=2,this.__cnv.height=2,this.__ctx=this.__cnv.getContext("2d"),this.__texture=new THREE.Texture(this.__cnv),t.repeat&&(this.__texture.wrapS=THREE.RepeatWrapping,this.__texture.wrapT=THREE.RepeatWrapping,this.__texture.repeat.set(t.repeat.x,t.repeat.y)),this.__material={},this.__reset(),this.material=new THREE.MeshBasicMaterial({map:this.__texture}),this.el.sceneEl.addBehavior(this),this.material},update:function(t){return h("update",t),this.__updateMaterial(t),this.__updateTexture(t),this.material},tick:function(t){this.__frames&&!this.paused()&&Date.now()-this.__startTime>=this.__nextFrameTime&&this.nextFrame()},__updateMaterial:function(t){var e=this.material,r=this.__getMaterialData(t);Object.keys(r).forEach(function(t){e[t]=r[t]})},__getMaterialData:function(t){return{fog:t.fog,color:new THREE.Color(t.color)}},__setTexure:function(t){h("__setTexure",t),"error"===t.status?(_("Error: "+t.message+"\nsrc: "+t.src),this.__reset()):"success"===t.status&&t.src!==this.__textureSrc&&(this.__reset(),this.__ready(t))},__updateTexture:function(t){var e=t.src,r=t.autoplay;"boolean"==typeof r?this.__autoplay=r:"undefined"==typeof r&&(this.__autoplay=!0),this.__autoplay&&this.__frames&&this.play(),e?this.__validateSrc(e,this.__setTexure.bind(this)):this.__reset()},__validateSrc:function(t,e){var r=n(t);if(r)return void this.__getImageSrc(r,e);var s=void 0,o=this.__validateAndGetQuerySelector(t);if(o&&"object"===("undefined"==typeof o?"undefined":i(o))){if(o.error)s=o.error;else{var _=o.tagName.toLowerCase();if("video"===_)t=o.src,s="For video, please use `aframe-video-shader`";else{if("img"===_)return void this.__getImageSrc(o.src,e);s="For <"+_+"> element, please use `aframe-html-shader`"}}if(s){var h=u[t],c=a(s,t);h&&h.callbacks?h.callbacks.forEach(function(t){return t(c)}):e(c),u[t]=c}}},__getImageSrc:function(t,e){function r(e){var r=a(e,t);n.callbacks&&(n.callbacks.forEach(function(t){return t(r)}),u[t]=r)}var i=this;if(t!==this.__textureSrc){var n=u[t];if(n&&n.callbacks){if(n.src)return void e(n);if(n.callbacks)return void n.callbacks.push(e)}else n=u[t]={callbacks:[]},n.callbacks.push(e);var o=new Image;o.crossOrigin="Anonymous",o.addEventListener("load",function(e){i.__getUnit8Array(t,function(e){return e?void(0,s.parseGIF)(e,function(e,r,a){var i={status:"success",src:t,times:e,cnt:r,frames:a,timestamp:Date.now()};n.callbacks&&(n.callbacks.forEach(function(t){return t(i)}),u[t]=i)},function(t){return r(t)}):void r("This is not gif. Please use `shader:flat` instead")})}),o.addEventListener("error",function(t){return r("Could be the following issue\n - Not Image\n - Not Found\n - Server Error\n - Cross-Origin Issue")}),o.src=t}},__getUnit8Array:function(t,e){if("function"==typeof e){var r=new XMLHttpRequest;r.open("GET",t),r.responseType="arraybuffer",r.addEventListener("load",function(t){for(var r=new Uint8Array(t.target.response),a=r.subarray(0,4),i="",s=0;s=this.__nextFrameTime;)this.__nextFrameTime+=this.__delayTimes[this.__frameIdx++],(this.__infinity||this.__loopCnt)&&this.__frameCnt<=this.__frameIdx&&(this.__frameIdx=0)},__clearCanvas:function(){this.__ctx.clearRect(0,0,this.__width,this.__height),this.__texture.needsUpdate=!0},__draw:function(){if(0!=this.__frameIdx){var t=this.__frames[this.__frameIdx-1];8!=t.disposalMethod&&9!=t.disposalMethod||this.__clearCanvas()}else this.__clearCanvas();var e=this.__frames[this.__frameIdx];"undefined"!=typeof e&&(this.__ctx.drawImage(e,0,0,this.__width,this.__height),this.__texture.needsUpdate=!0)},__ready:function(t){var e=t.src,r=t.times,a=t.cnt,i=t.frames;h("__ready"),this.__textureSrc=e,this.__delayTimes=r,a?this.__loopCnt=a:this.__infinity=!0,this.__frames=i,this.__frameCnt=r.length,this.__startTime=Date.now(),this.__width=THREE.Math.floorPowerOfTwo(i[0].width),this.__height=THREE.Math.floorPowerOfTwo(i[0].height),this.__cnv.width=this.__width,this.__cnv.height=this.__height,this.__draw(),this.__autoplay?this.play():this.pause()},__reset:function(){this.pause(),this.__clearCanvas(),this.__startTime=0,this.__nextFrameTime=0,this.__frameIdx=0,this.__frameCnt=0,this.__delayTimes=null,this.__infinity=!1,this.__loopCnt=0,this.__frames=null,this.__textureSrc=null}})},function(t,e){"use strict";e.parseGIF=function(t,e,r){var a=0,i=[],s=0,n=null,o=null,_=[],h=0;if(71!==t[0]||73!==t[1]||70!==t[2]||56!==t[3]||57!==t[4]&&55!==t[4]||97!==t[5])r&&r("parseGIF: no GIF89a");else{a+=13+ +!!(128&t[10])*Math.pow(2,(7&t[10])+1)*3;for(var u=t.subarray(0,a);t[a]&&59!==t[a];){var c=a,l=t[a];if(33===l){var f=t[++a];if([1,254,249,255].indexOf(f)===-1){r&&r("parseGIF: unknown label");break}for(249===f&&i.push(10*(t[a+3]+(t[a+4]<<8))),255===f&&(h=t[a+15]+(t[a+16]<<8));t[++a];)a+=t[a];249===f&&(n=t.subarray(c,a+1))}else{if(44!==l){r&&r("parseGIF: unknown blockId");break}for(a+=9,a+=1+ +!!(128&t[a])*(3*Math.pow(2,(7&t[a])+1));t[++a];)a+=t[a];var o=t.subarray(c,a+1),d={disposalMethod:n[3],blob:URL.createObjectURL(new Blob([u,n,o]))};_.push(d)}a++}}if(_.length){var p=document.createElement("canvas"),m=function(){for(var t=0;t<_.length;t++){var e=new Image;e.onload=function(t,r){0===r&&(p.width=e.width,p.height=e.height),s++,_[r]=this,s===_.length&&(s=0,v(1))}.bind(e,null,t),e.src=_[t].blob,e.disposalMethod=_[t].disposalMethod}},v=function t(r){var a=new Image;a.onload=function(r,a){s++,_[a]=this,s===_.length?(p=null,e&&e(i,h,_)):t(++a)}.bind(a),a.src=p.toDataURL("image/gif")};m()}}}]);
--------------------------------------------------------------------------------
/dist/aframe-gif-shader.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 |
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 |
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId])
10 | /******/ return installedModules[moduleId].exports;
11 |
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ exports: {},
15 | /******/ id: moduleId,
16 | /******/ loaded: false
17 | /******/ };
18 |
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 |
22 | /******/ // Flag the module as loaded
23 | /******/ module.loaded = true;
24 |
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 |
29 |
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 |
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 |
36 | /******/ // __webpack_public_path__
37 | /******/ __webpack_require__.p = "";
38 |
39 | /******/ // Load entry module and return exports
40 | /******/ return __webpack_require__(0);
41 | /******/ })
42 | /************************************************************************/
43 | /******/ ([
44 | /* 0 */
45 | /***/ function(module, exports, __webpack_require__) {
46 |
47 | 'use strict';
48 |
49 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
50 |
51 | var _gifsparser = __webpack_require__(1);
52 |
53 | if (typeof AFRAME === 'undefined') {
54 | throw 'Component attempted to register before AFRAME was available.';
55 | }
56 |
57 | /* get util from AFRAME */
58 | var parseUrl = AFRAME.utils.srcLoader.parseUrl;
59 | var debug = AFRAME.utils.debug;
60 | // debug.enable('shader:gif:*')
61 |
62 | debug.enable('shader:gif:warn');
63 | var warn = debug('shader:gif:warn');
64 | var log = debug('shader:gif:debug');
65 |
66 | /* store data so that you won't load same data */
67 | var gifData = {};
68 |
69 | /* create error message */
70 | function createError(err, src) {
71 | return { status: 'error', src: src, message: err, timestamp: Date.now() };
72 | }
73 |
74 | AFRAME.registerShader('gif', {
75 |
76 | /**
77 | * For material component:
78 | * @see https://github.com/aframevr/aframe/blob/60d198ef8e2bfbc57a13511ae5fca7b62e01691b/src/components/material.js
79 | * For example of `registerShader`:
80 | * @see https://github.com/aframevr/aframe/blob/41a50cd5ac65e462120ecc2e5091f5daefe3bd1e/src/shaders/flat.js
81 | * For MeshBasicMaterial
82 | * @see http://threejs.org/docs/#Reference/Materials/MeshBasicMaterial
83 | */
84 |
85 | schema: {
86 |
87 | /* For material */
88 | color: { type: 'color' },
89 | fog: { default: true },
90 |
91 | /* For texuture */
92 | src: { default: null },
93 | autoplay: { default: true }
94 |
95 | },
96 |
97 | /**
98 | * Initialize material. Called once.
99 | * @protected
100 | */
101 | init: function init(data) {
102 | log('init', data);
103 | log(this.el.components);
104 | this.__cnv = document.createElement('canvas');
105 | this.__cnv.width = 2;
106 | this.__cnv.height = 2;
107 | this.__ctx = this.__cnv.getContext('2d');
108 | this.__texture = new THREE.Texture(this.__cnv); //renders straight from a canvas
109 | this.__material = {};
110 | this.__reset();
111 | this.material = new THREE.MeshBasicMaterial({ map: this.__texture });
112 | this.el.sceneEl.addBehavior(this);
113 | this.__addPublicFunctions();
114 | return this.material;
115 | },
116 |
117 |
118 | /**
119 | * Update or create material.
120 | * @param {object|null} oldData
121 | */
122 | update: function update(oldData) {
123 | log('update', oldData);
124 | this.__updateMaterial(oldData);
125 | this.__updateTexture(oldData);
126 | return this.material;
127 | },
128 |
129 |
130 | /**
131 | * Called on each scene tick.
132 | * @protected
133 | */
134 | tick: function tick(t) {
135 | if (!this.__frames || this.paused()) return;
136 | if (Date.now() - this.__startTime >= this.__nextFrameTime) {
137 | this.nextFrame();
138 | }
139 | },
140 |
141 |
142 | /*================================
143 | = material =
144 | ================================*/
145 |
146 | /**
147 | * Updating existing material.
148 | * @param {object} data - Material component data.
149 | */
150 | __updateMaterial: function __updateMaterial(data) {
151 | var material = this.material;
152 |
153 | var newData = this.__getMaterialData(data);
154 | Object.keys(newData).forEach(function (key) {
155 | material[key] = newData[key];
156 | });
157 | },
158 |
159 |
160 | /**
161 | * Builds and normalize material data, normalizing stuff along the way.
162 | * @param {Object} data - Material data.
163 | * @return {Object} data - Processed material data.
164 | */
165 | __getMaterialData: function __getMaterialData(data) {
166 | return {
167 | fog: data.fog,
168 | color: new THREE.Color(data.color)
169 | };
170 | },
171 |
172 |
173 | /*==============================
174 | = texure =
175 | ==============================*/
176 |
177 | /**
178 | * set texure
179 | * @private
180 | * @param {Object} data
181 | * @property {string} status - success / error
182 | * @property {string} src - src url
183 | * @property {array} times - array of time length of each image
184 | * @property {number} cnt - total counts of gif images
185 | * @property {array} frames - array of each image
186 | * @property {Date} timestamp - created at the texure
187 | */
188 |
189 | __setTexure: function __setTexure(data) {
190 | log('__setTexure', data);
191 | if (data.status === 'error') {
192 | warn('Error: ' + data.message + '\nsrc: ' + data.src);
193 | this.__reset();
194 | } else if (data.status === 'success' && data.src !== this.__textureSrc) {
195 | this.__reset();
196 | /* Texture added or changed */
197 | this.__ready(data);
198 | }
199 | },
200 |
201 |
202 | /**
203 | * Update or create texure.
204 | * @param {Object} data - Material component data.
205 | */
206 | __updateTexture: function __updateTexture(data) {
207 | var src = data.src;
208 | var autoplay = data.autoplay;
209 |
210 | /* autoplay */
211 |
212 | if (typeof autoplay === 'boolean') {
213 | this.__autoplay = autoplay;
214 | } else if (typeof autoplay === 'undefined') {
215 | this.__autoplay = true;
216 | }
217 | if (this.__autoplay && this.__frames) {
218 | this.play();
219 | }
220 |
221 | /* src */
222 | if (src) {
223 | this.__validateSrc(src, this.__setTexure.bind(this));
224 | } else {
225 | /* Texture removed */
226 | this.__reset();
227 | }
228 | },
229 |
230 |
231 | /*=============================================
232 | = varidation for texure =
233 | =============================================*/
234 |
235 | __validateSrc: function __validateSrc(src, cb) {
236 |
237 | /* check if src is a url */
238 | var url = parseUrl(src);
239 | if (url) {
240 | this.__getImageSrc(url, cb);
241 | return;
242 | }
243 |
244 | var message = void 0;
245 |
246 | /* check if src is a query selector */
247 | var el = this.__validateAndGetQuerySelector(src);
248 | if (!el || (typeof el === 'undefined' ? 'undefined' : _typeof(el)) !== 'object') {
249 | return;
250 | }
251 | if (el.error) {
252 | message = el.error;
253 | } else {
254 | var tagName = el.tagName.toLowerCase();
255 | if (tagName === 'video') {
256 | src = el.src;
257 | message = 'For video, please use `aframe-video-shader`';
258 | } else if (tagName === 'img') {
259 | this.__getImageSrc(el.src, cb);
260 | return;
261 | } else {
262 | message = 'For <' + tagName + '> element, please use `aframe-html-shader`';
263 | }
264 | }
265 |
266 | /* if there is message, create error data */
267 | if (message) {
268 | (function () {
269 | var srcData = gifData[src];
270 | var errData = createError(message, src);
271 | /* callbacks */
272 | if (srcData && srcData.callbacks) {
273 | srcData.callbacks.forEach(function (cb) {
274 | return cb(errData);
275 | });
276 | } else {
277 | cb(errData);
278 | }
279 | /* overwrite */
280 | gifData[src] = errData;
281 | })();
282 | }
283 | },
284 |
285 |
286 | /**
287 | * Validate src is a valid image url
288 | * @param {string} src - url that will be tested
289 | * @param {function} cb - callback with the test result
290 | */
291 | __getImageSrc: function __getImageSrc(src, cb) {
292 | var _this = this;
293 |
294 | /* if src is same as previous, ignore this */
295 | if (src === this.__textureSrc) {
296 | return;
297 | }
298 |
299 | /* check if we already get the srcData */
300 | var srcData = gifData[src];
301 | if (!srcData || !srcData.callbacks) {
302 | /* create callback */
303 | srcData = gifData[src] = { callbacks: [] };
304 | srcData.callbacks.push(cb);
305 | } else if (srcData.src) {
306 | cb(srcData);
307 | return;
308 | } else if (srcData.callbacks) {
309 | /* add callback */
310 | srcData.callbacks.push(cb);
311 | return;
312 | }
313 | var tester = new Image();
314 | tester.crossOrigin = 'Anonymous';
315 | tester.addEventListener('load', function (e) {
316 | /* check if it is gif */
317 | _this.__getUnit8Array(src, function (arr) {
318 | if (!arr) {
319 | onError('This is not gif. Please use `shader:flat` instead');
320 | return;
321 | }
322 | /* parse data */
323 | (0, _gifsparser.parseGIF)(arr, function (times, cnt, frames) {
324 | /* store data */
325 | var newData = { status: 'success', src: src, times: times, cnt: cnt, frames: frames, timestamp: Date.now() };
326 | /* callbacks */
327 | if (srcData.callbacks) {
328 | srcData.callbacks.forEach(function (cb) {
329 | return cb(newData);
330 | });
331 | /* overwrite */
332 | gifData[src] = newData;
333 | }
334 | }, function (err) {
335 | return onError(err);
336 | });
337 | });
338 | });
339 | tester.addEventListener('error', function (e) {
340 | return onError('Could be the following issue\n - Not Image\n - Not Found\n - Server Error\n - Cross-Origin Issue');
341 | });
342 | function onError(message) {
343 | /* create error data */
344 | var errData = createError(message, src);
345 | /* callbacks */
346 | if (srcData.callbacks) {
347 | srcData.callbacks.forEach(function (cb) {
348 | return cb(errData);
349 | });
350 | /* overwrite */
351 | gifData[src] = errData;
352 | }
353 | }
354 | tester.src = src;
355 | },
356 |
357 |
358 | /**
359 | *
360 | * get mine type
361 | *
362 | */
363 | __getUnit8Array: function __getUnit8Array(src, cb) {
364 | if (typeof cb !== 'function') {
365 | return;
366 | }
367 |
368 | var xhr = new XMLHttpRequest();
369 | xhr.open('GET', src);
370 | xhr.responseType = 'arraybuffer';
371 | xhr.addEventListener('load', function (e) {
372 | var uint8Array = new Uint8Array(e.target.response);
373 | var arr = uint8Array.subarray(0, 4);
374 | // const header = arr.map(value => value.toString(16)).join('')
375 | var header = '';
376 | for (var i = 0; i < arr.length; i++) {
377 | header += arr[i].toString(16);
378 | }
379 | if (header === '47494638') {
380 | cb(uint8Array);
381 | } else {
382 | cb();
383 | }
384 | });
385 | xhr.addEventListener('error', function (e) {
386 | log(e);
387 | cb();
388 | });
389 | xhr.send();
390 | },
391 |
392 |
393 | /**
394 | * Query and validate a query selector,
395 | *
396 | * @param {string} selector - DOM selector.
397 | * @return {object} Selected DOM element | error message object.
398 | */
399 | __validateAndGetQuerySelector: function __validateAndGetQuerySelector(selector) {
400 | try {
401 | var el = document.querySelector(selector);
402 | if (!el) {
403 | return { error: 'No element was found matching the selector' };
404 | }
405 | return el;
406 | } catch (e) {
407 | // Capture exception if it's not a valid selector.
408 | return { error: 'no valid selector' };
409 | }
410 | },
411 |
412 |
413 | /*================================
414 | = playback =
415 | ================================*/
416 |
417 | /**
418 | * add public functions
419 | * @private
420 | */
421 | __addPublicFunctions: function __addPublicFunctions() {
422 | this.el.gif = {
423 | play: this.play.bind(this),
424 | pause: this.pause.bind(this),
425 | togglePlayback: this.togglePlayback.bind(this),
426 | paused: this.paused.bind(this),
427 | nextFrame: this.nextFrame.bind(this)
428 | };
429 | },
430 |
431 |
432 | /**
433 | * Pause gif
434 | * @public
435 | */
436 | pause: function pause() {
437 | log('pause');
438 | this.__paused = true;
439 | },
440 |
441 |
442 | /**
443 | * Play gif
444 | * @public
445 | */
446 | play: function play() {
447 | log('play');
448 | this.__paused = false;
449 | },
450 |
451 |
452 | /**
453 | * Toggle playback. play if paused and pause if played.
454 | * @public
455 | */
456 |
457 | togglePlayback: function togglePlayback() {
458 |
459 | if (this.paused()) {
460 | this.play();
461 | } else {
462 | this.pause();
463 | }
464 | },
465 |
466 |
467 | /**
468 | * Return if the playback is paused.
469 | * @public
470 | * @return {boolean}
471 | */
472 | paused: function paused() {
473 | return this.__paused;
474 | },
475 |
476 |
477 | /**
478 | * Go to next frame
479 | * @public
480 | */
481 | nextFrame: function nextFrame() {
482 | this.__draw();
483 |
484 | /* update next frame time */
485 | while (Date.now() - this.__startTime >= this.__nextFrameTime) {
486 |
487 | this.__nextFrameTime += this.__delayTimes[this.__frameIdx++];
488 | if ((this.__infinity || this.__loopCnt) && this.__frameCnt <= this.__frameIdx) {
489 | /* go back to the first */
490 | this.__frameIdx = 0;
491 | }
492 | }
493 | },
494 |
495 |
496 | /*==============================
497 | = canvas =
498 | ==============================*/
499 |
500 | /**
501 | * clear canvas
502 | * @private
503 | */
504 | __clearCanvas: function __clearCanvas() {
505 | this.__ctx.clearRect(0, 0, this.__width, this.__height);
506 | this.__texture.needsUpdate = true;
507 | },
508 |
509 |
510 | /**
511 | * draw
512 | * @private
513 | */
514 | __draw: function __draw() {
515 | this.__ctx.drawImage(this.__frames[this.__frameIdx], 0, 0, this.__width, this.__height);
516 | this.__texture.needsUpdate = true;
517 | },
518 |
519 |
520 | /*============================
521 | = ready =
522 | ============================*/
523 |
524 | /**
525 | * setup gif animation and play if autoplay is true
526 | * @private
527 | * @property {string} src - src url
528 | * @param {array} times - array of time length of each image
529 | * @param {number} cnt - total counts of gif images
530 | * @param {array} frames - array of each image
531 | */
532 | __ready: function __ready(_ref) {
533 | var src = _ref.src;
534 | var times = _ref.times;
535 | var cnt = _ref.cnt;
536 | var frames = _ref.frames;
537 |
538 | log('__ready');
539 | this.__textureSrc = src;
540 | this.__delayTimes = times;
541 | cnt ? this.__loopCnt = cnt : this.__infinity = true;
542 | this.__frames = frames;
543 | this.__frameCnt = times.length;
544 | this.__startTime = Date.now();
545 | this.__width = THREE.Math.floorPowerOfTwo(frames[0].width);
546 | this.__height = THREE.Math.floorPowerOfTwo(frames[0].height);
547 | this.__cnv.width = this.__width;
548 | this.__cnv.height = this.__height;
549 | this.__draw();
550 | if (this.__autoplay) {
551 | this.play();
552 | } else {
553 | this.pause();
554 | }
555 | },
556 |
557 |
558 | /*=============================
559 | = reset =
560 | =============================*/
561 |
562 | /**
563 | * @private
564 | */
565 |
566 | __reset: function __reset() {
567 | this.pause();
568 | this.__clearCanvas();
569 | this.__startTime = 0;
570 | this.__nextFrameTime = 0;
571 | this.__frameIdx = 0;
572 | this.__frameCnt = 0;
573 | this.__delayTimes = null;
574 | this.__infinity = false;
575 | this.__loopCnt = 0;
576 | this.__frames = null;
577 | this.__textureSrc = null;
578 | }
579 | });
580 |
581 | /***/ },
582 | /* 1 */
583 | /***/ function(module, exports) {
584 |
585 | 'use strict';
586 |
587 | /**
588 | *
589 | * Gif parser by @gtk2k
590 | * https://github.com/gtk2k/gtk2k.github.io/tree/master/animation_gif
591 | *
592 | */
593 |
594 | exports.parseGIF = function (gif, successCB, errorCB) {
595 |
596 | var pos = 0;
597 | var delayTimes = [];
598 | var loadCnt = 0;
599 | var graphicControl = null;
600 | var imageData = null;
601 | var frames = [];
602 | var loopCnt = 0;
603 | if (gif[0] === 0x47 && gif[1] === 0x49 && gif[2] === 0x46 && // 'GIF'
604 | gif[3] === 0x38 && gif[4] === 0x39 && gif[5] === 0x61) {
605 | // '89a'
606 | pos += 13 + +!!(gif[10] & 0x80) * Math.pow(2, (gif[10] & 0x07) + 1) * 3;
607 | var gifHeader = gif.subarray(0, pos);
608 | while (gif[pos] && gif[pos] !== 0x3b) {
609 | var offset = pos,
610 | blockId = gif[pos];
611 | if (blockId === 0x21) {
612 | var label = gif[++pos];
613 | if ([0x01, 0xfe, 0xf9, 0xff].indexOf(label) !== -1) {
614 | label === 0xf9 && delayTimes.push((gif[pos + 3] + (gif[pos + 4] << 8)) * 10);
615 | label === 0xff && (loopCnt = gif[pos + 15] + (gif[pos + 16] << 8));
616 | while (gif[++pos]) {
617 | pos += gif[pos];
618 | }label === 0xf9 && (graphicControl = gif.subarray(offset, pos + 1));
619 | } else {
620 | errorCB && errorCB('parseGIF: unknown label');break;
621 | }
622 | } else if (blockId === 0x2c) {
623 | pos += 9;
624 | pos += 1 + +!!(gif[pos] & 0x80) * (Math.pow(2, (gif[pos] & 0x07) + 1) * 3);
625 | while (gif[++pos]) {
626 | pos += gif[pos];
627 | }var imageData = gif.subarray(offset, pos + 1);
628 | frames.push(URL.createObjectURL(new Blob([gifHeader, graphicControl, imageData])));
629 | } else {
630 | errorCB && errorCB('parseGIF: unknown blockId');break;
631 | }
632 | pos++;
633 | }
634 | } else {
635 | errorCB && errorCB('parseGIF: no GIF89a');
636 | }
637 | if (frames.length) {
638 |
639 | var cnv = document.createElement('canvas');
640 | var loadImg = function loadImg() {
641 | frames.forEach(function (src, i) {
642 | var img = new Image();
643 | img.onload = function (e, i) {
644 | if (i === 0) {
645 | cnv.width = img.width;
646 | cnv.height = img.height;
647 | }
648 | loadCnt++;
649 | frames[i] = this;
650 | if (loadCnt === frames.length) {
651 | loadCnt = 0;
652 | imageFix(1);
653 | }
654 | }.bind(img, null, i);
655 | img.src = src;
656 | });
657 | };
658 | var imageFix = function imageFix(i) {
659 | var img = new Image();
660 | img.onload = function (e, i) {
661 | loadCnt++;
662 | frames[i] = this;
663 | if (loadCnt === frames.length) {
664 | cnv = null;
665 | successCB && successCB(delayTimes, loopCnt, frames);
666 | } else {
667 | imageFix(++i);
668 | }
669 | }.bind(img);
670 | img.src = cnv.toDataURL('image/gif');
671 | };
672 | loadImg();
673 | }
674 | };
675 |
676 | /***/ }
677 | /******/ ]);
678 |
--------------------------------------------------------------------------------
/dist/aframe-gif-shader.min.js:
--------------------------------------------------------------------------------
1 | !function(t){function e(r){if(i[r])return i[r].exports;var a=i[r]={exports:{},id:r,loaded:!1};return t[r].call(a.exports,a,a.exports,e),a.loaded=!0,a.exports}var i={};return e.m=t,e.c=i,e.p="",e(0)}([function(t,e,i){"use strict";function r(t,e){return{status:"error",src:e,message:t,timestamp:Date.now()}}var a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t},s=i(1);if("undefined"==typeof AFRAME)throw"Component attempted to register before AFRAME was available.";var n=AFRAME.utils.srcLoader.parseUrl,o=AFRAME.utils.debug;o.enable("shader:gif:warn");var _=o("shader:gif:warn"),h=o("shader:gif:debug"),u={};AFRAME.registerShader("gif",{schema:{color:{type:"color"},fog:{"default":!0},src:{"default":null},autoplay:{"default":!0}},init:function(t){return h("init",t),h(this.el.components),this.__cnv=document.createElement("canvas"),this.__cnv.width=2,this.__cnv.height=2,this.__ctx=this.__cnv.getContext("2d"),this.__texture=new THREE.Texture(this.__cnv),this.__material={},this.__reset(),this.material=new THREE.MeshBasicMaterial({map:this.__texture}),this.el.sceneEl.addBehavior(this),this.__addPublicFunctions(),this.material},update:function(t){return h("update",t),this.__updateMaterial(t),this.__updateTexture(t),this.material},tick:function(t){this.__frames&&!this.paused()&&Date.now()-this.__startTime>=this.__nextFrameTime&&this.nextFrame()},__updateMaterial:function(t){var e=this.material,i=this.__getMaterialData(t);Object.keys(i).forEach(function(t){e[t]=i[t]})},__getMaterialData:function(t){return{fog:t.fog,color:new THREE.Color(t.color)}},__setTexure:function(t){h("__setTexure",t),"error"===t.status?(_("Error: "+t.message+"\nsrc: "+t.src),this.__reset()):"success"===t.status&&t.src!==this.__textureSrc&&(this.__reset(),this.__ready(t))},__updateTexture:function(t){var e=t.src,i=t.autoplay;"boolean"==typeof i?this.__autoplay=i:"undefined"==typeof i&&(this.__autoplay=!0),this.__autoplay&&this.__frames&&this.play(),e?this.__validateSrc(e,this.__setTexure.bind(this)):this.__reset()},__validateSrc:function(t,e){var i=n(t);if(i)return void this.__getImageSrc(i,e);var s=void 0,o=this.__validateAndGetQuerySelector(t);if(o&&"object"===("undefined"==typeof o?"undefined":a(o))){if(o.error)s=o.error;else{var _=o.tagName.toLowerCase();if("video"===_)t=o.src,s="For video, please use `aframe-video-shader`";else{if("img"===_)return void this.__getImageSrc(o.src,e);s="For <"+_+"> element, please use `aframe-html-shader`"}}s&&!function(){var i=u[t],a=r(s,t);i&&i.callbacks?i.callbacks.forEach(function(t){return t(a)}):e(a),u[t]=a}()}},__getImageSrc:function(t,e){function i(e){var i=r(e,t);n.callbacks&&(n.callbacks.forEach(function(t){return t(i)}),u[t]=i)}var a=this;if(t!==this.__textureSrc){var n=u[t];if(n&&n.callbacks){if(n.src)return void e(n);if(n.callbacks)return void n.callbacks.push(e)}else n=u[t]={callbacks:[]},n.callbacks.push(e);var o=new Image;o.crossOrigin="Anonymous",o.addEventListener("load",function(e){a.__getUnit8Array(t,function(e){return e?void(0,s.parseGIF)(e,function(e,i,r){var a={status:"success",src:t,times:e,cnt:i,frames:r,timestamp:Date.now()};n.callbacks&&(n.callbacks.forEach(function(t){return t(a)}),u[t]=a)},function(t){return i(t)}):void i("This is not gif. Please use `shader:flat` instead")})}),o.addEventListener("error",function(t){return i("Could be the following issue\n - Not Image\n - Not Found\n - Server Error\n - Cross-Origin Issue")}),o.src=t}},__getUnit8Array:function(t,e){if("function"==typeof e){var i=new XMLHttpRequest;i.open("GET",t),i.responseType="arraybuffer",i.addEventListener("load",function(t){for(var i=new Uint8Array(t.target.response),r=i.subarray(0,4),a="",s=0;s=this.__nextFrameTime;)this.__nextFrameTime+=this.__delayTimes[this.__frameIdx++],(this.__infinity||this.__loopCnt)&&this.__frameCnt<=this.__frameIdx&&(this.__frameIdx=0)},__clearCanvas:function(){this.__ctx.clearRect(0,0,this.__width,this.__height),this.__texture.needsUpdate=!0},__draw:function(){this.__ctx.drawImage(this.__frames[this.__frameIdx],0,0,this.__width,this.__height),this.__texture.needsUpdate=!0},__ready:function(t){var e=t.src,i=t.times,r=t.cnt,a=t.frames;h("__ready"),this.__textureSrc=e,this.__delayTimes=i,r?this.__loopCnt=r:this.__infinity=!0,this.__frames=a,this.__frameCnt=i.length,this.__startTime=Date.now(),this.__width=THREE.Math.floorPowerOfTwo(a[0].width),this.__height=THREE.Math.floorPowerOfTwo(a[0].height),this.__cnv.width=this.__width,this.__cnv.height=this.__height,this.__draw(),this.__autoplay?this.play():this.pause()},__reset:function(){this.pause(),this.__clearCanvas(),this.__startTime=0,this.__nextFrameTime=0,this.__frameIdx=0,this.__frameCnt=0,this.__delayTimes=null,this.__infinity=!1,this.__loopCnt=0,this.__frames=null,this.__textureSrc=null}})},function(t,e){"use strict";e.parseGIF=function(t,e,i){var r=0,a=[],s=0,n=null,o=null,_=[],h=0;if(71===t[0]&&73===t[1]&&70===t[2]&&56===t[3]&&57===t[4]&&97===t[5]){r+=13+ +!!(128&t[10])*Math.pow(2,(7&t[10])+1)*3;for(var u=t.subarray(0,r);t[r]&&59!==t[r];){var c=r,l=t[r];if(33===l){var f=t[++r];if(-1===[1,254,249,255].indexOf(f)){i&&i("parseGIF: unknown label");break}for(249===f&&a.push(10*(t[r+3]+(t[r+4]<<8))),255===f&&(h=t[r+15]+(t[r+16]<<8));t[++r];)r+=t[r];249===f&&(n=t.subarray(c,r+1))}else{if(44!==l){i&&i("parseGIF: unknown blockId");break}for(r+=9,r+=1+ +!!(128&t[r])*(3*Math.pow(2,(7&t[r])+1));t[++r];)r+=t[r];var o=t.subarray(c,r+1);_.push(URL.createObjectURL(new Blob([u,n,o])))}r++}}else i&&i("parseGIF: no GIF89a");if(_.length){var d=document.createElement("canvas"),m=function(){_.forEach(function(t,e){var i=new Image;i.onload=function(t,e){0===e&&(d.width=i.width,d.height=i.height),s++,_[e]=this,s===_.length&&(s=0,p(1))}.bind(i,null,e),i.src=t})},p=function g(t){var i=new Image;i.onload=function(t,i){s++,_[i]=this,s===_.length?(d=null,e&&e(a,h,_)):g(++i)}.bind(i),i.src=d.toDataURL("image/gif")};m()}}}]);
2 |
--------------------------------------------------------------------------------
/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayognaise/aframe-gif-shader/95f9f7ddd01a247cf07afac583934bb79aafaeff/example.gif
--------------------------------------------------------------------------------
/examples/basic/banana.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayognaise/aframe-gif-shader/95f9f7ddd01a247cf07afac583934bb79aafaeff/examples/basic/banana.gif
--------------------------------------------------------------------------------
/examples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | A-Frame GIF Shader - Basic
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/basic/nyancat.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayognaise/aframe-gif-shader/95f9f7ddd01a247cf07afac583934bb79aafaeff/examples/basic/nyancat.gif
--------------------------------------------------------------------------------
/examples/basic/pusheen.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mayognaise/aframe-gif-shader/95f9f7ddd01a247cf07afac583934bb79aafaeff/examples/basic/pusheen.gif
--------------------------------------------------------------------------------
/examples/common.css:
--------------------------------------------------------------------------------
1 | html {
2 | position: inherit !important;
3 | }
4 | .buttons {
5 | position: absolute;
6 | z-index: 2;
7 | right: 0;
8 | text-align: right;
9 | height: 0;
10 | }
11 | .buttons a {
12 | display: inline-block;
13 | border: none;
14 | padding: 1em;
15 | margin: 1em 1em 0 0;
16 | background: gray;
17 | color: white;
18 | font: 14px monospace;
19 | text-decoration: none;
20 | }
21 | .buttons a:active {
22 | background: #333;
23 | }
24 | .spacer {
25 | position: relative;
26 | pointer-events: none;
27 | height: 100%;
28 | }
29 | .spacer2 {
30 | position: relative;
31 | pointer-events: none;
32 | height: 1px;
33 | }
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | A-Frame GIF Shader
6 |
7 |
28 |
29 |
30 | A-Frame GIF Shader
31 | Basic
32 |
33 |
38 |
39 |
--------------------------------------------------------------------------------
/examples/main.js:
--------------------------------------------------------------------------------
1 | import '../index.js';
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import { parseGIF } from './lib/gifsparser'
2 |
3 | if (typeof AFRAME === 'undefined') {
4 | throw 'Component attempted to register before AFRAME was available.'
5 | }
6 |
7 | /* get util from AFRAME */
8 | const { parseUrl } = AFRAME.utils.srcLoader
9 | const { debug } = AFRAME.utils
10 | // debug.enable('shader:gif:*')
11 | debug.enable('shader:gif:warn')
12 | const warn = debug('shader:gif:warn')
13 | const log = debug('shader:gif:debug')
14 |
15 | /* store data so that you won't load same data */
16 | const gifData = {}
17 |
18 | /* create error message */
19 | function createError (err, src) {
20 | return { status: 'error', src: src, message: err, timestamp: Date.now() }
21 | }
22 |
23 | AFRAME.registerShader('gif', {
24 |
25 | /**
26 | * For material component:
27 | * @see https://github.com/aframevr/aframe/blob/60d198ef8e2bfbc57a13511ae5fca7b62e01691b/src/components/material.js
28 | * For example of `registerShader`:
29 | * @see https://github.com/aframevr/aframe/blob/41a50cd5ac65e462120ecc2e5091f5daefe3bd1e/src/shaders/flat.js
30 | * For MeshBasicMaterial
31 | * @see http://threejs.org/docs/#Reference/Materials/MeshBasicMaterial
32 | */
33 | schema: {
34 |
35 | /* For material */
36 | color: { type: 'color' },
37 | fog: { default: true },
38 |
39 | /* For texuture */
40 | src: { default: null },
41 | autoplay: { default: true },
42 |
43 | },
44 |
45 | /**
46 | * Initialize material. Called once.
47 | * @protected
48 | */
49 | init (data) {
50 | log('init', data)
51 | log(this.el.components)
52 | this.__cnv = document.createElement('canvas')
53 | this.__cnv.width = 2
54 | this.__cnv.height = 2
55 | this.__ctx = this.__cnv.getContext('2d')
56 | this.__texture = new THREE.Texture(this.__cnv) //renders straight from a canvas
57 | if (data.repeat) {
58 | this.__texture.wrapS = THREE.RepeatWrapping;
59 | this.__texture.wrapT = THREE.RepeatWrapping;
60 | this.__texture.repeat.set( data.repeat.x, data.repeat.y );
61 | }
62 | this.__material = {}
63 | this.__reset()
64 | this.material = new THREE.MeshBasicMaterial({ map: this.__texture })
65 | this.el.sceneEl.addBehavior(this)
66 | return this.material
67 | },
68 |
69 | /**
70 | * Update or create material.
71 | * @param {object|null} oldData
72 | */
73 | update (oldData) {
74 | log('update', oldData)
75 | this.__updateMaterial(oldData)
76 | this.__updateTexture(oldData)
77 | return this.material
78 | },
79 |
80 | /**
81 | * Called on each scene tick.
82 | * @protected
83 | */
84 | tick (t) {
85 | if (!this.__frames || this.paused()) return
86 | if (Date.now() - this.__startTime >= this.__nextFrameTime) {
87 | this.nextFrame()
88 | }
89 | },
90 |
91 | /*================================
92 | = material =
93 | ================================*/
94 |
95 | /**
96 | * Updating existing material.
97 | * @param {object} data - Material component data.
98 | */
99 | __updateMaterial (data) {
100 | const { material } = this
101 | const newData = this.__getMaterialData(data)
102 | Object.keys(newData).forEach(key => {
103 | material[key] = newData[key]
104 | })
105 | },
106 |
107 |
108 | /**
109 | * Builds and normalize material data, normalizing stuff along the way.
110 | * @param {Object} data - Material data.
111 | * @return {Object} data - Processed material data.
112 | */
113 | __getMaterialData (data) {
114 | return {
115 | fog: data.fog,
116 | color: new THREE.Color(data.color),
117 | }
118 | },
119 |
120 |
121 | /*==============================
122 | = texure =
123 | ==============================*/
124 |
125 | /**
126 | * set texure
127 | * @private
128 | * @param {Object} data
129 | * @property {string} status - success / error
130 | * @property {string} src - src url
131 | * @property {array} times - array of time length of each image
132 | * @property {number} cnt - total counts of gif images
133 | * @property {array} frames - array of each image
134 | * @property {Date} timestamp - created at the texure
135 | */
136 |
137 | __setTexure (data) {
138 | log('__setTexure', data)
139 | if (data.status === 'error') {
140 | warn(`Error: ${data.message}\nsrc: ${data.src}`)
141 | this.__reset()
142 | }
143 | else if (data.status === 'success' && data.src !== this.__textureSrc) {
144 | this.__reset()
145 | /* Texture added or changed */
146 | this.__ready(data)
147 | }
148 | },
149 |
150 | /**
151 | * Update or create texure.
152 | * @param {Object} data - Material component data.
153 | */
154 | __updateTexture (data) {
155 | const { src, autoplay } = data
156 |
157 | /* autoplay */
158 | if (typeof autoplay === 'boolean') {
159 | this.__autoplay = autoplay
160 | }
161 | else if (typeof autoplay === 'undefined') {
162 | this.__autoplay = true
163 | }
164 | if (this.__autoplay && this.__frames) { this.play() }
165 |
166 | /* src */
167 | if (src) {
168 | this.__validateSrc(src, this.__setTexure.bind(this))
169 | } else {
170 | /* Texture removed */
171 | this.__reset()
172 | }
173 | },
174 |
175 | /*=============================================
176 | = varidation for texure =
177 | =============================================*/
178 |
179 | __validateSrc (src, cb) {
180 |
181 | /* check if src is a url */
182 | const url = parseUrl(src)
183 | if (url) {
184 | this.__getImageSrc(url, cb)
185 | return
186 | }
187 |
188 | let message
189 |
190 | /* check if src is a query selector */
191 | const el = this.__validateAndGetQuerySelector(src)
192 | if (!el || typeof el !== 'object') { return }
193 | if (el.error) {
194 | message = el.error
195 | }
196 | else {
197 | const tagName = el.tagName.toLowerCase()
198 | if (tagName === 'video') {
199 | src = el.src
200 | message = 'For video, please use `aframe-video-shader`'
201 | }
202 | else if (tagName === 'img') {
203 | this.__getImageSrc(el.src, cb)
204 | return
205 | }
206 | else {
207 | message = `For <${tagName}> element, please use \`aframe-html-shader\``
208 | }
209 | }
210 |
211 | /* if there is message, create error data */
212 | if (message) {
213 | const srcData = gifData[src]
214 | const errData = createError(message, src)
215 | /* callbacks */
216 | if (srcData && srcData.callbacks) {
217 | srcData.callbacks.forEach(cb => cb(errData))
218 | }
219 | else {
220 | cb(errData)
221 | }
222 | /* overwrite */
223 | gifData[src] = errData
224 | }
225 |
226 | },
227 |
228 | /**
229 | * Validate src is a valid image url
230 | * @param {string} src - url that will be tested
231 | * @param {function} cb - callback with the test result
232 | */
233 | __getImageSrc (src, cb) {
234 |
235 | /* if src is same as previous, ignore this */
236 | if (src === this.__textureSrc) { return }
237 |
238 | /* check if we already get the srcData */
239 | let srcData = gifData[src]
240 | if (!srcData || !srcData.callbacks) {
241 | /* create callback */
242 | srcData = gifData[src] = { callbacks: [] }
243 | srcData.callbacks.push(cb)
244 | }
245 | else if (srcData.src) {
246 | cb(srcData)
247 | return
248 | }
249 | else if (srcData.callbacks) {
250 | /* add callback */
251 | srcData.callbacks.push(cb)
252 | return
253 | }
254 | const tester = new Image()
255 | tester.crossOrigin = 'Anonymous'
256 | tester.addEventListener('load', e => {
257 | /* check if it is gif */
258 | this.__getUnit8Array(src, arr => {
259 | if (!arr) {
260 | onError('This is not gif. Please use `shader:flat` instead')
261 | return
262 | }
263 | /* parse data */
264 | parseGIF(arr, (times, cnt, frames) => {
265 | /* store data */
266 | const newData = { status: 'success', src: src, times: times, cnt: cnt, frames: frames, timestamp: Date.now() }
267 | /* callbacks */
268 | if (srcData.callbacks) {
269 | srcData.callbacks.forEach(cb => cb(newData))
270 | /* overwrite */
271 | gifData[src] = newData
272 | }
273 | }, (err) => onError(err))
274 | })
275 | })
276 | tester.addEventListener('error', e => onError('Could be the following issue\n - Not Image\n - Not Found\n - Server Error\n - Cross-Origin Issue'))
277 | function onError(message) {
278 | /* create error data */
279 | const errData = createError(message, src)
280 | /* callbacks */
281 | if (srcData.callbacks) {
282 | srcData.callbacks.forEach(cb => cb(errData))
283 | /* overwrite */
284 | gifData[src] = errData
285 | }
286 | }
287 | tester.src = src
288 | },
289 |
290 | /**
291 | *
292 | * get mine type
293 | *
294 | */
295 | __getUnit8Array(src, cb) {
296 | if (typeof cb !== 'function') { return }
297 |
298 | const xhr = new XMLHttpRequest()
299 | xhr.open('GET', src)
300 | xhr.responseType = 'arraybuffer'
301 | xhr.addEventListener('load', e => {
302 | const uint8Array = new Uint8Array(e.target.response)
303 | const arr = (uint8Array).subarray(0, 4)
304 | // const header = arr.map(value => value.toString(16)).join('')
305 | let header = ''
306 | for(let i = 0; i < arr.length; i++) {
307 | header += arr[i].toString(16)
308 | }
309 | if (header === '47494638') { cb(uint8Array) }
310 | else { cb() }
311 | })
312 | xhr.addEventListener('error', e => {
313 | log(e)
314 | cb()
315 | })
316 | xhr.send()
317 | },
318 |
319 |
320 | /**
321 | * Query and validate a query selector,
322 | *
323 | * @param {string} selector - DOM selector.
324 | * @return {object} Selected DOM element | error message object.
325 | */
326 | __validateAndGetQuerySelector (selector) {
327 | try {
328 | var el = document.querySelector(selector)
329 | if (!el) {
330 | return { error: 'No element was found matching the selector' }
331 | }
332 | return el
333 | } catch (e) { // Capture exception if it's not a valid selector.
334 | return { error: 'no valid selector' }
335 | }
336 | },
337 |
338 |
339 | /*================================
340 | = playback =
341 | ================================*/
342 |
343 | /**
344 | * Pause gif
345 | * @public
346 | */
347 | pause () {
348 | log('pause')
349 | this.__paused = true
350 | },
351 |
352 | /**
353 | * Play gif
354 | * @public
355 | */
356 | play () {
357 | log('play')
358 | this.__paused = false
359 | },
360 |
361 | /**
362 | * Toggle playback. play if paused and pause if played.
363 | * @public
364 | */
365 |
366 | togglePlayback () {
367 |
368 | if (this.paused()) { this.play() }
369 | else { this.pause() }
370 |
371 | },
372 |
373 | /**
374 | * Return if the playback is paused.
375 | * @public
376 | * @return {boolean}
377 | */
378 | paused () {
379 | return this.__paused
380 | },
381 |
382 |
383 | /**
384 | * Go to next frame
385 | * @public
386 | */
387 | nextFrame () {
388 | this.__draw()
389 |
390 | /* update next frame time */
391 | while ((Date.now() - this.__startTime) >= this.__nextFrameTime) {
392 |
393 | this.__nextFrameTime += this.__delayTimes[this.__frameIdx++]
394 | if ((this.__infinity || this.__loopCnt) && this.__frameCnt <= this.__frameIdx) {
395 | /* go back to the first */
396 | this.__frameIdx = 0
397 | }
398 | }
399 |
400 | },
401 |
402 | /*==============================
403 | = canvas =
404 | ==============================*/
405 |
406 | /**
407 | * clear canvas
408 | * @private
409 | */
410 | __clearCanvas () {
411 | this.__ctx.clearRect(0, 0, this.__width, this.__height)
412 | this.__texture.needsUpdate = true
413 | },
414 |
415 | /**
416 | * draw
417 | * @private
418 | */
419 | __draw () {
420 | if(this.__frameIdx != 0){
421 | const lastFrame = this.__frames[this.__frameIdx -1 ]
422 | // Disposal method indicates if you should clear or not the background.
423 | // This flag is represented in binary and is a packed field which can also represent transparency.
424 | // http://matthewflickinger.com/lab/whatsinagif/animation_and_transparency.asp
425 | if(lastFrame.disposalMethod == 8 || lastFrame.disposalMethod == 9){
426 | this.__clearCanvas();
427 | }
428 | } else {
429 | this.__clearCanvas();
430 | }
431 | const actualFrame = this.__frames[this.__frameIdx]
432 | if(typeof actualFrame !== 'undefined') {
433 | this.__ctx.drawImage(actualFrame, 0, 0, this.__width, this.__height)
434 | this.__texture.needsUpdate = true
435 | }
436 | },
437 |
438 | /*============================
439 | = ready =
440 | ============================*/
441 |
442 | /**
443 | * setup gif animation and play if autoplay is true
444 | * @private
445 | * @property {string} src - src url
446 | * @param {array} times - array of time length of each image
447 | * @param {number} cnt - total counts of gif images
448 | * @param {array} frames - array of each image
449 | */
450 | __ready ({ src, times, cnt, frames }) {
451 | log('__ready')
452 | this.__textureSrc = src
453 | this.__delayTimes = times
454 | cnt ? this.__loopCnt = cnt : this.__infinity = true
455 | this.__frames = frames
456 | this.__frameCnt = times.length
457 | this.__startTime = Date.now()
458 | this.__width = THREE.Math.floorPowerOfTwo(frames[0].width)
459 | this.__height = THREE.Math.floorPowerOfTwo(frames[0].height)
460 | this.__cnv.width = this.__width
461 | this.__cnv.height = this.__height
462 | this.__draw()
463 | if (this.__autoplay) {
464 | this.play()
465 | }
466 | else {
467 | this.pause()
468 | }
469 | },
470 |
471 | /*=============================
472 | = reset =
473 | =============================*/
474 | /**
475 | * @private
476 | */
477 | __reset () {
478 | this.pause()
479 | this.__clearCanvas()
480 | this.__startTime = 0
481 | this.__nextFrameTime = 0
482 | this.__frameIdx = 0
483 | this.__frameCnt = 0
484 | this.__delayTimes = null
485 | this.__infinity = false
486 | this.__loopCnt = 0
487 | this.__frames = null
488 | this.__textureSrc = null
489 | },
490 | })
491 |
--------------------------------------------------------------------------------
/lib/gifsparser.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Gif parser by @gtk2k
4 | * https://github.com/gtk2k/gtk2k.github.io/tree/master/animation_gif
5 | *
6 | */
7 |
8 | exports.parseGIF = function (gif, successCB, errorCB) {
9 |
10 | var pos = 0;
11 | var delayTimes = [];
12 | var loadCnt = 0;
13 | var graphicControl = null;
14 | var imageData = null;
15 | var frames = [];
16 | var loopCnt = 0;
17 | if (gif[0] === 0x47 && gif[1] === 0x49 && gif[2] === 0x46 && // 'GIF'
18 | gif[3] === 0x38 && (gif[4] === 0x39 || gif[4] === 0x37) && gif[5] === 0x61) { // '89a'
19 | pos += 13 + (+!!(gif[10] & 0x80) * Math.pow(2, (gif[10] & 0x07) + 1) * 3);
20 | var gifHeader = gif.subarray(0, pos);
21 | while (gif[pos] && gif[pos] !== 0x3b) {
22 | var offset = pos, blockId = gif[pos];
23 | if (blockId === 0x21) {
24 | var label = gif[++pos];
25 | if ([0x01, 0xfe, 0xf9, 0xff].indexOf(label) !== -1) {
26 | label === 0xf9 && (delayTimes.push((gif[pos + 3] + (gif[pos + 4] << 8)) * 10));
27 | label === 0xff && (loopCnt = gif[pos + 15] + (gif[pos + 16] << 8));
28 | while (gif[++pos]) pos += gif[pos];
29 | label === 0xf9 && (graphicControl = gif.subarray(offset, pos + 1));
30 | } else { errorCB && errorCB('parseGIF: unknown label'); break; }
31 | } else if (blockId === 0x2c) {
32 | pos += 9;
33 | pos += 1 + (+!!(gif[pos] & 0x80) * (Math.pow(2, (gif[pos] & 0x07) + 1) * 3));
34 | while (gif[++pos]) pos += gif[pos];
35 | var imageData = gif.subarray(offset, pos + 1);
36 | // Each frame should have an image and a flag to indicate how to dispose it.
37 | var frame = {
38 | // http://matthewflickinger.com/lab/whatsinagif/animation_and_transparency.asp
39 | // Disposal method is a flag stored in the 3rd byte of the graphics control
40 | // This byte is packed and stores more information, only 3 bits of it represent the disposal
41 | disposalMethod: graphicControl[3],
42 | blob:URL.createObjectURL(new Blob([gifHeader, graphicControl, imageData]))
43 | }
44 | frames.push(frame);
45 | } else { errorCB && errorCB('parseGIF: unknown blockId'); break; }
46 | pos++;
47 | }
48 | } else { errorCB && errorCB('parseGIF: no GIF89a'); }
49 | if (frames.length) {
50 |
51 | var cnv = document.createElement('canvas');
52 | var loadImg = function () {
53 | for(var i = 0; i < frames.length; i++){
54 | var img = new Image();
55 | img.onload = function (e, i) {
56 | if (i === 0) {
57 | cnv.width = img.width
58 | cnv.height = img.height
59 | }
60 | loadCnt++;
61 | frames[i] = this;
62 | if (loadCnt === frames.length) {
63 | loadCnt = 0;
64 | imageFix(1);
65 | }
66 | }.bind(img, null, i);
67 | // Link html image tag with the extracted GIF Frame
68 | img.src = frames[i].blob;
69 | img.disposalMethod = frames[i].disposalMethod;
70 | }
71 | }
72 | var imageFix = function (i) {
73 | var img = new Image();
74 | img.onload = function (e, i) {
75 | loadCnt++;
76 | frames[i] = this;
77 | if (loadCnt === frames.length) {
78 | cnv = null;
79 | successCB && successCB(delayTimes, loopCnt, frames);
80 | } else {
81 | imageFix(++i);
82 | }
83 | }.bind(img);
84 | img.src = cnv.toDataURL('image/gif');
85 | }
86 | loadImg();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aframe-gif-shader",
3 | "version": "0.9.1",
4 | "description": "A shader to display GIF for A-Frame VR.",
5 | "main": "dist/aframe-gif-shader.js",
6 | "scripts": {
7 | "build": "webpack -p examples/main.js examples/build.js",
8 | "dev": "budo examples/main.js:build.js --dir examples --port 8000 --live -- -t babelify",
9 | "dist": "webpack index.js dist/aframe-gif-component.js && webpack -p index.js dist/aframe-gif-component.min.js",
10 | "postpublish": "npm run dist",
11 | "preghpages": "npm run build && rm -rf gh-pages && cp -r examples gh-pages",
12 | "ghpages": "npm run preghpages && ghpages -p gh-pages"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/mayognaise/aframe-gif-shader.git"
17 | },
18 | "keywords": [
19 | "aframe",
20 | "aframe-shader",
21 | "aframe-vr",
22 | "vr",
23 | "aframe-layout",
24 | "mozvr",
25 | "webvr",
26 | "gif",
27 | "shader",
28 | "material"
29 | ],
30 | "author": "Mayo Tobita ",
31 | "license": "MIT",
32 | "bugs": {
33 | "url": "https://github.com/mayognaise/aframe-gif-shader/issues"
34 | },
35 | "homepage": "https://github.com/mayognaise/aframe-gif-shader#readme",
36 | "devDependencies": {
37 | "babel-core": "^6.26.3",
38 | "babel-loader": "^6.4.1",
39 | "babel-preset-es2015": "^6.24.1",
40 | "babelify": "^7.3.0",
41 | "browserify": "^13.3.0",
42 | "budo": "^8.4.0",
43 | "ghpages": "0.0.3",
44 | "webpack": "^1.15.0"
45 | },
46 | "peerDependencies": {
47 | "aframe": "^0.9.0"
48 | },
49 | "contributors": [
50 | "UXVirtual (http://www.uxvirtual.com)",
51 | "Uri Shaked (http://www.urish.org)",
52 | "Pablo Diego Silva da Silva (https://github.com/pablodiegoss)",
53 | "margauxdivernois (https://github.com/margauxdivernois)",
54 | "Danpollak (https://github.com/Danpollak)"
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | module: {
3 | loaders: [
4 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
5 | ]
6 | }
7 | }
--------------------------------------------------------------------------------