[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: http://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\ninsert_final_newline = true\ncharset = utf-8\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n*.orig\n.idea\n.sass-cache/\n.vscode\n.coveralls.yml\nnode_modules\ncoverage\ndoc\nrelease\n"
  },
  {
    "path": ".jshintrc",
    "content": "{\n  \"asi\": true,\n  \"bitwise\": true,\n  \"camelcase\": true,\n  \"curly\": true,\n  \"eqeqeq\": true,\n  \"forin\": true,\n  \"immed\": true,\n  \"latedef\": true,\n  \"newcap\": true,\n  \"noarg\": true,\n  \"nonew\": true,\n  \"quotmark\": \"single\",\n  \"strict\": true,\n  \"undef\": true,\n  \"unused\": true,\n  \"esnext\": true,\n  \"sub\": true,\n  \"browser\": false,\n  \"node\": true\n}\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nmatrix:\n  include:\n    - os: osx\naddons:\n  firefox: \"53.0\"\nnode_js:\n  - \"8\"\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2013 Alexander Gräfenstein\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": ""
  },
  {
    "path": "gulpfile.js",
    "content": "'use strict'\n\nconst del = require('del')\nconst gulp = require('gulp')\nconst uglify = require('gulp-uglify')\nconst concat = require('gulp-concat')\nconst path = require('path')\nconst shell = require('shelljs')\nconst rollup = require('rollup')\n\nconst dstDir = path.join(__dirname, 'release')\nconst srcDir = path.join(__dirname, 'src')\nconst docDir = path.join(dstDir, 'doc')\n\nfunction exec(command, cb) {\n  shell\n    .exec(command, { async: true })\n    .on('exit', (code) => cb(code === 0 ? null : code))\n}\n\ngulp.task('clean', () => del(dstDir))\n\ngulp.task('build:tsc', ['clean'], (cb) => {\n  exec('yarn run build', cb)\n})\n\ngulp.task('build:esm2015', ['clean'], (cb) => {\n  exec('yarn run build:esm2015', cb)\n})\n\ngulp.task('build:rollup', ['build:tsc'], () => {\n  const resolve = require('rollup-plugin-node-resolve')\n  const sourcemaps = require('rollup-plugin-sourcemaps')\n  const globals = {\n    '$': 'jquery',\n  }\n\n  return rollup.rollup({\n    amd: {id: `SpriteSpin`},\n    input: path.join(dstDir, 'src', 'index.js'),\n    onwarn: (warning, warn) => {\n      if (warning.code === 'THIS_IS_UNDEFINED') {return}\n      warn(warning);\n    },\n    plugins: [resolve(), sourcemaps()],\n    external: Object.keys(globals),\n  })\n  .then((bundle) => {\n    return bundle.write({\n      format: 'umd',\n      sourcemap: true,\n      file: path.join(dstDir, 'spritespin.js'),\n      name: 'SpriteSpin',\n      globals: globals,\n      exports: 'named',\n    })\n  })\n})\n\ngulp.task('build:uglify', ['build:rollup'], () => {\n  return gulp\n    .src(path.join(dstDir, 'spritespin.js'))\n    .pipe(uglify())\n    .pipe(concat('spritespin.min.js'))\n    .pipe(gulp.dest(dstDir))\n})\n\ngulp.task('build', ['build:tsc', 'build:esm2015', 'build:rollup', 'build:uglify'])\n\ngulp.task('watch', ['build', 'api:json'], () => {\n  gulp.watch([ path.join('src', '**', '*.ts') ], ['build', 'api:json'])\n})\n\ngulp.task('publish', ['build'], (cb) => {\n  exec('npm publish --access=public', cb)\n})\n\ngulp.task('api:json', ['build'], (cb) => {\n  const ae = require('@microsoft/api-extractor')\n  const config = ae.ExtractorConfig.prepare({\n    configObject: {\n      compiler: {\n        tsconfigFilePath: path.join(__dirname, 'tsconfig.json'),\n      },\n      apiReport: {\n        enabled: false,\n        reportFileName: 'spritespin.api.md',\n        reportFolder: path.join(__dirname, 'doc'),\n        reportTempFolder: path.join(__dirname, 'tmp'),\n      },\n      docModel: {\n        enabled: true,\n        apiJsonFilePath: path.join(__dirname, 'doc', 'spritespin.api.json'),\n      },\n      dtsRollup: {\n        enabled: false,\n      },\n      projectFolder: path.join(dstDir, 'src'),\n      mainEntryPointFilePath: path.join(dstDir, 'src', 'index.d.ts'),\n    }\n  })\n  config.packageFolder = process.cwd()\n  config.packageJson = require('./package.json')\n\n  const result = ae.Extractor.invoke(config, {\n    // Equivalent to the \"--local\" command-line parameter\n    localBuild: true,\n\n    // Equivalent to the \"--verbose\" command-line parameter\n    showVerboseMessages: true\n  })\n  if (result.succeeded) {\n    exec('./node_modules/.bin/api-documenter markdown -i doc -o doc', cb)\n  } else {\n    cb(1)\n  }\n})\n"
  },
  {
    "path": "karma.conf.js",
    "content": "'use strict'\n\nconst IS_COVERALLS = !!process.env.IS_COVERALLS\nconst IS_COVERAGE = IS_COVERALLS || !!process.env.IS_COVERAGE\nconst IS_TRAVIS = !!process.env.TRAVIS\n\nconst tsconfig = require('./tsconfig.json')\n\nmodule.exports = (config) => {\n  config.set({\n    plugins: [\n      'karma-jasmine',\n      'karma-phantomjs-launcher',\n      'karma-chrome-launcher',\n      'karma-firefox-launcher',\n      'karma-safari-launcher',\n      'karma-typescript',\n      'karma-mocha-reporter',\n    ],\n    logLevel: 'info',\n\n    frameworks: [\n      'jasmine',\n      'karma-typescript',\n    ],\n    browsers: [\n      IS_TRAVIS ? 'Firefox' : 'Chrome'\n    ],\n    files: [\n      'node_modules/jquery/dist/jquery.js',\n      'src/**/*.ts',\n    ],\n    preprocessors: {\n      '**/*.ts': ['karma-typescript'],\n    },\n    reporters: [\n      'mocha',\n      'karma-typescript',\n    ],\n\n    karmaTypescriptConfig: {\n      bundlerOptions: {\n        entrypoints: /\\.test\\.ts$/,\n        sourceMap: true,\n        validateSyntax: false,\n      },\n      exclude: ['node_modules', 'release'],\n      // compilerOptions: tsconfig.compilerOptions,\n      tsconfig: 'tsconfig.cjs.json',\n      // tsconfig: 'tsconfig.json',\n      // Pass options to remap-istanbul.\n      remapOptions: {\n        // a regex for excluding files from remapping\n        // exclude: '',\n        // a function for handling error messages\n        warn: (msg) => console.log(msg)\n      },\n      converageOptions: {\n        instrumentation: IS_COVERAGE,\n        exclude: /\\.(d|spec|test)\\.ts/i,\n      },\n      reports: {\n        'text-summary': '',\n        html: {\n          directory: 'coverage',\n          subdirectory: 'html',\n        },\n        lcovonly: {\n          directory: 'coverage',\n          subdirectory: 'lcov',\n        },\n      },\n    },\n\n  })\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"spritespin\",\n  \"description\": \"jQuery plugin for creating flipbook animations\",\n  \"version\": \"4.1.0\",\n  \"author\": \"Alexander Graefenstein\",\n  \"license\": \"MIT\",\n  \"keywords\": [\n    \"spritespin\",\n    \"360\",\n    \"animation\",\n    \"flipbook\",\n    \"panorama\",\n    \"jquery-plugin\",\n    \"ecosystem:jquery\"\n  ],\n  \"main\": \"release/spritespin.js\",\n  \"module\": \"release/src/index.js\",\n  \"typings\": \"release/src/index.d.ts\",\n  \"es2015\": \"release/esm2015/index.js\",\n  \"files\": [\n    \"release\",\n    \"README.md\",\n    \"LICENSE\",\n    \"package.json\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git://github.com/giniedp/spritespin.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/giniedp/spritespin/issues\"\n  },\n  \"engines\": {\n    \"node\": \">= 0.8.0\"\n  },\n  \"scripts\": {\n    \"lint\": \"tslint --config tslint.json src/**/*.ts\",\n    \"lint:silent\": \"tslint --config tslint.json src/**/*.ts || true\",\n    \"build\": \"tsc --project tsconfig.json\",\n    \"build:cjs\": \"tsc --project tsconfig.cjs.json\",\n    \"build:esm2015\": \"tsc --project tsconfig.esm2015.json\",\n    \"build:watch\": \"tsc -w\",\n    \"test\": \"karma start --no-auto-watch --single-run\",\n    \"test:watch\": \"karma start --auto-watch --no-single-run\",\n    \"test:coverage\": \"IS_COVERAGE=yes karma start --single-run\",\n    \"test:coveralls\": \"IS_COVERALLS=yes karma start --single-run && cat coverage/lcov/lcovonly | ./node_modules/.bin/coveralls\"\n  },\n  \"dependencies\": {},\n  \"peerDependencies\": {\n    \"jquery\": \">1.11.0\"\n  },\n  \"devDependencies\": {\n    \"@microsoft/api-documenter\": \"^7.2.0\",\n    \"@microsoft/api-extractor\": \"^7.2.2\",\n    \"@types/jasmine\": \"^2.5.54\",\n    \"@types/jquery\": \"^3.2.12\",\n    \"@types/node\": \"^8.0.27\",\n    \"coveralls\": \"^3.0.0\",\n    \"del\": \"^3.0.0\",\n    \"gulp\": \"^3.9.1\",\n    \"gulp-concat\": \"^2.6.1\",\n    \"gulp-uglify\": \"^3.0.0\",\n    \"jasmine-core\": \"^2.8.0\",\n    \"jquery\": \"^3.2.1\",\n    \"karma\": \"^1.7.1\",\n    \"karma-chrome-launcher\": \"^2.2.0\",\n    \"karma-firefox-launcher\": \"^1.0.1\",\n    \"karma-jasmine\": \"1.1.0\",\n    \"karma-mocha-reporter\": \"^2.2.4\",\n    \"karma-phantomjs-launcher\": \"^1.0.4\",\n    \"karma-safari-launcher\": \"^1.0.0\",\n    \"karma-typescript\": \"^3.0.12\",\n    \"rollup\": \"^0.56.5\",\n    \"rollup-plugin-node-resolve\": \"^3.2.0\",\n    \"rollup-plugin-sourcemaps\": \"^0.4.2\",\n    \"shelljs\": \"^0.8.1\",\n    \"through-gulp\": \"^0.5.0\",\n    \"tslint\": \"^5.7.0\",\n    \"typedoc\": \"^0.11.1\",\n    \"typescript\": \"^2.7.2\"\n  }\n}\n"
  },
  {
    "path": "src/api/common.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Api#common', () => {\n\n  let data: SpriteSpin.Data\n  let api: any\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: [t.RED40x30, t.GREEN40x30, t.BLUE40x30, t.RED40x30, t.GREEN40x30, t.BLUE40x30],\n      width: 40,\n      height: 30,\n      frameTime: 16,\n\n      animate: false,\n      onComplete: done\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n    api = t.get$El().spritespin('api')\n  })\n\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('#isPlaying', () => {\n    it ('detects if animation is running', () => {\n      expect(api.isPlaying()).toBe(false)\n      data.animate = true\n      SpriteSpin.applyAnimation(data)\n      expect(api.isPlaying()).toBe(true)\n      SpriteSpin.stopAnimation(data)\n      expect(api.isPlaying()).toBe(false)\n    })\n  })\n\n  describe('#isLooping', () => {\n    it ('returns data.loop property', () => {\n      data.loop = false\n      expect(api.isLooping()).toBe(false)\n      data.loop = true\n      expect(api.isLooping()).toBe(true)\n    })\n  })\n\n  describe('#toggleAnimation', () => {\n    it ('starts/stops the animation', () => {\n      api.toggleAnimation()\n      expect(api.isPlaying()).toBe(true, 'started')\n\n      api.toggleAnimation()\n      expect(api.isPlaying()).toBe(false, 'stopped')\n    })\n  })\n\n  describe('#loop', () => {\n    it ('sets the loop property', () => {\n      api.loop(false)\n      expect(api.isLooping()).toBe(false)\n      api.loop(true)\n      expect(api.isLooping()).toBe(true)\n    })\n\n    it ('starts the animation if animate=true', () => {\n      expect(api.isPlaying()).toBe(false)\n      data.animate = true\n      api.loop(false)\n      expect(api.isPlaying()).toBe(true)\n    })\n  })\n\n  describe('#currentFrame', () => {\n    it ('gets the current frame number', () => {\n      data.frame = 1337\n      expect(api.currentFrame()).toBe(1337)\n      data.frame = 42\n      expect(api.currentFrame()).toBe(42)\n    })\n  })\n\n  describe('#updateFrame', () => {\n    it ('updates the frame number', () => {\n      // spyOn(SpriteSpin, 'updateFrame').and.callThrough()\n      api.updateFrame(2)\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(1)\n      expect(data.frame).toBe(2)\n    })\n  })\n\n  describe('#skipFrames', () => {\n    it ('skips number of frames', () => {\n      expect(data.frame).toBe(0)\n      expect(data.frames).toBe(6)\n      // spyOn(SpriteSpin, 'updateFrame').and.callThrough()\n\n      api.skipFrames(2)\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(1)\n      expect(data.frame).toBe(2)\n\n      api.skipFrames(2)\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(2)\n      expect(data.frame).toBe(4)\n\n      api.skipFrames(2)\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(3)\n      expect(data.frame).toBe(0)\n    })\n\n    it ('steps backwards if reverse is true', () => {\n      data.reverse = true\n      expect(data.frame).toBe(0)\n      expect(data.frames).toBe(6)\n      // spyOn(SpriteSpin, 'updateFrame').and.callThrough()\n\n      api.skipFrames(2)\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(1)\n      expect(data.frame).toBe(4)\n\n      api.skipFrames(2)\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(2)\n      expect(data.frame).toBe(2)\n\n      api.skipFrames(2)\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(3)\n      expect(data.frame).toBe(0)\n    })\n  })\n\n  describe('#nextFrame', () => {\n    it ('increments frame', () => {\n      expect(data.frame).toBe(0)\n      expect(data.frames).toBe(6)\n      // spyOn(SpriteSpin, 'updateFrame').and.callThrough()\n\n      api.nextFrame()\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(1)\n      expect(data.frame).toBe(1)\n\n      api.nextFrame()\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(2)\n      expect(data.frame).toBe(2)\n\n      api.nextFrame()\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(3)\n      expect(data.frame).toBe(3)\n    })\n  })\n\n  describe('#prevFrame', () => {\n    it ('decrements frame', () => {\n      expect(data.frame).toBe(0)\n      expect(data.frames).toBe(6)\n      // spyOn(SpriteSpin, 'updateFrame').and.callThrough()\n\n      api.prevFrame()\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(1)\n      expect(data.frame).toBe(5)\n\n      api.prevFrame()\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(2)\n      expect(data.frame).toBe(4)\n\n      api.prevFrame()\n      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(3)\n      expect(data.frame).toBe(3)\n    })\n  })\n\n  describe('#playTo', () => {\n    it ('skips animation if already on frame', () => {\n      expect(api.isPlaying()).toBe(false)\n      api.playTo(0)\n      expect(api.isPlaying()).toBe(false)\n    })\n\n    it ('starts animation', () => {\n      api.playTo(5)\n      expect(api.isPlaying()).toBe(true)\n    })\n\n    it ('stops animation on given frame', (done) => {\n      api.playTo(3)\n      setTimeout(() => {\n        expect(api.currentFrame()).toBe(3)\n        expect(api.isPlaying()).toBe(false)\n        done()\n      }, 100)\n    })\n\n    describe('with nearest option', () => {\n      it ('plays forward, if needed', () => {\n        expect(data.reverse).toBe(false)\n        api.playTo(2, { nearest: true })\n        expect(data.reverse).toBe(false)\n      })\n\n      it ('plays backward, if needed', () => {\n        expect(data.reverse).toBe(false)\n        api.playTo(3, { nearest: true })\n        expect(data.reverse).toBe(true)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "src/api/common.ts",
    "content": "import * as SpriteSpin from '../core'\n\n// tslint:disable:object-literal-shorthand\n// tslint:disable:only-arrow-functions\n\nSpriteSpin.extendApi({\n  // Gets a value indicating whether the animation is currently running.\n  isPlaying: function() {\n    return SpriteSpin.getPlaybackState(this.data).handler != null\n  },\n\n  // Gets a value indicating whether the animation looping is enabled.\n  isLooping: function() {\n    return this.data.loop\n  },\n\n  // Starts/Stops the animation playback\n  toggleAnimation: function() {\n    if (this.isPlaying()) {\n      this.stopAnimation()\n    } else {\n      this.startAnimation()\n    }\n  },\n\n  // Stops animation playback\n  stopAnimation: function() {\n    this.data.animate = false\n    SpriteSpin.stopAnimation(this.data)\n  },\n\n  // Starts animation playback\n  startAnimation: function() {\n    this.data.animate = true\n    SpriteSpin.applyAnimation(this.data)\n  },\n\n  // Sets a value indicating whether the animation should be looped or not.\n  // This might start the animation (if the 'animate' data attribute is set to true)\n  loop: function(value) {\n    this.data.loop = value\n    SpriteSpin.applyAnimation(this.data)\n    return this\n  },\n\n  // Gets the current frame number\n  currentFrame: function() {\n    return this.data.frame\n  },\n\n  // Updates SpriteSpin to the specified frame.\n  updateFrame: function(frame, lane) {\n    SpriteSpin.updateFrame(this.data, frame, lane)\n    return this\n  },\n\n  // Skips the given number of frames\n  skipFrames: function(step) {\n    const data = this.data\n    SpriteSpin.updateFrame(data, data.frame + (data.reverse ? - step : + step))\n    return this\n  },\n\n  // Updates SpriteSpin so that the next frame is shown\n  nextFrame: function() {\n    return this.skipFrames(1)\n  },\n\n  // Updates SpriteSpin so that the previous frame is shown\n  prevFrame: function() {\n    return this.skipFrames(-1)\n  },\n\n  // Starts the animations that will play until the given frame number is reached\n  // options:\n  //   force [boolean] starts the animation, even if current frame is the target frame\n  //   nearest [boolean] animates to the direction with minimum distance to the target frame\n  playTo: function(frame, options) {\n    const data = this.data\n    options = options || {}\n    if (!options.force && data.frame === frame) {\n      return\n    }\n    if (options.nearest) {\n      // distance to the target frame\n      const a = frame - data.frame\n      // distance to last frame and the to target frame\n      const b = frame > data.frame ? a - data.frames : a + data.frames\n      // minimum distance\n      const c = Math.abs(a) < Math.abs(b) ? a : b\n      data.reverse = c < 0\n    }\n    data.animate = true\n    data.loop = false\n    data.stopFrame = frame\n    SpriteSpin.applyAnimation(data)\n    return this\n  }\n})\n"
  },
  {
    "path": "src/api/fullscreen.test.ts",
    "content": "import * as SpriteSpin from '..'\n\ndescribe('SpriteSpin.Api#fullscreen', () => {\n  // untestable\n})\n"
  },
  {
    "path": "src/api/fullscreen.ts",
    "content": "import { boot, Data, extendApi, namespace, SizeMode } from '../core'\nimport { $ } from '../utils'\n\nexport interface Options {\n  source?: string | string[],\n  sizeMode?: SizeMode\n}\n\nfunction pick(target, names: string[]): string {\n  for (const name of names) {\n    if (target[name] || name in target) {\n      return name\n    }\n  }\n  return names[0]\n}\n\nconst browser = {\n  requestFullscreen: pick(document.documentElement, [\n    'requestFullscreen',\n    'webkitRequestFullScreen',\n        'mozRequestFullScreen',\n        'msRequestFullscreen']),\n  exitFullscreen: pick(document, [\n    'exitFullscreen',\n    'webkitExitFullscreen',\n    'webkitCancelFullScreen',\n        'mozCancelFullScreen',\n        'msExitFullscreen']),\n  fullscreenElement: pick(document, [\n    'fullscreenElement',\n    'webkitFullscreenElement',\n    'webkitCurrentFullScreenElement',\n        'mozFullScreenElement',\n        'msFullscreenElement']),\n  fullscreenEnabled: pick(document, [\n    'fullscreenEnabled',\n    'webkitFullscreenEnabled',\n        'mozFullScreenEnabled',\n        'msFullscreenEnabled']),\n  fullscreenchange: pick(document, [\n    'onfullscreenchange',\n    'onwebkitfullscreenchange',\n        'onmozfullscreenchange',\n        'onMSFullscreenChange']).replace(/^on/, ''),\n  fullscreenerror: pick(document, [\n    'onfullscreenerror',\n    'onwebkitfullscreenerror',\n        'onmozfullscreenerror',\n        'onMSFullscreenError']).replace(/^on/, '')\n}\n\nconst changeEvent = browser.fullscreenchange + '.' + namespace + '-fullscreen'\nfunction unbindChangeEvent() {\n  $(document).unbind(changeEvent)\n}\n\nfunction bindChangeEvent(callback) {\n  unbindChangeEvent()\n  $(document).bind(changeEvent, callback)\n}\n\nconst orientationEvent = 'orientationchange.' + namespace + '-fullscreen'\nfunction unbindOrientationEvent() {\n  $(window).unbind(orientationEvent)\n}\nfunction bindOrientationEvent(callback) {\n  unbindOrientationEvent()\n  $(window).bind(orientationEvent, callback)\n}\n\nfunction requestFullscreenNative(e) {\n  e = e || document.documentElement\n  e[browser.requestFullscreen]()\n}\n\nexport function exitFullscreen() {\n  return document[browser.exitFullscreen]()\n}\n\nexport function fullscreenEnabled() {\n  return document[browser.fullscreenEnabled]\n}\n\nexport function fullscreenElement() {\n  return document[browser.fullscreenElement]\n}\n\nexport function isFullscreen() {\n  return !!fullscreenElement()\n}\n\nexport function toggleFullscreen(data: Data, opts: Options) {\n  if (isFullscreen()) {\n    this.apiRequestFullscreen(opts)\n  } else {\n    this.exitFullscreen()\n  }\n}\n\nexport function requestFullscreen(data: Data, opts: Options) {\n  opts = opts || {}\n  const oWidth = data.width\n  const oHeight = data.height\n  const oSource = data.source\n  const oSize = data.sizeMode\n  const oResponsive = data.responsive\n  const enter = () => {\n    data.width = window.screen.width\n    data.height = window.screen.height\n    data.source = (opts.source || oSource) as string[]\n    data.sizeMode = opts.sizeMode || 'fit'\n    data.responsive = false\n    boot(data)\n  }\n  const exit = () => {\n    data.width = oWidth\n    data.height = oHeight\n    data.source = oSource\n    data.sizeMode = oSize\n    data.responsive = oResponsive\n    boot(data)\n  }\n\n  bindChangeEvent(() => {\n    if (isFullscreen()) {\n      enter()\n      bindOrientationEvent(enter)\n    } else {\n      unbindChangeEvent()\n      unbindOrientationEvent()\n      exit()\n    }\n  })\n  requestFullscreenNative(data.target[0])\n}\n\nextendApi({\n  fullscreenEnabled,\n  fullscreenElement,\n  exitFullscreen,\n  toggleFullscreen: function(opts: Options) {\n    toggleFullscreen(this.data, opts)\n  },\n  requestFullscreen: function(opts: Options) {\n    requestFullscreen(this.data, opts)\n  }\n})\n"
  },
  {
    "path": "src/api/index.ts",
    "content": "import './common'\nimport './fullscreen'\n"
  },
  {
    "path": "src/core/api.ts",
    "content": "// tslint:disable ban-types\nimport { Data } from './models'\n\n/**\n * @internal\n */\nexport class Api {\n  constructor(public data: Data) { }\n}\n\n/**\n * Adds methods to the SpriteSpin api\n *\n * @public\n */\nexport function extendApi(methods: { [key: string]: Function }) {\n  const api = Api.prototype\n  for (const key in methods) {\n    if (methods.hasOwnProperty(key)) {\n      if (api[key]) {\n        throw new Error('API method is already defined: ' + key)\n      } else {\n        api[key] = methods[key]\n      }\n    }\n  }\n  return api\n}\n"
  },
  {
    "path": "src/core/boot.ts",
    "content": "import * as Utils from '../utils'\nimport { callbackNames, defaults, eventNames, eventsToPrevent, namespace } from './constants'\nimport { applyLayout } from './layout'\nimport { Data, Options } from './models'\nimport { applyAnimation, stopAnimation } from './playback'\nimport { applyPlugins } from './plugins'\n\nconst $ = Utils.$\n\nlet counter = 0\n/**\n * Collection of all SpriteSpin instances\n */\nexport const instances: {[key: string]: Data} = {}\n\nfunction pushInstance(data: Data) {\n  counter += 1\n  data.id = String(counter)\n  instances[data.id] = data\n}\n\nfunction popInstance(data: Data) {\n  delete instances[data.id]\n}\n\nfunction eachInstance(cb) {\n  for (const id in instances) {\n    if (instances.hasOwnProperty(id)) {\n      cb(instances[id])\n    }\n  }\n}\n\nlet lazyinit = () => {\n  // replace function with a noop\n  // this logic must run only once\n  lazyinit = () => { /* noop */ }\n\n  function onEvent(eventName, e) {\n    eachInstance((data: Data) => {\n      for (const module of data.plugins) {\n        if (typeof module[eventName] === 'function') {\n          module[eventName].apply(data.target, [e, data])\n        }\n      }\n    })\n  }\n\n  function onResize() {\n    eachInstance((data: Data) => {\n      if (data.responsive) {\n        boot(data)\n      }\n    })\n  }\n\n  for (const eventName of eventNames) {\n    $(window.document).bind(eventName + '.' + namespace, (e) => {\n      onEvent('document' + eventName, e)\n    })\n  }\n\n  let resizeTimeout = null\n  $(window).on('resize', () => {\n    window.clearTimeout(resizeTimeout)\n    resizeTimeout = window.setTimeout(onResize, 100)\n  })\n}\n\n/**\n * (re)binds all spritespin events on the target element\n *\n * @internal\n */\nexport function applyEvents(data: Data) {\n  const target = data.target\n\n  // Clear all SpriteSpin events on the target element\n  Utils.unbind(target)\n\n  // disable all default browser behavior on the following events\n  // mainly prevents image drag operation\n  for (const eName of eventsToPrevent) {\n    Utils.bind(target, eName, Utils.prevent)\n  }\n\n  // Bind module functions to SpriteSpin events\n  for (const plugin of data.plugins) {\n    for (const eName of eventNames) {\n      Utils.bind(target, eName, plugin[eName])\n    }\n    for (const eName of callbackNames) {\n      Utils.bind(target, eName, plugin[eName])\n    }\n  }\n\n  // bind auto start function to load event.\n  Utils.bind(target, 'onLoad', (e, d) => {\n    applyAnimation(d)\n  })\n\n  // bind all user events that have been passed on initialization\n  for (const eName of callbackNames) {\n    Utils.bind(target, eName, data[eName])\n  }\n}\n\nfunction applyMetrics(data: Data) {\n  if (!data.images) {\n    data.metrics = []\n  }\n  data.metrics = Utils.measure(data.images, data)\n  const spec = Utils.findSpecs(data.metrics, data.frames, 0, 0)\n  if (spec.sprite) {\n    // TODO: try to remove frameWidth/frameHeight\n    data.frameWidth = spec.sprite.width\n    data.frameHeight = spec.sprite.height\n  }\n}\n\n/**\n * Runs the boot process.\n *\n * @remarks\n * (re)initializes plugins, (re)initializes the layout, (re)binds events and loads source images.\n *\n * @internal\n */\nexport function boot(data: Data) {\n  applyPlugins(data)\n  applyEvents(data)\n  applyLayout(data)\n\n  data.source = Utils.toArray(data.source)\n  data.loading = true\n  data.target\n    .addClass('loading')\n    .trigger('onInit.' + namespace, data)\n  Utils.preload({\n    source: data.source,\n    crossOrigin: data.crossOrigin,\n    preloadCount: data.preloadCount,\n    progress: (progress) => {\n      data.progress = progress\n      data.target.trigger('onProgress.' + namespace, data)\n    },\n    complete: (images) => {\n      data.images = images\n      data.loading = false\n      data.frames = data.frames || images.length\n\n      applyMetrics(data)\n      applyLayout(data)\n      data.stage.show()\n      data.target\n        .removeClass('loading')\n        .trigger('onLoad.' + namespace, data)\n        .trigger('onFrame.' + namespace, data)\n        .trigger('onDraw.' + namespace, data)\n        .trigger('onComplete.' + namespace, data)\n    }\n  })\n}\n\n/**\n * Creates a new SpriteSpin instance\n *\n * @public\n */\nexport function create(options: Options): Data {\n  const target = options.target\n\n  // SpriteSpin is not initialized\n  // Create default settings object and extend with given options\n  const data = $.extend(true, {}, defaults, options) as Data\n\n  // ensure source is set\n  data.source = data.source || []\n  // ensure plugins are set\n  data.plugins = data.plugins || [\n    '360',\n    'drag'\n  ]\n\n  // if image tags are contained inside this DOM element\n  // use these images as the source files\n  target.find('img').each(() => {\n    if (!Array.isArray(data.source)) {\n      data.source = []\n    }\n    data.source.push($(this).attr('src'))\n  })\n\n  // build inner html\n  // <div>\n  //   <div class='spritespin-stage'></div>\n  //   <canvas class='spritespin-canvas'></canvas>\n  // </div>\n  target\n    .empty()\n    .addClass('spritespin-instance')\n    .append(\"<div class='spritespin-stage'></div>\")\n\n  // add the canvas element if canvas rendering is enabled and supported\n  if (data.renderer === 'canvas') {\n    const canvas = document.createElement('canvas')\n    if (!!(canvas.getContext && canvas.getContext('2d'))) {\n      data.canvas = $(canvas).addClass('spritespin-canvas') as any\n      data.context = canvas.getContext('2d')\n      target.append(data.canvas)\n      target.addClass('with-canvas')\n    } else {\n      // fallback to image rendering mode\n      data.renderer = 'image'\n    }\n  }\n\n  // setup references to DOM elements\n  data.target = target\n  data.stage = target.find('.spritespin-stage')\n\n  // store the data\n  target.data(namespace, data)\n  pushInstance(data)\n\n  return data\n}\n\n/**\n * Creates a new SpriteSpin instance, or updates an existing one\n *\n * @public\n */\nexport function createOrUpdate(options: Options): Data {\n  lazyinit()\n\n  let data  = options.target.data(namespace) as Data\n  if (!data) {\n    data = create(options)\n  } else {\n    $.extend(data, options)\n  }\n  boot(data)\n\n  return data\n}\n\n/**\n * Destroys the SpriteSpin instance\n *\n * @remarks\n * - stops running animation\n * - unbinds all events\n * - deletes the data on the target element\n *\n * @public\n */\nexport function destroy(data: Data) {\n  popInstance(data)\n  stopAnimation(data)\n  data.target\n    .trigger('onDestroy', data)\n    .html(null)\n    .attr('style', null)\n    .attr('unselectable', null)\n    .removeClass(['spritespin-instance', 'with-canvas'])\n  Utils.unbind(data.target)\n  data.target.removeData(namespace)\n}\n"
  },
  {
    "path": "src/core/constants.ts",
    "content": "import { Options } from './models'\n\n/**\n * The namespace that is used to bind functions to DOM events and store the data object\n */\nexport const namespace = 'spritespin'\n\n/**\n * Event names that are recognized by SpriteSpin. A module can implement any of these and they will be bound\n * to the target element on which the plugin is called.\n */\nexport const eventNames = [\n  'mousedown',\n  'mousemove',\n  'mouseup',\n  'mouseenter',\n  'mouseover',\n  'mouseleave',\n  'mousewheel',\n  'wheel',\n  'click',\n  'dblclick',\n  'touchstart',\n  'touchmove',\n  'touchend',\n  'touchcancel',\n  'selectstart',\n  'gesturestart',\n  'gesturechange',\n  'gestureend'\n]\n\n/**\n *\n */\nexport const callbackNames = [\n  'onInit',\n  'onProgress',\n  'onLoad',\n  'onFrameChanged',\n  'onFrame',\n  'onDraw',\n  'onComplete',\n  'onDestroy'\n]\n\n/**\n * Names of events for that the default behavior should be prevented.\n */\nexport const eventsToPrevent = [\n  'dragstart'\n]\n\n/**\n * Default set of SpriteSpin options. This also represents the majority of data attributes that are used during the\n * lifetime of a SpriteSpin instance. The data is stored inside the target DOM element on which the plugin is called.\n */\nexport const defaults: Options = {\n  source            : undefined,    // Stitched source image or array of files\n  width             : undefined,    // actual display width\n  height            : undefined,    // actual display height\n  frames            : undefined,    // Total number of frames\n  framesX           : undefined,    // Number of frames in one row of sprite sheet (if source is a sprite sheet)\n  lanes             : 1,            // Number of 360 sequences. Used for 3D like effect.\n  sizeMode          : undefined,    //\n\n  renderer          : 'canvas',     // The rendering mode to use\n\n  lane              : 0,            // The initial sequence number to play\n  frame             : 0,            // Initial (and current) frame number\n  frameTime         : 40,           // Time in ms between updates. 40 is exactly 25 FPS\n  animate           : true,         // If true starts the animation on load\n  retainAnimate     : false,        // If true, retains the animation after user interaction\n  reverse           : false,        // If true animation is played backward\n  loop              : true,         // If true loops the animation\n  stopFrame         : 0,            // Stops the animation at this frame if loop is disabled\n\n  wrap              : true,         // If true wraps around the frame index on user interaction\n  wrapLane          : false,        // If true wraps around the lane index on user interaction\n  sense             : 1,            // Interaction sensitivity used by behavior implementations\n  senseLane         : undefined,    // Interaction sensitivity used by behavior implementations\n  orientation       : 'horizontal', // Preferred axis for user interaction\n  detectSubsampling : true,         // Tries to detect whether the images are down sampled by the browser.\n  preloadCount      : undefined,    // Number of frames to preload. If nothing is set, all frames are preloaded.\n\n  touchScrollTimer  : [200, 1500],  // Time range in ms when touch scroll will be disabled during interaction with SpriteSpin\n  responsive        : undefined,\n  plugins           : undefined\n}\n"
  },
  {
    "path": "src/core/index.ts",
    "content": "export * from './api'\nexport * from './boot'\nexport * from './constants'\nexport * from './input'\nexport * from './layout'\nexport * from './models'\nexport * from './playback'\nexport * from './plugins'\nexport * from './state'\n\nimport './jquery'\n"
  },
  {
    "path": "src/core/input.ts",
    "content": "import { getCursorPosition } from '../utils'\nimport { Data } from './models'\nimport { getState } from './state'\n\n/**\n * Describes a SpriteSpin input state\n *\n * @public\n */\nexport interface InputState {\n  oldX: number\n  oldY: number\n  currentX: number\n  currentY: number\n  startX: number\n  startY: number\n  clickframe: number\n  clicklane: number\n  dX: number\n  dY: number\n  ddX: number\n  ddY: number\n  ndX: number\n  ndY: number\n  nddX: number\n  nddY: number\n}\n\n/**\n * Gets the current input state\n *\n * @public\n * @param data - The SpriteSpin instance data\n */\nexport function getInputState(data: Data): InputState {\n  return getState(data, 'input')\n}\n\n/**\n * Updates the input state using a mouse or touch event.\n *\n * @public\n * @param e - The input event\n * @param data - The SpriteSpin instance data\n */\nexport function updateInput(e, data: Data) {\n  const cursor = getCursorPosition(e)\n  const state = getInputState(data)\n\n  // cache positions from previous frame\n  state.oldX = state.currentX\n  state.oldY = state.currentY\n\n  state.currentX = cursor.x\n  state.currentY = cursor.y\n\n  // Fix old position.\n  if (state.oldX === undefined || state.oldY === undefined) {\n    state.oldX = state.currentX\n    state.oldY = state.currentY\n  }\n\n  // Cache the initial click/touch position and store the frame number at which the click happened.\n  // Useful for different behavior implementations. This must be restored when the click/touch is released.\n  if (state.startX === undefined || state.startY === undefined) {\n    state.startX = state.currentX\n    state.startY = state.currentY\n    state.clickframe = data.frame\n    state.clicklane = data.lane\n  }\n\n  // Calculate the vector from start position to current pointer position.\n  state.dX = state.currentX - state.startX\n  state.dY = state.currentY - state.startY\n\n  // Calculate the vector from last frame position to current pointer position.\n  state.ddX = state.currentX - state.oldX\n  state.ddY = state.currentY - state.oldY\n\n  // Normalize vectors to range [-1:+1]\n  state.ndX = state.dX / data.target.innerWidth()\n  state.ndY = state.dY / data.target.innerHeight()\n\n  state.nddX = state.ddX / data.target.innerWidth()\n  state.nddY = state.ddY / data.target.innerHeight()\n}\n\n/**\n * Resets the input state.\n *\n * @public\n */\nexport function resetInput(data: Data) {\n  const input = getInputState(data)\n  input.startX = input.startY = undefined\n  input.currentX = input.currentY = undefined\n  input.oldX = input.oldY = undefined\n  input.dX = input.dY = 0\n  input.ddX = input.ddY = 0\n  input.ndX = input.ndY = 0\n  input.nddX = input.nddY = 0\n}\n"
  },
  {
    "path": "src/core/jquery.ts",
    "content": "import { $ } from '../utils'\nimport { Api } from './api'\nimport { createOrUpdate, destroy } from './boot'\nimport { namespace } from './constants'\n\nfunction extension(option: string | any, value: any) {\n  const $target = $(this)\n  if (option === 'data') {\n    return $target.data(namespace)\n  }\n  if (option === 'api') {\n    const data = $target.data(namespace)\n    data.api = data.api || new Api(data)\n    return data.api\n  }\n  if (option === 'destroy') {\n    return $target.each(() => {\n      const data = $target.data(namespace)\n      if (data) {\n        destroy(data)\n      }\n    })\n  }\n  if (arguments.length === 2 && typeof option === 'string') {\n    option = { [option]: value }\n  }\n  if (typeof option === 'object') {\n    return createOrUpdate($.extend(true, { target: $target }, option)).target\n  }\n\n  throw new Error('Invalid call to spritespin')\n}\n\n$.fn[namespace] = extension\n"
  },
  {
    "path": "src/core/layout.ts",
    "content": "import * as Utils from '../utils'\nimport { Data } from './models'\n\n/**\n * Applies css attributes to layout the SpriteSpin containers.\n *\n * @internal\n */\nexport function applyLayout(data: Data) {\n  // disable selection\n  data.target\n    .attr('unselectable', 'on')\n    .css({\n      width: '',\n      height: '',\n      '-ms-user-select': 'none',\n      '-moz-user-select': 'none',\n      '-khtml-user-select': 'none',\n      '-webkit-user-select': 'none',\n      'user-select': 'none'\n    })\n\n  const size = data.responsive ? Utils.getComputedSize(data) : Utils.getOuterSize(data)\n  const layout = Utils.getInnerLayout(data.sizeMode, Utils.getInnerSize(data), size)\n\n  // apply layout on target\n  data.target.css({\n    width    : size.width,\n    height   : size.height,\n    position : 'relative',\n    overflow : 'hidden'\n  })\n\n  // apply layout on stage\n  data.stage\n    .css(layout)\n    .hide()\n\n  if (!data.canvas) { return }\n  // apply layout on canvas\n  data.canvas.css(layout).hide()\n  // apply pixel ratio on canvas\n  data.canvasRatio = data.canvasRatio || Utils.pixelRatio(data.context)\n  if (typeof layout.width === 'number' && typeof layout.height === 'number') {\n    data.canvas[0].width = ((layout.width as number) * data.canvasRatio) || size.width\n    data.canvas[0].height = ((layout.height as number) * data.canvasRatio) || size.height\n  } else {\n    data.canvas[0].width = (size.width * data.canvasRatio)\n    data.canvas[0].height = (size.height * data.canvasRatio)\n  }\n  // width and height must be set before calling scale\n  data.context.scale(data.canvasRatio, data.canvasRatio)\n}\n"
  },
  {
    "path": "src/core/models.ts",
    "content": "import { PreloadProgress, SheetSpec } from '../utils'\n\nexport type Callback = (e: any, data: Data) => void\n\n/**\n * Additional callback options for SpriteSpin\n *\n * @public\n */\nexport interface CallbackOptions {\n  /**\n   * Occurs when the plugin has been initialized, but before loading the source files.\n   */\n  onInit?: Callback\n  /**\n   * Occurs when any source file has been loaded and the progress has changed.\n   */\n  onProgress?: Callback\n  /**\n   * Occurs when all source files have been loaded and spritespin is ready to update and draw.\n   */\n  onLoad?: Callback\n  /**\n   * Occurs when the frame number has been updated (e.g. during animation)\n   */\n  onFrame?: Callback\n  /**\n   * Occurs when the frame number has changed.\n   */\n  onFrameChanged?: Callback\n  /**\n   * Occurs when all update is complete and frame can be drawn\n   */\n  onDraw?: Callback\n  /**\n   * Occurs when spritespin has been loaded and the first draw operation is complete\n   */\n  onComplete?: Callback\n}\n\nexport type SizeMode = 'original' | 'fit' | 'fill' | 'stretch'\nexport type RenderMode = 'canvas' | 'image' | 'background'\nexport type Orientation = 'horizontal' | 'vertical'\n\n/**\n * Options for SpriteSpin\n *\n * @public\n */\nexport interface Options extends CallbackOptions {\n  /**\n   * The target element which should hold the spritespin instance. This is usually already specified by the jQuery selector but can be overridden here.\n   */\n  target?: any,\n\n  /**\n   * Image URL or array of urls to be used.\n   */\n  source: string | string[]\n\n  /**\n   * The display width in pixels.\n   *\n   * @remarks\n   * Width and height should match the aspect ratio of the frames.\n   */\n  width?: number\n\n  /**\n   * The display height in pixels.\n   *\n   * @remarks\n   * Width and height should match the aspect ratio of the frames.\n   */\n  height?: number\n\n  /**\n   * Number of frames for a full 360 rotation.\n   *\n   * @remarks\n   * If multiple lanes are used, each lane must have this amount of frames.\n   */\n  frames: number\n\n  /**\n   * Number of frames in one row of a single sprite sheet.\n   */\n  framesX?: number\n\n  /**\n   * Number of frames in one column of a single sprite sheet.\n   */\n  framesY?: number\n\n  /**\n   * Number of sequences.\n   */\n  lanes?: number\n\n  /**\n   * Specifies how the frames are sized and scaled if it does not match the given\n   * width and height dimensions.\n   */\n  sizeMode?: SizeMode\n\n  /**\n   * The presentation module to use\n   *\n   * @deprecated please use plugins option instead\n   */\n  module?: string\n  /**\n   * The interaction module to use\n   *\n   * @deprecated please use plugins option instead\n   */\n  behavior?: string\n\n  /**\n   * Specifies the rendering mode.\n   */\n  renderer?: RenderMode\n\n  /**\n   * The initial sequence number to play.\n   *\n   * @remarks\n   * This value is updated each frame and also represents the current lane number.\n   */\n  lane?: number\n  /**\n   * Initial frame number.\n   *\n   * @remarks\n   * This value is updated each frame and also represents the current frame number.\n   */\n  frame?: number\n  /**\n   * Time in ms between updates. e.g. 40 is exactly 25 FPS\n   */\n  frameTime?: number\n  /**\n   * If true, starts the animation automatically on load\n   */\n  animate?: boolean\n  /**\n   * If true, retains the animation after user user interaction\n   */\n  retainAnimate?: boolean\n  /**\n   * If true, animation playback is reversed\n   */\n  reverse?: boolean\n  /**\n   * If true, continues playback in a loop.\n   */\n  loop?: boolean\n  /**\n   * Stops the animation on that frame if `loop` is false.\n   */\n  stopFrame?: number\n\n  /**\n   * If true, allows the user to drag the animation beyond the last frame and wrap over to the beginning.\n   */\n  wrap?: boolean\n  /**\n   * If true, allows the user to drag the animation beyond the last sequence and wrap over to the beginning.\n   */\n  wrapLane?: boolean\n  /**\n   * Sensitivity factor for user interaction\n   */\n  sense?: number\n  /**\n   * Sensitivity factor for user interaction\n   */\n  senseLane?: number\n  /**\n   * Preferred axis for user interaction\n   */\n  orientation?: Orientation | number\n  /**\n   * If true, tries to detect whether the images are down sampled by the browser.\n   */\n  detectSubsampling?: boolean\n  /**\n   * Number of images to preload. If nothing is set, all images are preloaded.\n   */\n  preloadCount?: number\n\n  /**\n   * If true, display width can be controlled by CSS.\n   *\n   * @remarks\n   * Width and height must still both be set and are used to calculate the aspect ratio.\n   */\n  responsive?: boolean\n\n  /**\n   * Time range in ms when touch scroll will be disabled during interaction with SpriteSpin\n   */\n  touchScrollTimer?: [number, number]\n\n  /**\n   * Array of plugins to load\n   */\n  plugins?: any[]\n\n  /**\n   * Allows to download images from foreign origins\n   *\n   * @remarks\n   * https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image\n   */\n  crossOrigin?: string\n}\n\n/**\n * The instance data of SpriteSpin\n *\n * @public\n */\nexport interface Data extends Options {\n  /**\n   * The unique spritespin instance identifier\n   */\n  id: string\n\n  /**\n   * Array of all image urls\n   */\n  source: string[]\n\n  /**\n   * Array of all image elements\n   */\n  images: HTMLImageElement[]\n\n  /**\n   * The current preload progress state\n   */\n  progress: null | PreloadProgress\n\n  /**\n   * Array with measurement information for each image\n   */\n  metrics: SheetSpec[]\n\n  /**\n   * The detected width of a single frame\n   */\n  frameWidth: number\n\n  /**\n   * The detected height of a single frame\n   */\n  frameHeight: number\n\n  /**\n   * Opaque state object. Plugins may store their information here.\n   */\n  state: any\n\n  /**\n   * Is true during the preload phase\n   */\n  loading: boolean\n\n  /**\n   * The target element\n   */\n  target: JQuery\n\n  /**\n   * The inner stage element\n   */\n  stage: JQuery\n\n  /**\n   * The inner canvas element\n   */\n  canvas: JQuery<HTMLCanvasElement>\n\n  /**\n   * The 2D context of the canvas element\n   */\n  context: CanvasRenderingContext2D\n\n  /**\n   * The pixel ratio of the canvas element\n   */\n  canvasRatio: number\n}\n"
  },
  {
    "path": "src/core/playback.ts",
    "content": "import { clamp, wrap } from '../utils'\nimport { namespace } from './constants'\nimport { Data } from './models'\nimport { getState } from './state'\n/**\n * Gets the playback state\n *\n * @public\n * @param data - The SpriteSpin instance data\n */\nexport function getPlaybackState(data: Data): PlaybackState {\n  return getState(data, 'playback')\n}\n\n/**\n * The playback state\n *\n * @public\n */\nexport interface PlaybackState {\n  frameTime: number\n  lastFrame: number\n  lastLane: number\n  handler: number\n}\n\nfunction updateLane(data: Data, lane: number) {\n  data.lane = data.wrapLane\n    ? wrap(lane, 0, data.lanes - 1, data.lanes)\n    : clamp(lane, 0, data.lanes - 1)\n}\n\nfunction updateAnimationFrame(data: Data) {\n  data.frame += (data.reverse ? -1 : 1)\n  // wrap the frame value to fit in range [0, data.frames)\n  data.frame = wrap(data.frame, 0, data.frames - 1, data.frames)\n  // stop animation if loop is disabled and the stopFrame is reached\n  if (!data.loop && (data.frame === data.stopFrame)) {\n    stopAnimation(data)\n  }\n}\n\nfunction updateInputFrame(data: Data, frame: number) {\n  data.frame = Number(frame)\n  data.frame = data.wrap\n    ? wrap(data.frame, 0, data.frames - 1, data.frames)\n    : clamp(data.frame, 0, data.frames - 1)\n}\n\nfunction updateAnimation(data: Data) {\n  const state = getPlaybackState(data)\n  if (state.handler) {\n    updateBefore(data)\n    updateAnimationFrame(data)\n    updateAfter(data)\n  }\n}\n\nfunction updateBefore(data: Data) {\n  const state = getPlaybackState(data)\n  state.lastFrame = data.frame\n  state.lastLane = data.lane\n}\n\nfunction updateAfter(data: Data) {\n  const state = getPlaybackState(data)\n  if (state.lastFrame !== data.frame || state.lastLane !== data.lane) {\n    data.target.trigger('onFrameChanged.' + namespace, data)\n  }\n  data.target.trigger('onFrame.' + namespace, data)\n  data.target.trigger('onDraw.' + namespace, data)\n}\n\n/**\n * Updates the frame or lane number of the SpriteSpin data.\n *\n * @public\n * @param data - The SpriteSpin instance data\n * @param frame - The frame number to set\n * @param lane - The lane number to set\n */\nexport function updateFrame(data: Data, frame?: number, lane?: number) {\n  updateBefore(data)\n  if (frame != null) {\n    updateInputFrame(data, frame)\n  }\n  if (lane != null) {\n    updateLane(data, lane)\n  }\n  updateAfter(data)\n}\n\n/**\n * Stops the running animation.\n *\n * @public\n * @param data - The SpriteSpin instance data\n */\nexport function stopAnimation(data: Data) {\n  data.animate = false\n  const state = getPlaybackState(data)\n  if (state.handler != null) {\n    window.clearInterval(state.handler)\n    state.handler = null\n  }\n}\n\n/**\n * Starts animation playback if needed.\n *\n * @remarks\n * Starts animation playback if `animate` property is `true` and the animation is not yet running.\n *\n * @public\n * @param data - The SpriteSpin instance data\n */\nexport function applyAnimation(data: Data) {\n  const state = getPlaybackState(data)\n  if (state.handler && (!data.animate || state.frameTime !== data.frameTime)) {\n    stopAnimation(data)\n  }\n  if (data.animate && !state.handler) {\n    state.frameTime = data.frameTime\n    state.handler = window.setInterval(() => updateAnimation(data), state.frameTime)\n  }\n}\n\n/**\n * Starts the animation playback\n *\n * @remarks\n * Starts the animation playback and also sets the `animate` property to `true`\n *\n * @public\n * @param data - The SpriteSpin instance data\n */\nexport function startAnimation(data: Data) {\n  data.animate = true\n  applyAnimation(data)\n}\n"
  },
  {
    "path": "src/core/plugins.ts",
    "content": "import { error, warn } from '../utils'\nimport { Callback, CallbackOptions, Data } from './models'\n\n/**\n * Describes a SpriteSpin plugin\n *\n * @public\n */\nexport interface SpriteSpinPlugin extends CallbackOptions {\n  [key: string]: Callback | string\n  name?: string\n}\n\nconst plugins: { [key: string]: SpriteSpinPlugin } = {}\n\n/**\n * Registers a plugin.\n *\n * @remarks\n * Use this to add custom Rendering or Updating modules that can be addressed with the 'module' option.\n *\n * @public\n * @param name - The name of the plugin\n * @param plugin - The plugin implementation\n */\nexport function registerPlugin(name: string, plugin: SpriteSpinPlugin) {\n  if (plugins[name]) {\n    error(`Plugin name \"${name}\" is already taken`)\n    return\n  }\n  plugin = plugin || {}\n  plugins[name] = plugin\n  return plugin\n}\n\n/**\n * Registers a plugin.\n *\n * @public\n * @deprecated Use {@link registerPlugin} instead\n * @param name - The name of the plugin\n * @param plugin - The plugin implementation\n */\nexport function registerModule(name: string, plugin: SpriteSpinPlugin) {\n  warn('\"registerModule\" is deprecated, use \"registerPlugin\" instead')\n  registerPlugin(name, plugin)\n}\n\n/**\n * Gets an active plugin by name\n *\n * @internal\n * @param name - The name of the plugin\n */\nexport function getPlugin(name) {\n  return plugins[name]\n}\n\n/**\n * Replaces module names on given SpriteSpin data and replaces them with actual implementations.\n * @internal\n */\nexport function applyPlugins(data: Data) {\n  fixPlugins(data)\n  for (let i = 0; i < data.plugins.length; i += 1) {\n    const name = data.plugins[i]\n    if (typeof name !== 'string') {\n      continue\n    }\n    const plugin = getPlugin(name)\n    if (!plugin) {\n      error('No plugin found with name ' + name)\n      continue\n    }\n    data.plugins[i] = plugin\n  }\n}\n\nfunction fixPlugins(data: Data) {\n  // tslint:disable no-string-literal\n\n  if (data['mods']) {\n    warn('\"mods\" option is deprecated, use \"plugins\" instead')\n    data.plugins = data['mods']\n    delete data['mods']\n  }\n\n  if (data['behavior']) {\n    warn('\"behavior\" option is deprecated, use \"plugins\" instead')\n    data.plugins.push(data['behavior'])\n    delete data['behavior']\n  }\n\n  if (data['module']) {\n    warn('\"module\" option is deprecated, use \"plugins\" instead')\n    data.plugins.push(data['module'])\n    delete data['module']\n  }\n}\n"
  },
  {
    "path": "src/core/state.ts",
    "content": "import { Data } from './models'\n\n/**\n * Gets a state object by name.\n * @internal\n * @param data - The SpriteSpin instance data\n * @param name - The name of the state object\n */\nexport function getState<T = any>(data: Data, name: string): T {\n  data.state = data.state || {}\n  data.state[name] = data.state[name] || {}\n  return data.state[name]\n}\n\n/**\n * Gets a plugin state object by name.\n *\n * @remarks\n * Plugins should use this method to get or create a state object where they can\n * store any instance variables.\n *\n * @public\n * @param data - The SpriteSpin instance data\n * @param name - The name of the plugin\n */\nexport function getPluginState<T = any>(data: Data, name: string): T {\n  const state = getState<T>(data, 'plugin')\n  state[name] = state[name] || {}\n  return state[name]\n}\n\n/**\n * Checks whether a flag is set. See {@link flag}.\n *\n * @public\n * @param data - The SpriteSpin instance data\n * @param key - The name of the flag\n */\nexport function is(data: Data, key: string): boolean {\n  return !!getState(data, 'flags')[key]\n}\n\n/**\n * Sets a flag value. See {@link is}.\n *\n * @public\n * @param data - The SpriteSpin instance data\n * @param key - The name of the flag\n * @param value - The value to set\n */\nexport function flag(data: Data, key: string, value: boolean) {\n  getState(data, 'flags')[key] = !!value\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "export * from './core'\nexport { sourceArray } from './utils'\n\nimport {\n  $,\n  bind,\n  clamp,\n  detectSubsampling,\n  error,\n  findSpecs,\n  getComputedSize,\n  getCursorPosition,\n  getInnerLayout,\n  getInnerSize,\n  getOuterSize,\n  isFunction,\n  Layout,\n  Layoutable,\n  log,\n  measure,\n  MeasureSheetOptions,\n  naturalSize,\n  noop,\n  pixelRatio,\n  preload,\n  PreloadOptions,\n  PreloadProgress,\n  prevent,\n  SheetSpec,\n  SizeWithAspect,\n  sourceArray,\n  SourceArrayOptions,\n  SpriteSpec,\n  toArray,\n  unbind,\n  warn,\n  wrap\n} from './utils'\n\nexport const Utils = {\n  $,\n  bind,\n  clamp,\n  detectSubsampling,\n  error,\n  findSpecs,\n  getComputedSize,\n  getCursorPosition,\n  getInnerLayout,\n  getInnerSize,\n  getOuterSize,\n  isFunction,\n  log,\n  measure,\n  naturalSize,\n  noop,\n  pixelRatio,\n  preload,\n  prevent,\n  sourceArray,\n  toArray,\n  unbind,\n  warn,\n  wrap\n}\n\nexport {\n  Layout,\n  Layoutable,\n  MeasureSheetOptions,\n  PreloadOptions,\n  PreloadProgress,\n  SheetSpec,\n  SizeWithAspect,\n  SourceArrayOptions,\n  SpriteSpec\n}\n\nimport './api'\nimport './plugins'\n"
  },
  {
    "path": "src/lib.test.ts",
    "content": "import { $ } from './utils'\n\nexport const WHITE40x30 = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAQAAAD01JRWAAAAJElEQVR42u3MMQEAAAgDINc/9Ezg5wkBSDuvIhQKhUKhUCi8LW/iO+NtzNg6AAAAAElFTkSuQmCC'\nexport const WHITE50x50 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAQAAAC0NkA6AAAALUlEQVR42u3NMQEAAAgDINc/9Mzg4QcFSDvvIpFIJBKJRCKRSCQSiUQikUhuFtAOY89wCn1dAAAAAElFTkSuQmCC'\nexport const RED40x30 = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAYAAABe3VzdAAAAMUlEQVR42u3OMREAAAjEMN6/aMAAO0N6FZB01f63AAICAgICAgICAgICAgICAgICXg1dLDvjAn5XTwAAAABJRU5ErkJggg=='\nexport const GREEN40x30 = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAYAAABe3VzdAAAAMElEQVR42u3OMQEAAAjDMOZfNGCAnyOtgaR6f1wAAQEBAQEBAQEBAQEBAQEBAQGvBj9KO+MrG0hPAAAAAElFTkSuQmCC'\nexport const BLUE40x30 = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAYAAABe3VzdAAAAL0lEQVR42u3OMQEAAAgDoK1/aE3g7wEJaDKTxyooKCgoKCgoKCgoKCgoKCgoKHhZIWg740ZlTUgAAAAASUVORK5CYII='\n\nexport function mouseEvent(name: string, clientX: number, clientY: number) {\n  const e = document.createEvent('MouseEvent') as MouseEvent\n  e.initMouseEvent(name, true, true, window, 0, 0, 0, clientX, clientY, false, false, false, false, 0, element)\n  return e\n}\n\nexport function mouseDown(el: HTMLElement, x, y) {\n  el.dispatchEvent(mouseEvent('mousedown', x, y))\n}\nexport function mouseUp(el: HTMLElement, x, y) {\n  el.dispatchEvent(mouseEvent('mouseup', x, y))\n}\nexport function mouseLeave(el: HTMLElement, x, y) {\n  el.dispatchEvent(mouseEvent('mouseleave', x, y))\n}\nexport function mouseMove(el: HTMLElement, x, y) {\n  el.dispatchEvent(mouseEvent('mousemove', x, y))\n}\nexport function touchStart(el: HTMLElement, x, y) {\n  el.dispatchEvent(mouseEvent('touchstart', x, y))\n}\nexport function touchMove(el: HTMLElement, x, y) {\n  el.dispatchEvent(mouseEvent('touchmove', x, y))\n}\nexport function touchEnd(el: HTMLElement, x, y) {\n  el.dispatchEvent(mouseEvent('touchend', x, y))\n}\n\nexport function moveMouse(el: HTMLElement, startX, startY, endX, endY) {\n  mouseMove(el, startX, startY)\n  mouseMove(el, endX, endY)\n}\nexport function moveTouch(el: HTMLElement, startX, startY, endX, endY) {\n  touchMove(el, startX, startY)\n  touchMove(el, endX, endY)\n}\nexport function dragMouse(el: HTMLElement, startX, startY, endX, endY) {\n  mouseDown(el, startX, startY)\n  mouseMove(el, endX, endY)\n  mouseUp(el, endX, endY)\n}\nexport function dragTouch(el: HTMLElement, startX, startY, endX, endY) {\n  touchStart(el, startX, startY)\n  touchMove(el, endX, endY)\n  touchEnd(el, endX, endY)\n}\n\nexport function getEl(): HTMLElement {\n  return element\n}\nexport function get$El(): any {\n  return $(element)\n}\n\nlet element: HTMLElement\n\nbeforeAll(() => {\n  $(document.body).css({ margin: 0, padding: 0 })\n})\n\nbeforeEach(() => {\n  $(document.body).append('<div class=\"spritespin\"></div>')\n  element = $('.spritespin')[0]\n})\nafterEach(() => {\n  element.remove()\n})\n"
  },
  {
    "path": "src/plugins/index.ts",
    "content": "import './input-click'\nimport './input-drag'\nimport './input-hold'\nimport './input-swipe'\nimport './input-wheel'\nimport './progress'\nimport './render-360'\nimport './render-blur'\nimport './render-ease'\nimport './render-gallery'\nimport './render-panorama'\nimport './render-zoom'\n"
  },
  {
    "path": "src/plugins/input-click.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#input-click', () => {\n\n  let data: SpriteSpin.Data\n\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: t.WHITE50x50,\n      width: 10,\n      height: 10,\n      frames: 25,\n      onLoad: done,\n      plugins: ['click', '360']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('interaction', () => {\n    describe('in loading state', () => {\n      it ('is idle', () => {\n        data.loading = true\n        expect(data.frame).toBe(0, 'initial')\n        t.mouseUp(t.getEl(), 0, 0)\n        expect(data.frame).toBe(0)\n        t.mouseUp(t.getEl(), 10, 0)\n        expect(data.frame).toBe(0)\n        t.mouseUp(t.getEl(), 0, 10)\n        expect(data.frame).toBe(0)\n        t.mouseUp(t.getEl(), 10, 10)\n        expect(data.frame).toBe(0)\n      })\n    })\n\n    describe('in horizontal mode', () => {\n      beforeEach(() => {\n        data.orientation = 'horizontal'\n      })\n      it ('decrements frame on left click', () => {\n        expect(data.frame).toBe(0, 'initial')\n        t.mouseUp(t.getEl(), 0, 5)\n        expect(data.frame).toBe(data.frames - 1)\n        t.mouseUp(t.getEl(), 5, 5)\n        expect(data.frame).toBe(data.frames - 2)\n      })\n\n      it ('increments frame on right click', () => {\n        expect(data.frame).toBe(0, 'initial')\n        t.mouseUp(t.getEl(), 6, 5)\n        expect(data.frame).toBe(1)\n        t.mouseUp(t.getEl(), 10, 5)\n        expect(data.frame).toBe(2)\n      })\n    })\n\n    describe('in vertical mode', () => {\n      beforeEach(() => {\n        data.orientation = 'vertical'\n      })\n\n      it ('decrements frame on upper click', () => {\n        expect(data.frame).toBe(0, 'initial')\n        t.mouseUp(t.getEl(), 5, 0)\n        expect(data.frame).toBe(data.frames - 1)\n        t.mouseUp(t.getEl(), 5, 5)\n        expect(data.frame).toBe(data.frames - 2)\n      })\n\n      it ('increments frame on lower click', () => {\n        expect(data.frame).toBe(0, 'initial')\n        t.mouseUp(t.getEl(), 5, 6)\n        expect(data.frame).toBe(1)\n        t.mouseUp(t.getEl(), 5, 10)\n        expect(data.frame).toBe(2)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "src/plugins/input-click.ts",
    "content": "import * as SpriteSpin from '../core'\n\n(() => {\n\nconst NAME = 'click'\nfunction click(e, data: SpriteSpin.Data) {\n  if (data.loading || !data.stage.is(':visible')) {\n    return\n  }\n  SpriteSpin.updateInput(e, data)\n  const input = SpriteSpin.getInputState(data)\n\n  let half, pos\n  const target = data.target, offset = target.offset()\n  if (data.orientation === 'horizontal') {\n    half = target.innerWidth() / 2\n    pos = input.currentX - offset.left\n  } else {\n    half = target.innerHeight() / 2\n    pos = input.currentY - offset.top\n  }\n  SpriteSpin.updateFrame(data, data.frame + (pos > half ? 1 : -1))\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n  mouseup: click,\n  touchend: click\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/input-drag.test.ts",
    "content": "import * as SpriteSpin from '../'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#input-drag', () => {\n\n  const FRAME_WIDHT = 10\n  const FRAME_HEIGHT = 10\n  let data: SpriteSpin.Data\n\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: t.WHITE50x50,\n      width: FRAME_WIDHT,\n      height: FRAME_HEIGHT,\n      frames: 25,\n      onLoad: done,\n      animate: false,\n      plugins: ['drag', '360']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('setup', () => {\n    it ('contains drag plugin', () => {\n      expect(data.plugins[0].name).toBe('drag')\n    })\n  })\n\n  describe('mouse interaction', () => {\n    describe('without click', () => {\n      it ('ignores events', () => {\n        expect(data.frame).toBe(0)\n        t.mouseMove(t.getEl(), 0, 0)\n        expect(data.frame).toBe(0)\n        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)\n        expect(data.frame).toBe(0)\n      })\n    })\n\n    describe('with click', () => {\n\n      it ('sets \"dragging\" flag on mousedown', () => {\n        expect(SpriteSpin.is(data, 'dragging')).toBe(false)\n        t.mouseDown(t.getEl(), 0, 0)\n        expect(SpriteSpin.is(data, 'dragging')).toBe(true)\n      })\n\n      it ('removes \"dragging\" flag on mouseup', () => {\n        SpriteSpin.flag(data, 'dragging', true)\n        expect(SpriteSpin.is(data, 'dragging')).toBe(true)\n        t.mouseUp(t.getEl(), 0, 0)\n        expect(SpriteSpin.is(data, 'dragging')).toBe(false)\n      })\n\n      it ('updates frame on horizontal movement', () => {\n        expect(data.frame).toBe(0, 'initial frame')\n        t.mouseDown(t.getEl(), 0, 0)\n        expect(data.frame).toBe(0, 'after click')\n        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)\n        expect(data.frame).toBe(12, 'on move right')\n        t.mouseMove(t.getEl(), 0, 0)\n        expect(data.frame).toBe(0, 'on move left')\n        t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)\n        expect(data.frame).toBe(0, 'on move down')\n        t.mouseMove(t.getEl(), 0, 0)\n        expect(data.frame).toBe(0, 'on move up')\n      })\n\n      it ('updates frame on vertical movement', () => {\n        data.orientation = 'vertical'\n        expect(data.frame).toBe(0, 'initial frame')\n        t.mouseDown(t.getEl(), 0, 0)\n        expect(data.frame).toBe(0, 'after click')\n        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)\n        expect(data.frame).toBe(0, 'on move right')\n        t.mouseMove(t.getEl(), 0, 0)\n        expect(data.frame).toBe(0, 'on move left')\n        t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)\n        expect(data.frame).toBe(12, 'on move vertical')\n        t.mouseMove(t.getEl(), 0, 0)\n        expect(data.frame).toBe(0, 'on move vertical')\n      })\n\n      it ('updates frame on angle axis movement', () => {\n        data.orientation = 45\n        expect(data.frame).toBe(0, 'initial frame')\n        t.mouseDown(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)\n        expect(data.frame).toBe(0, 'after click')\n        t.mouseMove(t.getEl(), FRAME_WIDHT, FRAME_WIDHT)\n        expect(data.frame).toBe(0, 'on move to lower right')\n        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)\n        expect(data.frame).toBe(0, 'on move to center')\n        t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)\n        expect(data.frame).toBe(16, 'on move to lower left')\n        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)\n        expect(data.frame).toBe(0, 'on move to center')\n      })\n\n      it ('updates the frame', () => {\n        expect(data.frame).toBe(0, 'initial frame')\n        t.mouseDown(t.getEl(), 0, 0)\n        expect(data.frame).toBe(0, 'after click')\n        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)\n        expect(data.frame).toBe(12, 'on move right')\n        t.mouseMove(t.getEl(), 0, 0)\n        expect(data.frame).toBe(0, 'on move left')\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "src/plugins/input-drag.ts",
    "content": "import * as SpriteSpin from '../core'\n\n(() => {\n\nconst NAME = 'drag'\n\ninterface DragState {\n  startAt?: number\n  endAt?: number\n  frame?: number\n  lane?: number\n  wasPlaying?: boolean\n  minTime: number\n  maxTime: number\n}\n\nfunction getState(data: SpriteSpin.Data) {\n  return SpriteSpin.getPluginState(data, NAME) as DragState\n}\n\nfunction getAxis(data: SpriteSpin.Data) {\n  if (typeof data.orientation === 'number') {\n    return data.orientation * Math.PI / 180\n  }\n  if (data.orientation === 'horizontal') {\n    return 0\n  }\n  return Math.PI / 2\n}\n\nfunction onInit(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  const d = [200, 1500]\n  const t = data.touchScrollTimer || d\n  state.minTime = t[0] || d[0]\n  state.maxTime = t[1] || d[1]\n}\n\nfunction dragStart(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  if (data.loading || SpriteSpin.is(data, 'dragging') || data['zoomPinFrame'] && !data.stage.is(':visible')) {\n    return\n  }\n\n  // Touch scroll can only be disabled by cancelling the 'touchstart' event.\n  // If we would try to cancel the 'touchmove' event during a scroll\n  // chrome browser raises an error\n  //\n  // When a user interacts with sprite spin, we don't know whether the intention\n  // is to scroll the page or to roll the spin.\n  //\n  // On first interaction with SpriteSpin the scroll is not disabled\n  // On double tap within 200ms the scroll is not disabled\n  // Scroll is only disabled if there was an interaction with SpriteSpin in the past 1500ms\n\n  const now = new Date().getTime()\n  if (state.endAt && (now - state.endAt > state.maxTime)) {\n    // reset timer if the user has no interaction with spritespin within 1500ms\n    state.startAt = null\n    state.endAt = null\n  }\n  if (state.startAt && (now - state.startAt > state.minTime)) {\n    // disable scroll only if there was already an interaction with spritespin\n    // however, allow scrolling on double tab within 200ms\n    e.preventDefault()\n  }\n\n  state.startAt = now\n  state.wasPlaying = !!SpriteSpin.getPlaybackState(data).handler\n\n  state.frame = data.frame || 0\n  state.lane = data.lane || 0\n  SpriteSpin.flag(data, 'dragging', true)\n  SpriteSpin.updateInput(e, data)\n}\n\nfunction dragEnd(e, data: SpriteSpin.Data) {\n  if (SpriteSpin.is(data, 'dragging')) {\n    getState(data).endAt = new Date().getTime()\n    SpriteSpin.flag(data, 'dragging', false)\n    SpriteSpin.resetInput(data)\n    if (data.retainAnimate && getState(data).wasPlaying) {\n      SpriteSpin.startAnimation(data)\n    }\n  }\n}\n\nfunction drag(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  const input = SpriteSpin.getInputState(data)\n  if (!SpriteSpin.is(data, 'dragging')) { return }\n  SpriteSpin.updateInput(e, data)\n\n  const rad = getAxis(data)\n  const sn = Math.sin(rad)\n  const cs = Math.cos(rad)\n  const x = ((input.nddX * cs - input.nddY * sn) * data.sense) || 0\n  const y = ((input.nddX * sn + input.nddY * cs) * (data.senseLane || data.sense)) || 0\n\n  // accumulate\n  state.frame += data.frames * x\n  state.lane += data.lanes * y\n\n  // update spritespin\n  const oldFrame = data.frame\n  const oldLane = data.lane\n  SpriteSpin.updateFrame(data, Math.floor(state.frame), Math.floor(state.lane))\n  SpriteSpin.stopAnimation(data)\n}\n\nfunction mousemove(e, data) {\n  dragStart(e, data)\n  drag(e, data)\n}\n\nSpriteSpin.registerPlugin('drag', {\n  name: 'drag',\n  onInit: onInit,\n\n  mousedown: dragStart,\n  mousemove: drag,\n  mouseup: dragEnd,\n\n  documentmousemove: drag,\n  documentmouseup: dragEnd,\n\n  touchstart: dragStart,\n  touchmove: drag,\n  touchend: dragEnd,\n  touchcancel: dragEnd\n})\n\nSpriteSpin.registerPlugin('move', {\n  name: 'move',\n  onInit: onInit,\n\n  mousemove: mousemove,\n  mouseleave: dragEnd,\n\n  touchstart: dragStart,\n  touchmove: drag,\n  touchend: dragEnd,\n  touchcancel: dragEnd\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/input-hold.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#input-hold', () => {\n\n  const FRAME_WIDHT = 10\n  const FRAME_HEIGHT = 10\n\n  let data: SpriteSpin.Data\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: t.WHITE50x50,\n      width: FRAME_WIDHT,\n      height: FRAME_HEIGHT,\n      frames: 25,\n      onLoad: done,\n      animate: false,\n      plugins: ['hold', '360']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('setup', () => {\n    it ('contains hold plugin', () => {\n      expect(data.plugins[0].name).toBe('hold')\n    })\n  })\n\n  describe('mouse interaction', () => {\n\n    it ('sets \"dragging\" flag on mousedown', () => {\n      expect(SpriteSpin.is(data, 'dragging')).toBe(false)\n      t.mouseDown(t.getEl(), 0, 0)\n      expect(SpriteSpin.is(data, 'dragging')).toBe(true)\n    })\n\n    it ('starts animation mousedown', () => {\n      expect(data.animate).toBe(false)\n      t.mouseDown(t.getEl(), 0, 0)\n      expect(data.animate).toBe(true)\n    })\n\n    it ('removes \"dragging\" flag on mouseup', () => {\n      SpriteSpin.flag(data, 'dragging', true)\n      expect(SpriteSpin.is(data, 'dragging')).toBe(true)\n      t.mouseUp(t.getEl(), 0, 0)\n      expect(SpriteSpin.is(data, 'dragging')).toBe(false)\n    })\n\n    xit ('removes \"dragging\" flag on mouseleave', () => {\n      SpriteSpin.flag(data, 'dragging', true)\n      expect(SpriteSpin.is(data, 'dragging')).toBe(true)\n      t.mouseLeave(t.getEl(), 0, 0)\n      expect(SpriteSpin.is(data, 'dragging')).toBe(false)\n    })\n\n    it ('ignores move event if not dragging', () => {\n      expect(SpriteSpin.is(data, 'dragging')).toBe(false)\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(SpriteSpin.is(data, 'dragging')).toBe(false)\n    })\n\n    it ('update frameTime on horizontal move', () => {\n      const time = data.frameTime\n      t.mouseDown(t.getEl(), 0, 0)\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(data.frameTime).toBe(20)\n      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)\n      expect(data.frameTime).toBe(100)\n      t.mouseMove(t.getEl(), FRAME_WIDHT, 0)\n      expect(data.frameTime).toBe(20)\n    })\n\n    it ('update frameTime on vertical move', () => {\n      const time = data.frameTime\n      data.orientation = 'vertical'\n      t.mouseDown(t.getEl(), 0, 0)\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(data.frameTime).toBe(20)\n      t.mouseMove(t.getEl(), 0, FRAME_HEIGHT / 2)\n      expect(data.frameTime).toBe(100)\n      t.mouseMove(t.getEl(), 0, FRAME_HEIGHT)\n      expect(data.frameTime).toBe(20)\n    })\n  })\n})\n"
  },
  {
    "path": "src/plugins/input-hold.ts",
    "content": "import * as SpriteSpin from '../core'\n\n(() => {\n\nconst NAME = 'hold'\n\ninterface HoldState {\n  frameTime: number\n  animate: boolean\n  reverse: boolean\n}\n\nfunction getState(data: SpriteSpin.Data) {\n  return SpriteSpin.getPluginState(data, NAME) as HoldState\n}\n\nfunction rememberOptions(data: SpriteSpin.Data) {\n  const state = getState(data)\n  state.frameTime = data.frameTime\n  state.animate = data.animate\n  state.reverse = data.reverse\n}\n\nfunction restoreOptions(data: SpriteSpin.Data) {\n  const state = getState(data)\n  data.frameTime = state.frameTime\n  data.animate = state.animate\n  data.reverse = state.reverse\n}\n\nfunction start(e, data: SpriteSpin.Data) {\n  if (SpriteSpin.is(data, 'loading') || SpriteSpin.is(data, 'dragging') || !data.stage.is(':visible')) {\n    return\n  }\n  rememberOptions(data)\n  SpriteSpin.updateInput(e, data)\n  SpriteSpin.flag(data, 'dragging', true)\n  data.animate = true\n  SpriteSpin.applyAnimation(data)\n}\n\nfunction stop(e, data: SpriteSpin.Data) {\n  SpriteSpin.flag(data, 'dragging', false)\n  SpriteSpin.resetInput(data)\n  SpriteSpin.stopAnimation(data)\n  restoreOptions(data)\n  SpriteSpin.applyAnimation(data)\n}\n\nfunction update(e, data: SpriteSpin.Data) {\n  if (!SpriteSpin.is(data, 'dragging')) {\n    return\n  }\n  SpriteSpin.updateInput(e, data)\n  const input = SpriteSpin.getInputState(data)\n\n  let half, delta\n  const target = data.target, offset = target.offset()\n  if (data.orientation === 'horizontal') {\n    half = target.innerWidth() / 2\n    delta = (input.currentX - offset.left - half) / half\n  } else {\n    half = (data.height / 2)\n    delta = (input.currentY - offset.top - half) / half\n  }\n  data.reverse = delta < 0\n  delta = delta < 0 ? -delta : delta\n  data.frameTime = 80 * (1 - delta) + 20\n\n  if (((data.orientation === 'horizontal') && (input.dX < input.dY)) ||\n    ((data.orientation === 'vertical') && (input.dX < input.dY))) {\n    e.preventDefault()\n  }\n}\n\nfunction onFrame(e, data: SpriteSpin.Data) {\n  data.animate = true\n  SpriteSpin.applyAnimation(data)\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n\n  mousedown: start,\n  mousemove: update,\n  mouseup: stop,\n  mouseleave: stop,\n\n  touchstart: start,\n  touchmove: update,\n  touchend: stop,\n  touchcancel: stop,\n\n  onFrame: onFrame\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/input-move.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#input-move', () => {\n\n  const FRAME_WIDHT = 10\n  const FRAME_HEIGHT = 10\n\n  let data: SpriteSpin.Data\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: t.WHITE50x50,\n      width: FRAME_WIDHT,\n      height: FRAME_HEIGHT,\n      frames: 25,\n      onLoad: done,\n      animate: false,\n      plugins: ['move', '360']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('setup', () => {\n    it ('contains move plugin', () => {\n      expect(data.plugins[0].name).toBe('move')\n    })\n  })\n\n  describe('mouse interaction', () => {\n\n    it ('sets \"dragging\" flag on mousemove', () => {\n      expect(SpriteSpin.is(data, 'dragging')).toBe(false)\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(SpriteSpin.is(data, 'dragging')).toBe(true)\n    })\n\n    xit ('removes \"dragging\" flag on mouseleave', () => {\n      SpriteSpin.flag(data, 'dragging', true)\n      expect(SpriteSpin.is(data, 'dragging')).toBe(true)\n      t.mouseLeave(t.getEl(), 0, 0)\n      expect(SpriteSpin.is(data, 'dragging')).toBe(false)\n    })\n\n    it ('updates frame on horizontal movement', () => {\n      expect(data.frame).toBe(0, 'initial frame')\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(data.frame).toBe(0, 'after click')\n      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)\n      expect(data.frame).toBe(12, 'on move right')\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(data.frame).toBe(0, 'on move left')\n      t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)\n      expect(data.frame).toBe(0, 'on move down')\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(data.frame).toBe(0, 'on move up')\n    })\n\n    it ('updates frame on vertical movement', () => {\n      data.orientation = 'vertical'\n      expect(data.frame).toBe(0, 'initial frame')\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(data.frame).toBe(0, 'after click')\n      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)\n      expect(data.frame).toBe(0, 'on move right')\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(data.frame).toBe(0, 'on move left')\n      t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)\n      expect(data.frame).toBe(12, 'on move vertical')\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(data.frame).toBe(0, 'on move vertical')\n    })\n\n    it ('updates frame on angle axis movement', () => {\n      data.orientation = 45\n      expect(data.frame).toBe(0, 'initial frame')\n      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)\n      expect(data.frame).toBe(0, 'after click')\n      t.mouseMove(t.getEl(), FRAME_WIDHT, FRAME_WIDHT)\n      expect(data.frame).toBe(0, 'on move to lower right')\n      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)\n      expect(data.frame).toBe(0, 'on move to center')\n      t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)\n      expect(data.frame).toBe(16, 'on move to lower left')\n      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)\n      expect(data.frame).toBe(0, 'on move to center')\n    })\n\n    it ('updates the frame', () => {\n      expect(data.frame).toBe(0, 'initial frame')\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(data.frame).toBe(0, 'after click')\n      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)\n      expect(data.frame).toBe(12, 'on move right')\n      t.mouseMove(t.getEl(), 0, 0)\n      expect(data.frame).toBe(0, 'on move left')\n    })\n  })\n})\n"
  },
  {
    "path": "src/plugins/input-swipe.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#input-swipe', () => {\n\n  const FRAME_WIDHT = 10\n  const FRAME_HEIGHT = 10\n\n  let data: SpriteSpin.Data\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: t.WHITE50x50,\n      width: FRAME_WIDHT,\n      height: FRAME_HEIGHT,\n      frames: 25,\n      onLoad: done,\n      animate: false,\n      plugins: ['swipe', '360']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('setup', () => {\n    it ('contains swipe plugin', () => {\n      expect(data.plugins[0].name).toBe('swipe')\n    })\n  })\n\n  describe('swipe horizontal', () => {\n    it ('updates frame if swipe distance is 50%', () => {\n      expect(data.frame).toBe(0, 'initial frame')\n      t.dragMouse(t.getEl(), 10, 0, 5, 0)\n      expect(data.frame).toBe(1, 'after swipe')\n      t.dragMouse(t.getEl(), 0, 0, 5, 0)\n      expect(data.frame).toBe(0, 'after swipe')\n    })\n  })\n\n  describe('swipe vertical', () => {\n    it ('updates frame if swipe distance is 50%', () => {\n      data.orientation = 'vertical'\n      expect(data.frame).toBe(0, 'initial frame')\n      t.dragMouse(t.getEl(), 0, 10, 0, 5)\n      expect(data.frame).toBe(1, 'after swipe')\n      t.dragMouse(t.getEl(), 0, 0, 0, 5)\n      expect(data.frame).toBe(0, 'after swipe')\n    })\n  })\n})\n"
  },
  {
    "path": "src/plugins/input-swipe.ts",
    "content": "import * as SpriteSpin from '../core'\n\n(() => {\n\nconst NAME = 'swipe'\n\ninterface SwipeState {\n  fling: number\n  snap: number\n}\n\nfunction getState(data) {\n  return SpriteSpin.getPluginState(data, NAME) as SwipeState\n}\nfunction getOption(data, name, fallback) {\n  return data[name] || fallback\n}\n\nfunction init(e, data) {\n  const state = getState(data)\n  state.fling = getOption(data, 'swipeFling', 10)\n  state.snap = getOption(data, 'swipeSnap', 0.50)\n}\n\nfunction start(e, data: SpriteSpin.Data) {\n  if (!data.loading && !SpriteSpin.is(data, 'dragging')) {\n    SpriteSpin.updateInput(e, data)\n    SpriteSpin.flag(data, 'dragging', true)\n  }\n}\n\nfunction update(e, data: SpriteSpin.Data) {\n  if (!SpriteSpin.is(data, 'dragging')) {\n    return\n  }\n  SpriteSpin.updateInput(e, data)\n  const frame = data.frame\n  const lane = data.lane\n  SpriteSpin.updateFrame(data, frame, lane)\n}\n\nfunction end(e, data: SpriteSpin.Data) {\n  if (!SpriteSpin.is(data, 'dragging')) {\n    return\n  }\n  SpriteSpin.flag(data, 'dragging', false)\n\n  const state = getState(data)\n  const input = SpriteSpin.getInputState(data)\n\n  let frame = data.frame\n  const lane = data.lane\n  const snap = state.snap\n  const fling = state.fling\n  let dS, dF\n  if (data.orientation === 'horizontal') {\n    dS = input.ndX\n    dF = input.ddX\n  } else {\n    dS = input.ndY\n    dF = input.ddY\n  }\n\n  if (dS >= snap || dF >= fling) {\n    frame = data.frame - 1\n  } else if (dS <= -snap || dF <= -fling) {\n    frame = data.frame + 1\n  }\n\n  SpriteSpin.resetInput(data)\n  SpriteSpin.updateFrame(data, frame, lane)\n  SpriteSpin.stopAnimation(data)\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n  onLoad: init,\n  mousedown: start,\n  mousemove: update,\n  mouseup: end,\n  mouseleave: end,\n\n  touchstart: start,\n  touchmove: update,\n  touchend: end,\n  touchcancel: end\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/input-wheel.ts",
    "content": "import * as SpriteSpin from '../core'\n\n(() => {\n\nconst NAME = 'wheel'\nfunction wheel(e: JQueryMouseEventObject, data: SpriteSpin.Data) {\n  if (!data.loading && data.stage.is(':visible')) {\n    e.preventDefault()\n\n    const we = e.originalEvent as WheelEvent\n    const signX = we.deltaX === 0 ? 0 : we.deltaX > 0 ? 1 : -1\n    const signY = we.deltaY === 0 ? 0 : we.deltaY > 0 ? 1 : -1\n\n    SpriteSpin.updateFrame(data, data.frame + signY, data.lane + signX)\n  }\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n  wheel: wheel\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/progress.ts",
    "content": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\ninterface State {\n  stage: JQuery<HTMLElement>\n}\n\nconst template = `\n<div class='spritespin-progress'>\n  <div class='spritespin-progress-label'></div>\n  <div class='spritespin-progress-bar'></div>\n</div>\n`\n\nfunction getState(data: SpriteSpin.Data) {\n  return SpriteSpin.getPluginState<State>(data, NAME)\n}\n\nconst NAME = 'progress'\nfunction onInit(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  if (!state.stage) {\n    state.stage = Utils.$(template)\n    state.stage.appendTo(data.target)\n  }\n\n  state.stage.find('.spritespin-progress-label')\n    .text(`0%`)\n    .css({ 'text-align': 'center' })\n  state.stage.find('.spritespin-progress-bar').css({\n    width: `0%`\n  })\n\n  state.stage.hide().fadeIn()\n}\nfunction onProgress(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  state.stage.find('.spritespin-progress-label')\n    .text(`${data.progress.percent}%`)\n    .css({ 'text-align': 'center' })\n  state.stage.find('.spritespin-progress-bar').css({\n    width: `${data.progress.percent}%`\n  })\n}\n\nfunction onLoad(e, data: SpriteSpin.Data) {\n  Utils.$(getState(data).stage).fadeOut()\n}\n\nfunction onDestroy(e, data: SpriteSpin.Data) {\n  Utils.$(getState(data).stage).remove()\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n  onInit: onInit,\n  onProgress: onProgress,\n  onLoad: onLoad,\n  onDestroy: onDestroy\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/render-360.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport { $ } from '../utils'\n\ndescribe('SpriteSpin.Plugins#render-360', () => {\n\n  const WIDTH = 50\n  const HEIGHT = 50\n  let data: SpriteSpin.Data\n\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: t.WHITE50x50,\n      width: 10,\n      height: 10,\n      frames: 25,\n      onComplete: done,\n      renderer: 'canvas',\n      plugins: ['360']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('renderer = canvas', () => {\n\n    beforeEach((done) => {\n      t.get$El().spritespin({\n        onComplete: done,\n        renderer: 'canvas'\n      })\n    })\n\n    it ('has empty stage', () => {\n      expect(data.stage.find('*').length).toBe(0)\n    })\n\n    it ('shows the stage element', () => {\n      expect(data.stage.is(':visible')).toBe(true)\n    })\n\n    it ('shows the canvas element', () => {\n      expect(data.canvas.is(':visible')).toBe(true)\n    })\n  })\n\n  describe('renderer = background', () => {\n\n    beforeEach((done) => {\n      t.get$El().spritespin({\n        onComplete: done,\n        renderer: 'background'\n      })\n    })\n\n    it ('has empty stage', () => {\n      expect(data.stage.find('*').length).toBe(0)\n    })\n\n    it ('shows the stage element', () => {\n      expect(data.stage.is(':visible')).toBe(true)\n    })\n\n    it ('hides the canvas element', () => {\n      expect(data.canvas.is(':visible')).toBe(false)\n    })\n\n    it ('shows background on stage', () => {\n      expect(data.stage.css('background-image')).toContain(t.WHITE50x50)\n    })\n  })\n\n  describe('renderer = image', () => {\n\n    beforeEach((done) => {\n      t.get$El().spritespin({\n        onComplete: done,\n        renderer: 'image'\n      })\n    })\n\n    it ('has images inside stage', () => {\n      expect(data.stage.find('img').length).toBe(1)\n    })\n\n    it ('shows the stage element', () => {\n      expect(data.stage.is(':visible')).toBe(true)\n    })\n\n    it ('hides the canvas element', () => {\n      expect(data.canvas.is(':visible')).toBe(false)\n    })\n\n    it ('shows the image element', () => {\n      expect($(data.images[0]).is(':visible')).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "src/plugins/render-360.ts",
    "content": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst floor = Math.floor\n\nconst NAME = '360'\n\nfunction onLoad(e, data: SpriteSpin.Data) {\n  data.stage.find('.spritespin-frames').detach()\n  if (data.renderer === 'image') {\n    $(data.images).addClass('spritespin-frames').appendTo(data.stage)\n  }\n}\n\nfunction onDraw(e, data: SpriteSpin.Data) {\n  const specs = Utils.findSpecs(data.metrics, data.frames, data.frame, data.lane)\n  const sheet = specs.sheet\n  const sprite = specs.sprite\n\n  if (!sheet || !sprite) { return }\n  const src = data.source[sheet.id]\n  const image = data.images[sheet.id]\n\n  if (data.renderer === 'canvas') {\n    data.canvas.show()\n    const w = data.canvas[0].width / data.canvasRatio\n    const h = data.canvas[0].height / data.canvasRatio\n    data.context.clearRect(0, 0, w, h)\n    data.context.drawImage(image, sprite.sampledX, sprite.sampledY, sprite.sampledWidth, sprite.sampledHeight, 0, 0, w, h)\n    return\n  }\n\n  const scaleX = data.stage.innerWidth() / sprite.sampledWidth\n  const scaleY = data.stage.innerHeight() / sprite.sampledHeight\n  const top = Math.floor(-sprite.sampledY * scaleY)\n  const left = Math.floor(-sprite.sampledX * scaleX)\n  const width = Math.floor(sheet.sampledWidth * scaleX)\n  const height = Math.floor(sheet.sampledHeight * scaleY)\n  if (data.renderer === 'background') {\n    data.stage.css({\n      'background-image'    : `url('${src}')`,\n      'background-position' : `${left}px ${top}px`,\n      'background-repeat'   : 'no-repeat',\n      // set custom background size to enable responsive rendering\n      '-webkit-background-size' : `${width}px ${height}px`, /* Safari 3-4 Chrome 1-3 */\n      '-moz-background-size'    : `${width}px ${height}px`, /* Firefox 3.6 */\n      '-o-background-size'      : `${width}px ${height}px`, /* Opera 9.5 */\n      'background-size'         : `${width}px ${height}px`  /* Chrome, Firefox 4+, IE 9+, Opera, Safari 5+ */\n    })\n    return\n  }\n\n  $(data.images).hide()\n  $(image).show().css({\n    position: 'absolute',\n    top: top,\n    left: left,\n    'max-width' : 'initial',\n    width: width,\n    height: height\n  })\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n  onLoad,\n  onDraw\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/render-blur.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#render-blur', () => {\n\n  let data: SpriteSpin.Data\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: [t.RED40x30, t.GREEN40x30, t.BLUE40x30],\n      width: 40,\n      height: 30,\n\n      animate: false,\n      onComplete: done,\n      plugins: ['blur']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('basics', () => {\n    it ('has has ease plugin', () => {\n      expect(data.plugins[0].name).toBe('blur')\n    })\n\n    it ('doesnt break', () => {\n      // can not test blur programmatically\n      // thus just call updateFrame several times to step through frames\n      SpriteSpin.updateFrame(data, 0)\n      SpriteSpin.updateFrame(data, 1)\n      SpriteSpin.updateFrame(data, 2)\n    })\n  })\n})\n"
  },
  {
    "path": "src/plugins/render-blur.ts",
    "content": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst NAME = 'blur'\n\ninterface BlurStep {\n  frame: number\n  lane: number\n  live: number\n  step: number\n  d: number\n  alpha: number\n}\ninterface BlurState {\n  canvas: any\n  context: CanvasRenderingContext2D\n  steps: BlurStep[]\n  fadeTime: number\n  frameTime: number\n  trackTime: number\n  cssBlur: boolean\n\n  timeout: number\n}\n\nfunction getState(data) {\n  return SpriteSpin.getPluginState(data, NAME) as BlurState\n}\nfunction getOption(data, name, fallback) {\n  return data[name] || fallback\n}\n\nfunction init(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n\n  state.canvas = state.canvas || Utils.$(\"<canvas class='blur-layer'></canvas>\")\n  state.context = state.context || state.canvas[0].getContext('2d')\n  state.steps = state.steps || []\n  state.fadeTime = Math.max(getOption(data, 'blurFadeTime', 200), 1)\n  state.frameTime = Math.max(getOption(data, 'blurFrameTime', data.frameTime), 16)\n  state.trackTime = null\n  state.cssBlur = !!getOption(data, 'blurCss', false)\n\n  const inner = Utils.getInnerSize(data)\n  const outer = data.responsive ? Utils.getComputedSize(data) : Utils.getOuterSize(data)\n  const css = Utils.getInnerLayout(data.sizeMode, inner, outer)\n\n  state.canvas[0].width = data.width * data.canvasRatio\n  state.canvas[0].height = data.height * data.canvasRatio\n  state.canvas.css(css).show()\n  state.context.scale(data.canvasRatio, data.canvasRatio)\n  data.target.append(state.canvas)\n}\n\nfunction onFrame(e, data) {\n  const state = getState(data)\n  trackFrame(data)\n  if (state.timeout == null) {\n    loop(data)\n  }\n}\n\nfunction trackFrame(data: SpriteSpin.Data) {\n  const state = getState(data)\n  const ani = SpriteSpin.getPlaybackState(data)\n\n  // distance between frames\n  let d = Math.abs(data.frame - ani.lastFrame)\n  // shortest distance\n  d = d >= data.frames / 2 ? data.frames - d : d\n\n  state.steps.unshift({\n    frame: data.frame,\n    lane: data.lane,\n    live: 1,\n    step: state.frameTime / state.fadeTime,\n    d: d,\n    alpha: 0\n  })\n}\n\nconst toRemove = []\nfunction removeOldFrames(frames) {\n  toRemove.length = 0\n  for (let i = 0; i < frames.length; i += 1) {\n    if (frames[i].alpha <= 0) {\n      toRemove.push(i)\n    }\n  }\n  for (const item of toRemove) {\n    frames.splice(item, 1)\n  }\n}\n\nfunction loop(data: SpriteSpin.Data) {\n  const state = getState(data)\n  state.timeout = window.setTimeout(() => { tick(data) }, state.frameTime)\n}\n\nfunction killLoop(data: SpriteSpin.Data) {\n  const state = getState(data)\n  window.clearTimeout(state.timeout)\n  state.timeout = null\n}\n\nfunction applyCssBlur(canvas, d) {\n  const amount = Math.min(Math.max((d / 2) - 4, 0), 2.5)\n  const blur = `blur(${amount}px)`\n  canvas.css({\n    '-webkit-filter': blur,\n    filter: blur\n  })\n}\n\nfunction clearFrame(data: SpriteSpin.Data, state: BlurState) {\n  state.canvas.show()\n  const w = state.canvas[0].width / data.canvasRatio\n  const h = state.canvas[0].height / data.canvasRatio\n  // state.context.clearRect(0, 0, w, h)\n}\n\nfunction drawFrame(data: SpriteSpin.Data, state: BlurState, step: BlurStep) {\n  if (step.alpha <= 0) { return }\n\n  const specs = Utils.findSpecs(data.metrics, data.frames, step.frame, step.lane)\n  const sheet = specs.sheet\n  const sprite = specs.sprite\n  if (!sheet || !sprite) { return }\n\n  const src = data.source[sheet.id]\n  const image = data.images[sheet.id]\n  if (image.complete === false) { return }\n\n  state.canvas.show()\n  const w = state.canvas[0].width / data.canvasRatio\n  const h = state.canvas[0].height / data.canvasRatio\n  state.context.globalAlpha = step.alpha\n  state.context.drawImage(image, sprite.sampledX, sprite.sampledY, sprite.sampledWidth, sprite.sampledHeight, 0, 0, w, h)\n}\n\nfunction tick(data: SpriteSpin.Data) {\n  const state = getState(data)\n  killLoop(data)\n  if (!state.context) {\n    return\n  }\n\n  let d = 0\n  clearFrame(data, state)\n  state.context.clearRect(0, 0, data.width, data.height)\n  for (const step of state.steps) {\n    step.live = Math.max(step.live - step.step, 0)\n    step.alpha = Math.max(step.live - 0.25, 0)\n    drawFrame(data, state, step)\n    d += step.alpha + step.d\n  }\n  if (state.cssBlur) {\n    applyCssBlur(state.canvas, d)\n  }\n  removeOldFrames(state.steps)\n  if (state.steps.length) {\n    loop(data)\n  }\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n\n  onLoad: init,\n  onFrameChanged: onFrame\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/render-ease.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#render-ease', () => {\n\n  let data: SpriteSpin.Data\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: [t.RED40x30, t.GREEN40x30, t.BLUE40x30],\n      width: 40,\n      height: 30,\n\n      animate: false,\n      onComplete: done,\n      plugins: ['drag', 'ease']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('basics', () => {\n    it ('has has ease plugin', () => {\n      expect(data.plugins[1].name).toBe('ease')\n    })\n    it ('doesnt break', (done) => {\n      // can not test ease programmatically\n      // thus just emulate mouse drag\n      t.mouseDown(t.getEl(), 0, 0)\n      setTimeout(() => {\n        t.mouseMove(t.getEl(), 5, 5)\n        setTimeout(() => {\n          t.mouseMove(t.getEl(), 10, 5)\n          t.mouseUp(t.getEl(), 10, 5)\n          done()\n        }, 16)\n      }, 16)\n    })\n  })\n\n})\n"
  },
  {
    "path": "src/plugins/render-ease.ts",
    "content": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst max = Math.max\nconst min = Math.min\n\nconst NAME = 'ease'\n\ninterface EaseSample {\n  time: number\n  frame: number\n  lane: number\n}\n\ninterface EaseState {\n  damping: number\n  maxSamples: number\n  updateTime: number\n  abortTime: number\n  samples: EaseSample[]\n  steps: number[]\n  handler: number\n\n  lane: number\n  lanes: number\n  laneStep: number\n\n  frame: number\n  frames: number\n  frameStep: number\n}\n\nfunction getState(data) {\n  return SpriteSpin.getPluginState(data, NAME) as EaseState\n}\n\nfunction getOption(data, name, fallback) {\n  return data[name] || fallback\n}\n\nfunction init(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  state.maxSamples = max(getOption(data, 'easeMaxSamples', 5), 0)\n  state.damping = max(min(getOption(data, 'easeDamping', 0.9), 0.999), 0)\n  state.abortTime = max(getOption(data, 'easeAbortTime', 250), 16)\n  state.updateTime = max(getOption(data, 'easeUpdateTime', data.frameTime), 16)\n  state.samples = []\n  state.steps = []\n}\n\nfunction update(e, data: SpriteSpin.Data) {\n  if (SpriteSpin.is(data, 'dragging')) {\n    killLoop(data)\n    sampleInput(data)\n  }\n}\n\nfunction end(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  const samples = state.samples\n\n  let last\n  let lanes = 0\n  let frames = 0\n  let time = 0\n  for (const sample of samples) {\n    if (!last) {\n      last = sample\n      continue\n    }\n\n    const dt = sample.time - last.time\n    if (dt > state.abortTime) {\n      lanes = frames = time = 0\n      return killLoop(data)\n    }\n\n    frames += sample.frame - last.frame\n    lanes += sample.lane - last.lane\n    time += dt\n    last = sample\n  }\n  samples.length = 0\n  if (!time) {\n    return\n  }\n\n  state.lane = data.lane\n  state.lanes = 0\n  state.laneStep = lanes / time * state.updateTime\n\n  state.frame = data.frame\n  state.frames = 0\n  state.frameStep = frames / time * state.updateTime\n\n  loop(data)\n}\n\nfunction sampleInput(data: SpriteSpin.Data) {\n  const state = getState(data)\n  // add a new sample\n  state.samples.push({\n    time: new Date().getTime(),\n    frame: data.frame,\n    lane: data.lane\n  })\n  // drop old samples\n  while (state.samples.length > state.maxSamples) {\n    state.samples.shift()\n  }\n}\n\nfunction killLoop(data: SpriteSpin.Data) {\n  const state = getState(data)\n  if (state.handler != null) {\n    window.clearTimeout(state.handler)\n    state.handler = null\n  }\n}\n\nfunction loop(data: SpriteSpin.Data) {\n  const state = getState(data)\n  state.handler = window.setTimeout(() => { tick(data) }, state.updateTime)\n}\n\nfunction tick(data: SpriteSpin.Data) {\n  const state = getState(data)\n  state.lanes += state.laneStep\n  state.frames += state.frameStep\n  state.laneStep *= state.damping\n  state.frameStep *= state.damping\n  const frame = Math.floor(state.frame + state.frames)\n  const lane = Math.floor(state.lane + state.lanes)\n\n  SpriteSpin.updateFrame(data, frame, lane)\n\n  if (SpriteSpin.is(data, 'dragging')) {\n    killLoop(data)\n  } else if (Math.abs(state.frameStep) > 0.005 || Math.abs(state.laneStep) > 0.005) {\n    loop(data)\n  } else {\n    killLoop(data)\n  }\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n\n  onLoad: init,\n\n  mousemove: update,\n  mouseup: end,\n  mouseleave: end,\n\n  touchmove: update,\n  touchend: end,\n  touchcancel: end\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/render-gallery.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#render-gallery', () => {\n\n  let data: SpriteSpin.Data\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: [t.RED40x30, t.GREEN40x30, t.BLUE40x30],\n      width: 40,\n      height: 30,\n\n      gallerySpeed: 1,\n      galleryOpacity: 0.25,\n\n      animate: false,\n      onComplete: done,\n      plugins: ['gallery']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('basics', () => {\n\n    it ('has has gallery plugin', () => {\n      expect(data.plugins[0].name).toBe('gallery')\n    })\n\n    it ('adds a gallery-stage element', () => {\n      expect(data.target.find('.gallery-stage').length).toBe(1)\n    })\n\n    it ('adds images to gallery-stage', () => {\n      expect(data.target.find('.gallery-stage img').length).toBe(3)\n    })\n\n    it ('highlights current frame image, dims other', (done) => {\n      SpriteSpin.updateFrame(data, 1)\n      setTimeout(() => {\n        expect(data.target.find('.gallery-stage img:nth-child(1)').css('opacity')).toBe('0.25', 'frame 0')\n        expect(data.target.find('.gallery-stage img:nth-child(2)').css('opacity')).toBe('1', 'frame 1')\n        expect(data.target.find('.gallery-stage img:nth-child(3)').css('opacity')).toBe('0.25', 'frame 2')\n\n        SpriteSpin.updateFrame(data, 2)\n        setTimeout(() => {\n          expect(data.target.find('.gallery-stage img:nth-child(1)').css('opacity')).toBe('0.25', 'frame 0')\n          expect(data.target.find('.gallery-stage img:nth-child(2)').css('opacity')).toBe('0.25', 'frame 1')\n          expect(data.target.find('.gallery-stage img:nth-child(3)').css('opacity')).toBe('1', 'frame 2')\n          done()\n        }, 32)\n      }, 32)\n    })\n  })\n})\n"
  },
  {
    "path": "src/plugins/render-gallery.ts",
    "content": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst NAME = 'gallery'\n\ninterface GalleryState {\n  images: any[]\n  offsets: any[]\n  speed: number\n  opacity: number\n  frame: number\n  stage: any\n  dX: number\n  ddX: number\n}\n\nfunction getState(data) {\n  return SpriteSpin.getPluginState(data, NAME) as GalleryState\n}\n\nfunction getOption(data, name, fallback) {\n  return data[name] || fallback\n}\n\nfunction load(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n\n  state.images = []\n  state.offsets = []\n  state.frame = data.frame\n  state.speed = getOption(data, 'gallerySpeed', 500)\n  state.opacity = getOption(data, 'galleryOpacity', 0.25)\n  state.stage = getOption(data, 'galleryStage', Utils.$('<div></div>'))\n\n  state.stage.empty().addClass('gallery-stage').prependTo(data.stage)\n\n  let size = 0\n  for (const image of data.images) {\n    const naturalSize = Utils.naturalSize(image)\n    const scale = data.height / naturalSize.height\n\n    const img = Utils.$(image)\n    state.stage.append(img)\n    state.images.push(img)\n    state.offsets.push(-size + (data.width - image.width * scale) / 2)\n    size += data.width\n    img.css({\n      'max-width' : 'initial',\n      opacity : state.opacity,\n      width: data.width,\n      height: data.height\n    })\n  }\n  const innerSize = Utils.getInnerSize(data)\n  const outerSize = data.responsive ? Utils.getComputedSize(data) : Utils.getOuterSize(data)\n  const layout = Utils.getInnerLayout(data.sizeMode, innerSize, outerSize)\n  state.stage.css(layout).css({ width: size, left: state.offsets[state.frame] })\n  state.images[state.frame].animate({ opacity : 1 }, { duration: state.speed })\n}\n\nfunction draw(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  const input = SpriteSpin.getInputState(data)\n  const isDragging = SpriteSpin.is(data, 'dragging')\n  if (state.frame !== data.frame && !isDragging) {\n    state.stage.stop(true, false).animate({ left : state.offsets[data.frame] }, { duration: state.speed })\n\n    state.images[state.frame].animate({ opacity : state.opacity }, { duration: state.speed })\n    state.frame = data.frame\n    state.images[state.frame].animate({ opacity : 1 }, { duration: state.speed })\n    state.stage.animate({ left : state.offsets[state.frame] })\n  } else if (isDragging || state.dX !== input.dX) {\n    state.dX = input.dX\n    state.ddX = input.ddX\n    state.stage.stop(true, true).css({ left : state.offsets[state.frame] + state.dX })\n  }\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n  onLoad: load,\n  onDraw: draw\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/render-panorama.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#render-panorama', () => {\n\n  let data: SpriteSpin.Data\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: t.WHITE40x30,\n      width: 10,\n      height: 10,\n      animate: false,\n      onComplete: done,\n      plugins: ['panorama']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('basics', () => {\n\n    it ('has has panorama plugin', () => {\n      expect(data.plugins[0].name).toBe('panorama')\n    })\n\n    it ('renders the image as background', () => {\n      expect(data.stage.css('background-image')).toContain(t.WHITE40x30)\n    })\n  })\n\n  describe('horizontal', () => {\n    beforeEach((done) => {\n      t.get$El().spritespin({\n        orientation: 'horizontal',\n        width: 10,\n        height: 30,\n        onComplete: done\n      })\n    })\n\n    it('has horizontal orientation', () => {\n      expect(data.orientation).toBe('horizontal')\n    })\n\n    it('sets frames to image width', () => {\n      expect(data.frames).toBe(40)\n    })\n\n    it('shifts image horizontally', () => {\n      SpriteSpin.updateFrame(data, 1, data.lane)\n      expect(data.stage.css('background-position')).toBe('1px 0px')\n      SpriteSpin.updateFrame(data, 2, data.lane)\n      expect(data.stage.css('background-position')).toBe('2px 0px')\n    })\n  })\n\n  describe('vertical', () => {\n    beforeEach((done) => {\n      t.get$El().spritespin({\n        orientation: 'vertical',\n        width: 40,\n        height: 10,\n        onComplete: done\n      })\n    })\n\n    it('has horizontal orientation', () => {\n      expect(data.orientation).toBe('vertical')\n    })\n\n    it('sets frames to image height', () => {\n      expect(data.frames).toBe(30)\n    })\n\n    it('shifts image vertically', () => {\n      SpriteSpin.updateFrame(data, 1, data.lane)\n      expect(data.stage.css('background-position')).toBe('0px 1px')\n      SpriteSpin.updateFrame(data, 2, data.lane)\n      expect(data.stage.css('background-position')).toBe('0px 2px')\n    })\n  })\n})\n"
  },
  {
    "path": "src/plugins/render-panorama.ts",
    "content": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst NAME = 'panorama'\n\ninterface PanoramaState {\n  scale: number\n}\n\nfunction getState(data) {\n  return SpriteSpin.getPluginState(data, NAME) as PanoramaState\n}\n\nfunction onLoad(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  const sprite = data.metrics[0]\n  if (!sprite) {\n    return\n  }\n\n  if (data.orientation === 'horizontal') {\n    state.scale = data.target.innerHeight() / sprite.sampledHeight\n    data.frames = sprite.sampledWidth\n  } else {\n    state.scale = data.target.innerWidth() / sprite.sampledWidth\n    data.frames = sprite.sampledHeight\n  }\n  const width = Math.floor(sprite.sampledWidth * state.scale)\n  const height = Math.floor(sprite.sampledHeight * state.scale)\n  data.stage.css({\n    'background-image'        : `url(${data.source[sprite.id]})`,\n    'background-repeat'       : 'repeat-both',\n    // set custom background size to enable responsive rendering\n    '-webkit-background-size' : `${width}px ${height}px`, /* Safari 3-4 Chrome 1-3 */\n    '-moz-background-size'    : `${width}px ${height}px`, /* Firefox 3.6 */\n    '-o-background-size'      : `${width}px ${height}px`, /* Opera 9.5 */\n    'background-size'         : `${width}px ${height}px`  /* Chrome, Firefox 4+, IE 9+, Opera, Safari 5+ */\n  })\n}\n\nfunction onDraw(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  const px = data.orientation === 'horizontal' ? 1 : 0\n  const py = px ? 0 : 1\n\n  const offset = data.frame % data.frames\n  const left = Math.round(px * offset * state.scale)\n  const top = Math.round(py * offset * state.scale)\n  data.stage.css({ 'background-position' : `${left}px ${top}px` })\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n  onLoad: onLoad,\n  onDraw: onDraw\n})\n\n})()\n"
  },
  {
    "path": "src/plugins/render-zoom.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport { $ } from '../utils'\n\ndescribe('SpriteSpin.Plugins#render-zoom', () => {\n\n  function doubleTap(x, y, cb) {\n    t.getEl().dispatchEvent(t.mouseEvent('mousedown', x, y))\n    setTimeout(() => {\n      t.getEl().dispatchEvent(t.mouseEvent('mousedown', x, y))\n      setTimeout(cb, 16)\n    }, 16)\n  }\n\n  let data: SpriteSpin.Data\n  beforeEach((done) => {\n    t.get$El().spritespin({\n      source: [t.RED40x30, t.GREEN40x30, t.BLUE40x30],\n      width: 40,\n      height: 30,\n\n      gallerySpeed: 1,\n      galleryOpacity: 0.25,\n\n      animate: false,\n      onComplete: done,\n      plugins: ['zoom']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('basics', () => {\n    it ('has has zoom plugin', () => {\n      expect(data.plugins[0].name).toBe('zoom')\n    })\n\n    it ('adds a zoom-stage element', () => {\n      expect(data.target.find('.zoom-stage').length).toBe(1)\n    })\n\n    it ('hides zoom-stage initially', () => {\n      expect(data.target.find('.zoom-stage').is(':visible')).toBe(false)\n    })\n  })\n\n  describe('double tap', () => {\n    beforeEach(() => {\n      $.fx.off = true\n    })\n    afterEach(() => {\n      $.fx.off = false\n    })\n\n    it ('toggles zoom-stage', (done) => {\n      expect(data.target.find('.zoom-stage').is(':visible')).toBe(false)\n      doubleTap(0, 0, () => {\n        expect(data.target.find('.zoom-stage').is(':visible')).toBe(true)\n        doubleTap(0, 0, () => {\n          expect(data.target.find('.zoom-stage').is(':visible')).toBe(false)\n          done()\n        })\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "src/plugins/render-zoom.ts",
    "content": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst NAME = 'zoom'\n\ninterface ZoomState {\n  source: string[]\n  stage: any\n\n  oldX: number\n  oldY: number\n  currentX: number\n  currentY: number\n\n  clickTime: number\n  doubleClickTime: number\n  useWheel: boolean | number\n  useClick: number\n  pinFrame: boolean\n}\n\nfunction getState(data) {\n  return SpriteSpin.getPluginState(data, NAME) as ZoomState\n}\nfunction getOption(data, name, fallback) {\n  return name in data ? data[name] : fallback\n}\n\nfunction onInit(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  state.source = getOption(data, 'zoomSource', data.source)\n  state.useWheel = getOption(data, 'zoomUseWheel', false)\n  state.useClick = getOption(data, 'zoomUseClick', true)\n  state.pinFrame = getOption(data, 'zoomPinFrame', true)\n  state.doubleClickTime = getOption(data, 'zoomDoubleClickTime', 500)\n  state.stage = state.stage || Utils.$(\"<div class='zoom-stage'></div>\")\n  state.stage.css({\n    width    : '100%',\n    height   : '100%',\n    top      : 0,\n    left     : 0,\n    bottom   : 0,\n    right    : 0,\n    position : 'absolute'\n  })\n  .appendTo(data.target)\n  .hide()\n}\n\nfunction onDestroy(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  if (state.stage) {\n    state.stage.remove()\n    delete state.stage\n  }\n}\n\nfunction updateInput(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  if (!state.stage.is(':visible')) {\n    return\n  }\n\n  e.preventDefault()\n\n  if (state.pinFrame) {\n    // hack into drag/move module and disable dragging\n    // prevents frame change during zoom mode\n    SpriteSpin.flag(data, 'dragging', false)\n  }\n\n  // grab touch/cursor position\n  const cursor = Utils.getCursorPosition(e)\n  // normalize cursor position into [0:1] range\n  const x = cursor.x / data.width\n  const y = cursor.y / data.height\n\n  if (state.oldX == null) {\n    state.oldX = x\n    state.oldY = y\n  }\n  if (state.currentX == null) {\n    state.currentX = x\n    state.currentY = y\n  }\n\n  // calculate move delta since last frame and remember current position\n  let dx = x - state.oldX\n  let dy = y - state.oldY\n  state.oldX = x\n  state.oldY = y\n\n  // invert drag direction for touch events to enable 'natural' scrolling\n  if (e.type.match(/touch/)) {\n    dx = -dx\n    dy = -dy\n  }\n\n  // accumulate display coordinates\n  state.currentX = Utils.clamp(state.currentX + dx, 0, 1)\n  state.currentY = Utils.clamp(state.currentY + dy, 0, 1)\n\n  SpriteSpin.updateFrame(data, data.frame, data.lane)\n}\n\nfunction onClick(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  if (!state.useClick) {\n    return\n  }\n\n  e.preventDefault()\n  // simulate double click\n\n  const clickTime = new Date().getTime()\n  if (!state.clickTime) {\n    // on first click\n    state.clickTime = clickTime\n    return\n  }\n\n  // on second click\n  const timeDelta = clickTime - state.clickTime\n  if (timeDelta > state.doubleClickTime) {\n    // took too long, back to first click\n    state.clickTime = clickTime\n    return\n  }\n\n  // on valid double click\n  state.clickTime = undefined\n  if (toggleZoom(data)) {\n    updateInput(e, data)\n  }\n}\n\nfunction onMove(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n  if (state.stage.is(':visible')) {\n    updateInput(e, data)\n  }\n}\n\nfunction onDraw(e, data: SpriteSpin.Data) {\n  const state = getState(data)\n\n  // calculate the frame index\n  const index = data.lane * data.frames + data.frame\n  // get the zoom image. Use original frames as fallback. This won't work for sprite sheets\n  const source = state.source[index]\n  const spec = Utils.findSpecs(data.metrics, data.frames, data.frame, data.lane)\n\n  // get display position\n  let x = state.currentX\n  let y = state.currentY\n  // fallback to centered position\n  if (x == null) {\n    x = state.currentX = 0.5\n    y = state.currentY = 0.5\n  }\n\n  if (source) {\n    // scale up from [0:1] to [0:100] range\n    x = Math.floor(x * 100)\n    y = Math.floor(y * 100)\n\n    // update background image and position\n    state.stage.css({\n      'background-repeat'   : 'no-repeat',\n      'background-image'    : `url('${source}')`,\n      'background-position' : `${x}% ${y}%`\n    })\n  } else if (spec.sheet && spec.sprite) {\n    const sprite = spec.sprite\n    const sheet = spec.sheet\n    const src = data.source[sheet.id]\n    const left = -Math.floor(sprite.sampledX + x * (sprite.sampledWidth - data.width))\n    const top = -Math.floor(sprite.sampledY + y * (sprite.sampledHeight - data.height))\n    const width = sheet.sampledWidth\n    const height = sheet.sampledHeight\n    state.stage.css({\n      'background-image'    : `url('${src}')`,\n      'background-position' : `${left}px ${top}px`,\n      'background-repeat'   : 'no-repeat',\n      // set custom background size to enable responsive rendering\n      '-webkit-background-size' : `${width}px ${height}px`, /* Safari 3-4 Chrome 1-3 */\n      '-moz-background-size'    : `${width}px ${height}px`, /* Firefox 3.6 */\n      '-o-background-size'      : `${width}px ${height}px`, /* Opera 9.5 */\n      'background-size'         : `${width}px ${height}px`  /* Chrome, Firefox 4+, IE 9+, Opera, Safari 5+ */\n    })\n  }\n}\n\nfunction toggleZoom(data) {\n  const state = getState(data)\n  if (!state.stage) {\n    throw new Error('zoom module is not initialized or is not available.')\n  }\n  if (state.stage.is(':visible')) {\n    showZoom(data)\n  } else {\n    hideZoom(data)\n    return true\n  }\n  return false\n}\n\nfunction showZoom(data) {\n  const state = getState(data)\n  state.stage.fadeOut()\n  data.stage.fadeIn()\n}\n\nfunction hideZoom(data) {\n  const state = getState(data)\n  state.stage.fadeIn()\n  data.stage.fadeOut()\n}\n\nfunction wheel(e: JQueryMouseEventObject, data: SpriteSpin.Data) {\n  const state = getState(data)\n  if (!data.loading && state.useWheel) {\n\n    const we = e.originalEvent as WheelEvent\n\n    let signY = we.deltaY === 0 ? 0 : we.deltaY > 0 ? 1 : -1\n    if (typeof state.useWheel === 'number') {\n      signY *= state.useWheel\n    }\n\n    if (state.stage.is(':visible') && signY > 0) {\n      e.preventDefault()\n      showZoom(data)\n    }\n    if (!state.stage.is(':visible') && signY < 0) {\n      e.preventDefault()\n      hideZoom(data)\n    }\n  }\n}\n\nSpriteSpin.registerPlugin(NAME, {\n  name: NAME,\n\n  mousedown: onClick,\n  touchstart: onClick,\n  mousemove: onMove,\n  touchmove: onMove,\n  wheel: wheel,\n\n  onInit: onInit,\n  onDestroy: onDestroy,\n  onDraw: onDraw\n})\n\nSpriteSpin.extendApi({\n  toggleZoom: function() { toggleZoom(this.data) } // tslint:disable-line\n})\n\n})()\n"
  },
  {
    "path": "src/spritespin.test.ts",
    "content": "import * as SpriteSpin from './core'\nimport * as t from './lib.test'\nimport { $ } from './utils'\n\ndescribe('SpriteSpin', () => {\n\n  const WIDTH = 50\n  const HEIGHT = 50\n\n  let data: SpriteSpin.Data\n  beforeEach((done) => {\n    $.noConflict(true)\n    t.get$El().spritespin({\n      source: t.WHITE50x50,\n      width: 10,\n      height: 10,\n      frames: 25,\n      onLoad: done,\n      plugins: ['click', '360']\n    })\n    data = t.get$El().data(SpriteSpin.namespace)\n  })\n  afterEach(() => {\n    SpriteSpin.destroy(data)\n  })\n\n  describe('#getAnimationState', () => {\n    it ('returns state.animation', () => {\n      const result = SpriteSpin.getPlaybackState(data)\n      expect(result).toBeDefined()\n      expect(result).toBe(data.state.playback)\n    })\n  })\n\n  describe('#getInputState', () => {\n    it ('returns state.input', () => {\n      const result = SpriteSpin.getInputState(data)\n      expect(result).toBeDefined()\n      expect(result).toBe(data.state.input)\n    })\n  })\n\n  describe('#getPluginState', () => {\n    it ('returns state.plugin.NAME', () => {\n      const result = SpriteSpin.getPluginState(data, 'lorem ipsum')\n      expect(result).toBeDefined()\n      expect(result).toBe(data.state.plugin['lorem ipsum'])\n    })\n  })\n\n  describe('#is / #flag', () => {\n    it ('gets and sets a state flag', () => {\n      const result = SpriteSpin.is(data, 'loading')\n      expect(result).toBe(false)\n\n      SpriteSpin.flag(data, 'loading', true)\n    })\n  })\n\n  describe('#extendApi', () => {\n    afterEach(() => {\n      SpriteSpin.Api.prototype = {} as any\n    })\n    it ('adds methods to SpriteSpin.Api.prototype', () => {\n      function a() { /*noop*/ }\n      function b() { /*noop*/ }\n      const proto = SpriteSpin.Api.prototype as any\n\n      expect(proto.a).toBeUndefined()\n      expect(proto.b).toBeUndefined()\n\n      SpriteSpin.extendApi({ a, b })\n\n      expect(proto.a).toBe(a)\n      expect(proto.b).toBe(b)\n    })\n\n    it ('throws error on method override', () => {\n      function a() { /*noop*/ }\n      function b() { /*noop*/ }\n      const proto = SpriteSpin.Api.prototype as any\n\n      expect(proto.a).toBeUndefined()\n\n      expect(() => {\n        SpriteSpin.extendApi({ a: a })\n        SpriteSpin.extendApi({ a: b })\n      }).toThrowError()\n    })\n  })\n\n  describe('#updateInput', () => {\n    let input: SpriteSpin.InputState\n    beforeEach(() => {\n      input = SpriteSpin.getInputState(data)\n    })\n\n    it ('sets and keeps startX and startY', () => {\n      // initial update sets values\n      SpriteSpin.updateInput({ clientX: 5, clientY: 5 }, data)\n      expect(input.startX).toBe(5)\n      expect(input.startY).toBe(5)\n\n      // successive update keeps values\n      SpriteSpin.updateInput({ clientX: 6, clientY: 7 }, data)\n      expect(input.startX).toBe(5)\n      expect(input.startY).toBe(5)\n    })\n\n    it ('tracks currentX and currentY', () => {\n      // initial update sets values\n      SpriteSpin.updateInput({ clientX: 5, clientY: 5 }, data)\n      expect(input.currentX).toBe(5)\n      expect(input.currentY).toBe(5)\n\n      // successive update updates values\n      SpriteSpin.updateInput({ clientX: 6, clientY: 7 }, data)\n      expect(input.currentX).toBe(6)\n      expect(input.currentY).toBe(7)\n    })\n\n    it ('tracks oldX and oldY', () => {\n      // initial update sets values\n      SpriteSpin.updateInput({ clientX: 5, clientY: 5 }, data)\n      expect(input.oldX).toBe(5)\n      expect(input.oldY).toBe(5)\n\n      // successive update sets previous values\n      SpriteSpin.updateInput({ clientX: 6, clientY: 7 }, data)\n      expect(input.oldX).toBe(5)\n      expect(input.oldY).toBe(5)\n\n      // successive update sets previous values\n      SpriteSpin.updateInput({ clientX: 8, clientY: 9 }, data)\n      expect(input.oldX).toBe(6)\n      expect(input.oldY).toBe(7)\n    })\n\n    it ('consumes touch events', () => {\n      // initial update sets values\n      SpriteSpin.updateInput({ touches: [{ clientX: 5, clientY: 5 }]}, data)\n      expect(input.oldX).toBe(5)\n      expect(input.oldY).toBe(5)\n    })\n  })\n\n  describe('#updateFrame', () => {\n    //\n  })\n\n  describe('#resetInput', () => {\n\n    ['current', 'start', 'old'].forEach((name) => {\n\n      it (`resets ${name}X and ${name}Y`, () => {\n        let input: SpriteSpin.InputState\n        input = SpriteSpin.getInputState(data)\n        input[`${name}X`] = 10\n        input[`${name}Y`] = 10\n\n        expect(input[`${name}X`]).toBe(10)\n        expect(input[`${name}Y`]).toBe(10)\n        SpriteSpin.resetInput(data)\n        expect(input[`${name}X`]).toBeUndefined()\n        expect(input[`${name}Y`]).toBeUndefined()\n      })\n    });\n\n    ['d', 'dd', 'nd', 'ndd'].forEach((name) => {\n      it (`resets ${name}X and ${name}Y`, () => {\n        let input: SpriteSpin.InputState\n        input = SpriteSpin.getInputState(data)\n        input[`${name}X`] = 10\n        input[`${name}Y`] = 10\n\n        expect(input[`${name}X`]).toBe(10)\n        expect(input[`${name}Y`]).toBe(10)\n        SpriteSpin.resetInput(data)\n        expect(input[`${name}X`]).toBe(0)\n        expect(input[`${name}Y`]).toBe(0)\n      })\n    })\n  })\n\n  describe('$ extension', () => {\n    describe('spritespin(\"data\")', () => {\n      it ('returns the data object', () => {\n        expect(t.get$El().spritespin('data')).toBeDefined()\n        expect(t.get$El().spritespin('data')).toBe(data)\n      })\n    })\n\n    describe('spritespin(\"api\")', () => {\n      it ('returns the Api instance', () => {\n        const api = t.get$El().spritespin('api')\n        expect(api instanceof SpriteSpin.Api).toBe(true)\n      })\n    })\n\n    describe('spritespin(\"destroy\")', () => {\n      it ('destroys the instance', () => {\n\n        t.get$El().spritespin('destroy')\n        expect(t.get$El().data('spritespin')).toBeUndefined()\n      })\n    })\n\n    describe('spritespin(\"xxx\", \"yyy\")', () => {\n      it ('sets property of data object', () => {\n        expect(data['xxx']).toBeUndefined() // tslint:disable-line\n        t.get$El().spritespin('xxx', 'yyy')\n        expect(data['xxx']).toBe('yyy') // tslint:disable-line\n      })\n\n      it ('calls SpriteSpin.createOrUpdate', () => {\n        expect(data['xxx']).toBeUndefined()\n        t.get$El().spritespin('xxx', 'yyy')\n        expect(data['xxx']).toBe('yyy')\n      })\n    })\n\n    describe('spritespin(\"xxx\")', () => {\n      it ('throws an error', () => {\n        expect(() => {\n          t.get$El().spritespin('xxx')\n        }).toThrowError()\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "src/utils/cursor.ts",
    "content": "export function getCursorPosition(event: any) {\n  let touches = event.touches\n  let source = event\n\n  // jQuery Event normalization does not preserve the 'event.touches'\n  // try to grab touches from the original event\n  if (event.touches === undefined && event.originalEvent !== undefined) {\n    touches = event.originalEvent.touches\n  }\n  // get current touch or mouse position\n  if (touches !== undefined && touches.length > 0) {\n    source = touches[0]\n  }\n  return {\n    x: source.clientX || 0,\n    y: source.clientY || 0\n  }\n}\n"
  },
  {
    "path": "src/utils/detectSubsampling.test.ts",
    "content": "import * as t from '../lib.test'\nimport * as Utils from './index'\n\ndescribe('SpriteSpin.Utils', () => {\n\n  const WIDTH = 50\n  const HEIGHT = 50\n\n  let image: HTMLImageElement\n\n  beforeEach((done) => {\n    Utils.preload({\n      source: [t.WHITE50x50],\n      complete: (result) => {\n        image = result[0]\n        done()\n      }\n    })\n  })\n\n  describe('#detectSubsampling', () => {\n    describe('with small image', () => {\n      it ('resolves to false', () => {\n        const result = Utils.detectSubsampling(image, WIDTH, HEIGHT)\n        expect(result).toBe(false)\n      })\n    })\n\n    describe('with (fake) subsampled image', () => {\n      it ('resolves to true', () => {\n        const result = Utils.detectSubsampling(image, 1025, 1025)\n        expect(result).toBe(true)\n      })\n    })\n\n  })\n})\n"
  },
  {
    "path": "src/utils/detectSubsampling.ts",
    "content": "let canvas: HTMLCanvasElement\nlet context: CanvasRenderingContext2D\n\nfunction detectionContext() {\n  if (context) {\n    return context\n  }\n\n  if (!canvas) {\n    canvas = document.createElement('canvas')\n  }\n  if (!canvas || !canvas.getContext) {\n    return null\n  }\n\n  context = canvas.getContext('2d')\n  return context\n}\n\n/**\n * Idea taken from https://github.com/stomita/ios-imagefile-megapixel\n * Detects whether the image has been sub sampled by the browser and does not have its original dimensions.\n * This method unfortunately does not work for images that have transparent background.\n */\nexport function detectSubsampling(img: HTMLImageElement, width: number, height: number) {\n\n  if (!detectionContext()) {\n    return false\n  }\n\n  // sub sampling happens on images above 1 megapixel\n  if (width * height <= 1024 * 1024) {\n    return false\n  }\n\n  // set canvas to 1x1 pixel size and fill it with magenta color\n  canvas.width = canvas.height = 1\n  context.fillStyle = '#FF00FF'\n  context.fillRect(0, 0, 1, 1)\n  // render the image with a negative offset to the left so that it would\n  // fill the canvas pixel with the top right pixel of the image.\n  context.drawImage(img, -width + 1, 0)\n\n  // check color value to confirm image is covering edge pixel or not.\n  // if color still magenta, the image is assumed to be sub sampled.\n  try {\n    const dat = context.getImageData(0, 0, 1, 1).data\n    return (dat[0] === 255) && (dat[1] === 0) && (dat[2] === 255)\n  } catch (err) {\n    // avoids cross origin exception for chrome when code runs without a server\n    return false\n  }\n}\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "export * from './jquery'\nexport * from './cursor'\nexport * from './detectSubsampling'\nexport * from './layout'\nexport * from './measure'\nexport * from './naturalSize'\nexport * from './preload'\nexport * from './sourceArray'\nexport * from './utils'\n"
  },
  {
    "path": "src/utils/jquery.ts",
    "content": "export const $: JQueryStatic<HTMLElement> = (window as any).jQuery || (window as any).$\n"
  },
  {
    "path": "src/utils/layout.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpin.Utils', () => {\n\n  describe('#getOuterSize', () => {\n\n    it ('returns width, height and aspect', () => {\n      const result = Utils.getOuterSize({ width: 100, height: 200 } as any)\n      expect(result.width).toBe(100)\n      expect(result.height).toBe(200)\n      expect(result.aspect).toBe(0.5)\n    })\n  })\n\n  describe('#getInnerSize', () => {\n    it ('returns frameWidth, frameHeight and aspect', () => {\n      const result = Utils.getInnerSize({ frameWidth: 100, frameHeight: 200 } as any)\n      expect(result.width).toBe(100)\n      expect(result.height).toBe(200)\n      expect(result.aspect).toBe(0.5)\n    })\n  })\n\n  describe('#getInnerLayout', () => {\n    const data: Utils.Layoutable = {\n      target: null,\n      width: 100, height: 200,\n      frameWidth: 100, frameHeight: 200,\n      sizeMode: 'original'\n    }\n    let inner: Utils.SizeWithAspect\n    let outer: Utils.SizeWithAspect\n    const modes: SpriteSpin.SizeMode[] = ['original', 'fit', 'fill', 'stretch']\n\n    describe('with equal outer and inner size', () => {\n      beforeEach(() => {\n        data.frameWidth = data.width = 100\n        data.frameHeight = data.height = 200\n      })\n\n      modes.forEach((mode) => {\n        describe(`with '${mode}' mode`, () => {\n          let result: Utils.Layout\n\n          beforeEach(() => {\n            data.sizeMode = mode\n            inner = Utils.getInnerSize(data)\n            outer = Utils.getOuterSize(data)\n            result = Utils.getInnerLayout(data.sizeMode, inner, outer)\n          })\n\n          it('returns matching layout', () => {\n            expect(result.top).toBe(0, 'top')\n            expect(result.right).toBe(0, 'right')\n            expect(result.bottom).toBe(0, 'bottom')\n            expect(result.left).toBe(0, 'left')\n            expect(result.position).toBe('absolute')\n          })\n        })\n      })\n    })\n\n    describe('with different outer and inner size', () => {\n      beforeEach(() => {\n        data.width = 100\n        data.height = 200\n\n        data.frameWidth = 300\n        data.frameHeight = 400\n      })\n\n      describe(`with 'original' mode`, () => {\n        let result: Utils.Layout\n\n        beforeEach(() => {\n          data.sizeMode = 'original'\n          inner = Utils.getInnerSize(data)\n          outer = Utils.getOuterSize(data)\n          result = Utils.getInnerLayout(data.sizeMode, inner, outer)\n        })\n\n        it('returns matching layout', () => {\n          expect(result.position).toBe('absolute', 'position')\n          expect(result.width).toBe(data.frameWidth, 'frameWidth')\n          expect(result.height).toBe(data.frameHeight, 'frameHeight')\n          expect(result.top).toBe(-100, 'top')\n          expect(result.bottom).toBe(-100, 'bottom')\n          expect(result.right).toBe(-100, 'right')\n          expect(result.left).toBe(-100, 'left')\n        })\n      })\n\n      describe(`with 'fit' mode`, () => {\n        let result: Utils.Layout\n\n        beforeEach(() => {\n          data.sizeMode = 'fit'\n          inner = Utils.getInnerSize(data)\n          outer = Utils.getOuterSize(data)\n          result = Utils.getInnerLayout(data.sizeMode, inner, outer)\n        })\n\n        it('returns matching layout', () => {\n          expect(result.position).toBe('absolute', 'position')\n          expect(result.width).toBe(100, 'frameWidth')\n          expect(result.height).toBe(133, 'frameHeight')\n          expect(result.top).toBe(33, 'top')\n          expect(result.bottom).toBe(33, 'bottom')\n          expect(result.right).toBe(0, 'right')\n          expect(result.left).toBe(0, 'left')\n        })\n      })\n\n      describe(`with 'fill' mode`, () => {\n        let result: Utils.Layout\n\n        beforeEach(() => {\n          data.sizeMode = 'fill'\n          inner = Utils.getInnerSize(data)\n          outer = Utils.getOuterSize(data)\n          result = Utils.getInnerLayout(data.sizeMode, inner, outer)\n        })\n\n        it('returns matching layout', () => {\n          expect(result.position).toBe('absolute', 'position')\n          expect(result.width).toBe(150, 'frameWidth')\n          expect(result.height).toBe(200, 'frameHeight')\n          expect(result.top).toBe(0, 'top')\n          expect(result.bottom).toBe(0, 'bottom')\n          expect(result.right).toBe(-25, 'right')\n          expect(result.left).toBe(-25, 'left')\n        })\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "src/utils/layout.ts",
    "content": "import { SizeMode } from '../core/models'\n\nexport interface Layoutable {\n  width?: number\n  height?: number\n  frameWidth?: number\n  frameHeight?: number\n  target: any\n  sizeMode?: SizeMode\n}\n\nexport interface Layout {\n  [key: string]: any\n  width: string | number\n  height: string | number\n  top: number\n  left: number\n  bottom: number\n  right: number\n  position: 'absolute'\n  overflow: 'hidden'\n}\n\nexport interface SizeWithAspect {\n  width: number\n  height: number\n  aspect: number\n}\n\n/**\n *\n */\nexport function getOuterSize(data: Layoutable): SizeWithAspect {\n  const width = Math.floor(data.width || data.frameWidth || data.target.innerWidth())\n  const height = Math.floor(data.height || data.frameHeight || data.target.innerHeight())\n  return {\n    aspect: width / height,\n    height,\n    width\n  }\n}\n\nexport function getComputedSize(data: Layoutable): SizeWithAspect {\n  const size = getOuterSize(data)\n  if (typeof window.getComputedStyle !== 'function') { return size }\n\n  const style = window.getComputedStyle(data.target[0])\n  if (!style.width) { return size }\n\n  size.width = Math.floor(Number(style.width.replace('px', '')))\n  size.height = Math.floor(size.width / size.aspect)\n  return size\n}\n\n/**\n *\n */\nexport function getInnerSize(data: Layoutable): SizeWithAspect {\n  const width = Math.floor(data.frameWidth || data.width || data.target.innerWidth())\n  const height = Math.floor(data.frameHeight || data.height || data.target.innerHeight())\n  return {\n    aspect: width / height,\n    height,\n    width\n  }\n}\n\n/**\n *\n */\nexport function getInnerLayout(mode: SizeMode, inner: SizeWithAspect, outer: SizeWithAspect): Layout {\n\n  // get mode\n  const isFit = mode === 'fit'\n  const isFill = mode === 'fill'\n  const isMatch = mode === 'stretch'\n\n  // resulting layout\n  const layout: Layout = {\n    width    : '100%',\n    height   : '100%',\n    top      : 0,\n    left     : 0,\n    bottom   : 0,\n    right    : 0,\n    position : 'absolute',\n    overflow : 'hidden'\n  }\n\n  // no calculation here\n  if (!mode || isMatch) {\n    return layout\n  }\n\n  // get size and aspect\n  const aspectIsGreater = inner.aspect >= outer.aspect\n\n  // mode == original\n  let width = inner.width\n  let height = inner.height\n\n  // keep aspect ratio but fit/fill into container\n  if (isFit && aspectIsGreater || isFill && !aspectIsGreater) {\n    width = outer.width\n    height = outer.width / inner.aspect\n  }\n  if (isFill && aspectIsGreater || isFit && !aspectIsGreater) {\n    height = outer.height\n    width = outer.height * inner.aspect\n  }\n\n  // floor the numbers\n  width = Math.floor(width)\n  height = Math.floor(height)\n\n  // position in center\n  layout.width = width\n  layout.height = height\n  layout.top = Math.floor((outer.height - height) / 2)\n  layout.left = Math.floor((outer.width - width) / 2)\n  layout.right = layout.left\n  layout.bottom = layout.top\n\n  return layout\n}\n"
  },
  {
    "path": "src/utils/measure.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpin.Utils', () => {\n\n  const WIDTH = 50\n  const HEIGHT = 50\n\n  let image\n\n  beforeEach((done) => {\n    Utils.preload({\n      source: [t.WHITE50x50],\n      complete: (result) => {\n        image = result[0]\n        done()\n      }\n    })\n  })\n\n  describe('#measure', () => {\n\n    let result: Utils.SheetSpec[]\n\n    describe('a sprite sheet', () => {\n      const FRAMES = 95\n      const FRAMESX = 10\n\n      beforeEach(() => {\n        result = Utils.measure([image], { frames: FRAMES, framesX: FRAMESX })\n      })\n\n      it('resolves sheet spec', () => {\n        expect(result.length).toBe(1)\n        result.forEach((sheet, index) => {\n          expect(sheet.id).toBe(index)\n          expect(sheet.width).toBe(WIDTH)\n          expect(sheet.width).toBe(HEIGHT)\n          expect(sheet.sampledHeight).toBe(sheet.width)\n          expect(sheet.sampledHeight).toBe(sheet.height)\n        })\n      })\n\n      it('resolves sprite specs', () => {\n        expect(result[0].sprites.length).toBe(FRAMES)\n        result[0].sprites.forEach((sprite, index) => {\n          expect(sprite.id).toBe(index)\n          expect(sprite.width).toBe(WIDTH / FRAMESX)\n          expect(sprite.height).toBe(HEIGHT / (Math.ceil(FRAMES / FRAMESX)))\n        })\n      })\n    })\n\n    describe('an array of frames', () => {\n      let IMAGES = [image, image, image, image]\n      let FRAMES = IMAGES.length\n\n      beforeEach(() => {\n        IMAGES = [image, image, image, image]\n        FRAMES = IMAGES.length\n        result = Utils.measure(IMAGES, { frames: FRAMES })\n      })\n\n      it('resolves sheet spec', () => {\n        expect(result.length).toBe(FRAMES)\n        result.forEach((sheet, index) => {\n          expect(sheet.id).toBe(index)\n          expect(sheet.width).toBe(WIDTH)\n          expect(sheet.height).toBe(HEIGHT)\n          expect(sheet.sampledHeight).toBe(sheet.width)\n          expect(sheet.sampledHeight).toBe(sheet.height)\n        })\n      })\n\n      it('resolves sprite specs', () => {\n        result.forEach((sheet) => {\n          expect(sheet.sprites.length).toBe(1)\n          sheet.sprites.forEach((sprite, index) => {\n            expect(sprite.id).toBe(index)\n            expect(sprite.width).toBe(WIDTH)\n            expect(sprite.height).toBe(HEIGHT)\n            expect(sprite.sampledWidth).toBe(sprite.width)\n            expect(sprite.sampledHeight).toBe(sprite.height)\n          })\n        })\n      })\n    })\n  })\n\n  describe('#findSpec', () => {\n    const metrics = [\n      { sprites: ['a1', 'a2', 'a3', 'b1', 'b2', 'b3', 'c1', 'c2', 'c3']},\n      { sprites: ['x1', 'x2', 'x3', 'y1', 'y2', 'y3', 'z1', 'z2', 'z3']}\n    ]\n\n    it('finds the correct data', () => {\n      expect(Utils.findSpecs(metrics as any, 3, 0, 0).sprite as any).toBe('a1')\n      expect(Utils.findSpecs(metrics as any, 3, 0, 1).sprite as any).toBe('b1')\n      expect(Utils.findSpecs(metrics as any, 3, 0, 2).sprite as any).toBe('c1')\n      expect(Utils.findSpecs(metrics as any, 3, 0, 3).sprite as any).toBe('x1')\n      expect(Utils.findSpecs(metrics as any, 3, 0, 4).sprite as any).toBe('y1')\n      expect(Utils.findSpecs(metrics as any, 3, 0, 5).sprite as any).toBe('z1')\n\n      expect(Utils.findSpecs(metrics as any, 3, 1, 0).sprite as any).toBe('a2')\n      expect(Utils.findSpecs(metrics as any, 3, 1, 1).sprite as any).toBe('b2')\n      expect(Utils.findSpecs(metrics as any, 3, 1, 2).sprite as any).toBe('c2')\n      expect(Utils.findSpecs(metrics as any, 3, 1, 3).sprite as any).toBe('x2')\n      expect(Utils.findSpecs(metrics as any, 3, 1, 4).sprite as any).toBe('y2')\n      expect(Utils.findSpecs(metrics as any, 3, 1, 5).sprite as any).toBe('z2')\n\n      expect(Utils.findSpecs(metrics as any, 3, 2, 0).sprite as any).toBe('a3')\n      expect(Utils.findSpecs(metrics as any, 3, 2, 1).sprite as any).toBe('b3')\n      expect(Utils.findSpecs(metrics as any, 3, 2, 2).sprite as any).toBe('c3')\n      expect(Utils.findSpecs(metrics as any, 3, 2, 3).sprite as any).toBe('x3')\n      expect(Utils.findSpecs(metrics as any, 3, 2, 4).sprite as any).toBe('y3')\n      expect(Utils.findSpecs(metrics as any, 3, 2, 5).sprite as any).toBe('z3')\n    })\n  })\n})\n"
  },
  {
    "path": "src/utils/measure.ts",
    "content": "import { detectSubsampling } from './detectSubsampling'\nimport { naturalSize } from './naturalSize'\n/**\n *\n */\nexport interface MeasureSheetOptions {\n  frames: number\n  framesX?: number\n  framesY?: number\n  detectSubsampling?: boolean\n}\n\n/**\n *\n */\nexport interface SheetSpec {\n  id: number\n  width: number\n  height: number\n  sprites: SpriteSpec[]\n\n  sampledWidth?: number\n  sampledHeight?: number\n  isSubsampled?: boolean\n}\n\n/**\n *\n */\nexport interface SpriteSpec {\n  id: number\n  x: number\n  y: number\n  width: number\n  height: number\n\n  sampledX?: number\n  sampledY?: number\n  sampledWidth?: number\n  sampledHeight?: number\n}\n\n/**\n * Measures the image frames that are used in the given data object\n */\nexport function measure(images: HTMLImageElement[], options: MeasureSheetOptions): SheetSpec[] {\n  if (images.length === 1) {\n    return [measureSheet(images[0], options)]\n  } else if (options.framesX && options.framesY) {\n    return measureMutipleSheets(images, options)\n  } else {\n    return measureFrames(images, options)\n  }\n}\n\nfunction measureSheet(image: HTMLImageElement, options: MeasureSheetOptions): SheetSpec {\n  const result: SheetSpec = { id: 0, sprites: [] } as any\n  measureImage(image, options, result)\n  const frames = options.frames\n  const framesX = Number(options.framesX) || frames\n  const framesY = Math.ceil(frames / framesX)\n  const frameWidth = Math.floor(result.width / framesX)\n  const frameHeight = Math.floor(result.height / framesY)\n  const divisor = result.isSubsampled ? 2 : 1\n  for (let i = 0; i < frames; i++) {\n    const x = (i % framesX) * frameWidth\n    const y = Math.floor(i / framesX) * frameHeight\n    result.sprites.push({\n      id: i,\n      x: x, y: y,\n      width: frameWidth,\n      height: frameHeight,\n      sampledX: x / divisor,\n      sampledY: y / divisor,\n      sampledWidth: frameWidth / divisor,\n      sampledHeight: frameHeight / divisor\n    })\n  }\n  return result\n}\n\nfunction measureFrames(images: HTMLImageElement[], options: MeasureSheetOptions): SheetSpec[] {\n  const result: SheetSpec[] = []\n  for (let id = 0; id < images.length; id++) {\n    // TODO: optimize\n    // don't measure images with same size twice\n    const sheet = measureSheet(images[id], { frames: 1, framesX: 1, detectSubsampling: options.detectSubsampling })\n    sheet.id = id\n    result.push(sheet)\n  }\n  return result\n}\n\nfunction measureMutipleSheets(images: HTMLImageElement[], options: MeasureSheetOptions): SheetSpec[] {\n  const result: SheetSpec[] = []\n  for (let id = 0; id < images.length; id++) {\n    // TODO: optimize\n    // don't measure images with same size twice\n    const sheet = measureSheet(images[id], {\n      frames: undefined,\n      framesX: options.framesX,\n      framesY: options.framesY,\n      detectSubsampling: options.detectSubsampling\n    })\n    sheet.id = id\n    result.push(sheet)\n  }\n  return result\n}\n\nfunction measureImage(image: HTMLImageElement, options: MeasureSheetOptions, result: SheetSpec): SheetSpec {\n  const size = naturalSize(image)\n  result.isSubsampled = options.detectSubsampling && detectSubsampling(image, size.width, size.height)\n  result.width = size.width\n  result.height = size.height\n  result.sampledWidth = size.width / (result.isSubsampled ? 2 : 1)\n  result.sampledHeight = size.height / (result.isSubsampled ? 2 : 1)\n  return result\n}\n\nexport function findSpecs(metrics: SheetSpec[], frames: number, frame: number, lane: number) {\n  let spriteId = lane * frames + frame\n  let sheetId = 0\n  let sprite: SpriteSpec = null\n  let sheet: SheetSpec = null\n\n  while (true) {\n    sheet = metrics[sheetId]\n    if (!sheet) { break }\n\n    if (spriteId >= sheet.sprites.length) {\n      spriteId -= sheet.sprites.length\n      sheetId++\n      continue\n    }\n\n    sprite = sheet.sprites[spriteId]\n    break\n  }\n  return { sprite, sheet }\n}\n"
  },
  {
    "path": "src/utils/naturalSize.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpin.Utils', () => {\n\n  const WIDTH = 50\n  const HEIGHT = 50\n\n  let image: HTMLImageElement\n\n  beforeEach((done) => {\n    Utils.preload({\n      source: [t.WHITE50x50],\n      complete: (result) => {\n        image = result[0]\n        done()\n      }\n    })\n  })\n\n  describe('#naturalSize', () => {\n    it ('resolves to naturalWidth and naturalHeight', () => {\n      const result = Utils.naturalSize({ naturalWidth: WIDTH, naturalHeight: HEIGHT } as any)\n      expect(result.width).toBe(WIDTH)\n      expect(result.height).toBe(HEIGHT)\n    })\n\n    it ('resolves to width and height from preloaded src', () => {\n      const result = Utils.naturalSize({ src: t.WHITE50x50 } as any)\n      expect(result.width).toBe(WIDTH)\n      expect(result.height).toBe(HEIGHT)\n    })\n  })\n})\n"
  },
  {
    "path": "src/utils/naturalSize.ts",
    "content": "let img: HTMLImageElement\n\n/**\n * gets the original width and height of an image element\n */\nexport function naturalSize(image: HTMLImageElement) {\n  // for browsers that support naturalWidth and naturalHeight properties\n  if (image.naturalWidth) {\n    return {\n      height: image.naturalHeight,\n      width: image.naturalWidth\n    }\n  }\n\n  // browsers that do not support naturalWidth and naturalHeight properties have to fall back to the width and\n  // height properties. However, the image might have a css style applied so width and height would return the\n  // css size. To avoid that create a new Image object that is free of css rules and grab width and height\n  // properties\n  //\n  // assume that the src has already been downloaded, so no onload callback is needed.\n  img = img || new Image()\n  img.crossOrigin = image.crossOrigin\n  img.src = image.src\n  return {\n    height: img.height,\n    width: img.width\n  }\n}\n"
  },
  {
    "path": "src/utils/preload.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpin.Utils', () => {\n\n  describe('#preload', () => {\n\n    const source = [t.RED40x30, t.GREEN40x30, t.BLUE40x30]\n\n    function expectArrayOfImages(input, output) {\n      expect(Array.isArray(output)).toBe(true)\n      expect(output.length).toBe(input.length)\n      expect(output.every((it) => it instanceof Image)).toBe(true)\n    }\n\n    it ('accepts string input', (done) => {\n      Utils.preload({\n        source: t.RED40x30,\n        initiated: (result) => {\n          expectArrayOfImages([t.RED40x30], result)\n          done()\n        }\n      })\n    })\n\n    it ('reports array of Image elements when initiated', (done) => {\n      Utils.preload({\n        source: source,\n        initiated: (result) => {\n          expectArrayOfImages(source, result)\n          done()\n        }\n      })\n    })\n\n    it ('reports array of Image elements on complete', (done) => {\n      Utils.preload({\n        source: source,\n        complete: (result) => {\n          expectArrayOfImages(source, result)\n          done()\n        }\n      })\n    })\n\n    it ('reports progress for each image', (done) => {\n      let count = 0\n      Utils.preload({\n        source: source,\n        progress: () => { count++ },\n        complete:  () => {\n          expect(count).toBe(source.length)\n          done()\n        }\n      })\n    })\n\n    it ('completes if preload count is reached', (done) => {\n      let count = 0\n      const preloadCount = 2\n      Utils.preload({\n        source: source,\n        preloadCount: preloadCount,\n        progress: () => { count++ },\n        complete: (result) => {\n          expect(count).toBe(preloadCount)\n          done()\n        }\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "src/utils/preload.ts",
    "content": "function indexOf(element: any, arr: any[]) {\n  for (let i = 0; i < arr.length; i++) {\n    if (arr[i] === element) {\n      return i\n    }\n  }\n}\n\nfunction noop() {\n  //\n}\n\nexport interface PreloadOptions {\n  source: string | string[]\n  crossOrigin?: string\n  preloadCount?: number\n  initiated?: (images: HTMLImageElement[]) => void\n  progress?: (p: PreloadProgress) => void\n  complete?: (images: HTMLImageElement[]) => void\n}\n\nexport interface PreloadProgress {\n  // The image index that currently has been loaded\n  index: number\n  // The number of images that have been loaded so far\n  loaded: number\n  // The total number of images to load\n  total: number\n  // Percentage value\n  percent: number\n}\n\nexport function preload(opts: PreloadOptions) {\n  let src: string[]\n  const input = opts.source\n  src = typeof input === 'string' ? [input] : input\n  // const src: string[] =  ? [opts.source] : opts.source\n\n  const images = []\n  const targetCount = (opts.preloadCount || src.length)\n  const onInitiated = opts.initiated || noop\n  const onProgress = opts.progress || noop\n  const onComplete = opts.complete || noop\n\n  let count = 0\n  let completed = false\n  let firstLoaded = false\n\n  const tick = function () { // tslint:disable-line\n    count += 1\n\n    onProgress({\n      index: indexOf(this, images),\n      loaded: count,\n      total: src.length,\n      percent: Math.round((count / src.length) * 100)\n    })\n\n    firstLoaded = firstLoaded || (this === images[0])\n\n    if (firstLoaded && !completed && (count >= targetCount)) {\n      completed = true\n      onComplete(images)\n    }\n  }\n\n  for (const url of src) {\n    const img = new Image()\n    // push result\n    images.push(img)\n    // https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image\n    img.crossOrigin = opts.crossOrigin\n    // bind logic, don't care about abort/errors\n    img.onload = img.onabort = img.onerror = tick\n    // begin load\n    img.src = url\n  }\n\n  onInitiated(images)\n}\n"
  },
  {
    "path": "src/utils/sourceArray.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpin.Utils', () => {\n\n  describe('#sourceArray', () => {\n    it ('generates array of urls', () => {\n      const output = Utils.sourceArray('http://example.com/image_{frame}.jpg', { frame: [1, 3] })\n      expect(output[0]).toBe('http://example.com/image_01.jpg')\n      expect(output[1]).toBe('http://example.com/image_02.jpg')\n      expect(output[2]).toBe('http://example.com/image_03.jpg')\n    })\n\n    it ('accepts \"digits\" option', () => {\n      const output = Utils.sourceArray('http://example.com/image_{frame}.jpg', { frame: [1, 3], digits: 3 })\n      expect(output[0]).toBe('http://example.com/image_001.jpg')\n      expect(output[1]).toBe('http://example.com/image_002.jpg')\n      expect(output[2]).toBe('http://example.com/image_003.jpg')\n    })\n\n    it ('accepts \"lane\" option', () => {\n      const output = Utils.sourceArray('http://example.com/image_{lane}x{frame}.jpg', { frame: [1, 2], lane: [1, 2] })\n      expect(output[0]).toBe('http://example.com/image_01x01.jpg')\n      expect(output[1]).toBe('http://example.com/image_01x02.jpg')\n      expect(output[2]).toBe('http://example.com/image_02x01.jpg')\n      expect(output[3]).toBe('http://example.com/image_02x02.jpg')\n    })\n  })\n})\n"
  },
  {
    "path": "src/utils/sourceArray.ts",
    "content": "function padNumber(num: number, length: number, pad: string): string {\n  let result = String(num)\n  while (result.length < length) {\n    result = String(pad) + result\n  }\n  return result\n}\n\n/**\n * Options for {@link sourceArray} function\n */\nexport interface SourceArrayOptions {\n  /**\n   * Minimum number of digits\n   */\n  digits?: number\n  /**\n   * Start and end frame numbers\n   */\n  frame?: number[]\n  /**\n   * Start and end lane numbers\n   */\n  lane?: number[]\n  /**\n   * Variable to be replaced by a frame number e.g. '{frame}'\n   */\n  framePlacer?: string\n  /**\n   * Variable to be replaced by a lane number e.g. '{lane}'\n   */\n  lanePlacer?: string\n}\n\n/**\n * Generates an array of source strings\n *\n * @remarks\n * Takes a template string and generates an array of strings by interpolating {lane} and {frame} placeholders.\n *\n * ```\n * sourceArray('http://example.com/image_{frame}.jpg, { frame: [1, 3], digits: 2 })\n * // gives:\n * // [ 'http://example.com/image_01.jpg', 'http://example.com/image_02.jpg', 'http://example.com/image_03.jpg' ]\n *\n * sourceArray('http://example.com/image_FRAME.jpg, { frame: [1, 3], digits: 2, framePlacer: 'FRAME' })\n * // gives:\n * // [ 'http://example.com/image_01.jpg', 'http://example.com/image_02.jpg', 'http://example.com/image_03.jpg' ]\n * ```\n *\n * @param template - The template string\n * @param opts - Interpolation options\n *\n * @public\n */\nexport function sourceArray(template: string, opts: SourceArrayOptions) {\n  const digits = opts.digits || 2\n  const lPlacer = opts.lanePlacer || '{lane}'\n  const fPlacer = opts.framePlacer || '{frame}'\n\n  let fStart = 0\n  let fEnd = 0\n  if (opts.frame) {\n    fStart = opts.frame[0]\n    fEnd = opts.frame[1]\n  }\n  let lStart = 0\n  let lEnd = 0\n  if (opts.lane) {\n    lStart = opts.lane[0]\n    lEnd = opts.lane[1]\n  }\n  const result = []\n  for (let lane = lStart; lane <= lEnd; lane += 1) {\n    for (let frame = fStart; frame <= fEnd; frame += 1) {\n      result.push(template\n        .replace(lPlacer, padNumber(lane, digits, '0'))\n        .replace(fPlacer, padNumber(frame, digits, '0'))\n      )\n    }\n  }\n  return result\n}\n"
  },
  {
    "path": "src/utils/utils.test.ts",
    "content": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpin.Utils', () => {\n\n  describe('#toArray', () => {\n    it ('wraps string to array', () => {\n      const input = 'foo'\n      const output = Utils.toArray(input)\n      expect(Array.isArray(output)).toBe(true)\n      expect(output[0]).toBe(input)\n    })\n\n    it ('doesnt transform arrays', () => {\n      const input = ['foo']\n      const output = Utils.toArray(input)\n      expect(output).toBe(input)\n    })\n  })\n\n  describe('#clamp', () => {\n    it ('clamps to lower bound', () => {\n      const min = 10\n      const max = 20\n      const output = Utils.clamp(5, min, max)\n      expect(output).toBe(min)\n    })\n    it ('clamps to upper bound', () => {\n      const min = 10\n      const max = 20\n      const output = Utils.clamp(25, min, max)\n      expect(output).toBe(max)\n    })\n    it ('preserves inside bounds', () => {\n      const min = 10\n      const max = 20\n      expect(Utils.clamp(min, min, max)).toBe(min)\n      expect(Utils.clamp(max, min, max)).toBe(max)\n      expect(Utils.clamp(15, min, max)).toBe(15)\n    })\n  })\n})\n"
  },
  {
    "path": "src/utils/utils.ts",
    "content": "import { namespace } from '../core/constants'\n\nexport function noop() {\n  // noop\n}\n\nfunction wrapConsole(type: string): (message?: any, ...optionalParams: any[]) => void {\n  return console && console[type] ? (...args: any[]) => console.log.apply(console, args) : noop\n}\n\nexport const log = wrapConsole('log')\nexport const warn = wrapConsole('warn')\nexport const error = wrapConsole('error')\n\nexport function toArray<T>(value: T | T[]): T[] {\n  return Array.isArray(value) ? value : [value]\n}\n\n/**\n * clamps the given value by the given min and max values\n */\nexport function clamp(value: number, min: number, max: number) {\n  return (value > max ? max : (value < min ? min : value))\n}\n\n/**\n *\n */\nexport function wrap(value: number, min: number, max: number, size: number) {\n  while (value > max) { value -= size }\n  while (value < min) { value += size }\n  return value\n}\n\n/**\n * prevents default action on the given event\n */\nexport function prevent(e) {\n  e.preventDefault()\n  return false\n}\n\n/**\n * Binds on the given target and event the given function.\n * The SpriteSpin namespace is attached to the event name\n */\nexport function bind(target: JQuery, event: string, func: (...args: any[]) => any) {\n  if (func) {\n    target.bind(event + '.' + namespace, (e) => {\n      func.apply(target, [e, (target as any).spritespin('data')])\n    })\n  }\n}\n\n/**\n * Unbinds all SpriteSpin events from given target element\n */\nexport function unbind(target: JQuery): void {\n  target.unbind('.' + namespace)\n}\n\n/**\n * Checks if given object is a function\n */\nexport function isFunction(fn: any): boolean {\n  return typeof fn === 'function'\n}\n\nexport function pixelRatio(context) {\n  const devicePixelRatio = window.devicePixelRatio || 1\n  const backingStoreRatio =\n    context.webkitBackingStorePixelRatio ||\n    context.mozBackingStorePixelRatio ||\n    context.msBackingStorePixelRatio ||\n    context.oBackingStorePixelRatio ||\n    context.backingStorePixelRatio || 1\n  return devicePixelRatio / backingStoreRatio\n}\n"
  },
  {
    "path": "tsconfig.cjs.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"release/cjs\",\n    \"module\": \"commonjs\"\n  },\n  \"exclude\": []\n}\n"
  },
  {
    "path": "tsconfig.esm2015.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"release/esm2015\",\n    \"module\": \"es2015\",\n    \"target\": \"es2015\"\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"noResolve\": true,\n    \"outDir\": \"release/src\",\n    \"sourceMap\": true,\n    \"inlineSources\": true,\n    \"declaration\": true,\n    \"newLine\": \"LF\",\n    \"rootDir\": \"src\",\n    \"module\": \"es2015\",\n    \"moduleResolution\": \"node\",\n    \"alwaysStrict\": true,\n    \"noEmitHelpers\": true,\n    \"typeRoots\": [\n      \"node_modules/@types\"\n    ],\n    \"types\": [\n      \"jasmine\",\n      \"jquery\",\n      \"node\"\n    ]\n  },\n  \"exclude\": [\n    \"src/**/*.test.ts\",\n    \"node_modules\",\n    \"release\"\n  ]\n}\n"
  },
  {
    "path": "tslint.json",
    "content": "{\n  \"extends\": \"tslint:latest\",\n  \"rules\": {\n    \"no-namespace\": false,\n    \"no-internal-module\": false,\n    \"interface-name\": [true, \"never-prefix\"],\n    \"quotemark\": [true, \"single\", \"avoid-escape\"],\n    \"trailing-comma\": [true, {\n      \"multiline\": \"never\",\n      \"singleline\": \"never\"\n    }],\n    \"semicolon\": [true, \"never\"],\n    \"member-ordering\": [\n      false\n    ],\n    \"max-line-length\": [\n      140\n    ],\n    \"one-variable-per-declaration\": [false],\n    \"object-literal-sort-keys\": false,\n    \"object-literal-key-quotes\": [true, \"as-needed\"],\n    \"object-literal-shorthand\": false,\n    \"no-reference\": false\n  }\n}\n"
  }
]