├── .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 | ![example](example.gif) 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 |
34 |
35 | Fork me on GitHub 36 |
37 |
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 | } --------------------------------------------------------------------------------