├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── aframe_spritesheet.gif ├── dist ├── aframe-spritesheet-component.js └── aframe-spritesheet-component.min.js ├── examples ├── build.js ├── json │ ├── index.html │ ├── toaster.json │ └── toaster.png ├── main.js └── rowscols │ ├── index.html │ └── npc_piggy_explorer__x1_walk_png_1354837301.png ├── index.js ├── package.json ├── webpack.config.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "google", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "installedESLint": true, 9 | "parserOptions": { 10 | "ecmaVersion": 6 11 | }, 12 | "rules": { 13 | "arrow-parens": "off", 14 | "brace-style": ["error", "1tbs", { "allowSingleLine": true }], 15 | "comma-dangle": "off", 16 | "quotes": ["error", "single"], 17 | "guard-for-in": "off", 18 | "indent": ["error", 4], 19 | "linebreak-style": ["error", "unix"], 20 | "max-len": "off", 21 | "max-statements-per-line": ["error", { "max": 2 }], 22 | "no-alert": "off", 23 | "no-console": "off", 24 | "no-warning-comments": "off", 25 | "object-curly-spacing": ["error", "always"], 26 | "padded-blocks": "off", 27 | "require-jsdoc": "off" 28 | } 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright 2016 Eko 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of 7 | the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations under 15 | the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repo is no longer actively maintained # 2 | 3 | ## A-Frame Spritesheet Component 4 | 5 | 6 | 7 | Animated spritesheet support for [A-Frame](https://aframe.io). 8 | 9 | ### Demo / Using spritesheets 10 | 11 | Spritesheets are a common way to play pre-rendered animation. This component allows you to load up a spritesheet image to an `a-image` element and easily control its animation. It allows usage of two types of spritesheet formats: 12 | 13 | **[Rows and Cols](https://ekolabs.github.io/aframe-spritesheet-component/examples/rowscols/)** 14 | 15 | A grid representing all frames of the animation. All of the frames must be of the same dimensions, and the animation index is assumed to be scanned left to right, top to bottom. If your last frame is not the one on the bottom right, you'll have to specify the index of the last frame using the `lastFrame` property. 16 | 17 | **[JSON data format](https://ekolabs.github.io/aframe-spritesheet-component/examples/json/)** 18 | 19 | The spritesheet image file can be made more compact by using a dictionary automatically generated with [TexturePacker](https://www.codeandweb.com/texturepacker). This will help reduce file size. 20 | 21 | #### Browser 22 | 23 | Install and use by directly including the [browser files](dist): 24 | 25 | ```html 26 | 27 | My A-Frame Scene 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ``` 41 | 42 | 43 | 52 | 53 | #### npm 54 | 55 | Install via npm: 56 | 57 | ```bash 58 | npm install @ekolabs/aframe-spritesheet-component 59 | ``` 60 | 61 | Then require and use. 62 | 63 | ```js 64 | require('aframe'); 65 | require('aframe-spritesheet-component'); 66 | ``` 67 | 68 | ### API 69 | 70 | | Property | Description | Default Value | 71 | | -------- | ----------- | ------------- | 72 | | progress | A value between 0 and 1 that represents animation progression. the index of the animation frame is calculated from this attribute. Used if no frameIndex or frameName specified | 0 | 73 | | frameIndex | Explicit index of the animation frame to use. Used if no frameName specified | null | 74 | | frameName | Explicit name of the animation frame to use, if using dataUrl | null | 75 | | cols | number of cols in the spritesheet image (not needed if using dataUrl)| 1 | 76 | | rows | number of rows spritesheet image (not needed if using dataUrl) | 1 | 77 | | firstFrame| index of the first frame of the animation, ordered left to right starting at the first row | 0 | 78 | | lastFrame| index of the last frame of the animation, ordered left to right starting at the first row . If not specified and not using the JSON format, value is `rows * cols - 1`| null | 79 | | cloneTexture | if using separate instances of the same image, set this to true | false | 80 | | dataUrl | If using a JSON format, url pointing to the json file| null | 81 | 82 | 83 | ### Acknowledgment 84 | Walking pig sprite taken from glitchthegame.com, under a Public Domain Dedication license. 85 | 86 | Interesting bit of Trivia: [Tiny Speck](https://tinyspeck.com/), the company behind the now-defunct Glitch game is now actually [Slack](https://slack.com)! 87 | 88 | ### Author 89 | Developed by [Opher Vishnia](http://twitter.com/opherv) of [Eko](http://www.helloeko.com) 90 | 91 | ### License 92 | Apache 2 93 | -------------------------------------------------------------------------------- /aframe_spritesheet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/aframe-spritesheet-component/e37234442733ac59f46c8cf73b05f794c801de17/aframe_spritesheet.gif -------------------------------------------------------------------------------- /dist/aframe-spritesheet-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 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 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.l = 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 | /******/ // identity function for calling harmony imports with the correct context 37 | /******/ __webpack_require__.i = function(value) { return value; }; 38 | 39 | /******/ // define getter function for harmony exports 40 | /******/ __webpack_require__.d = function(exports, name, getter) { 41 | /******/ if(!__webpack_require__.o(exports, name)) { 42 | /******/ Object.defineProperty(exports, name, { 43 | /******/ configurable: false, 44 | /******/ enumerable: true, 45 | /******/ get: getter 46 | /******/ }); 47 | /******/ } 48 | /******/ }; 49 | 50 | /******/ // getDefaultExport function for compatibility with non-harmony modules 51 | /******/ __webpack_require__.n = function(module) { 52 | /******/ var getter = module && module.__esModule ? 53 | /******/ function getDefault() { return module['default']; } : 54 | /******/ function getModuleExports() { return module; }; 55 | /******/ __webpack_require__.d(getter, 'a', getter); 56 | /******/ return getter; 57 | /******/ }; 58 | 59 | /******/ // Object.prototype.hasOwnProperty.call 60 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 61 | 62 | /******/ // __webpack_public_path__ 63 | /******/ __webpack_require__.p = ""; 64 | 65 | /******/ // Load entry module and return exports 66 | /******/ return __webpack_require__(__webpack_require__.s = 0); 67 | /******/ }) 68 | /************************************************************************/ 69 | /******/ ([ 70 | /* 0 */ 71 | /***/ (function(module, exports, __webpack_require__) { 72 | 73 | "use strict"; 74 | 75 | 76 | /** 77 | * A-Frame Spritesheet Component for A-Frame. 78 | * Enables dynamic control of animation spritesheets 79 | */ 80 | var SpriteSheet = AFRAME.registerComponent('sprite-sheet', { 81 | schema: { 82 | progress: { type: 'number', default: 0 }, 83 | cols: { type: 'number', default: 1 }, 84 | rows: { type: 'number', default: 1 }, 85 | firstFrame: { type: 'number', default: 0 }, 86 | lastFrame: { type: 'number', default: null }, 87 | cloneTexture: { default: false }, 88 | dataUrl: { type: 'string', default: null } 89 | }, 90 | 91 | /** 92 | * Called once when component is attached. 93 | */ 94 | init: function init() { 95 | var _this = this; 96 | 97 | console.log('init'); 98 | 99 | // if specified load spritesheet json data 100 | if (this.data.dataUrl) { 101 | this.mapCanvas = document.createElement('canvas'); 102 | this.textureCtx = this.mapCanvas.getContext('2d'); 103 | this.texture = new THREE.Texture(this.mapCanvas); 104 | this.getSpriteSheetData(this.data.dataUrl); 105 | 106 | // callback for when the image has loaded 107 | this.el.addEventListener('materialtextureloaded', function () { 108 | _this.imageLoaded = true; 109 | 110 | // save reference to the original image 111 | _this.spriteSheetImage = _this.el.object3D.children[0].material.map.image; 112 | _this.texture = new THREE.Texture(_this.mapCanvas); 113 | _this.el.object3D.children[0].material.map = _this.texture; 114 | }); 115 | } else { 116 | // use rows and cols 117 | this.numFrames = this.data.rows * this.data.cols; 118 | 119 | // callback for when the image has loaded 120 | this.el.addEventListener('materialtextureloaded', function () { 121 | _this.imageLoaded = true; 122 | // useful if animating multiple sprites 123 | if (_this.data.cloneTexture) { 124 | _this.el.object3D.children[0].material.map = _this.el.object3D.children[0].material.map.clone(); 125 | _this.el.object3D.children[0].material.map.needsUpdate = true; 126 | } 127 | 128 | _this.texture = _this.el.object3D.children[0].material.map; 129 | _this.texture.wrapS = _this.texture.wrapT = THREE.RepeatWrapping; 130 | 131 | // set size of one sprite 132 | _this.texture.repeat.set(_this.texture.image.width / _this.data.cols / _this.texture.image.width, _this.texture.image.height / _this.data.rows / _this.texture.image.height); 133 | _this.update(); 134 | }); 135 | } 136 | 137 | this.currentFrame = 0; 138 | }, 139 | 140 | /** 141 | * Called when component is attached and when component data changes. 142 | */ 143 | update: function update() { 144 | // no actual animation 145 | if (!this.framesData && this.data.firstFrame == this.data.lastFrame) return; 146 | 147 | // if no last frame is specified use the number of available frames 148 | var lastFrame = this.data.lastFrame ? this.data.lastFrame : this.numFrames - 1; 149 | 150 | this.currentFrame = Math.round(this.data.progress * (lastFrame - this.data.firstFrame)) + this.data.firstFrame; 151 | this.adjustTexture(this.currentFrame); 152 | }, 153 | 154 | /** 155 | * Called when a component is removed (e.g., via removeAttribute). 156 | */ 157 | remove: function remove() { 158 | // Cleanup 159 | this.mapCanvas = null; 160 | this.textureCtx = null; 161 | this.texture = null; 162 | this.spriteSheetImage = null; 163 | this.spriteSheetData = null; 164 | this.framesData = null; 165 | }, 166 | 167 | /** 168 | * Load a TexturePacker JSON based spritesheet 169 | * Requires the A-Scene to have an 'a-assets' element present 170 | * @param {string} url 171 | */ 172 | getSpriteSheetData: function getSpriteSheetData(url) { 173 | var _this2 = this; 174 | 175 | var assetsEl = document.querySelector('a-assets'); 176 | if (assetsEl) { 177 | assetsEl.fileLoader.load(url, function (data) { 178 | _this2.spriteSheetData = JSON.parse(data); 179 | _this2.framesData = Object.keys(_this2.spriteSheetData.frames).map(function (key) { 180 | return _this2.spriteSheetData.frames[key]; 181 | }); 182 | _this2.numFrames = _this2.framesData.length; 183 | 184 | _this2.frameWidth = _this2.framesData[0].sourceSize.w; 185 | _this2.frameHeight = _this2.framesData[0].sourceSize.h; 186 | 187 | _this2.mapCanvas.width = pow2ceil(_this2.frameWidth); 188 | _this2.mapCanvas.height = pow2ceil(_this2.frameHeight); 189 | 190 | _this2.texture.repeat.set(_this2.frameWidth / _this2.mapCanvas.width, _this2.frameHeight / _this2.mapCanvas.height); 191 | 192 | _this2.texture.offset.x = 0; 193 | _this2.texture.offset.y = 1 - _this2.frameHeight / _this2.mapCanvas.height; 194 | }); 195 | } else { 196 | console.warn('Can\'t load spritesheet URL. No a-assets element present on the A-Scene!'); 197 | } 198 | }, 199 | 200 | /** 201 | * Adjust the texture to a specific frame index 202 | * @param {number} frameNum 203 | */ 204 | adjustTexture: function adjustTexture(frameNum) { 205 | console.log(frameNum); 206 | // image hasn't loaded, can't draw anything 207 | if (!this.imageLoaded) return; 208 | 209 | // no need to draw the same frame twice 210 | if (this.lastDrawnFrame == frameNum) return; 211 | 212 | // if using spritesheet json 213 | if (this.framesData) { 214 | this.adjustFrameBySpriteSheet(frameNum); 215 | } else { 216 | this.adjustFrameByRowsCols(frameNum); 217 | } 218 | 219 | this.lastDrawnFrame = frameNum; 220 | }, 221 | 222 | /** 223 | * Adjust the spritesheet texture to a certain frame on a row/col grid image 224 | * @param {number} frameNum 225 | */ 226 | adjustFrameByRowsCols: function adjustFrameByRowsCols(frameNum) { 227 | this.texture.offset.x = frameNum % this.data.cols / this.data.cols; 228 | this.texture.offset.y = -1 / this.data.rows - Math.floor(frameNum / this.data.cols) / this.data.rows; 229 | }, 230 | 231 | /** 232 | * Adjust the spritesheet texture to a certain frame on a TexturePacker JSON spritesheet 233 | * @param {number} frameNum 234 | */ 235 | adjustFrameBySpriteSheet: function adjustFrameBySpriteSheet(frameNum) { 236 | var frameData = this.framesData[frameNum]; 237 | 238 | this.textureCtx.clearRect(0, 0, this.mapCanvas.width, this.mapCanvas.height); 239 | this.textureCtx.save(); 240 | if (frameData.rotated) { 241 | 242 | // drawing is rotated, so axes are rotated 243 | var drawData = { 244 | dX: this.frameHeight - (frameData.frame.h + frameData.spriteSourceSize.y), 245 | dY: frameData.spriteSourceSize.x, 246 | width: frameData.frame.h, 247 | height: frameData.frame.w 248 | }; 249 | 250 | this.textureCtx.rotate(-90 * Math.PI / 180); 251 | this.textureCtx.translate(-this.frameHeight, 0); 252 | 253 | this.textureCtx.drawImage(this.spriteSheetImage, frameData.frame.x, frameData.frame.y, frameData.frame.h, frameData.frame.w, drawData.dX, drawData.dY, drawData.width, drawData.height); 254 | } else { 255 | var _drawData = { 256 | dX: this.frameWidth / 2 + (frameData.spriteSourceSize.x - frameData.sourceSize.w / 2), 257 | dY: this.frameHeight / 2 + (frameData.spriteSourceSize.y - frameData.sourceSize.h / 2), 258 | width: frameData.frame.w, 259 | height: frameData.frame.h 260 | }; 261 | 262 | this.textureCtx.drawImage(this.spriteSheetImage, frameData.frame.x, frameData.frame.y, frameData.frame.w, frameData.frame.h, _drawData.dX, _drawData.dY, _drawData.width, _drawData.height); 263 | } 264 | 265 | this.textureCtx.restore(); 266 | this.texture.needsUpdate = true; 267 | } 268 | }); 269 | 270 | /** 271 | * Returns the next highest number which is a power of 2 272 | * @param {number} number 273 | * @return {number} 274 | */ 275 | function pow2ceil(number) { 276 | number--; 277 | var p = 2; 278 | while (number >>= 1) { 279 | p <<= 1; 280 | } 281 | return p; 282 | } 283 | 284 | module.exports = SpriteSheet; 285 | 286 | /***/ }) 287 | /******/ ]); -------------------------------------------------------------------------------- /dist/aframe-spritesheet-component.min.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(r){if(a[r])return a[r].exports;var i=a[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var a={};return t.m=e,t.c=a,t.i=function(e){return e},t.d=function(e,a,r){t.o(e,a)||Object.defineProperty(e,a,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var a=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(a,"a",a),a},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,a){"use strict";function r(e){e--;for(var t=2;e>>=1;)t<<=1;return t}var i=AFRAME.registerComponent("sprite-sheet",{schema:{progress:{type:"number",default:0},cols:{type:"number",default:1},rows:{type:"number",default:1},firstFrame:{type:"number",default:0},lastFrame:{type:"number",default:null},cloneTexture:{default:!1},dataUrl:{type:"string",default:null}},init:function(){var e=this;console.log("init"),this.data.dataUrl?(this.mapCanvas=document.createElement("canvas"),this.textureCtx=this.mapCanvas.getContext("2d"),this.texture=new THREE.Texture(this.mapCanvas),this.getSpriteSheetData(this.data.dataUrl),this.el.addEventListener("materialtextureloaded",function(){e.imageLoaded=!0,e.spriteSheetImage=e.el.object3D.children[0].material.map.image,e.texture=new THREE.Texture(e.mapCanvas),e.el.object3D.children[0].material.map=e.texture})):(this.numFrames=this.data.rows*this.data.cols,this.el.addEventListener("materialtextureloaded",function(){e.imageLoaded=!0,e.data.cloneTexture&&(e.el.object3D.children[0].material.map=e.el.object3D.children[0].material.map.clone(),e.el.object3D.children[0].material.map.needsUpdate=!0),e.texture=e.el.object3D.children[0].material.map,e.texture.wrapS=e.texture.wrapT=THREE.RepeatWrapping,e.texture.repeat.set(e.texture.image.width/e.data.cols/e.texture.image.width,e.texture.image.height/e.data.rows/e.texture.image.height),e.update()})),this.currentFrame=0},update:function(){if(this.framesData||this.data.firstFrame!=this.data.lastFrame){var e=this.data.lastFrame?this.data.lastFrame:this.numFrames-1;this.currentFrame=Math.round(this.data.progress*(e-this.data.firstFrame))+this.data.firstFrame,this.adjustTexture(this.currentFrame)}},remove:function(){this.mapCanvas=null,this.textureCtx=null,this.texture=null,this.spriteSheetImage=null,this.spriteSheetData=null,this.framesData=null},getSpriteSheetData:function(e){var t=this,a=document.querySelector("a-assets");a?a.fileLoader.load(e,function(e){t.spriteSheetData=JSON.parse(e),t.framesData=Object.keys(t.spriteSheetData.frames).map(function(e){return t.spriteSheetData.frames[e]}),t.numFrames=t.framesData.length,t.frameWidth=t.framesData[0].sourceSize.w,t.frameHeight=t.framesData[0].sourceSize.h,t.mapCanvas.width=r(t.frameWidth),t.mapCanvas.height=r(t.frameHeight),t.texture.repeat.set(t.frameWidth/t.mapCanvas.width,t.frameHeight/t.mapCanvas.height),t.texture.offset.x=0,t.texture.offset.y=1-t.frameHeight/t.mapCanvas.height}):console.warn("Can't load spritesheet URL. No a-assets element present on the A-Scene!")},adjustTexture:function(e){console.log(e),this.imageLoaded&&this.lastDrawnFrame!=e&&(this.framesData?this.adjustFrameBySpriteSheet(e):this.adjustFrameByRowsCols(e),this.lastDrawnFrame=e)},adjustFrameByRowsCols:function(e){this.texture.offset.x=e%this.data.cols/this.data.cols,this.texture.offset.y=-1/this.data.rows-Math.floor(e/this.data.cols)/this.data.rows},adjustFrameBySpriteSheet:function(e){var t=this.framesData[e];if(this.textureCtx.clearRect(0,0,this.mapCanvas.width,this.mapCanvas.height),this.textureCtx.save(),t.rotated){var a={dX:this.frameHeight-(t.frame.h+t.spriteSourceSize.y),dY:t.spriteSourceSize.x,width:t.frame.h,height:t.frame.w};this.textureCtx.rotate(-90*Math.PI/180),this.textureCtx.translate(-this.frameHeight,0),this.textureCtx.drawImage(this.spriteSheetImage,t.frame.x,t.frame.y,t.frame.h,t.frame.w,a.dX,a.dY,a.width,a.height)}else{var r={dX:this.frameWidth/2+(t.spriteSourceSize.x-t.sourceSize.w/2),dY:this.frameHeight/2+(t.spriteSourceSize.y-t.sourceSize.h/2),width:t.frame.w,height:t.frame.h};this.textureCtx.drawImage(this.spriteSheetImage,t.frame.x,t.frame.y,t.frame.w,t.frame.h,r.dX,r.dY,r.width,r.height)}this.textureCtx.restore(),this.texture.needsUpdate=!0}});e.exports=i}]); -------------------------------------------------------------------------------- /examples/json/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame A-Frame Spritesheet Component Component - Basic 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 34 | 35 |
36 | Sprite taken from glitchthegame.com, under a Public Domain Dedication license 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |