Full Code of giniedp/spritespin for AI

master 8da389913a53 cached
73 files
144.2 KB
41.5k tokens
204 symbols
1 requests
Download .txt
Repository: giniedp/spritespin
Branch: master
Commit: 8da389913a53
Files: 73
Total size: 144.2 KB

Directory structure:
gitextract_2mzudef5/

├── .editorconfig
├── .gitignore
├── .jshintrc
├── .travis.yml
├── LICENSE
├── README.md
├── gulpfile.js
├── karma.conf.js
├── package.json
├── src/
│   ├── api/
│   │   ├── common.test.ts
│   │   ├── common.ts
│   │   ├── fullscreen.test.ts
│   │   ├── fullscreen.ts
│   │   └── index.ts
│   ├── core/
│   │   ├── api.ts
│   │   ├── boot.ts
│   │   ├── constants.ts
│   │   ├── index.ts
│   │   ├── input.ts
│   │   ├── jquery.ts
│   │   ├── layout.ts
│   │   ├── models.ts
│   │   ├── playback.ts
│   │   ├── plugins.ts
│   │   └── state.ts
│   ├── index.ts
│   ├── lib.test.ts
│   ├── plugins/
│   │   ├── index.ts
│   │   ├── input-click.test.ts
│   │   ├── input-click.ts
│   │   ├── input-drag.test.ts
│   │   ├── input-drag.ts
│   │   ├── input-hold.test.ts
│   │   ├── input-hold.ts
│   │   ├── input-move.test.ts
│   │   ├── input-swipe.test.ts
│   │   ├── input-swipe.ts
│   │   ├── input-wheel.ts
│   │   ├── progress.ts
│   │   ├── render-360.test.ts
│   │   ├── render-360.ts
│   │   ├── render-blur.test.ts
│   │   ├── render-blur.ts
│   │   ├── render-ease.test.ts
│   │   ├── render-ease.ts
│   │   ├── render-gallery.test.ts
│   │   ├── render-gallery.ts
│   │   ├── render-panorama.test.ts
│   │   ├── render-panorama.ts
│   │   ├── render-zoom.test.ts
│   │   └── render-zoom.ts
│   ├── spritespin.test.ts
│   └── utils/
│       ├── cursor.ts
│       ├── detectSubsampling.test.ts
│       ├── detectSubsampling.ts
│       ├── index.ts
│       ├── jquery.ts
│       ├── layout.test.ts
│       ├── layout.ts
│       ├── measure.test.ts
│       ├── measure.ts
│       ├── naturalSize.test.ts
│       ├── naturalSize.ts
│       ├── preload.test.ts
│       ├── preload.ts
│       ├── sourceArray.test.ts
│       ├── sourceArray.ts
│       ├── utils.test.ts
│       └── utils.ts
├── tsconfig.cjs.json
├── tsconfig.esm2015.json
├── tsconfig.json
└── tslint.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true


================================================
FILE: .gitignore
================================================
.DS_Store
*.orig
.idea
.sass-cache/
.vscode
.coveralls.yml
node_modules
coverage
doc
release


================================================
FILE: .jshintrc
================================================
{
  "asi": true,
  "bitwise": true,
  "camelcase": true,
  "curly": true,
  "eqeqeq": true,
  "forin": true,
  "immed": true,
  "latedef": true,
  "newcap": true,
  "noarg": true,
  "nonew": true,
  "quotmark": "single",
  "strict": true,
  "undef": true,
  "unused": true,
  "esnext": true,
  "sub": true,
  "browser": false,
  "node": true
}


================================================
FILE: .travis.yml
================================================
language: node_js
matrix:
  include:
    - os: osx
addons:
  firefox: "53.0"
node_js:
  - "8"


================================================
FILE: LICENSE
================================================
Copyright (c) 2013 Alexander Gräfenstein

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================


================================================
FILE: gulpfile.js
================================================
'use strict'

const del = require('del')
const gulp = require('gulp')
const uglify = require('gulp-uglify')
const concat = require('gulp-concat')
const path = require('path')
const shell = require('shelljs')
const rollup = require('rollup')

const dstDir = path.join(__dirname, 'release')
const srcDir = path.join(__dirname, 'src')
const docDir = path.join(dstDir, 'doc')

function exec(command, cb) {
  shell
    .exec(command, { async: true })
    .on('exit', (code) => cb(code === 0 ? null : code))
}

gulp.task('clean', () => del(dstDir))

gulp.task('build:tsc', ['clean'], (cb) => {
  exec('yarn run build', cb)
})

gulp.task('build:esm2015', ['clean'], (cb) => {
  exec('yarn run build:esm2015', cb)
})

gulp.task('build:rollup', ['build:tsc'], () => {
  const resolve = require('rollup-plugin-node-resolve')
  const sourcemaps = require('rollup-plugin-sourcemaps')
  const globals = {
    '$': 'jquery',
  }

  return rollup.rollup({
    amd: {id: `SpriteSpin`},
    input: path.join(dstDir, 'src', 'index.js'),
    onwarn: (warning, warn) => {
      if (warning.code === 'THIS_IS_UNDEFINED') {return}
      warn(warning);
    },
    plugins: [resolve(), sourcemaps()],
    external: Object.keys(globals),
  })
  .then((bundle) => {
    return bundle.write({
      format: 'umd',
      sourcemap: true,
      file: path.join(dstDir, 'spritespin.js'),
      name: 'SpriteSpin',
      globals: globals,
      exports: 'named',
    })
  })
})

gulp.task('build:uglify', ['build:rollup'], () => {
  return gulp
    .src(path.join(dstDir, 'spritespin.js'))
    .pipe(uglify())
    .pipe(concat('spritespin.min.js'))
    .pipe(gulp.dest(dstDir))
})

gulp.task('build', ['build:tsc', 'build:esm2015', 'build:rollup', 'build:uglify'])

gulp.task('watch', ['build', 'api:json'], () => {
  gulp.watch([ path.join('src', '**', '*.ts') ], ['build', 'api:json'])
})

gulp.task('publish', ['build'], (cb) => {
  exec('npm publish --access=public', cb)
})

gulp.task('api:json', ['build'], (cb) => {
  const ae = require('@microsoft/api-extractor')
  const config = ae.ExtractorConfig.prepare({
    configObject: {
      compiler: {
        tsconfigFilePath: path.join(__dirname, 'tsconfig.json'),
      },
      apiReport: {
        enabled: false,
        reportFileName: 'spritespin.api.md',
        reportFolder: path.join(__dirname, 'doc'),
        reportTempFolder: path.join(__dirname, 'tmp'),
      },
      docModel: {
        enabled: true,
        apiJsonFilePath: path.join(__dirname, 'doc', 'spritespin.api.json'),
      },
      dtsRollup: {
        enabled: false,
      },
      projectFolder: path.join(dstDir, 'src'),
      mainEntryPointFilePath: path.join(dstDir, 'src', 'index.d.ts'),
    }
  })
  config.packageFolder = process.cwd()
  config.packageJson = require('./package.json')

  const result = ae.Extractor.invoke(config, {
    // Equivalent to the "--local" command-line parameter
    localBuild: true,

    // Equivalent to the "--verbose" command-line parameter
    showVerboseMessages: true
  })
  if (result.succeeded) {
    exec('./node_modules/.bin/api-documenter markdown -i doc -o doc', cb)
  } else {
    cb(1)
  }
})


================================================
FILE: karma.conf.js
================================================
'use strict'

const IS_COVERALLS = !!process.env.IS_COVERALLS
const IS_COVERAGE = IS_COVERALLS || !!process.env.IS_COVERAGE
const IS_TRAVIS = !!process.env.TRAVIS

const tsconfig = require('./tsconfig.json')

module.exports = (config) => {
  config.set({
    plugins: [
      'karma-jasmine',
      'karma-phantomjs-launcher',
      'karma-chrome-launcher',
      'karma-firefox-launcher',
      'karma-safari-launcher',
      'karma-typescript',
      'karma-mocha-reporter',
    ],
    logLevel: 'info',

    frameworks: [
      'jasmine',
      'karma-typescript',
    ],
    browsers: [
      IS_TRAVIS ? 'Firefox' : 'Chrome'
    ],
    files: [
      'node_modules/jquery/dist/jquery.js',
      'src/**/*.ts',
    ],
    preprocessors: {
      '**/*.ts': ['karma-typescript'],
    },
    reporters: [
      'mocha',
      'karma-typescript',
    ],

    karmaTypescriptConfig: {
      bundlerOptions: {
        entrypoints: /\.test\.ts$/,
        sourceMap: true,
        validateSyntax: false,
      },
      exclude: ['node_modules', 'release'],
      // compilerOptions: tsconfig.compilerOptions,
      tsconfig: 'tsconfig.cjs.json',
      // tsconfig: 'tsconfig.json',
      // Pass options to remap-istanbul.
      remapOptions: {
        // a regex for excluding files from remapping
        // exclude: '',
        // a function for handling error messages
        warn: (msg) => console.log(msg)
      },
      converageOptions: {
        instrumentation: IS_COVERAGE,
        exclude: /\.(d|spec|test)\.ts/i,
      },
      reports: {
        'text-summary': '',
        html: {
          directory: 'coverage',
          subdirectory: 'html',
        },
        lcovonly: {
          directory: 'coverage',
          subdirectory: 'lcov',
        },
      },
    },

  })
}


================================================
FILE: package.json
================================================
{
  "name": "spritespin",
  "description": "jQuery plugin for creating flipbook animations",
  "version": "4.1.0",
  "author": "Alexander Graefenstein",
  "license": "MIT",
  "keywords": [
    "spritespin",
    "360",
    "animation",
    "flipbook",
    "panorama",
    "jquery-plugin",
    "ecosystem:jquery"
  ],
  "main": "release/spritespin.js",
  "module": "release/src/index.js",
  "typings": "release/src/index.d.ts",
  "es2015": "release/esm2015/index.js",
  "files": [
    "release",
    "README.md",
    "LICENSE",
    "package.json"
  ],
  "repository": {
    "type": "git",
    "url": "git://github.com/giniedp/spritespin.git"
  },
  "bugs": {
    "url": "https://github.com/giniedp/spritespin/issues"
  },
  "engines": {
    "node": ">= 0.8.0"
  },
  "scripts": {
    "lint": "tslint --config tslint.json src/**/*.ts",
    "lint:silent": "tslint --config tslint.json src/**/*.ts || true",
    "build": "tsc --project tsconfig.json",
    "build:cjs": "tsc --project tsconfig.cjs.json",
    "build:esm2015": "tsc --project tsconfig.esm2015.json",
    "build:watch": "tsc -w",
    "test": "karma start --no-auto-watch --single-run",
    "test:watch": "karma start --auto-watch --no-single-run",
    "test:coverage": "IS_COVERAGE=yes karma start --single-run",
    "test:coveralls": "IS_COVERALLS=yes karma start --single-run && cat coverage/lcov/lcovonly | ./node_modules/.bin/coveralls"
  },
  "dependencies": {},
  "peerDependencies": {
    "jquery": ">1.11.0"
  },
  "devDependencies": {
    "@microsoft/api-documenter": "^7.2.0",
    "@microsoft/api-extractor": "^7.2.2",
    "@types/jasmine": "^2.5.54",
    "@types/jquery": "^3.2.12",
    "@types/node": "^8.0.27",
    "coveralls": "^3.0.0",
    "del": "^3.0.0",
    "gulp": "^3.9.1",
    "gulp-concat": "^2.6.1",
    "gulp-uglify": "^3.0.0",
    "jasmine-core": "^2.8.0",
    "jquery": "^3.2.1",
    "karma": "^1.7.1",
    "karma-chrome-launcher": "^2.2.0",
    "karma-firefox-launcher": "^1.0.1",
    "karma-jasmine": "1.1.0",
    "karma-mocha-reporter": "^2.2.4",
    "karma-phantomjs-launcher": "^1.0.4",
    "karma-safari-launcher": "^1.0.0",
    "karma-typescript": "^3.0.12",
    "rollup": "^0.56.5",
    "rollup-plugin-node-resolve": "^3.2.0",
    "rollup-plugin-sourcemaps": "^0.4.2",
    "shelljs": "^0.8.1",
    "through-gulp": "^0.5.0",
    "tslint": "^5.7.0",
    "typedoc": "^0.11.1",
    "typescript": "^2.7.2"
  }
}


================================================
FILE: src/api/common.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'

describe('SpriteSpin.Api#common', () => {

  let data: SpriteSpin.Data
  let api: any
  beforeEach((done) => {
    t.get$El().spritespin({
      source: [t.RED40x30, t.GREEN40x30, t.BLUE40x30, t.RED40x30, t.GREEN40x30, t.BLUE40x30],
      width: 40,
      height: 30,
      frameTime: 16,

      animate: false,
      onComplete: done
    })
    data = t.get$El().data(SpriteSpin.namespace)
    api = t.get$El().spritespin('api')
  })

  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('#isPlaying', () => {
    it ('detects if animation is running', () => {
      expect(api.isPlaying()).toBe(false)
      data.animate = true
      SpriteSpin.applyAnimation(data)
      expect(api.isPlaying()).toBe(true)
      SpriteSpin.stopAnimation(data)
      expect(api.isPlaying()).toBe(false)
    })
  })

  describe('#isLooping', () => {
    it ('returns data.loop property', () => {
      data.loop = false
      expect(api.isLooping()).toBe(false)
      data.loop = true
      expect(api.isLooping()).toBe(true)
    })
  })

  describe('#toggleAnimation', () => {
    it ('starts/stops the animation', () => {
      api.toggleAnimation()
      expect(api.isPlaying()).toBe(true, 'started')

      api.toggleAnimation()
      expect(api.isPlaying()).toBe(false, 'stopped')
    })
  })

  describe('#loop', () => {
    it ('sets the loop property', () => {
      api.loop(false)
      expect(api.isLooping()).toBe(false)
      api.loop(true)
      expect(api.isLooping()).toBe(true)
    })

    it ('starts the animation if animate=true', () => {
      expect(api.isPlaying()).toBe(false)
      data.animate = true
      api.loop(false)
      expect(api.isPlaying()).toBe(true)
    })
  })

  describe('#currentFrame', () => {
    it ('gets the current frame number', () => {
      data.frame = 1337
      expect(api.currentFrame()).toBe(1337)
      data.frame = 42
      expect(api.currentFrame()).toBe(42)
    })
  })

  describe('#updateFrame', () => {
    it ('updates the frame number', () => {
      // spyOn(SpriteSpin, 'updateFrame').and.callThrough()
      api.updateFrame(2)
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(1)
      expect(data.frame).toBe(2)
    })
  })

  describe('#skipFrames', () => {
    it ('skips number of frames', () => {
      expect(data.frame).toBe(0)
      expect(data.frames).toBe(6)
      // spyOn(SpriteSpin, 'updateFrame').and.callThrough()

      api.skipFrames(2)
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(1)
      expect(data.frame).toBe(2)

      api.skipFrames(2)
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(2)
      expect(data.frame).toBe(4)

      api.skipFrames(2)
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(3)
      expect(data.frame).toBe(0)
    })

    it ('steps backwards if reverse is true', () => {
      data.reverse = true
      expect(data.frame).toBe(0)
      expect(data.frames).toBe(6)
      // spyOn(SpriteSpin, 'updateFrame').and.callThrough()

      api.skipFrames(2)
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(1)
      expect(data.frame).toBe(4)

      api.skipFrames(2)
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(2)
      expect(data.frame).toBe(2)

      api.skipFrames(2)
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(3)
      expect(data.frame).toBe(0)
    })
  })

  describe('#nextFrame', () => {
    it ('increments frame', () => {
      expect(data.frame).toBe(0)
      expect(data.frames).toBe(6)
      // spyOn(SpriteSpin, 'updateFrame').and.callThrough()

      api.nextFrame()
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(1)
      expect(data.frame).toBe(1)

      api.nextFrame()
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(2)
      expect(data.frame).toBe(2)

      api.nextFrame()
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(3)
      expect(data.frame).toBe(3)
    })
  })

  describe('#prevFrame', () => {
    it ('decrements frame', () => {
      expect(data.frame).toBe(0)
      expect(data.frames).toBe(6)
      // spyOn(SpriteSpin, 'updateFrame').and.callThrough()

      api.prevFrame()
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(1)
      expect(data.frame).toBe(5)

      api.prevFrame()
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(2)
      expect(data.frame).toBe(4)

      api.prevFrame()
      // expect(SpriteSpin.updateFrame).toHaveBeenCalledTimes(3)
      expect(data.frame).toBe(3)
    })
  })

  describe('#playTo', () => {
    it ('skips animation if already on frame', () => {
      expect(api.isPlaying()).toBe(false)
      api.playTo(0)
      expect(api.isPlaying()).toBe(false)
    })

    it ('starts animation', () => {
      api.playTo(5)
      expect(api.isPlaying()).toBe(true)
    })

    it ('stops animation on given frame', (done) => {
      api.playTo(3)
      setTimeout(() => {
        expect(api.currentFrame()).toBe(3)
        expect(api.isPlaying()).toBe(false)
        done()
      }, 100)
    })

    describe('with nearest option', () => {
      it ('plays forward, if needed', () => {
        expect(data.reverse).toBe(false)
        api.playTo(2, { nearest: true })
        expect(data.reverse).toBe(false)
      })

      it ('plays backward, if needed', () => {
        expect(data.reverse).toBe(false)
        api.playTo(3, { nearest: true })
        expect(data.reverse).toBe(true)
      })
    })
  })
})


================================================
FILE: src/api/common.ts
================================================
import * as SpriteSpin from '../core'

// tslint:disable:object-literal-shorthand
// tslint:disable:only-arrow-functions

SpriteSpin.extendApi({
  // Gets a value indicating whether the animation is currently running.
  isPlaying: function() {
    return SpriteSpin.getPlaybackState(this.data).handler != null
  },

  // Gets a value indicating whether the animation looping is enabled.
  isLooping: function() {
    return this.data.loop
  },

  // Starts/Stops the animation playback
  toggleAnimation: function() {
    if (this.isPlaying()) {
      this.stopAnimation()
    } else {
      this.startAnimation()
    }
  },

  // Stops animation playback
  stopAnimation: function() {
    this.data.animate = false
    SpriteSpin.stopAnimation(this.data)
  },

  // Starts animation playback
  startAnimation: function() {
    this.data.animate = true
    SpriteSpin.applyAnimation(this.data)
  },

  // Sets a value indicating whether the animation should be looped or not.
  // This might start the animation (if the 'animate' data attribute is set to true)
  loop: function(value) {
    this.data.loop = value
    SpriteSpin.applyAnimation(this.data)
    return this
  },

  // Gets the current frame number
  currentFrame: function() {
    return this.data.frame
  },

  // Updates SpriteSpin to the specified frame.
  updateFrame: function(frame, lane) {
    SpriteSpin.updateFrame(this.data, frame, lane)
    return this
  },

  // Skips the given number of frames
  skipFrames: function(step) {
    const data = this.data
    SpriteSpin.updateFrame(data, data.frame + (data.reverse ? - step : + step))
    return this
  },

  // Updates SpriteSpin so that the next frame is shown
  nextFrame: function() {
    return this.skipFrames(1)
  },

  // Updates SpriteSpin so that the previous frame is shown
  prevFrame: function() {
    return this.skipFrames(-1)
  },

  // Starts the animations that will play until the given frame number is reached
  // options:
  //   force [boolean] starts the animation, even if current frame is the target frame
  //   nearest [boolean] animates to the direction with minimum distance to the target frame
  playTo: function(frame, options) {
    const data = this.data
    options = options || {}
    if (!options.force && data.frame === frame) {
      return
    }
    if (options.nearest) {
      // distance to the target frame
      const a = frame - data.frame
      // distance to last frame and the to target frame
      const b = frame > data.frame ? a - data.frames : a + data.frames
      // minimum distance
      const c = Math.abs(a) < Math.abs(b) ? a : b
      data.reverse = c < 0
    }
    data.animate = true
    data.loop = false
    data.stopFrame = frame
    SpriteSpin.applyAnimation(data)
    return this
  }
})


================================================
FILE: src/api/fullscreen.test.ts
================================================
import * as SpriteSpin from '..'

describe('SpriteSpin.Api#fullscreen', () => {
  // untestable
})


================================================
FILE: src/api/fullscreen.ts
================================================
import { boot, Data, extendApi, namespace, SizeMode } from '../core'
import { $ } from '../utils'

export interface Options {
  source?: string | string[],
  sizeMode?: SizeMode
}

function pick(target, names: string[]): string {
  for (const name of names) {
    if (target[name] || name in target) {
      return name
    }
  }
  return names[0]
}

const browser = {
  requestFullscreen: pick(document.documentElement, [
    'requestFullscreen',
    'webkitRequestFullScreen',
        'mozRequestFullScreen',
        'msRequestFullscreen']),
  exitFullscreen: pick(document, [
    'exitFullscreen',
    'webkitExitFullscreen',
    'webkitCancelFullScreen',
        'mozCancelFullScreen',
        'msExitFullscreen']),
  fullscreenElement: pick(document, [
    'fullscreenElement',
    'webkitFullscreenElement',
    'webkitCurrentFullScreenElement',
        'mozFullScreenElement',
        'msFullscreenElement']),
  fullscreenEnabled: pick(document, [
    'fullscreenEnabled',
    'webkitFullscreenEnabled',
        'mozFullScreenEnabled',
        'msFullscreenEnabled']),
  fullscreenchange: pick(document, [
    'onfullscreenchange',
    'onwebkitfullscreenchange',
        'onmozfullscreenchange',
        'onMSFullscreenChange']).replace(/^on/, ''),
  fullscreenerror: pick(document, [
    'onfullscreenerror',
    'onwebkitfullscreenerror',
        'onmozfullscreenerror',
        'onMSFullscreenError']).replace(/^on/, '')
}

const changeEvent = browser.fullscreenchange + '.' + namespace + '-fullscreen'
function unbindChangeEvent() {
  $(document).unbind(changeEvent)
}

function bindChangeEvent(callback) {
  unbindChangeEvent()
  $(document).bind(changeEvent, callback)
}

const orientationEvent = 'orientationchange.' + namespace + '-fullscreen'
function unbindOrientationEvent() {
  $(window).unbind(orientationEvent)
}
function bindOrientationEvent(callback) {
  unbindOrientationEvent()
  $(window).bind(orientationEvent, callback)
}

function requestFullscreenNative(e) {
  e = e || document.documentElement
  e[browser.requestFullscreen]()
}

export function exitFullscreen() {
  return document[browser.exitFullscreen]()
}

export function fullscreenEnabled() {
  return document[browser.fullscreenEnabled]
}

export function fullscreenElement() {
  return document[browser.fullscreenElement]
}

export function isFullscreen() {
  return !!fullscreenElement()
}

export function toggleFullscreen(data: Data, opts: Options) {
  if (isFullscreen()) {
    this.apiRequestFullscreen(opts)
  } else {
    this.exitFullscreen()
  }
}

export function requestFullscreen(data: Data, opts: Options) {
  opts = opts || {}
  const oWidth = data.width
  const oHeight = data.height
  const oSource = data.source
  const oSize = data.sizeMode
  const oResponsive = data.responsive
  const enter = () => {
    data.width = window.screen.width
    data.height = window.screen.height
    data.source = (opts.source || oSource) as string[]
    data.sizeMode = opts.sizeMode || 'fit'
    data.responsive = false
    boot(data)
  }
  const exit = () => {
    data.width = oWidth
    data.height = oHeight
    data.source = oSource
    data.sizeMode = oSize
    data.responsive = oResponsive
    boot(data)
  }

  bindChangeEvent(() => {
    if (isFullscreen()) {
      enter()
      bindOrientationEvent(enter)
    } else {
      unbindChangeEvent()
      unbindOrientationEvent()
      exit()
    }
  })
  requestFullscreenNative(data.target[0])
}

extendApi({
  fullscreenEnabled,
  fullscreenElement,
  exitFullscreen,
  toggleFullscreen: function(opts: Options) {
    toggleFullscreen(this.data, opts)
  },
  requestFullscreen: function(opts: Options) {
    requestFullscreen(this.data, opts)
  }
})


================================================
FILE: src/api/index.ts
================================================
import './common'
import './fullscreen'


================================================
FILE: src/core/api.ts
================================================
// tslint:disable ban-types
import { Data } from './models'

/**
 * @internal
 */
export class Api {
  constructor(public data: Data) { }
}

/**
 * Adds methods to the SpriteSpin api
 *
 * @public
 */
export function extendApi(methods: { [key: string]: Function }) {
  const api = Api.prototype
  for (const key in methods) {
    if (methods.hasOwnProperty(key)) {
      if (api[key]) {
        throw new Error('API method is already defined: ' + key)
      } else {
        api[key] = methods[key]
      }
    }
  }
  return api
}


================================================
FILE: src/core/boot.ts
================================================
import * as Utils from '../utils'
import { callbackNames, defaults, eventNames, eventsToPrevent, namespace } from './constants'
import { applyLayout } from './layout'
import { Data, Options } from './models'
import { applyAnimation, stopAnimation } from './playback'
import { applyPlugins } from './plugins'

const $ = Utils.$

let counter = 0
/**
 * Collection of all SpriteSpin instances
 */
export const instances: {[key: string]: Data} = {}

function pushInstance(data: Data) {
  counter += 1
  data.id = String(counter)
  instances[data.id] = data
}

function popInstance(data: Data) {
  delete instances[data.id]
}

function eachInstance(cb) {
  for (const id in instances) {
    if (instances.hasOwnProperty(id)) {
      cb(instances[id])
    }
  }
}

let lazyinit = () => {
  // replace function with a noop
  // this logic must run only once
  lazyinit = () => { /* noop */ }

  function onEvent(eventName, e) {
    eachInstance((data: Data) => {
      for (const module of data.plugins) {
        if (typeof module[eventName] === 'function') {
          module[eventName].apply(data.target, [e, data])
        }
      }
    })
  }

  function onResize() {
    eachInstance((data: Data) => {
      if (data.responsive) {
        boot(data)
      }
    })
  }

  for (const eventName of eventNames) {
    $(window.document).bind(eventName + '.' + namespace, (e) => {
      onEvent('document' + eventName, e)
    })
  }

  let resizeTimeout = null
  $(window).on('resize', () => {
    window.clearTimeout(resizeTimeout)
    resizeTimeout = window.setTimeout(onResize, 100)
  })
}

/**
 * (re)binds all spritespin events on the target element
 *
 * @internal
 */
export function applyEvents(data: Data) {
  const target = data.target

  // Clear all SpriteSpin events on the target element
  Utils.unbind(target)

  // disable all default browser behavior on the following events
  // mainly prevents image drag operation
  for (const eName of eventsToPrevent) {
    Utils.bind(target, eName, Utils.prevent)
  }

  // Bind module functions to SpriteSpin events
  for (const plugin of data.plugins) {
    for (const eName of eventNames) {
      Utils.bind(target, eName, plugin[eName])
    }
    for (const eName of callbackNames) {
      Utils.bind(target, eName, plugin[eName])
    }
  }

  // bind auto start function to load event.
  Utils.bind(target, 'onLoad', (e, d) => {
    applyAnimation(d)
  })

  // bind all user events that have been passed on initialization
  for (const eName of callbackNames) {
    Utils.bind(target, eName, data[eName])
  }
}

function applyMetrics(data: Data) {
  if (!data.images) {
    data.metrics = []
  }
  data.metrics = Utils.measure(data.images, data)
  const spec = Utils.findSpecs(data.metrics, data.frames, 0, 0)
  if (spec.sprite) {
    // TODO: try to remove frameWidth/frameHeight
    data.frameWidth = spec.sprite.width
    data.frameHeight = spec.sprite.height
  }
}

/**
 * Runs the boot process.
 *
 * @remarks
 * (re)initializes plugins, (re)initializes the layout, (re)binds events and loads source images.
 *
 * @internal
 */
export function boot(data: Data) {
  applyPlugins(data)
  applyEvents(data)
  applyLayout(data)

  data.source = Utils.toArray(data.source)
  data.loading = true
  data.target
    .addClass('loading')
    .trigger('onInit.' + namespace, data)
  Utils.preload({
    source: data.source,
    crossOrigin: data.crossOrigin,
    preloadCount: data.preloadCount,
    progress: (progress) => {
      data.progress = progress
      data.target.trigger('onProgress.' + namespace, data)
    },
    complete: (images) => {
      data.images = images
      data.loading = false
      data.frames = data.frames || images.length

      applyMetrics(data)
      applyLayout(data)
      data.stage.show()
      data.target
        .removeClass('loading')
        .trigger('onLoad.' + namespace, data)
        .trigger('onFrame.' + namespace, data)
        .trigger('onDraw.' + namespace, data)
        .trigger('onComplete.' + namespace, data)
    }
  })
}

/**
 * Creates a new SpriteSpin instance
 *
 * @public
 */
export function create(options: Options): Data {
  const target = options.target

  // SpriteSpin is not initialized
  // Create default settings object and extend with given options
  const data = $.extend(true, {}, defaults, options) as Data

  // ensure source is set
  data.source = data.source || []
  // ensure plugins are set
  data.plugins = data.plugins || [
    '360',
    'drag'
  ]

  // if image tags are contained inside this DOM element
  // use these images as the source files
  target.find('img').each(() => {
    if (!Array.isArray(data.source)) {
      data.source = []
    }
    data.source.push($(this).attr('src'))
  })

  // build inner html
  // <div>
  //   <div class='spritespin-stage'></div>
  //   <canvas class='spritespin-canvas'></canvas>
  // </div>
  target
    .empty()
    .addClass('spritespin-instance')
    .append("<div class='spritespin-stage'></div>")

  // add the canvas element if canvas rendering is enabled and supported
  if (data.renderer === 'canvas') {
    const canvas = document.createElement('canvas')
    if (!!(canvas.getContext && canvas.getContext('2d'))) {
      data.canvas = $(canvas).addClass('spritespin-canvas') as any
      data.context = canvas.getContext('2d')
      target.append(data.canvas)
      target.addClass('with-canvas')
    } else {
      // fallback to image rendering mode
      data.renderer = 'image'
    }
  }

  // setup references to DOM elements
  data.target = target
  data.stage = target.find('.spritespin-stage')

  // store the data
  target.data(namespace, data)
  pushInstance(data)

  return data
}

/**
 * Creates a new SpriteSpin instance, or updates an existing one
 *
 * @public
 */
export function createOrUpdate(options: Options): Data {
  lazyinit()

  let data  = options.target.data(namespace) as Data
  if (!data) {
    data = create(options)
  } else {
    $.extend(data, options)
  }
  boot(data)

  return data
}

/**
 * Destroys the SpriteSpin instance
 *
 * @remarks
 * - stops running animation
 * - unbinds all events
 * - deletes the data on the target element
 *
 * @public
 */
export function destroy(data: Data) {
  popInstance(data)
  stopAnimation(data)
  data.target
    .trigger('onDestroy', data)
    .html(null)
    .attr('style', null)
    .attr('unselectable', null)
    .removeClass(['spritespin-instance', 'with-canvas'])
  Utils.unbind(data.target)
  data.target.removeData(namespace)
}


================================================
FILE: src/core/constants.ts
================================================
import { Options } from './models'

/**
 * The namespace that is used to bind functions to DOM events and store the data object
 */
export const namespace = 'spritespin'

/**
 * Event names that are recognized by SpriteSpin. A module can implement any of these and they will be bound
 * to the target element on which the plugin is called.
 */
export const eventNames = [
  'mousedown',
  'mousemove',
  'mouseup',
  'mouseenter',
  'mouseover',
  'mouseleave',
  'mousewheel',
  'wheel',
  'click',
  'dblclick',
  'touchstart',
  'touchmove',
  'touchend',
  'touchcancel',
  'selectstart',
  'gesturestart',
  'gesturechange',
  'gestureend'
]

/**
 *
 */
export const callbackNames = [
  'onInit',
  'onProgress',
  'onLoad',
  'onFrameChanged',
  'onFrame',
  'onDraw',
  'onComplete',
  'onDestroy'
]

/**
 * Names of events for that the default behavior should be prevented.
 */
export const eventsToPrevent = [
  'dragstart'
]

/**
 * Default set of SpriteSpin options. This also represents the majority of data attributes that are used during the
 * lifetime of a SpriteSpin instance. The data is stored inside the target DOM element on which the plugin is called.
 */
export const defaults: Options = {
  source            : undefined,    // Stitched source image or array of files
  width             : undefined,    // actual display width
  height            : undefined,    // actual display height
  frames            : undefined,    // Total number of frames
  framesX           : undefined,    // Number of frames in one row of sprite sheet (if source is a sprite sheet)
  lanes             : 1,            // Number of 360 sequences. Used for 3D like effect.
  sizeMode          : undefined,    //

  renderer          : 'canvas',     // The rendering mode to use

  lane              : 0,            // The initial sequence number to play
  frame             : 0,            // Initial (and current) frame number
  frameTime         : 40,           // Time in ms between updates. 40 is exactly 25 FPS
  animate           : true,         // If true starts the animation on load
  retainAnimate     : false,        // If true, retains the animation after user interaction
  reverse           : false,        // If true animation is played backward
  loop              : true,         // If true loops the animation
  stopFrame         : 0,            // Stops the animation at this frame if loop is disabled

  wrap              : true,         // If true wraps around the frame index on user interaction
  wrapLane          : false,        // If true wraps around the lane index on user interaction
  sense             : 1,            // Interaction sensitivity used by behavior implementations
  senseLane         : undefined,    // Interaction sensitivity used by behavior implementations
  orientation       : 'horizontal', // Preferred axis for user interaction
  detectSubsampling : true,         // Tries to detect whether the images are down sampled by the browser.
  preloadCount      : undefined,    // Number of frames to preload. If nothing is set, all frames are preloaded.

  touchScrollTimer  : [200, 1500],  // Time range in ms when touch scroll will be disabled during interaction with SpriteSpin
  responsive        : undefined,
  plugins           : undefined
}


================================================
FILE: src/core/index.ts
================================================
export * from './api'
export * from './boot'
export * from './constants'
export * from './input'
export * from './layout'
export * from './models'
export * from './playback'
export * from './plugins'
export * from './state'

import './jquery'


================================================
FILE: src/core/input.ts
================================================
import { getCursorPosition } from '../utils'
import { Data } from './models'
import { getState } from './state'

/**
 * Describes a SpriteSpin input state
 *
 * @public
 */
export interface InputState {
  oldX: number
  oldY: number
  currentX: number
  currentY: number
  startX: number
  startY: number
  clickframe: number
  clicklane: number
  dX: number
  dY: number
  ddX: number
  ddY: number
  ndX: number
  ndY: number
  nddX: number
  nddY: number
}

/**
 * Gets the current input state
 *
 * @public
 * @param data - The SpriteSpin instance data
 */
export function getInputState(data: Data): InputState {
  return getState(data, 'input')
}

/**
 * Updates the input state using a mouse or touch event.
 *
 * @public
 * @param e - The input event
 * @param data - The SpriteSpin instance data
 */
export function updateInput(e, data: Data) {
  const cursor = getCursorPosition(e)
  const state = getInputState(data)

  // cache positions from previous frame
  state.oldX = state.currentX
  state.oldY = state.currentY

  state.currentX = cursor.x
  state.currentY = cursor.y

  // Fix old position.
  if (state.oldX === undefined || state.oldY === undefined) {
    state.oldX = state.currentX
    state.oldY = state.currentY
  }

  // Cache the initial click/touch position and store the frame number at which the click happened.
  // Useful for different behavior implementations. This must be restored when the click/touch is released.
  if (state.startX === undefined || state.startY === undefined) {
    state.startX = state.currentX
    state.startY = state.currentY
    state.clickframe = data.frame
    state.clicklane = data.lane
  }

  // Calculate the vector from start position to current pointer position.
  state.dX = state.currentX - state.startX
  state.dY = state.currentY - state.startY

  // Calculate the vector from last frame position to current pointer position.
  state.ddX = state.currentX - state.oldX
  state.ddY = state.currentY - state.oldY

  // Normalize vectors to range [-1:+1]
  state.ndX = state.dX / data.target.innerWidth()
  state.ndY = state.dY / data.target.innerHeight()

  state.nddX = state.ddX / data.target.innerWidth()
  state.nddY = state.ddY / data.target.innerHeight()
}

/**
 * Resets the input state.
 *
 * @public
 */
export function resetInput(data: Data) {
  const input = getInputState(data)
  input.startX = input.startY = undefined
  input.currentX = input.currentY = undefined
  input.oldX = input.oldY = undefined
  input.dX = input.dY = 0
  input.ddX = input.ddY = 0
  input.ndX = input.ndY = 0
  input.nddX = input.nddY = 0
}


================================================
FILE: src/core/jquery.ts
================================================
import { $ } from '../utils'
import { Api } from './api'
import { createOrUpdate, destroy } from './boot'
import { namespace } from './constants'

function extension(option: string | any, value: any) {
  const $target = $(this)
  if (option === 'data') {
    return $target.data(namespace)
  }
  if (option === 'api') {
    const data = $target.data(namespace)
    data.api = data.api || new Api(data)
    return data.api
  }
  if (option === 'destroy') {
    return $target.each(() => {
      const data = $target.data(namespace)
      if (data) {
        destroy(data)
      }
    })
  }
  if (arguments.length === 2 && typeof option === 'string') {
    option = { [option]: value }
  }
  if (typeof option === 'object') {
    return createOrUpdate($.extend(true, { target: $target }, option)).target
  }

  throw new Error('Invalid call to spritespin')
}

$.fn[namespace] = extension


================================================
FILE: src/core/layout.ts
================================================
import * as Utils from '../utils'
import { Data } from './models'

/**
 * Applies css attributes to layout the SpriteSpin containers.
 *
 * @internal
 */
export function applyLayout(data: Data) {
  // disable selection
  data.target
    .attr('unselectable', 'on')
    .css({
      width: '',
      height: '',
      '-ms-user-select': 'none',
      '-moz-user-select': 'none',
      '-khtml-user-select': 'none',
      '-webkit-user-select': 'none',
      'user-select': 'none'
    })

  const size = data.responsive ? Utils.getComputedSize(data) : Utils.getOuterSize(data)
  const layout = Utils.getInnerLayout(data.sizeMode, Utils.getInnerSize(data), size)

  // apply layout on target
  data.target.css({
    width    : size.width,
    height   : size.height,
    position : 'relative',
    overflow : 'hidden'
  })

  // apply layout on stage
  data.stage
    .css(layout)
    .hide()

  if (!data.canvas) { return }
  // apply layout on canvas
  data.canvas.css(layout).hide()
  // apply pixel ratio on canvas
  data.canvasRatio = data.canvasRatio || Utils.pixelRatio(data.context)
  if (typeof layout.width === 'number' && typeof layout.height === 'number') {
    data.canvas[0].width = ((layout.width as number) * data.canvasRatio) || size.width
    data.canvas[0].height = ((layout.height as number) * data.canvasRatio) || size.height
  } else {
    data.canvas[0].width = (size.width * data.canvasRatio)
    data.canvas[0].height = (size.height * data.canvasRatio)
  }
  // width and height must be set before calling scale
  data.context.scale(data.canvasRatio, data.canvasRatio)
}


================================================
FILE: src/core/models.ts
================================================
import { PreloadProgress, SheetSpec } from '../utils'

export type Callback = (e: any, data: Data) => void

/**
 * Additional callback options for SpriteSpin
 *
 * @public
 */
export interface CallbackOptions {
  /**
   * Occurs when the plugin has been initialized, but before loading the source files.
   */
  onInit?: Callback
  /**
   * Occurs when any source file has been loaded and the progress has changed.
   */
  onProgress?: Callback
  /**
   * Occurs when all source files have been loaded and spritespin is ready to update and draw.
   */
  onLoad?: Callback
  /**
   * Occurs when the frame number has been updated (e.g. during animation)
   */
  onFrame?: Callback
  /**
   * Occurs when the frame number has changed.
   */
  onFrameChanged?: Callback
  /**
   * Occurs when all update is complete and frame can be drawn
   */
  onDraw?: Callback
  /**
   * Occurs when spritespin has been loaded and the first draw operation is complete
   */
  onComplete?: Callback
}

export type SizeMode = 'original' | 'fit' | 'fill' | 'stretch'
export type RenderMode = 'canvas' | 'image' | 'background'
export type Orientation = 'horizontal' | 'vertical'

/**
 * Options for SpriteSpin
 *
 * @public
 */
export interface Options extends CallbackOptions {
  /**
   * The target element which should hold the spritespin instance. This is usually already specified by the jQuery selector but can be overridden here.
   */
  target?: any,

  /**
   * Image URL or array of urls to be used.
   */
  source: string | string[]

  /**
   * The display width in pixels.
   *
   * @remarks
   * Width and height should match the aspect ratio of the frames.
   */
  width?: number

  /**
   * The display height in pixels.
   *
   * @remarks
   * Width and height should match the aspect ratio of the frames.
   */
  height?: number

  /**
   * Number of frames for a full 360 rotation.
   *
   * @remarks
   * If multiple lanes are used, each lane must have this amount of frames.
   */
  frames: number

  /**
   * Number of frames in one row of a single sprite sheet.
   */
  framesX?: number

  /**
   * Number of frames in one column of a single sprite sheet.
   */
  framesY?: number

  /**
   * Number of sequences.
   */
  lanes?: number

  /**
   * Specifies how the frames are sized and scaled if it does not match the given
   * width and height dimensions.
   */
  sizeMode?: SizeMode

  /**
   * The presentation module to use
   *
   * @deprecated please use plugins option instead
   */
  module?: string
  /**
   * The interaction module to use
   *
   * @deprecated please use plugins option instead
   */
  behavior?: string

  /**
   * Specifies the rendering mode.
   */
  renderer?: RenderMode

  /**
   * The initial sequence number to play.
   *
   * @remarks
   * This value is updated each frame and also represents the current lane number.
   */
  lane?: number
  /**
   * Initial frame number.
   *
   * @remarks
   * This value is updated each frame and also represents the current frame number.
   */
  frame?: number
  /**
   * Time in ms between updates. e.g. 40 is exactly 25 FPS
   */
  frameTime?: number
  /**
   * If true, starts the animation automatically on load
   */
  animate?: boolean
  /**
   * If true, retains the animation after user user interaction
   */
  retainAnimate?: boolean
  /**
   * If true, animation playback is reversed
   */
  reverse?: boolean
  /**
   * If true, continues playback in a loop.
   */
  loop?: boolean
  /**
   * Stops the animation on that frame if `loop` is false.
   */
  stopFrame?: number

  /**
   * If true, allows the user to drag the animation beyond the last frame and wrap over to the beginning.
   */
  wrap?: boolean
  /**
   * If true, allows the user to drag the animation beyond the last sequence and wrap over to the beginning.
   */
  wrapLane?: boolean
  /**
   * Sensitivity factor for user interaction
   */
  sense?: number
  /**
   * Sensitivity factor for user interaction
   */
  senseLane?: number
  /**
   * Preferred axis for user interaction
   */
  orientation?: Orientation | number
  /**
   * If true, tries to detect whether the images are down sampled by the browser.
   */
  detectSubsampling?: boolean
  /**
   * Number of images to preload. If nothing is set, all images are preloaded.
   */
  preloadCount?: number

  /**
   * If true, display width can be controlled by CSS.
   *
   * @remarks
   * Width and height must still both be set and are used to calculate the aspect ratio.
   */
  responsive?: boolean

  /**
   * Time range in ms when touch scroll will be disabled during interaction with SpriteSpin
   */
  touchScrollTimer?: [number, number]

  /**
   * Array of plugins to load
   */
  plugins?: any[]

  /**
   * Allows to download images from foreign origins
   *
   * @remarks
   * https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
   */
  crossOrigin?: string
}

/**
 * The instance data of SpriteSpin
 *
 * @public
 */
export interface Data extends Options {
  /**
   * The unique spritespin instance identifier
   */
  id: string

  /**
   * Array of all image urls
   */
  source: string[]

  /**
   * Array of all image elements
   */
  images: HTMLImageElement[]

  /**
   * The current preload progress state
   */
  progress: null | PreloadProgress

  /**
   * Array with measurement information for each image
   */
  metrics: SheetSpec[]

  /**
   * The detected width of a single frame
   */
  frameWidth: number

  /**
   * The detected height of a single frame
   */
  frameHeight: number

  /**
   * Opaque state object. Plugins may store their information here.
   */
  state: any

  /**
   * Is true during the preload phase
   */
  loading: boolean

  /**
   * The target element
   */
  target: JQuery

  /**
   * The inner stage element
   */
  stage: JQuery

  /**
   * The inner canvas element
   */
  canvas: JQuery<HTMLCanvasElement>

  /**
   * The 2D context of the canvas element
   */
  context: CanvasRenderingContext2D

  /**
   * The pixel ratio of the canvas element
   */
  canvasRatio: number
}


================================================
FILE: src/core/playback.ts
================================================
import { clamp, wrap } from '../utils'
import { namespace } from './constants'
import { Data } from './models'
import { getState } from './state'
/**
 * Gets the playback state
 *
 * @public
 * @param data - The SpriteSpin instance data
 */
export function getPlaybackState(data: Data): PlaybackState {
  return getState(data, 'playback')
}

/**
 * The playback state
 *
 * @public
 */
export interface PlaybackState {
  frameTime: number
  lastFrame: number
  lastLane: number
  handler: number
}

function updateLane(data: Data, lane: number) {
  data.lane = data.wrapLane
    ? wrap(lane, 0, data.lanes - 1, data.lanes)
    : clamp(lane, 0, data.lanes - 1)
}

function updateAnimationFrame(data: Data) {
  data.frame += (data.reverse ? -1 : 1)
  // wrap the frame value to fit in range [0, data.frames)
  data.frame = wrap(data.frame, 0, data.frames - 1, data.frames)
  // stop animation if loop is disabled and the stopFrame is reached
  if (!data.loop && (data.frame === data.stopFrame)) {
    stopAnimation(data)
  }
}

function updateInputFrame(data: Data, frame: number) {
  data.frame = Number(frame)
  data.frame = data.wrap
    ? wrap(data.frame, 0, data.frames - 1, data.frames)
    : clamp(data.frame, 0, data.frames - 1)
}

function updateAnimation(data: Data) {
  const state = getPlaybackState(data)
  if (state.handler) {
    updateBefore(data)
    updateAnimationFrame(data)
    updateAfter(data)
  }
}

function updateBefore(data: Data) {
  const state = getPlaybackState(data)
  state.lastFrame = data.frame
  state.lastLane = data.lane
}

function updateAfter(data: Data) {
  const state = getPlaybackState(data)
  if (state.lastFrame !== data.frame || state.lastLane !== data.lane) {
    data.target.trigger('onFrameChanged.' + namespace, data)
  }
  data.target.trigger('onFrame.' + namespace, data)
  data.target.trigger('onDraw.' + namespace, data)
}

/**
 * Updates the frame or lane number of the SpriteSpin data.
 *
 * @public
 * @param data - The SpriteSpin instance data
 * @param frame - The frame number to set
 * @param lane - The lane number to set
 */
export function updateFrame(data: Data, frame?: number, lane?: number) {
  updateBefore(data)
  if (frame != null) {
    updateInputFrame(data, frame)
  }
  if (lane != null) {
    updateLane(data, lane)
  }
  updateAfter(data)
}

/**
 * Stops the running animation.
 *
 * @public
 * @param data - The SpriteSpin instance data
 */
export function stopAnimation(data: Data) {
  data.animate = false
  const state = getPlaybackState(data)
  if (state.handler != null) {
    window.clearInterval(state.handler)
    state.handler = null
  }
}

/**
 * Starts animation playback if needed.
 *
 * @remarks
 * Starts animation playback if `animate` property is `true` and the animation is not yet running.
 *
 * @public
 * @param data - The SpriteSpin instance data
 */
export function applyAnimation(data: Data) {
  const state = getPlaybackState(data)
  if (state.handler && (!data.animate || state.frameTime !== data.frameTime)) {
    stopAnimation(data)
  }
  if (data.animate && !state.handler) {
    state.frameTime = data.frameTime
    state.handler = window.setInterval(() => updateAnimation(data), state.frameTime)
  }
}

/**
 * Starts the animation playback
 *
 * @remarks
 * Starts the animation playback and also sets the `animate` property to `true`
 *
 * @public
 * @param data - The SpriteSpin instance data
 */
export function startAnimation(data: Data) {
  data.animate = true
  applyAnimation(data)
}


================================================
FILE: src/core/plugins.ts
================================================
import { error, warn } from '../utils'
import { Callback, CallbackOptions, Data } from './models'

/**
 * Describes a SpriteSpin plugin
 *
 * @public
 */
export interface SpriteSpinPlugin extends CallbackOptions {
  [key: string]: Callback | string
  name?: string
}

const plugins: { [key: string]: SpriteSpinPlugin } = {}

/**
 * Registers a plugin.
 *
 * @remarks
 * Use this to add custom Rendering or Updating modules that can be addressed with the 'module' option.
 *
 * @public
 * @param name - The name of the plugin
 * @param plugin - The plugin implementation
 */
export function registerPlugin(name: string, plugin: SpriteSpinPlugin) {
  if (plugins[name]) {
    error(`Plugin name "${name}" is already taken`)
    return
  }
  plugin = plugin || {}
  plugins[name] = plugin
  return plugin
}

/**
 * Registers a plugin.
 *
 * @public
 * @deprecated Use {@link registerPlugin} instead
 * @param name - The name of the plugin
 * @param plugin - The plugin implementation
 */
export function registerModule(name: string, plugin: SpriteSpinPlugin) {
  warn('"registerModule" is deprecated, use "registerPlugin" instead')
  registerPlugin(name, plugin)
}

/**
 * Gets an active plugin by name
 *
 * @internal
 * @param name - The name of the plugin
 */
export function getPlugin(name) {
  return plugins[name]
}

/**
 * Replaces module names on given SpriteSpin data and replaces them with actual implementations.
 * @internal
 */
export function applyPlugins(data: Data) {
  fixPlugins(data)
  for (let i = 0; i < data.plugins.length; i += 1) {
    const name = data.plugins[i]
    if (typeof name !== 'string') {
      continue
    }
    const plugin = getPlugin(name)
    if (!plugin) {
      error('No plugin found with name ' + name)
      continue
    }
    data.plugins[i] = plugin
  }
}

function fixPlugins(data: Data) {
  // tslint:disable no-string-literal

  if (data['mods']) {
    warn('"mods" option is deprecated, use "plugins" instead')
    data.plugins = data['mods']
    delete data['mods']
  }

  if (data['behavior']) {
    warn('"behavior" option is deprecated, use "plugins" instead')
    data.plugins.push(data['behavior'])
    delete data['behavior']
  }

  if (data['module']) {
    warn('"module" option is deprecated, use "plugins" instead')
    data.plugins.push(data['module'])
    delete data['module']
  }
}


================================================
FILE: src/core/state.ts
================================================
import { Data } from './models'

/**
 * Gets a state object by name.
 * @internal
 * @param data - The SpriteSpin instance data
 * @param name - The name of the state object
 */
export function getState<T = any>(data: Data, name: string): T {
  data.state = data.state || {}
  data.state[name] = data.state[name] || {}
  return data.state[name]
}

/**
 * Gets a plugin state object by name.
 *
 * @remarks
 * Plugins should use this method to get or create a state object where they can
 * store any instance variables.
 *
 * @public
 * @param data - The SpriteSpin instance data
 * @param name - The name of the plugin
 */
export function getPluginState<T = any>(data: Data, name: string): T {
  const state = getState<T>(data, 'plugin')
  state[name] = state[name] || {}
  return state[name]
}

/**
 * Checks whether a flag is set. See {@link flag}.
 *
 * @public
 * @param data - The SpriteSpin instance data
 * @param key - The name of the flag
 */
export function is(data: Data, key: string): boolean {
  return !!getState(data, 'flags')[key]
}

/**
 * Sets a flag value. See {@link is}.
 *
 * @public
 * @param data - The SpriteSpin instance data
 * @param key - The name of the flag
 * @param value - The value to set
 */
export function flag(data: Data, key: string, value: boolean) {
  getState(data, 'flags')[key] = !!value
}


================================================
FILE: src/index.ts
================================================
export * from './core'
export { sourceArray } from './utils'

import {
  $,
  bind,
  clamp,
  detectSubsampling,
  error,
  findSpecs,
  getComputedSize,
  getCursorPosition,
  getInnerLayout,
  getInnerSize,
  getOuterSize,
  isFunction,
  Layout,
  Layoutable,
  log,
  measure,
  MeasureSheetOptions,
  naturalSize,
  noop,
  pixelRatio,
  preload,
  PreloadOptions,
  PreloadProgress,
  prevent,
  SheetSpec,
  SizeWithAspect,
  sourceArray,
  SourceArrayOptions,
  SpriteSpec,
  toArray,
  unbind,
  warn,
  wrap
} from './utils'

export const Utils = {
  $,
  bind,
  clamp,
  detectSubsampling,
  error,
  findSpecs,
  getComputedSize,
  getCursorPosition,
  getInnerLayout,
  getInnerSize,
  getOuterSize,
  isFunction,
  log,
  measure,
  naturalSize,
  noop,
  pixelRatio,
  preload,
  prevent,
  sourceArray,
  toArray,
  unbind,
  warn,
  wrap
}

export {
  Layout,
  Layoutable,
  MeasureSheetOptions,
  PreloadOptions,
  PreloadProgress,
  SheetSpec,
  SizeWithAspect,
  SourceArrayOptions,
  SpriteSpec
}

import './api'
import './plugins'


================================================
FILE: src/lib.test.ts
================================================
import { $ } from './utils'

export const WHITE40x30 = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAQAAAD01JRWAAAAJElEQVR42u3MMQEAAAgDINc/9Ezg5wkBSDuvIhQKhUKhUCi8LW/iO+NtzNg6AAAAAElFTkSuQmCC'
export const WHITE50x50 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAQAAAC0NkA6AAAALUlEQVR42u3NMQEAAAgDINc/9Mzg4QcFSDvvIpFIJBKJRCKRSCQSiUQikUhuFtAOY89wCn1dAAAAAElFTkSuQmCC'
export const RED40x30 = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAYAAABe3VzdAAAAMUlEQVR42u3OMREAAAjEMN6/aMAAO0N6FZB01f63AAICAgICAgICAgICAgICAgICXg1dLDvjAn5XTwAAAABJRU5ErkJggg=='
export const GREEN40x30 = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAYAAABe3VzdAAAAMElEQVR42u3OMQEAAAjDMOZfNGCAnyOtgaR6f1wAAQEBAQEBAQEBAQEBAQEBAQGvBj9KO+MrG0hPAAAAAElFTkSuQmCC'
export const BLUE40x30 = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAYAAABe3VzdAAAAL0lEQVR42u3OMQEAAAgDoK1/aE3g7wEJaDKTxyooKCgoKCgoKCgoKCgoKCgoKHhZIWg740ZlTUgAAAAASUVORK5CYII='

export function mouseEvent(name: string, clientX: number, clientY: number) {
  const e = document.createEvent('MouseEvent') as MouseEvent
  e.initMouseEvent(name, true, true, window, 0, 0, 0, clientX, clientY, false, false, false, false, 0, element)
  return e
}

export function mouseDown(el: HTMLElement, x, y) {
  el.dispatchEvent(mouseEvent('mousedown', x, y))
}
export function mouseUp(el: HTMLElement, x, y) {
  el.dispatchEvent(mouseEvent('mouseup', x, y))
}
export function mouseLeave(el: HTMLElement, x, y) {
  el.dispatchEvent(mouseEvent('mouseleave', x, y))
}
export function mouseMove(el: HTMLElement, x, y) {
  el.dispatchEvent(mouseEvent('mousemove', x, y))
}
export function touchStart(el: HTMLElement, x, y) {
  el.dispatchEvent(mouseEvent('touchstart', x, y))
}
export function touchMove(el: HTMLElement, x, y) {
  el.dispatchEvent(mouseEvent('touchmove', x, y))
}
export function touchEnd(el: HTMLElement, x, y) {
  el.dispatchEvent(mouseEvent('touchend', x, y))
}

export function moveMouse(el: HTMLElement, startX, startY, endX, endY) {
  mouseMove(el, startX, startY)
  mouseMove(el, endX, endY)
}
export function moveTouch(el: HTMLElement, startX, startY, endX, endY) {
  touchMove(el, startX, startY)
  touchMove(el, endX, endY)
}
export function dragMouse(el: HTMLElement, startX, startY, endX, endY) {
  mouseDown(el, startX, startY)
  mouseMove(el, endX, endY)
  mouseUp(el, endX, endY)
}
export function dragTouch(el: HTMLElement, startX, startY, endX, endY) {
  touchStart(el, startX, startY)
  touchMove(el, endX, endY)
  touchEnd(el, endX, endY)
}

export function getEl(): HTMLElement {
  return element
}
export function get$El(): any {
  return $(element)
}

let element: HTMLElement

beforeAll(() => {
  $(document.body).css({ margin: 0, padding: 0 })
})

beforeEach(() => {
  $(document.body).append('<div class="spritespin"></div>')
  element = $('.spritespin')[0]
})
afterEach(() => {
  element.remove()
})


================================================
FILE: src/plugins/index.ts
================================================
import './input-click'
import './input-drag'
import './input-hold'
import './input-swipe'
import './input-wheel'
import './progress'
import './render-360'
import './render-blur'
import './render-ease'
import './render-gallery'
import './render-panorama'
import './render-zoom'


================================================
FILE: src/plugins/input-click.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'

describe('SpriteSpin.Plugins#input-click', () => {

  let data: SpriteSpin.Data

  beforeEach((done) => {
    t.get$El().spritespin({
      source: t.WHITE50x50,
      width: 10,
      height: 10,
      frames: 25,
      onLoad: done,
      plugins: ['click', '360']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })
  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('interaction', () => {
    describe('in loading state', () => {
      it ('is idle', () => {
        data.loading = true
        expect(data.frame).toBe(0, 'initial')
        t.mouseUp(t.getEl(), 0, 0)
        expect(data.frame).toBe(0)
        t.mouseUp(t.getEl(), 10, 0)
        expect(data.frame).toBe(0)
        t.mouseUp(t.getEl(), 0, 10)
        expect(data.frame).toBe(0)
        t.mouseUp(t.getEl(), 10, 10)
        expect(data.frame).toBe(0)
      })
    })

    describe('in horizontal mode', () => {
      beforeEach(() => {
        data.orientation = 'horizontal'
      })
      it ('decrements frame on left click', () => {
        expect(data.frame).toBe(0, 'initial')
        t.mouseUp(t.getEl(), 0, 5)
        expect(data.frame).toBe(data.frames - 1)
        t.mouseUp(t.getEl(), 5, 5)
        expect(data.frame).toBe(data.frames - 2)
      })

      it ('increments frame on right click', () => {
        expect(data.frame).toBe(0, 'initial')
        t.mouseUp(t.getEl(), 6, 5)
        expect(data.frame).toBe(1)
        t.mouseUp(t.getEl(), 10, 5)
        expect(data.frame).toBe(2)
      })
    })

    describe('in vertical mode', () => {
      beforeEach(() => {
        data.orientation = 'vertical'
      })

      it ('decrements frame on upper click', () => {
        expect(data.frame).toBe(0, 'initial')
        t.mouseUp(t.getEl(), 5, 0)
        expect(data.frame).toBe(data.frames - 1)
        t.mouseUp(t.getEl(), 5, 5)
        expect(data.frame).toBe(data.frames - 2)
      })

      it ('increments frame on lower click', () => {
        expect(data.frame).toBe(0, 'initial')
        t.mouseUp(t.getEl(), 5, 6)
        expect(data.frame).toBe(1)
        t.mouseUp(t.getEl(), 5, 10)
        expect(data.frame).toBe(2)
      })
    })
  })
})


================================================
FILE: src/plugins/input-click.ts
================================================
import * as SpriteSpin from '../core'

(() => {

const NAME = 'click'
function click(e, data: SpriteSpin.Data) {
  if (data.loading || !data.stage.is(':visible')) {
    return
  }
  SpriteSpin.updateInput(e, data)
  const input = SpriteSpin.getInputState(data)

  let half, pos
  const target = data.target, offset = target.offset()
  if (data.orientation === 'horizontal') {
    half = target.innerWidth() / 2
    pos = input.currentX - offset.left
  } else {
    half = target.innerHeight() / 2
    pos = input.currentY - offset.top
  }
  SpriteSpin.updateFrame(data, data.frame + (pos > half ? 1 : -1))
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,
  mouseup: click,
  touchend: click
})

})()


================================================
FILE: src/plugins/input-drag.test.ts
================================================
import * as SpriteSpin from '../'
import * as t from '../lib.test'

describe('SpriteSpin.Plugins#input-drag', () => {

  const FRAME_WIDHT = 10
  const FRAME_HEIGHT = 10
  let data: SpriteSpin.Data

  beforeEach((done) => {
    t.get$El().spritespin({
      source: t.WHITE50x50,
      width: FRAME_WIDHT,
      height: FRAME_HEIGHT,
      frames: 25,
      onLoad: done,
      animate: false,
      plugins: ['drag', '360']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })
  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('setup', () => {
    it ('contains drag plugin', () => {
      expect(data.plugins[0].name).toBe('drag')
    })
  })

  describe('mouse interaction', () => {
    describe('without click', () => {
      it ('ignores events', () => {
        expect(data.frame).toBe(0)
        t.mouseMove(t.getEl(), 0, 0)
        expect(data.frame).toBe(0)
        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)
        expect(data.frame).toBe(0)
      })
    })

    describe('with click', () => {

      it ('sets "dragging" flag on mousedown', () => {
        expect(SpriteSpin.is(data, 'dragging')).toBe(false)
        t.mouseDown(t.getEl(), 0, 0)
        expect(SpriteSpin.is(data, 'dragging')).toBe(true)
      })

      it ('removes "dragging" flag on mouseup', () => {
        SpriteSpin.flag(data, 'dragging', true)
        expect(SpriteSpin.is(data, 'dragging')).toBe(true)
        t.mouseUp(t.getEl(), 0, 0)
        expect(SpriteSpin.is(data, 'dragging')).toBe(false)
      })

      it ('updates frame on horizontal movement', () => {
        expect(data.frame).toBe(0, 'initial frame')
        t.mouseDown(t.getEl(), 0, 0)
        expect(data.frame).toBe(0, 'after click')
        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)
        expect(data.frame).toBe(12, 'on move right')
        t.mouseMove(t.getEl(), 0, 0)
        expect(data.frame).toBe(0, 'on move left')
        t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)
        expect(data.frame).toBe(0, 'on move down')
        t.mouseMove(t.getEl(), 0, 0)
        expect(data.frame).toBe(0, 'on move up')
      })

      it ('updates frame on vertical movement', () => {
        data.orientation = 'vertical'
        expect(data.frame).toBe(0, 'initial frame')
        t.mouseDown(t.getEl(), 0, 0)
        expect(data.frame).toBe(0, 'after click')
        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)
        expect(data.frame).toBe(0, 'on move right')
        t.mouseMove(t.getEl(), 0, 0)
        expect(data.frame).toBe(0, 'on move left')
        t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)
        expect(data.frame).toBe(12, 'on move vertical')
        t.mouseMove(t.getEl(), 0, 0)
        expect(data.frame).toBe(0, 'on move vertical')
      })

      it ('updates frame on angle axis movement', () => {
        data.orientation = 45
        expect(data.frame).toBe(0, 'initial frame')
        t.mouseDown(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)
        expect(data.frame).toBe(0, 'after click')
        t.mouseMove(t.getEl(), FRAME_WIDHT, FRAME_WIDHT)
        expect(data.frame).toBe(0, 'on move to lower right')
        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)
        expect(data.frame).toBe(0, 'on move to center')
        t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)
        expect(data.frame).toBe(16, 'on move to lower left')
        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)
        expect(data.frame).toBe(0, 'on move to center')
      })

      it ('updates the frame', () => {
        expect(data.frame).toBe(0, 'initial frame')
        t.mouseDown(t.getEl(), 0, 0)
        expect(data.frame).toBe(0, 'after click')
        t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)
        expect(data.frame).toBe(12, 'on move right')
        t.mouseMove(t.getEl(), 0, 0)
        expect(data.frame).toBe(0, 'on move left')
      })
    })
  })
})


================================================
FILE: src/plugins/input-drag.ts
================================================
import * as SpriteSpin from '../core'

(() => {

const NAME = 'drag'

interface DragState {
  startAt?: number
  endAt?: number
  frame?: number
  lane?: number
  wasPlaying?: boolean
  minTime: number
  maxTime: number
}

function getState(data: SpriteSpin.Data) {
  return SpriteSpin.getPluginState(data, NAME) as DragState
}

function getAxis(data: SpriteSpin.Data) {
  if (typeof data.orientation === 'number') {
    return data.orientation * Math.PI / 180
  }
  if (data.orientation === 'horizontal') {
    return 0
  }
  return Math.PI / 2
}

function onInit(e, data: SpriteSpin.Data) {
  const state = getState(data)
  const d = [200, 1500]
  const t = data.touchScrollTimer || d
  state.minTime = t[0] || d[0]
  state.maxTime = t[1] || d[1]
}

function dragStart(e, data: SpriteSpin.Data) {
  const state = getState(data)
  if (data.loading || SpriteSpin.is(data, 'dragging') || data['zoomPinFrame'] && !data.stage.is(':visible')) {
    return
  }

  // Touch scroll can only be disabled by cancelling the 'touchstart' event.
  // If we would try to cancel the 'touchmove' event during a scroll
  // chrome browser raises an error
  //
  // When a user interacts with sprite spin, we don't know whether the intention
  // is to scroll the page or to roll the spin.
  //
  // On first interaction with SpriteSpin the scroll is not disabled
  // On double tap within 200ms the scroll is not disabled
  // Scroll is only disabled if there was an interaction with SpriteSpin in the past 1500ms

  const now = new Date().getTime()
  if (state.endAt && (now - state.endAt > state.maxTime)) {
    // reset timer if the user has no interaction with spritespin within 1500ms
    state.startAt = null
    state.endAt = null
  }
  if (state.startAt && (now - state.startAt > state.minTime)) {
    // disable scroll only if there was already an interaction with spritespin
    // however, allow scrolling on double tab within 200ms
    e.preventDefault()
  }

  state.startAt = now
  state.wasPlaying = !!SpriteSpin.getPlaybackState(data).handler

  state.frame = data.frame || 0
  state.lane = data.lane || 0
  SpriteSpin.flag(data, 'dragging', true)
  SpriteSpin.updateInput(e, data)
}

function dragEnd(e, data: SpriteSpin.Data) {
  if (SpriteSpin.is(data, 'dragging')) {
    getState(data).endAt = new Date().getTime()
    SpriteSpin.flag(data, 'dragging', false)
    SpriteSpin.resetInput(data)
    if (data.retainAnimate && getState(data).wasPlaying) {
      SpriteSpin.startAnimation(data)
    }
  }
}

function drag(e, data: SpriteSpin.Data) {
  const state = getState(data)
  const input = SpriteSpin.getInputState(data)
  if (!SpriteSpin.is(data, 'dragging')) { return }
  SpriteSpin.updateInput(e, data)

  const rad = getAxis(data)
  const sn = Math.sin(rad)
  const cs = Math.cos(rad)
  const x = ((input.nddX * cs - input.nddY * sn) * data.sense) || 0
  const y = ((input.nddX * sn + input.nddY * cs) * (data.senseLane || data.sense)) || 0

  // accumulate
  state.frame += data.frames * x
  state.lane += data.lanes * y

  // update spritespin
  const oldFrame = data.frame
  const oldLane = data.lane
  SpriteSpin.updateFrame(data, Math.floor(state.frame), Math.floor(state.lane))
  SpriteSpin.stopAnimation(data)
}

function mousemove(e, data) {
  dragStart(e, data)
  drag(e, data)
}

SpriteSpin.registerPlugin('drag', {
  name: 'drag',
  onInit: onInit,

  mousedown: dragStart,
  mousemove: drag,
  mouseup: dragEnd,

  documentmousemove: drag,
  documentmouseup: dragEnd,

  touchstart: dragStart,
  touchmove: drag,
  touchend: dragEnd,
  touchcancel: dragEnd
})

SpriteSpin.registerPlugin('move', {
  name: 'move',
  onInit: onInit,

  mousemove: mousemove,
  mouseleave: dragEnd,

  touchstart: dragStart,
  touchmove: drag,
  touchend: dragEnd,
  touchcancel: dragEnd
})

})()


================================================
FILE: src/plugins/input-hold.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'

describe('SpriteSpin.Plugins#input-hold', () => {

  const FRAME_WIDHT = 10
  const FRAME_HEIGHT = 10

  let data: SpriteSpin.Data
  beforeEach((done) => {
    t.get$El().spritespin({
      source: t.WHITE50x50,
      width: FRAME_WIDHT,
      height: FRAME_HEIGHT,
      frames: 25,
      onLoad: done,
      animate: false,
      plugins: ['hold', '360']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })
  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('setup', () => {
    it ('contains hold plugin', () => {
      expect(data.plugins[0].name).toBe('hold')
    })
  })

  describe('mouse interaction', () => {

    it ('sets "dragging" flag on mousedown', () => {
      expect(SpriteSpin.is(data, 'dragging')).toBe(false)
      t.mouseDown(t.getEl(), 0, 0)
      expect(SpriteSpin.is(data, 'dragging')).toBe(true)
    })

    it ('starts animation mousedown', () => {
      expect(data.animate).toBe(false)
      t.mouseDown(t.getEl(), 0, 0)
      expect(data.animate).toBe(true)
    })

    it ('removes "dragging" flag on mouseup', () => {
      SpriteSpin.flag(data, 'dragging', true)
      expect(SpriteSpin.is(data, 'dragging')).toBe(true)
      t.mouseUp(t.getEl(), 0, 0)
      expect(SpriteSpin.is(data, 'dragging')).toBe(false)
    })

    xit ('removes "dragging" flag on mouseleave', () => {
      SpriteSpin.flag(data, 'dragging', true)
      expect(SpriteSpin.is(data, 'dragging')).toBe(true)
      t.mouseLeave(t.getEl(), 0, 0)
      expect(SpriteSpin.is(data, 'dragging')).toBe(false)
    })

    it ('ignores move event if not dragging', () => {
      expect(SpriteSpin.is(data, 'dragging')).toBe(false)
      t.mouseMove(t.getEl(), 0, 0)
      expect(SpriteSpin.is(data, 'dragging')).toBe(false)
    })

    it ('update frameTime on horizontal move', () => {
      const time = data.frameTime
      t.mouseDown(t.getEl(), 0, 0)
      t.mouseMove(t.getEl(), 0, 0)
      expect(data.frameTime).toBe(20)
      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)
      expect(data.frameTime).toBe(100)
      t.mouseMove(t.getEl(), FRAME_WIDHT, 0)
      expect(data.frameTime).toBe(20)
    })

    it ('update frameTime on vertical move', () => {
      const time = data.frameTime
      data.orientation = 'vertical'
      t.mouseDown(t.getEl(), 0, 0)
      t.mouseMove(t.getEl(), 0, 0)
      expect(data.frameTime).toBe(20)
      t.mouseMove(t.getEl(), 0, FRAME_HEIGHT / 2)
      expect(data.frameTime).toBe(100)
      t.mouseMove(t.getEl(), 0, FRAME_HEIGHT)
      expect(data.frameTime).toBe(20)
    })
  })
})


================================================
FILE: src/plugins/input-hold.ts
================================================
import * as SpriteSpin from '../core'

(() => {

const NAME = 'hold'

interface HoldState {
  frameTime: number
  animate: boolean
  reverse: boolean
}

function getState(data: SpriteSpin.Data) {
  return SpriteSpin.getPluginState(data, NAME) as HoldState
}

function rememberOptions(data: SpriteSpin.Data) {
  const state = getState(data)
  state.frameTime = data.frameTime
  state.animate = data.animate
  state.reverse = data.reverse
}

function restoreOptions(data: SpriteSpin.Data) {
  const state = getState(data)
  data.frameTime = state.frameTime
  data.animate = state.animate
  data.reverse = state.reverse
}

function start(e, data: SpriteSpin.Data) {
  if (SpriteSpin.is(data, 'loading') || SpriteSpin.is(data, 'dragging') || !data.stage.is(':visible')) {
    return
  }
  rememberOptions(data)
  SpriteSpin.updateInput(e, data)
  SpriteSpin.flag(data, 'dragging', true)
  data.animate = true
  SpriteSpin.applyAnimation(data)
}

function stop(e, data: SpriteSpin.Data) {
  SpriteSpin.flag(data, 'dragging', false)
  SpriteSpin.resetInput(data)
  SpriteSpin.stopAnimation(data)
  restoreOptions(data)
  SpriteSpin.applyAnimation(data)
}

function update(e, data: SpriteSpin.Data) {
  if (!SpriteSpin.is(data, 'dragging')) {
    return
  }
  SpriteSpin.updateInput(e, data)
  const input = SpriteSpin.getInputState(data)

  let half, delta
  const target = data.target, offset = target.offset()
  if (data.orientation === 'horizontal') {
    half = target.innerWidth() / 2
    delta = (input.currentX - offset.left - half) / half
  } else {
    half = (data.height / 2)
    delta = (input.currentY - offset.top - half) / half
  }
  data.reverse = delta < 0
  delta = delta < 0 ? -delta : delta
  data.frameTime = 80 * (1 - delta) + 20

  if (((data.orientation === 'horizontal') && (input.dX < input.dY)) ||
    ((data.orientation === 'vertical') && (input.dX < input.dY))) {
    e.preventDefault()
  }
}

function onFrame(e, data: SpriteSpin.Data) {
  data.animate = true
  SpriteSpin.applyAnimation(data)
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,

  mousedown: start,
  mousemove: update,
  mouseup: stop,
  mouseleave: stop,

  touchstart: start,
  touchmove: update,
  touchend: stop,
  touchcancel: stop,

  onFrame: onFrame
})

})()


================================================
FILE: src/plugins/input-move.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'

describe('SpriteSpin.Plugins#input-move', () => {

  const FRAME_WIDHT = 10
  const FRAME_HEIGHT = 10

  let data: SpriteSpin.Data
  beforeEach((done) => {
    t.get$El().spritespin({
      source: t.WHITE50x50,
      width: FRAME_WIDHT,
      height: FRAME_HEIGHT,
      frames: 25,
      onLoad: done,
      animate: false,
      plugins: ['move', '360']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })
  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('setup', () => {
    it ('contains move plugin', () => {
      expect(data.plugins[0].name).toBe('move')
    })
  })

  describe('mouse interaction', () => {

    it ('sets "dragging" flag on mousemove', () => {
      expect(SpriteSpin.is(data, 'dragging')).toBe(false)
      t.mouseMove(t.getEl(), 0, 0)
      expect(SpriteSpin.is(data, 'dragging')).toBe(true)
    })

    xit ('removes "dragging" flag on mouseleave', () => {
      SpriteSpin.flag(data, 'dragging', true)
      expect(SpriteSpin.is(data, 'dragging')).toBe(true)
      t.mouseLeave(t.getEl(), 0, 0)
      expect(SpriteSpin.is(data, 'dragging')).toBe(false)
    })

    it ('updates frame on horizontal movement', () => {
      expect(data.frame).toBe(0, 'initial frame')
      t.mouseMove(t.getEl(), 0, 0)
      expect(data.frame).toBe(0, 'after click')
      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)
      expect(data.frame).toBe(12, 'on move right')
      t.mouseMove(t.getEl(), 0, 0)
      expect(data.frame).toBe(0, 'on move left')
      t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)
      expect(data.frame).toBe(0, 'on move down')
      t.mouseMove(t.getEl(), 0, 0)
      expect(data.frame).toBe(0, 'on move up')
    })

    it ('updates frame on vertical movement', () => {
      data.orientation = 'vertical'
      expect(data.frame).toBe(0, 'initial frame')
      t.mouseMove(t.getEl(), 0, 0)
      expect(data.frame).toBe(0, 'after click')
      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)
      expect(data.frame).toBe(0, 'on move right')
      t.mouseMove(t.getEl(), 0, 0)
      expect(data.frame).toBe(0, 'on move left')
      t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)
      expect(data.frame).toBe(12, 'on move vertical')
      t.mouseMove(t.getEl(), 0, 0)
      expect(data.frame).toBe(0, 'on move vertical')
    })

    it ('updates frame on angle axis movement', () => {
      data.orientation = 45
      expect(data.frame).toBe(0, 'initial frame')
      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)
      expect(data.frame).toBe(0, 'after click')
      t.mouseMove(t.getEl(), FRAME_WIDHT, FRAME_WIDHT)
      expect(data.frame).toBe(0, 'on move to lower right')
      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)
      expect(data.frame).toBe(0, 'on move to center')
      t.mouseMove(t.getEl(), 0, FRAME_WIDHT / 2)
      expect(data.frame).toBe(16, 'on move to lower left')
      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, FRAME_WIDHT / 2)
      expect(data.frame).toBe(0, 'on move to center')
    })

    it ('updates the frame', () => {
      expect(data.frame).toBe(0, 'initial frame')
      t.mouseMove(t.getEl(), 0, 0)
      expect(data.frame).toBe(0, 'after click')
      t.mouseMove(t.getEl(), FRAME_WIDHT / 2, 0)
      expect(data.frame).toBe(12, 'on move right')
      t.mouseMove(t.getEl(), 0, 0)
      expect(data.frame).toBe(0, 'on move left')
    })
  })
})


================================================
FILE: src/plugins/input-swipe.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'

describe('SpriteSpin.Plugins#input-swipe', () => {

  const FRAME_WIDHT = 10
  const FRAME_HEIGHT = 10

  let data: SpriteSpin.Data
  beforeEach((done) => {
    t.get$El().spritespin({
      source: t.WHITE50x50,
      width: FRAME_WIDHT,
      height: FRAME_HEIGHT,
      frames: 25,
      onLoad: done,
      animate: false,
      plugins: ['swipe', '360']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })
  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('setup', () => {
    it ('contains swipe plugin', () => {
      expect(data.plugins[0].name).toBe('swipe')
    })
  })

  describe('swipe horizontal', () => {
    it ('updates frame if swipe distance is 50%', () => {
      expect(data.frame).toBe(0, 'initial frame')
      t.dragMouse(t.getEl(), 10, 0, 5, 0)
      expect(data.frame).toBe(1, 'after swipe')
      t.dragMouse(t.getEl(), 0, 0, 5, 0)
      expect(data.frame).toBe(0, 'after swipe')
    })
  })

  describe('swipe vertical', () => {
    it ('updates frame if swipe distance is 50%', () => {
      data.orientation = 'vertical'
      expect(data.frame).toBe(0, 'initial frame')
      t.dragMouse(t.getEl(), 0, 10, 0, 5)
      expect(data.frame).toBe(1, 'after swipe')
      t.dragMouse(t.getEl(), 0, 0, 0, 5)
      expect(data.frame).toBe(0, 'after swipe')
    })
  })
})


================================================
FILE: src/plugins/input-swipe.ts
================================================
import * as SpriteSpin from '../core'

(() => {

const NAME = 'swipe'

interface SwipeState {
  fling: number
  snap: number
}

function getState(data) {
  return SpriteSpin.getPluginState(data, NAME) as SwipeState
}
function getOption(data, name, fallback) {
  return data[name] || fallback
}

function init(e, data) {
  const state = getState(data)
  state.fling = getOption(data, 'swipeFling', 10)
  state.snap = getOption(data, 'swipeSnap', 0.50)
}

function start(e, data: SpriteSpin.Data) {
  if (!data.loading && !SpriteSpin.is(data, 'dragging')) {
    SpriteSpin.updateInput(e, data)
    SpriteSpin.flag(data, 'dragging', true)
  }
}

function update(e, data: SpriteSpin.Data) {
  if (!SpriteSpin.is(data, 'dragging')) {
    return
  }
  SpriteSpin.updateInput(e, data)
  const frame = data.frame
  const lane = data.lane
  SpriteSpin.updateFrame(data, frame, lane)
}

function end(e, data: SpriteSpin.Data) {
  if (!SpriteSpin.is(data, 'dragging')) {
    return
  }
  SpriteSpin.flag(data, 'dragging', false)

  const state = getState(data)
  const input = SpriteSpin.getInputState(data)

  let frame = data.frame
  const lane = data.lane
  const snap = state.snap
  const fling = state.fling
  let dS, dF
  if (data.orientation === 'horizontal') {
    dS = input.ndX
    dF = input.ddX
  } else {
    dS = input.ndY
    dF = input.ddY
  }

  if (dS >= snap || dF >= fling) {
    frame = data.frame - 1
  } else if (dS <= -snap || dF <= -fling) {
    frame = data.frame + 1
  }

  SpriteSpin.resetInput(data)
  SpriteSpin.updateFrame(data, frame, lane)
  SpriteSpin.stopAnimation(data)
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,
  onLoad: init,
  mousedown: start,
  mousemove: update,
  mouseup: end,
  mouseleave: end,

  touchstart: start,
  touchmove: update,
  touchend: end,
  touchcancel: end
})

})()


================================================
FILE: src/plugins/input-wheel.ts
================================================
import * as SpriteSpin from '../core'

(() => {

const NAME = 'wheel'
function wheel(e: JQueryMouseEventObject, data: SpriteSpin.Data) {
  if (!data.loading && data.stage.is(':visible')) {
    e.preventDefault()

    const we = e.originalEvent as WheelEvent
    const signX = we.deltaX === 0 ? 0 : we.deltaX > 0 ? 1 : -1
    const signY = we.deltaY === 0 ? 0 : we.deltaY > 0 ? 1 : -1

    SpriteSpin.updateFrame(data, data.frame + signY, data.lane + signX)
  }
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,
  wheel: wheel
})

})()


================================================
FILE: src/plugins/progress.ts
================================================
import * as SpriteSpin from '../core'
import * as Utils from '../utils'

(() => {

interface State {
  stage: JQuery<HTMLElement>
}

const template = `
<div class='spritespin-progress'>
  <div class='spritespin-progress-label'></div>
  <div class='spritespin-progress-bar'></div>
</div>
`

function getState(data: SpriteSpin.Data) {
  return SpriteSpin.getPluginState<State>(data, NAME)
}

const NAME = 'progress'
function onInit(e, data: SpriteSpin.Data) {
  const state = getState(data)
  if (!state.stage) {
    state.stage = Utils.$(template)
    state.stage.appendTo(data.target)
  }

  state.stage.find('.spritespin-progress-label')
    .text(`0%`)
    .css({ 'text-align': 'center' })
  state.stage.find('.spritespin-progress-bar').css({
    width: `0%`
  })

  state.stage.hide().fadeIn()
}
function onProgress(e, data: SpriteSpin.Data) {
  const state = getState(data)
  state.stage.find('.spritespin-progress-label')
    .text(`${data.progress.percent}%`)
    .css({ 'text-align': 'center' })
  state.stage.find('.spritespin-progress-bar').css({
    width: `${data.progress.percent}%`
  })
}

function onLoad(e, data: SpriteSpin.Data) {
  Utils.$(getState(data).stage).fadeOut()
}

function onDestroy(e, data: SpriteSpin.Data) {
  Utils.$(getState(data).stage).remove()
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,
  onInit: onInit,
  onProgress: onProgress,
  onLoad: onLoad,
  onDestroy: onDestroy
})

})()


================================================
FILE: src/plugins/render-360.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'
import { $ } from '../utils'

describe('SpriteSpin.Plugins#render-360', () => {

  const WIDTH = 50
  const HEIGHT = 50
  let data: SpriteSpin.Data

  beforeEach((done) => {
    t.get$El().spritespin({
      source: t.WHITE50x50,
      width: 10,
      height: 10,
      frames: 25,
      onComplete: done,
      renderer: 'canvas',
      plugins: ['360']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })

  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('renderer = canvas', () => {

    beforeEach((done) => {
      t.get$El().spritespin({
        onComplete: done,
        renderer: 'canvas'
      })
    })

    it ('has empty stage', () => {
      expect(data.stage.find('*').length).toBe(0)
    })

    it ('shows the stage element', () => {
      expect(data.stage.is(':visible')).toBe(true)
    })

    it ('shows the canvas element', () => {
      expect(data.canvas.is(':visible')).toBe(true)
    })
  })

  describe('renderer = background', () => {

    beforeEach((done) => {
      t.get$El().spritespin({
        onComplete: done,
        renderer: 'background'
      })
    })

    it ('has empty stage', () => {
      expect(data.stage.find('*').length).toBe(0)
    })

    it ('shows the stage element', () => {
      expect(data.stage.is(':visible')).toBe(true)
    })

    it ('hides the canvas element', () => {
      expect(data.canvas.is(':visible')).toBe(false)
    })

    it ('shows background on stage', () => {
      expect(data.stage.css('background-image')).toContain(t.WHITE50x50)
    })
  })

  describe('renderer = image', () => {

    beforeEach((done) => {
      t.get$El().spritespin({
        onComplete: done,
        renderer: 'image'
      })
    })

    it ('has images inside stage', () => {
      expect(data.stage.find('img').length).toBe(1)
    })

    it ('shows the stage element', () => {
      expect(data.stage.is(':visible')).toBe(true)
    })

    it ('hides the canvas element', () => {
      expect(data.canvas.is(':visible')).toBe(false)
    })

    it ('shows the image element', () => {
      expect($(data.images[0]).is(':visible')).toBe(true)
    })
  })
})


================================================
FILE: src/plugins/render-360.ts
================================================
import * as SpriteSpin from '../core'
import * as Utils from '../utils'

(() => {

const floor = Math.floor

const NAME = '360'

function onLoad(e, data: SpriteSpin.Data) {
  data.stage.find('.spritespin-frames').detach()
  if (data.renderer === 'image') {
    $(data.images).addClass('spritespin-frames').appendTo(data.stage)
  }
}

function onDraw(e, data: SpriteSpin.Data) {
  const specs = Utils.findSpecs(data.metrics, data.frames, data.frame, data.lane)
  const sheet = specs.sheet
  const sprite = specs.sprite

  if (!sheet || !sprite) { return }
  const src = data.source[sheet.id]
  const image = data.images[sheet.id]

  if (data.renderer === 'canvas') {
    data.canvas.show()
    const w = data.canvas[0].width / data.canvasRatio
    const h = data.canvas[0].height / data.canvasRatio
    data.context.clearRect(0, 0, w, h)
    data.context.drawImage(image, sprite.sampledX, sprite.sampledY, sprite.sampledWidth, sprite.sampledHeight, 0, 0, w, h)
    return
  }

  const scaleX = data.stage.innerWidth() / sprite.sampledWidth
  const scaleY = data.stage.innerHeight() / sprite.sampledHeight
  const top = Math.floor(-sprite.sampledY * scaleY)
  const left = Math.floor(-sprite.sampledX * scaleX)
  const width = Math.floor(sheet.sampledWidth * scaleX)
  const height = Math.floor(sheet.sampledHeight * scaleY)
  if (data.renderer === 'background') {
    data.stage.css({
      'background-image'    : `url('${src}')`,
      'background-position' : `${left}px ${top}px`,
      'background-repeat'   : 'no-repeat',
      // set custom background size to enable responsive rendering
      '-webkit-background-size' : `${width}px ${height}px`, /* Safari 3-4 Chrome 1-3 */
      '-moz-background-size'    : `${width}px ${height}px`, /* Firefox 3.6 */
      '-o-background-size'      : `${width}px ${height}px`, /* Opera 9.5 */
      'background-size'         : `${width}px ${height}px`  /* Chrome, Firefox 4+, IE 9+, Opera, Safari 5+ */
    })
    return
  }

  $(data.images).hide()
  $(image).show().css({
    position: 'absolute',
    top: top,
    left: left,
    'max-width' : 'initial',
    width: width,
    height: height
  })
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,
  onLoad,
  onDraw
})

})()


================================================
FILE: src/plugins/render-blur.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'

describe('SpriteSpin.Plugins#render-blur', () => {

  let data: SpriteSpin.Data
  beforeEach((done) => {
    t.get$El().spritespin({
      source: [t.RED40x30, t.GREEN40x30, t.BLUE40x30],
      width: 40,
      height: 30,

      animate: false,
      onComplete: done,
      plugins: ['blur']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })

  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('basics', () => {
    it ('has has ease plugin', () => {
      expect(data.plugins[0].name).toBe('blur')
    })

    it ('doesnt break', () => {
      // can not test blur programmatically
      // thus just call updateFrame several times to step through frames
      SpriteSpin.updateFrame(data, 0)
      SpriteSpin.updateFrame(data, 1)
      SpriteSpin.updateFrame(data, 2)
    })
  })
})


================================================
FILE: src/plugins/render-blur.ts
================================================
import * as SpriteSpin from '../core'
import * as Utils from '../utils'

(() => {

const NAME = 'blur'

interface BlurStep {
  frame: number
  lane: number
  live: number
  step: number
  d: number
  alpha: number
}
interface BlurState {
  canvas: any
  context: CanvasRenderingContext2D
  steps: BlurStep[]
  fadeTime: number
  frameTime: number
  trackTime: number
  cssBlur: boolean

  timeout: number
}

function getState(data) {
  return SpriteSpin.getPluginState(data, NAME) as BlurState
}
function getOption(data, name, fallback) {
  return data[name] || fallback
}

function init(e, data: SpriteSpin.Data) {
  const state = getState(data)

  state.canvas = state.canvas || Utils.$("<canvas class='blur-layer'></canvas>")
  state.context = state.context || state.canvas[0].getContext('2d')
  state.steps = state.steps || []
  state.fadeTime = Math.max(getOption(data, 'blurFadeTime', 200), 1)
  state.frameTime = Math.max(getOption(data, 'blurFrameTime', data.frameTime), 16)
  state.trackTime = null
  state.cssBlur = !!getOption(data, 'blurCss', false)

  const inner = Utils.getInnerSize(data)
  const outer = data.responsive ? Utils.getComputedSize(data) : Utils.getOuterSize(data)
  const css = Utils.getInnerLayout(data.sizeMode, inner, outer)

  state.canvas[0].width = data.width * data.canvasRatio
  state.canvas[0].height = data.height * data.canvasRatio
  state.canvas.css(css).show()
  state.context.scale(data.canvasRatio, data.canvasRatio)
  data.target.append(state.canvas)
}

function onFrame(e, data) {
  const state = getState(data)
  trackFrame(data)
  if (state.timeout == null) {
    loop(data)
  }
}

function trackFrame(data: SpriteSpin.Data) {
  const state = getState(data)
  const ani = SpriteSpin.getPlaybackState(data)

  // distance between frames
  let d = Math.abs(data.frame - ani.lastFrame)
  // shortest distance
  d = d >= data.frames / 2 ? data.frames - d : d

  state.steps.unshift({
    frame: data.frame,
    lane: data.lane,
    live: 1,
    step: state.frameTime / state.fadeTime,
    d: d,
    alpha: 0
  })
}

const toRemove = []
function removeOldFrames(frames) {
  toRemove.length = 0
  for (let i = 0; i < frames.length; i += 1) {
    if (frames[i].alpha <= 0) {
      toRemove.push(i)
    }
  }
  for (const item of toRemove) {
    frames.splice(item, 1)
  }
}

function loop(data: SpriteSpin.Data) {
  const state = getState(data)
  state.timeout = window.setTimeout(() => { tick(data) }, state.frameTime)
}

function killLoop(data: SpriteSpin.Data) {
  const state = getState(data)
  window.clearTimeout(state.timeout)
  state.timeout = null
}

function applyCssBlur(canvas, d) {
  const amount = Math.min(Math.max((d / 2) - 4, 0), 2.5)
  const blur = `blur(${amount}px)`
  canvas.css({
    '-webkit-filter': blur,
    filter: blur
  })
}

function clearFrame(data: SpriteSpin.Data, state: BlurState) {
  state.canvas.show()
  const w = state.canvas[0].width / data.canvasRatio
  const h = state.canvas[0].height / data.canvasRatio
  // state.context.clearRect(0, 0, w, h)
}

function drawFrame(data: SpriteSpin.Data, state: BlurState, step: BlurStep) {
  if (step.alpha <= 0) { return }

  const specs = Utils.findSpecs(data.metrics, data.frames, step.frame, step.lane)
  const sheet = specs.sheet
  const sprite = specs.sprite
  if (!sheet || !sprite) { return }

  const src = data.source[sheet.id]
  const image = data.images[sheet.id]
  if (image.complete === false) { return }

  state.canvas.show()
  const w = state.canvas[0].width / data.canvasRatio
  const h = state.canvas[0].height / data.canvasRatio
  state.context.globalAlpha = step.alpha
  state.context.drawImage(image, sprite.sampledX, sprite.sampledY, sprite.sampledWidth, sprite.sampledHeight, 0, 0, w, h)
}

function tick(data: SpriteSpin.Data) {
  const state = getState(data)
  killLoop(data)
  if (!state.context) {
    return
  }

  let d = 0
  clearFrame(data, state)
  state.context.clearRect(0, 0, data.width, data.height)
  for (const step of state.steps) {
    step.live = Math.max(step.live - step.step, 0)
    step.alpha = Math.max(step.live - 0.25, 0)
    drawFrame(data, state, step)
    d += step.alpha + step.d
  }
  if (state.cssBlur) {
    applyCssBlur(state.canvas, d)
  }
  removeOldFrames(state.steps)
  if (state.steps.length) {
    loop(data)
  }
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,

  onLoad: init,
  onFrameChanged: onFrame
})

})()


================================================
FILE: src/plugins/render-ease.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'

describe('SpriteSpin.Plugins#render-ease', () => {

  let data: SpriteSpin.Data
  beforeEach((done) => {
    t.get$El().spritespin({
      source: [t.RED40x30, t.GREEN40x30, t.BLUE40x30],
      width: 40,
      height: 30,

      animate: false,
      onComplete: done,
      plugins: ['drag', 'ease']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })

  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('basics', () => {
    it ('has has ease plugin', () => {
      expect(data.plugins[1].name).toBe('ease')
    })
    it ('doesnt break', (done) => {
      // can not test ease programmatically
      // thus just emulate mouse drag
      t.mouseDown(t.getEl(), 0, 0)
      setTimeout(() => {
        t.mouseMove(t.getEl(), 5, 5)
        setTimeout(() => {
          t.mouseMove(t.getEl(), 10, 5)
          t.mouseUp(t.getEl(), 10, 5)
          done()
        }, 16)
      }, 16)
    })
  })

})


================================================
FILE: src/plugins/render-ease.ts
================================================
import * as SpriteSpin from '../core'
import * as Utils from '../utils'

(() => {

const max = Math.max
const min = Math.min

const NAME = 'ease'

interface EaseSample {
  time: number
  frame: number
  lane: number
}

interface EaseState {
  damping: number
  maxSamples: number
  updateTime: number
  abortTime: number
  samples: EaseSample[]
  steps: number[]
  handler: number

  lane: number
  lanes: number
  laneStep: number

  frame: number
  frames: number
  frameStep: number
}

function getState(data) {
  return SpriteSpin.getPluginState(data, NAME) as EaseState
}

function getOption(data, name, fallback) {
  return data[name] || fallback
}

function init(e, data: SpriteSpin.Data) {
  const state = getState(data)
  state.maxSamples = max(getOption(data, 'easeMaxSamples', 5), 0)
  state.damping = max(min(getOption(data, 'easeDamping', 0.9), 0.999), 0)
  state.abortTime = max(getOption(data, 'easeAbortTime', 250), 16)
  state.updateTime = max(getOption(data, 'easeUpdateTime', data.frameTime), 16)
  state.samples = []
  state.steps = []
}

function update(e, data: SpriteSpin.Data) {
  if (SpriteSpin.is(data, 'dragging')) {
    killLoop(data)
    sampleInput(data)
  }
}

function end(e, data: SpriteSpin.Data) {
  const state = getState(data)
  const samples = state.samples

  let last
  let lanes = 0
  let frames = 0
  let time = 0
  for (const sample of samples) {
    if (!last) {
      last = sample
      continue
    }

    const dt = sample.time - last.time
    if (dt > state.abortTime) {
      lanes = frames = time = 0
      return killLoop(data)
    }

    frames += sample.frame - last.frame
    lanes += sample.lane - last.lane
    time += dt
    last = sample
  }
  samples.length = 0
  if (!time) {
    return
  }

  state.lane = data.lane
  state.lanes = 0
  state.laneStep = lanes / time * state.updateTime

  state.frame = data.frame
  state.frames = 0
  state.frameStep = frames / time * state.updateTime

  loop(data)
}

function sampleInput(data: SpriteSpin.Data) {
  const state = getState(data)
  // add a new sample
  state.samples.push({
    time: new Date().getTime(),
    frame: data.frame,
    lane: data.lane
  })
  // drop old samples
  while (state.samples.length > state.maxSamples) {
    state.samples.shift()
  }
}

function killLoop(data: SpriteSpin.Data) {
  const state = getState(data)
  if (state.handler != null) {
    window.clearTimeout(state.handler)
    state.handler = null
  }
}

function loop(data: SpriteSpin.Data) {
  const state = getState(data)
  state.handler = window.setTimeout(() => { tick(data) }, state.updateTime)
}

function tick(data: SpriteSpin.Data) {
  const state = getState(data)
  state.lanes += state.laneStep
  state.frames += state.frameStep
  state.laneStep *= state.damping
  state.frameStep *= state.damping
  const frame = Math.floor(state.frame + state.frames)
  const lane = Math.floor(state.lane + state.lanes)

  SpriteSpin.updateFrame(data, frame, lane)

  if (SpriteSpin.is(data, 'dragging')) {
    killLoop(data)
  } else if (Math.abs(state.frameStep) > 0.005 || Math.abs(state.laneStep) > 0.005) {
    loop(data)
  } else {
    killLoop(data)
  }
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,

  onLoad: init,

  mousemove: update,
  mouseup: end,
  mouseleave: end,

  touchmove: update,
  touchend: end,
  touchcancel: end
})

})()


================================================
FILE: src/plugins/render-gallery.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'

describe('SpriteSpin.Plugins#render-gallery', () => {

  let data: SpriteSpin.Data
  beforeEach((done) => {
    t.get$El().spritespin({
      source: [t.RED40x30, t.GREEN40x30, t.BLUE40x30],
      width: 40,
      height: 30,

      gallerySpeed: 1,
      galleryOpacity: 0.25,

      animate: false,
      onComplete: done,
      plugins: ['gallery']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })

  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('basics', () => {

    it ('has has gallery plugin', () => {
      expect(data.plugins[0].name).toBe('gallery')
    })

    it ('adds a gallery-stage element', () => {
      expect(data.target.find('.gallery-stage').length).toBe(1)
    })

    it ('adds images to gallery-stage', () => {
      expect(data.target.find('.gallery-stage img').length).toBe(3)
    })

    it ('highlights current frame image, dims other', (done) => {
      SpriteSpin.updateFrame(data, 1)
      setTimeout(() => {
        expect(data.target.find('.gallery-stage img:nth-child(1)').css('opacity')).toBe('0.25', 'frame 0')
        expect(data.target.find('.gallery-stage img:nth-child(2)').css('opacity')).toBe('1', 'frame 1')
        expect(data.target.find('.gallery-stage img:nth-child(3)').css('opacity')).toBe('0.25', 'frame 2')

        SpriteSpin.updateFrame(data, 2)
        setTimeout(() => {
          expect(data.target.find('.gallery-stage img:nth-child(1)').css('opacity')).toBe('0.25', 'frame 0')
          expect(data.target.find('.gallery-stage img:nth-child(2)').css('opacity')).toBe('0.25', 'frame 1')
          expect(data.target.find('.gallery-stage img:nth-child(3)').css('opacity')).toBe('1', 'frame 2')
          done()
        }, 32)
      }, 32)
    })
  })
})


================================================
FILE: src/plugins/render-gallery.ts
================================================
import * as SpriteSpin from '../core'
import * as Utils from '../utils'

(() => {

const NAME = 'gallery'

interface GalleryState {
  images: any[]
  offsets: any[]
  speed: number
  opacity: number
  frame: number
  stage: any
  dX: number
  ddX: number
}

function getState(data) {
  return SpriteSpin.getPluginState(data, NAME) as GalleryState
}

function getOption(data, name, fallback) {
  return data[name] || fallback
}

function load(e, data: SpriteSpin.Data) {
  const state = getState(data)

  state.images = []
  state.offsets = []
  state.frame = data.frame
  state.speed = getOption(data, 'gallerySpeed', 500)
  state.opacity = getOption(data, 'galleryOpacity', 0.25)
  state.stage = getOption(data, 'galleryStage', Utils.$('<div></div>'))

  state.stage.empty().addClass('gallery-stage').prependTo(data.stage)

  let size = 0
  for (const image of data.images) {
    const naturalSize = Utils.naturalSize(image)
    const scale = data.height / naturalSize.height

    const img = Utils.$(image)
    state.stage.append(img)
    state.images.push(img)
    state.offsets.push(-size + (data.width - image.width * scale) / 2)
    size += data.width
    img.css({
      'max-width' : 'initial',
      opacity : state.opacity,
      width: data.width,
      height: data.height
    })
  }
  const innerSize = Utils.getInnerSize(data)
  const outerSize = data.responsive ? Utils.getComputedSize(data) : Utils.getOuterSize(data)
  const layout = Utils.getInnerLayout(data.sizeMode, innerSize, outerSize)
  state.stage.css(layout).css({ width: size, left: state.offsets[state.frame] })
  state.images[state.frame].animate({ opacity : 1 }, { duration: state.speed })
}

function draw(e, data: SpriteSpin.Data) {
  const state = getState(data)
  const input = SpriteSpin.getInputState(data)
  const isDragging = SpriteSpin.is(data, 'dragging')
  if (state.frame !== data.frame && !isDragging) {
    state.stage.stop(true, false).animate({ left : state.offsets[data.frame] }, { duration: state.speed })

    state.images[state.frame].animate({ opacity : state.opacity }, { duration: state.speed })
    state.frame = data.frame
    state.images[state.frame].animate({ opacity : 1 }, { duration: state.speed })
    state.stage.animate({ left : state.offsets[state.frame] })
  } else if (isDragging || state.dX !== input.dX) {
    state.dX = input.dX
    state.ddX = input.ddX
    state.stage.stop(true, true).css({ left : state.offsets[state.frame] + state.dX })
  }
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,
  onLoad: load,
  onDraw: draw
})

})()


================================================
FILE: src/plugins/render-panorama.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'

describe('SpriteSpin.Plugins#render-panorama', () => {

  let data: SpriteSpin.Data
  beforeEach((done) => {
    t.get$El().spritespin({
      source: t.WHITE40x30,
      width: 10,
      height: 10,
      animate: false,
      onComplete: done,
      plugins: ['panorama']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })

  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('basics', () => {

    it ('has has panorama plugin', () => {
      expect(data.plugins[0].name).toBe('panorama')
    })

    it ('renders the image as background', () => {
      expect(data.stage.css('background-image')).toContain(t.WHITE40x30)
    })
  })

  describe('horizontal', () => {
    beforeEach((done) => {
      t.get$El().spritespin({
        orientation: 'horizontal',
        width: 10,
        height: 30,
        onComplete: done
      })
    })

    it('has horizontal orientation', () => {
      expect(data.orientation).toBe('horizontal')
    })

    it('sets frames to image width', () => {
      expect(data.frames).toBe(40)
    })

    it('shifts image horizontally', () => {
      SpriteSpin.updateFrame(data, 1, data.lane)
      expect(data.stage.css('background-position')).toBe('1px 0px')
      SpriteSpin.updateFrame(data, 2, data.lane)
      expect(data.stage.css('background-position')).toBe('2px 0px')
    })
  })

  describe('vertical', () => {
    beforeEach((done) => {
      t.get$El().spritespin({
        orientation: 'vertical',
        width: 40,
        height: 10,
        onComplete: done
      })
    })

    it('has horizontal orientation', () => {
      expect(data.orientation).toBe('vertical')
    })

    it('sets frames to image height', () => {
      expect(data.frames).toBe(30)
    })

    it('shifts image vertically', () => {
      SpriteSpin.updateFrame(data, 1, data.lane)
      expect(data.stage.css('background-position')).toBe('0px 1px')
      SpriteSpin.updateFrame(data, 2, data.lane)
      expect(data.stage.css('background-position')).toBe('0px 2px')
    })
  })
})


================================================
FILE: src/plugins/render-panorama.ts
================================================
import * as SpriteSpin from '../core'
import * as Utils from '../utils'

(() => {

const NAME = 'panorama'

interface PanoramaState {
  scale: number
}

function getState(data) {
  return SpriteSpin.getPluginState(data, NAME) as PanoramaState
}

function onLoad(e, data: SpriteSpin.Data) {
  const state = getState(data)
  const sprite = data.metrics[0]
  if (!sprite) {
    return
  }

  if (data.orientation === 'horizontal') {
    state.scale = data.target.innerHeight() / sprite.sampledHeight
    data.frames = sprite.sampledWidth
  } else {
    state.scale = data.target.innerWidth() / sprite.sampledWidth
    data.frames = sprite.sampledHeight
  }
  const width = Math.floor(sprite.sampledWidth * state.scale)
  const height = Math.floor(sprite.sampledHeight * state.scale)
  data.stage.css({
    'background-image'        : `url(${data.source[sprite.id]})`,
    'background-repeat'       : 'repeat-both',
    // set custom background size to enable responsive rendering
    '-webkit-background-size' : `${width}px ${height}px`, /* Safari 3-4 Chrome 1-3 */
    '-moz-background-size'    : `${width}px ${height}px`, /* Firefox 3.6 */
    '-o-background-size'      : `${width}px ${height}px`, /* Opera 9.5 */
    'background-size'         : `${width}px ${height}px`  /* Chrome, Firefox 4+, IE 9+, Opera, Safari 5+ */
  })
}

function onDraw(e, data: SpriteSpin.Data) {
  const state = getState(data)
  const px = data.orientation === 'horizontal' ? 1 : 0
  const py = px ? 0 : 1

  const offset = data.frame % data.frames
  const left = Math.round(px * offset * state.scale)
  const top = Math.round(py * offset * state.scale)
  data.stage.css({ 'background-position' : `${left}px ${top}px` })
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,
  onLoad: onLoad,
  onDraw: onDraw
})

})()


================================================
FILE: src/plugins/render-zoom.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'
import { $ } from '../utils'

describe('SpriteSpin.Plugins#render-zoom', () => {

  function doubleTap(x, y, cb) {
    t.getEl().dispatchEvent(t.mouseEvent('mousedown', x, y))
    setTimeout(() => {
      t.getEl().dispatchEvent(t.mouseEvent('mousedown', x, y))
      setTimeout(cb, 16)
    }, 16)
  }

  let data: SpriteSpin.Data
  beforeEach((done) => {
    t.get$El().spritespin({
      source: [t.RED40x30, t.GREEN40x30, t.BLUE40x30],
      width: 40,
      height: 30,

      gallerySpeed: 1,
      galleryOpacity: 0.25,

      animate: false,
      onComplete: done,
      plugins: ['zoom']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })

  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('basics', () => {
    it ('has has zoom plugin', () => {
      expect(data.plugins[0].name).toBe('zoom')
    })

    it ('adds a zoom-stage element', () => {
      expect(data.target.find('.zoom-stage').length).toBe(1)
    })

    it ('hides zoom-stage initially', () => {
      expect(data.target.find('.zoom-stage').is(':visible')).toBe(false)
    })
  })

  describe('double tap', () => {
    beforeEach(() => {
      $.fx.off = true
    })
    afterEach(() => {
      $.fx.off = false
    })

    it ('toggles zoom-stage', (done) => {
      expect(data.target.find('.zoom-stage').is(':visible')).toBe(false)
      doubleTap(0, 0, () => {
        expect(data.target.find('.zoom-stage').is(':visible')).toBe(true)
        doubleTap(0, 0, () => {
          expect(data.target.find('.zoom-stage').is(':visible')).toBe(false)
          done()
        })
      })
    })
  })
})


================================================
FILE: src/plugins/render-zoom.ts
================================================
import * as SpriteSpin from '../core'
import * as Utils from '../utils'

(() => {

const NAME = 'zoom'

interface ZoomState {
  source: string[]
  stage: any

  oldX: number
  oldY: number
  currentX: number
  currentY: number

  clickTime: number
  doubleClickTime: number
  useWheel: boolean | number
  useClick: number
  pinFrame: boolean
}

function getState(data) {
  return SpriteSpin.getPluginState(data, NAME) as ZoomState
}
function getOption(data, name, fallback) {
  return name in data ? data[name] : fallback
}

function onInit(e, data: SpriteSpin.Data) {
  const state = getState(data)
  state.source = getOption(data, 'zoomSource', data.source)
  state.useWheel = getOption(data, 'zoomUseWheel', false)
  state.useClick = getOption(data, 'zoomUseClick', true)
  state.pinFrame = getOption(data, 'zoomPinFrame', true)
  state.doubleClickTime = getOption(data, 'zoomDoubleClickTime', 500)
  state.stage = state.stage || Utils.$("<div class='zoom-stage'></div>")
  state.stage.css({
    width    : '100%',
    height   : '100%',
    top      : 0,
    left     : 0,
    bottom   : 0,
    right    : 0,
    position : 'absolute'
  })
  .appendTo(data.target)
  .hide()
}

function onDestroy(e, data: SpriteSpin.Data) {
  const state = getState(data)
  if (state.stage) {
    state.stage.remove()
    delete state.stage
  }
}

function updateInput(e, data: SpriteSpin.Data) {
  const state = getState(data)
  if (!state.stage.is(':visible')) {
    return
  }

  e.preventDefault()

  if (state.pinFrame) {
    // hack into drag/move module and disable dragging
    // prevents frame change during zoom mode
    SpriteSpin.flag(data, 'dragging', false)
  }

  // grab touch/cursor position
  const cursor = Utils.getCursorPosition(e)
  // normalize cursor position into [0:1] range
  const x = cursor.x / data.width
  const y = cursor.y / data.height

  if (state.oldX == null) {
    state.oldX = x
    state.oldY = y
  }
  if (state.currentX == null) {
    state.currentX = x
    state.currentY = y
  }

  // calculate move delta since last frame and remember current position
  let dx = x - state.oldX
  let dy = y - state.oldY
  state.oldX = x
  state.oldY = y

  // invert drag direction for touch events to enable 'natural' scrolling
  if (e.type.match(/touch/)) {
    dx = -dx
    dy = -dy
  }

  // accumulate display coordinates
  state.currentX = Utils.clamp(state.currentX + dx, 0, 1)
  state.currentY = Utils.clamp(state.currentY + dy, 0, 1)

  SpriteSpin.updateFrame(data, data.frame, data.lane)
}

function onClick(e, data: SpriteSpin.Data) {
  const state = getState(data)
  if (!state.useClick) {
    return
  }

  e.preventDefault()
  // simulate double click

  const clickTime = new Date().getTime()
  if (!state.clickTime) {
    // on first click
    state.clickTime = clickTime
    return
  }

  // on second click
  const timeDelta = clickTime - state.clickTime
  if (timeDelta > state.doubleClickTime) {
    // took too long, back to first click
    state.clickTime = clickTime
    return
  }

  // on valid double click
  state.clickTime = undefined
  if (toggleZoom(data)) {
    updateInput(e, data)
  }
}

function onMove(e, data: SpriteSpin.Data) {
  const state = getState(data)
  if (state.stage.is(':visible')) {
    updateInput(e, data)
  }
}

function onDraw(e, data: SpriteSpin.Data) {
  const state = getState(data)

  // calculate the frame index
  const index = data.lane * data.frames + data.frame
  // get the zoom image. Use original frames as fallback. This won't work for sprite sheets
  const source = state.source[index]
  const spec = Utils.findSpecs(data.metrics, data.frames, data.frame, data.lane)

  // get display position
  let x = state.currentX
  let y = state.currentY
  // fallback to centered position
  if (x == null) {
    x = state.currentX = 0.5
    y = state.currentY = 0.5
  }

  if (source) {
    // scale up from [0:1] to [0:100] range
    x = Math.floor(x * 100)
    y = Math.floor(y * 100)

    // update background image and position
    state.stage.css({
      'background-repeat'   : 'no-repeat',
      'background-image'    : `url('${source}')`,
      'background-position' : `${x}% ${y}%`
    })
  } else if (spec.sheet && spec.sprite) {
    const sprite = spec.sprite
    const sheet = spec.sheet
    const src = data.source[sheet.id]
    const left = -Math.floor(sprite.sampledX + x * (sprite.sampledWidth - data.width))
    const top = -Math.floor(sprite.sampledY + y * (sprite.sampledHeight - data.height))
    const width = sheet.sampledWidth
    const height = sheet.sampledHeight
    state.stage.css({
      'background-image'    : `url('${src}')`,
      'background-position' : `${left}px ${top}px`,
      'background-repeat'   : 'no-repeat',
      // set custom background size to enable responsive rendering
      '-webkit-background-size' : `${width}px ${height}px`, /* Safari 3-4 Chrome 1-3 */
      '-moz-background-size'    : `${width}px ${height}px`, /* Firefox 3.6 */
      '-o-background-size'      : `${width}px ${height}px`, /* Opera 9.5 */
      'background-size'         : `${width}px ${height}px`  /* Chrome, Firefox 4+, IE 9+, Opera, Safari 5+ */
    })
  }
}

function toggleZoom(data) {
  const state = getState(data)
  if (!state.stage) {
    throw new Error('zoom module is not initialized or is not available.')
  }
  if (state.stage.is(':visible')) {
    showZoom(data)
  } else {
    hideZoom(data)
    return true
  }
  return false
}

function showZoom(data) {
  const state = getState(data)
  state.stage.fadeOut()
  data.stage.fadeIn()
}

function hideZoom(data) {
  const state = getState(data)
  state.stage.fadeIn()
  data.stage.fadeOut()
}

function wheel(e: JQueryMouseEventObject, data: SpriteSpin.Data) {
  const state = getState(data)
  if (!data.loading && state.useWheel) {

    const we = e.originalEvent as WheelEvent

    let signY = we.deltaY === 0 ? 0 : we.deltaY > 0 ? 1 : -1
    if (typeof state.useWheel === 'number') {
      signY *= state.useWheel
    }

    if (state.stage.is(':visible') && signY > 0) {
      e.preventDefault()
      showZoom(data)
    }
    if (!state.stage.is(':visible') && signY < 0) {
      e.preventDefault()
      hideZoom(data)
    }
  }
}

SpriteSpin.registerPlugin(NAME, {
  name: NAME,

  mousedown: onClick,
  touchstart: onClick,
  mousemove: onMove,
  touchmove: onMove,
  wheel: wheel,

  onInit: onInit,
  onDestroy: onDestroy,
  onDraw: onDraw
})

SpriteSpin.extendApi({
  toggleZoom: function() { toggleZoom(this.data) } // tslint:disable-line
})

})()


================================================
FILE: src/spritespin.test.ts
================================================
import * as SpriteSpin from './core'
import * as t from './lib.test'
import { $ } from './utils'

describe('SpriteSpin', () => {

  const WIDTH = 50
  const HEIGHT = 50

  let data: SpriteSpin.Data
  beforeEach((done) => {
    $.noConflict(true)
    t.get$El().spritespin({
      source: t.WHITE50x50,
      width: 10,
      height: 10,
      frames: 25,
      onLoad: done,
      plugins: ['click', '360']
    })
    data = t.get$El().data(SpriteSpin.namespace)
  })
  afterEach(() => {
    SpriteSpin.destroy(data)
  })

  describe('#getAnimationState', () => {
    it ('returns state.animation', () => {
      const result = SpriteSpin.getPlaybackState(data)
      expect(result).toBeDefined()
      expect(result).toBe(data.state.playback)
    })
  })

  describe('#getInputState', () => {
    it ('returns state.input', () => {
      const result = SpriteSpin.getInputState(data)
      expect(result).toBeDefined()
      expect(result).toBe(data.state.input)
    })
  })

  describe('#getPluginState', () => {
    it ('returns state.plugin.NAME', () => {
      const result = SpriteSpin.getPluginState(data, 'lorem ipsum')
      expect(result).toBeDefined()
      expect(result).toBe(data.state.plugin['lorem ipsum'])
    })
  })

  describe('#is / #flag', () => {
    it ('gets and sets a state flag', () => {
      const result = SpriteSpin.is(data, 'loading')
      expect(result).toBe(false)

      SpriteSpin.flag(data, 'loading', true)
    })
  })

  describe('#extendApi', () => {
    afterEach(() => {
      SpriteSpin.Api.prototype = {} as any
    })
    it ('adds methods to SpriteSpin.Api.prototype', () => {
      function a() { /*noop*/ }
      function b() { /*noop*/ }
      const proto = SpriteSpin.Api.prototype as any

      expect(proto.a).toBeUndefined()
      expect(proto.b).toBeUndefined()

      SpriteSpin.extendApi({ a, b })

      expect(proto.a).toBe(a)
      expect(proto.b).toBe(b)
    })

    it ('throws error on method override', () => {
      function a() { /*noop*/ }
      function b() { /*noop*/ }
      const proto = SpriteSpin.Api.prototype as any

      expect(proto.a).toBeUndefined()

      expect(() => {
        SpriteSpin.extendApi({ a: a })
        SpriteSpin.extendApi({ a: b })
      }).toThrowError()
    })
  })

  describe('#updateInput', () => {
    let input: SpriteSpin.InputState
    beforeEach(() => {
      input = SpriteSpin.getInputState(data)
    })

    it ('sets and keeps startX and startY', () => {
      // initial update sets values
      SpriteSpin.updateInput({ clientX: 5, clientY: 5 }, data)
      expect(input.startX).toBe(5)
      expect(input.startY).toBe(5)

      // successive update keeps values
      SpriteSpin.updateInput({ clientX: 6, clientY: 7 }, data)
      expect(input.startX).toBe(5)
      expect(input.startY).toBe(5)
    })

    it ('tracks currentX and currentY', () => {
      // initial update sets values
      SpriteSpin.updateInput({ clientX: 5, clientY: 5 }, data)
      expect(input.currentX).toBe(5)
      expect(input.currentY).toBe(5)

      // successive update updates values
      SpriteSpin.updateInput({ clientX: 6, clientY: 7 }, data)
      expect(input.currentX).toBe(6)
      expect(input.currentY).toBe(7)
    })

    it ('tracks oldX and oldY', () => {
      // initial update sets values
      SpriteSpin.updateInput({ clientX: 5, clientY: 5 }, data)
      expect(input.oldX).toBe(5)
      expect(input.oldY).toBe(5)

      // successive update sets previous values
      SpriteSpin.updateInput({ clientX: 6, clientY: 7 }, data)
      expect(input.oldX).toBe(5)
      expect(input.oldY).toBe(5)

      // successive update sets previous values
      SpriteSpin.updateInput({ clientX: 8, clientY: 9 }, data)
      expect(input.oldX).toBe(6)
      expect(input.oldY).toBe(7)
    })

    it ('consumes touch events', () => {
      // initial update sets values
      SpriteSpin.updateInput({ touches: [{ clientX: 5, clientY: 5 }]}, data)
      expect(input.oldX).toBe(5)
      expect(input.oldY).toBe(5)
    })
  })

  describe('#updateFrame', () => {
    //
  })

  describe('#resetInput', () => {

    ['current', 'start', 'old'].forEach((name) => {

      it (`resets ${name}X and ${name}Y`, () => {
        let input: SpriteSpin.InputState
        input = SpriteSpin.getInputState(data)
        input[`${name}X`] = 10
        input[`${name}Y`] = 10

        expect(input[`${name}X`]).toBe(10)
        expect(input[`${name}Y`]).toBe(10)
        SpriteSpin.resetInput(data)
        expect(input[`${name}X`]).toBeUndefined()
        expect(input[`${name}Y`]).toBeUndefined()
      })
    });

    ['d', 'dd', 'nd', 'ndd'].forEach((name) => {
      it (`resets ${name}X and ${name}Y`, () => {
        let input: SpriteSpin.InputState
        input = SpriteSpin.getInputState(data)
        input[`${name}X`] = 10
        input[`${name}Y`] = 10

        expect(input[`${name}X`]).toBe(10)
        expect(input[`${name}Y`]).toBe(10)
        SpriteSpin.resetInput(data)
        expect(input[`${name}X`]).toBe(0)
        expect(input[`${name}Y`]).toBe(0)
      })
    })
  })

  describe('$ extension', () => {
    describe('spritespin("data")', () => {
      it ('returns the data object', () => {
        expect(t.get$El().spritespin('data')).toBeDefined()
        expect(t.get$El().spritespin('data')).toBe(data)
      })
    })

    describe('spritespin("api")', () => {
      it ('returns the Api instance', () => {
        const api = t.get$El().spritespin('api')
        expect(api instanceof SpriteSpin.Api).toBe(true)
      })
    })

    describe('spritespin("destroy")', () => {
      it ('destroys the instance', () => {

        t.get$El().spritespin('destroy')
        expect(t.get$El().data('spritespin')).toBeUndefined()
      })
    })

    describe('spritespin("xxx", "yyy")', () => {
      it ('sets property of data object', () => {
        expect(data['xxx']).toBeUndefined() // tslint:disable-line
        t.get$El().spritespin('xxx', 'yyy')
        expect(data['xxx']).toBe('yyy') // tslint:disable-line
      })

      it ('calls SpriteSpin.createOrUpdate', () => {
        expect(data['xxx']).toBeUndefined()
        t.get$El().spritespin('xxx', 'yyy')
        expect(data['xxx']).toBe('yyy')
      })
    })

    describe('spritespin("xxx")', () => {
      it ('throws an error', () => {
        expect(() => {
          t.get$El().spritespin('xxx')
        }).toThrowError()
      })
    })
  })
})


================================================
FILE: src/utils/cursor.ts
================================================
export function getCursorPosition(event: any) {
  let touches = event.touches
  let source = event

  // jQuery Event normalization does not preserve the 'event.touches'
  // try to grab touches from the original event
  if (event.touches === undefined && event.originalEvent !== undefined) {
    touches = event.originalEvent.touches
  }
  // get current touch or mouse position
  if (touches !== undefined && touches.length > 0) {
    source = touches[0]
  }
  return {
    x: source.clientX || 0,
    y: source.clientY || 0
  }
}


================================================
FILE: src/utils/detectSubsampling.test.ts
================================================
import * as t from '../lib.test'
import * as Utils from './index'

describe('SpriteSpin.Utils', () => {

  const WIDTH = 50
  const HEIGHT = 50

  let image: HTMLImageElement

  beforeEach((done) => {
    Utils.preload({
      source: [t.WHITE50x50],
      complete: (result) => {
        image = result[0]
        done()
      }
    })
  })

  describe('#detectSubsampling', () => {
    describe('with small image', () => {
      it ('resolves to false', () => {
        const result = Utils.detectSubsampling(image, WIDTH, HEIGHT)
        expect(result).toBe(false)
      })
    })

    describe('with (fake) subsampled image', () => {
      it ('resolves to true', () => {
        const result = Utils.detectSubsampling(image, 1025, 1025)
        expect(result).toBe(true)
      })
    })

  })
})


================================================
FILE: src/utils/detectSubsampling.ts
================================================
let canvas: HTMLCanvasElement
let context: CanvasRenderingContext2D

function detectionContext() {
  if (context) {
    return context
  }

  if (!canvas) {
    canvas = document.createElement('canvas')
  }
  if (!canvas || !canvas.getContext) {
    return null
  }

  context = canvas.getContext('2d')
  return context
}

/**
 * Idea taken from https://github.com/stomita/ios-imagefile-megapixel
 * Detects whether the image has been sub sampled by the browser and does not have its original dimensions.
 * This method unfortunately does not work for images that have transparent background.
 */
export function detectSubsampling(img: HTMLImageElement, width: number, height: number) {

  if (!detectionContext()) {
    return false
  }

  // sub sampling happens on images above 1 megapixel
  if (width * height <= 1024 * 1024) {
    return false
  }

  // set canvas to 1x1 pixel size and fill it with magenta color
  canvas.width = canvas.height = 1
  context.fillStyle = '#FF00FF'
  context.fillRect(0, 0, 1, 1)
  // render the image with a negative offset to the left so that it would
  // fill the canvas pixel with the top right pixel of the image.
  context.drawImage(img, -width + 1, 0)

  // check color value to confirm image is covering edge pixel or not.
  // if color still magenta, the image is assumed to be sub sampled.
  try {
    const dat = context.getImageData(0, 0, 1, 1).data
    return (dat[0] === 255) && (dat[1] === 0) && (dat[2] === 255)
  } catch (err) {
    // avoids cross origin exception for chrome when code runs without a server
    return false
  }
}


================================================
FILE: src/utils/index.ts
================================================
export * from './jquery'
export * from './cursor'
export * from './detectSubsampling'
export * from './layout'
export * from './measure'
export * from './naturalSize'
export * from './preload'
export * from './sourceArray'
export * from './utils'


================================================
FILE: src/utils/jquery.ts
================================================
export const $: JQueryStatic<HTMLElement> = (window as any).jQuery || (window as any).$


================================================
FILE: src/utils/layout.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'
import * as Utils from '../utils'

describe('SpriteSpin.Utils', () => {

  describe('#getOuterSize', () => {

    it ('returns width, height and aspect', () => {
      const result = Utils.getOuterSize({ width: 100, height: 200 } as any)
      expect(result.width).toBe(100)
      expect(result.height).toBe(200)
      expect(result.aspect).toBe(0.5)
    })
  })

  describe('#getInnerSize', () => {
    it ('returns frameWidth, frameHeight and aspect', () => {
      const result = Utils.getInnerSize({ frameWidth: 100, frameHeight: 200 } as any)
      expect(result.width).toBe(100)
      expect(result.height).toBe(200)
      expect(result.aspect).toBe(0.5)
    })
  })

  describe('#getInnerLayout', () => {
    const data: Utils.Layoutable = {
      target: null,
      width: 100, height: 200,
      frameWidth: 100, frameHeight: 200,
      sizeMode: 'original'
    }
    let inner: Utils.SizeWithAspect
    let outer: Utils.SizeWithAspect
    const modes: SpriteSpin.SizeMode[] = ['original', 'fit', 'fill', 'stretch']

    describe('with equal outer and inner size', () => {
      beforeEach(() => {
        data.frameWidth = data.width = 100
        data.frameHeight = data.height = 200
      })

      modes.forEach((mode) => {
        describe(`with '${mode}' mode`, () => {
          let result: Utils.Layout

          beforeEach(() => {
            data.sizeMode = mode
            inner = Utils.getInnerSize(data)
            outer = Utils.getOuterSize(data)
            result = Utils.getInnerLayout(data.sizeMode, inner, outer)
          })

          it('returns matching layout', () => {
            expect(result.top).toBe(0, 'top')
            expect(result.right).toBe(0, 'right')
            expect(result.bottom).toBe(0, 'bottom')
            expect(result.left).toBe(0, 'left')
            expect(result.position).toBe('absolute')
          })
        })
      })
    })

    describe('with different outer and inner size', () => {
      beforeEach(() => {
        data.width = 100
        data.height = 200

        data.frameWidth = 300
        data.frameHeight = 400
      })

      describe(`with 'original' mode`, () => {
        let result: Utils.Layout

        beforeEach(() => {
          data.sizeMode = 'original'
          inner = Utils.getInnerSize(data)
          outer = Utils.getOuterSize(data)
          result = Utils.getInnerLayout(data.sizeMode, inner, outer)
        })

        it('returns matching layout', () => {
          expect(result.position).toBe('absolute', 'position')
          expect(result.width).toBe(data.frameWidth, 'frameWidth')
          expect(result.height).toBe(data.frameHeight, 'frameHeight')
          expect(result.top).toBe(-100, 'top')
          expect(result.bottom).toBe(-100, 'bottom')
          expect(result.right).toBe(-100, 'right')
          expect(result.left).toBe(-100, 'left')
        })
      })

      describe(`with 'fit' mode`, () => {
        let result: Utils.Layout

        beforeEach(() => {
          data.sizeMode = 'fit'
          inner = Utils.getInnerSize(data)
          outer = Utils.getOuterSize(data)
          result = Utils.getInnerLayout(data.sizeMode, inner, outer)
        })

        it('returns matching layout', () => {
          expect(result.position).toBe('absolute', 'position')
          expect(result.width).toBe(100, 'frameWidth')
          expect(result.height).toBe(133, 'frameHeight')
          expect(result.top).toBe(33, 'top')
          expect(result.bottom).toBe(33, 'bottom')
          expect(result.right).toBe(0, 'right')
          expect(result.left).toBe(0, 'left')
        })
      })

      describe(`with 'fill' mode`, () => {
        let result: Utils.Layout

        beforeEach(() => {
          data.sizeMode = 'fill'
          inner = Utils.getInnerSize(data)
          outer = Utils.getOuterSize(data)
          result = Utils.getInnerLayout(data.sizeMode, inner, outer)
        })

        it('returns matching layout', () => {
          expect(result.position).toBe('absolute', 'position')
          expect(result.width).toBe(150, 'frameWidth')
          expect(result.height).toBe(200, 'frameHeight')
          expect(result.top).toBe(0, 'top')
          expect(result.bottom).toBe(0, 'bottom')
          expect(result.right).toBe(-25, 'right')
          expect(result.left).toBe(-25, 'left')
        })
      })
    })
  })
})


================================================
FILE: src/utils/layout.ts
================================================
import { SizeMode } from '../core/models'

export interface Layoutable {
  width?: number
  height?: number
  frameWidth?: number
  frameHeight?: number
  target: any
  sizeMode?: SizeMode
}

export interface Layout {
  [key: string]: any
  width: string | number
  height: string | number
  top: number
  left: number
  bottom: number
  right: number
  position: 'absolute'
  overflow: 'hidden'
}

export interface SizeWithAspect {
  width: number
  height: number
  aspect: number
}

/**
 *
 */
export function getOuterSize(data: Layoutable): SizeWithAspect {
  const width = Math.floor(data.width || data.frameWidth || data.target.innerWidth())
  const height = Math.floor(data.height || data.frameHeight || data.target.innerHeight())
  return {
    aspect: width / height,
    height,
    width
  }
}

export function getComputedSize(data: Layoutable): SizeWithAspect {
  const size = getOuterSize(data)
  if (typeof window.getComputedStyle !== 'function') { return size }

  const style = window.getComputedStyle(data.target[0])
  if (!style.width) { return size }

  size.width = Math.floor(Number(style.width.replace('px', '')))
  size.height = Math.floor(size.width / size.aspect)
  return size
}

/**
 *
 */
export function getInnerSize(data: Layoutable): SizeWithAspect {
  const width = Math.floor(data.frameWidth || data.width || data.target.innerWidth())
  const height = Math.floor(data.frameHeight || data.height || data.target.innerHeight())
  return {
    aspect: width / height,
    height,
    width
  }
}

/**
 *
 */
export function getInnerLayout(mode: SizeMode, inner: SizeWithAspect, outer: SizeWithAspect): Layout {

  // get mode
  const isFit = mode === 'fit'
  const isFill = mode === 'fill'
  const isMatch = mode === 'stretch'

  // resulting layout
  const layout: Layout = {
    width    : '100%',
    height   : '100%',
    top      : 0,
    left     : 0,
    bottom   : 0,
    right    : 0,
    position : 'absolute',
    overflow : 'hidden'
  }

  // no calculation here
  if (!mode || isMatch) {
    return layout
  }

  // get size and aspect
  const aspectIsGreater = inner.aspect >= outer.aspect

  // mode == original
  let width = inner.width
  let height = inner.height

  // keep aspect ratio but fit/fill into container
  if (isFit && aspectIsGreater || isFill && !aspectIsGreater) {
    width = outer.width
    height = outer.width / inner.aspect
  }
  if (isFill && aspectIsGreater || isFit && !aspectIsGreater) {
    height = outer.height
    width = outer.height * inner.aspect
  }

  // floor the numbers
  width = Math.floor(width)
  height = Math.floor(height)

  // position in center
  layout.width = width
  layout.height = height
  layout.top = Math.floor((outer.height - height) / 2)
  layout.left = Math.floor((outer.width - width) / 2)
  layout.right = layout.left
  layout.bottom = layout.top

  return layout
}


================================================
FILE: src/utils/measure.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'
import * as Utils from '../utils'

describe('SpriteSpin.Utils', () => {

  const WIDTH = 50
  const HEIGHT = 50

  let image

  beforeEach((done) => {
    Utils.preload({
      source: [t.WHITE50x50],
      complete: (result) => {
        image = result[0]
        done()
      }
    })
  })

  describe('#measure', () => {

    let result: Utils.SheetSpec[]

    describe('a sprite sheet', () => {
      const FRAMES = 95
      const FRAMESX = 10

      beforeEach(() => {
        result = Utils.measure([image], { frames: FRAMES, framesX: FRAMESX })
      })

      it('resolves sheet spec', () => {
        expect(result.length).toBe(1)
        result.forEach((sheet, index) => {
          expect(sheet.id).toBe(index)
          expect(sheet.width).toBe(WIDTH)
          expect(sheet.width).toBe(HEIGHT)
          expect(sheet.sampledHeight).toBe(sheet.width)
          expect(sheet.sampledHeight).toBe(sheet.height)
        })
      })

      it('resolves sprite specs', () => {
        expect(result[0].sprites.length).toBe(FRAMES)
        result[0].sprites.forEach((sprite, index) => {
          expect(sprite.id).toBe(index)
          expect(sprite.width).toBe(WIDTH / FRAMESX)
          expect(sprite.height).toBe(HEIGHT / (Math.ceil(FRAMES / FRAMESX)))
        })
      })
    })

    describe('an array of frames', () => {
      let IMAGES = [image, image, image, image]
      let FRAMES = IMAGES.length

      beforeEach(() => {
        IMAGES = [image, image, image, image]
        FRAMES = IMAGES.length
        result = Utils.measure(IMAGES, { frames: FRAMES })
      })

      it('resolves sheet spec', () => {
        expect(result.length).toBe(FRAMES)
        result.forEach((sheet, index) => {
          expect(sheet.id).toBe(index)
          expect(sheet.width).toBe(WIDTH)
          expect(sheet.height).toBe(HEIGHT)
          expect(sheet.sampledHeight).toBe(sheet.width)
          expect(sheet.sampledHeight).toBe(sheet.height)
        })
      })

      it('resolves sprite specs', () => {
        result.forEach((sheet) => {
          expect(sheet.sprites.length).toBe(1)
          sheet.sprites.forEach((sprite, index) => {
            expect(sprite.id).toBe(index)
            expect(sprite.width).toBe(WIDTH)
            expect(sprite.height).toBe(HEIGHT)
            expect(sprite.sampledWidth).toBe(sprite.width)
            expect(sprite.sampledHeight).toBe(sprite.height)
          })
        })
      })
    })
  })

  describe('#findSpec', () => {
    const metrics = [
      { sprites: ['a1', 'a2', 'a3', 'b1', 'b2', 'b3', 'c1', 'c2', 'c3']},
      { sprites: ['x1', 'x2', 'x3', 'y1', 'y2', 'y3', 'z1', 'z2', 'z3']}
    ]

    it('finds the correct data', () => {
      expect(Utils.findSpecs(metrics as any, 3, 0, 0).sprite as any).toBe('a1')
      expect(Utils.findSpecs(metrics as any, 3, 0, 1).sprite as any).toBe('b1')
      expect(Utils.findSpecs(metrics as any, 3, 0, 2).sprite as any).toBe('c1')
      expect(Utils.findSpecs(metrics as any, 3, 0, 3).sprite as any).toBe('x1')
      expect(Utils.findSpecs(metrics as any, 3, 0, 4).sprite as any).toBe('y1')
      expect(Utils.findSpecs(metrics as any, 3, 0, 5).sprite as any).toBe('z1')

      expect(Utils.findSpecs(metrics as any, 3, 1, 0).sprite as any).toBe('a2')
      expect(Utils.findSpecs(metrics as any, 3, 1, 1).sprite as any).toBe('b2')
      expect(Utils.findSpecs(metrics as any, 3, 1, 2).sprite as any).toBe('c2')
      expect(Utils.findSpecs(metrics as any, 3, 1, 3).sprite as any).toBe('x2')
      expect(Utils.findSpecs(metrics as any, 3, 1, 4).sprite as any).toBe('y2')
      expect(Utils.findSpecs(metrics as any, 3, 1, 5).sprite as any).toBe('z2')

      expect(Utils.findSpecs(metrics as any, 3, 2, 0).sprite as any).toBe('a3')
      expect(Utils.findSpecs(metrics as any, 3, 2, 1).sprite as any).toBe('b3')
      expect(Utils.findSpecs(metrics as any, 3, 2, 2).sprite as any).toBe('c3')
      expect(Utils.findSpecs(metrics as any, 3, 2, 3).sprite as any).toBe('x3')
      expect(Utils.findSpecs(metrics as any, 3, 2, 4).sprite as any).toBe('y3')
      expect(Utils.findSpecs(metrics as any, 3, 2, 5).sprite as any).toBe('z3')
    })
  })
})


================================================
FILE: src/utils/measure.ts
================================================
import { detectSubsampling } from './detectSubsampling'
import { naturalSize } from './naturalSize'
/**
 *
 */
export interface MeasureSheetOptions {
  frames: number
  framesX?: number
  framesY?: number
  detectSubsampling?: boolean
}

/**
 *
 */
export interface SheetSpec {
  id: number
  width: number
  height: number
  sprites: SpriteSpec[]

  sampledWidth?: number
  sampledHeight?: number
  isSubsampled?: boolean
}

/**
 *
 */
export interface SpriteSpec {
  id: number
  x: number
  y: number
  width: number
  height: number

  sampledX?: number
  sampledY?: number
  sampledWidth?: number
  sampledHeight?: number
}

/**
 * Measures the image frames that are used in the given data object
 */
export function measure(images: HTMLImageElement[], options: MeasureSheetOptions): SheetSpec[] {
  if (images.length === 1) {
    return [measureSheet(images[0], options)]
  } else if (options.framesX && options.framesY) {
    return measureMutipleSheets(images, options)
  } else {
    return measureFrames(images, options)
  }
}

function measureSheet(image: HTMLImageElement, options: MeasureSheetOptions): SheetSpec {
  const result: SheetSpec = { id: 0, sprites: [] } as any
  measureImage(image, options, result)
  const frames = options.frames
  const framesX = Number(options.framesX) || frames
  const framesY = Math.ceil(frames / framesX)
  const frameWidth = Math.floor(result.width / framesX)
  const frameHeight = Math.floor(result.height / framesY)
  const divisor = result.isSubsampled ? 2 : 1
  for (let i = 0; i < frames; i++) {
    const x = (i % framesX) * frameWidth
    const y = Math.floor(i / framesX) * frameHeight
    result.sprites.push({
      id: i,
      x: x, y: y,
      width: frameWidth,
      height: frameHeight,
      sampledX: x / divisor,
      sampledY: y / divisor,
      sampledWidth: frameWidth / divisor,
      sampledHeight: frameHeight / divisor
    })
  }
  return result
}

function measureFrames(images: HTMLImageElement[], options: MeasureSheetOptions): SheetSpec[] {
  const result: SheetSpec[] = []
  for (let id = 0; id < images.length; id++) {
    // TODO: optimize
    // don't measure images with same size twice
    const sheet = measureSheet(images[id], { frames: 1, framesX: 1, detectSubsampling: options.detectSubsampling })
    sheet.id = id
    result.push(sheet)
  }
  return result
}

function measureMutipleSheets(images: HTMLImageElement[], options: MeasureSheetOptions): SheetSpec[] {
  const result: SheetSpec[] = []
  for (let id = 0; id < images.length; id++) {
    // TODO: optimize
    // don't measure images with same size twice
    const sheet = measureSheet(images[id], {
      frames: undefined,
      framesX: options.framesX,
      framesY: options.framesY,
      detectSubsampling: options.detectSubsampling
    })
    sheet.id = id
    result.push(sheet)
  }
  return result
}

function measureImage(image: HTMLImageElement, options: MeasureSheetOptions, result: SheetSpec): SheetSpec {
  const size = naturalSize(image)
  result.isSubsampled = options.detectSubsampling && detectSubsampling(image, size.width, size.height)
  result.width = size.width
  result.height = size.height
  result.sampledWidth = size.width / (result.isSubsampled ? 2 : 1)
  result.sampledHeight = size.height / (result.isSubsampled ? 2 : 1)
  return result
}

export function findSpecs(metrics: SheetSpec[], frames: number, frame: number, lane: number) {
  let spriteId = lane * frames + frame
  let sheetId = 0
  let sprite: SpriteSpec = null
  let sheet: SheetSpec = null

  while (true) {
    sheet = metrics[sheetId]
    if (!sheet) { break }

    if (spriteId >= sheet.sprites.length) {
      spriteId -= sheet.sprites.length
      sheetId++
      continue
    }

    sprite = sheet.sprites[spriteId]
    break
  }
  return { sprite, sheet }
}


================================================
FILE: src/utils/naturalSize.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'
import * as Utils from '../utils'

describe('SpriteSpin.Utils', () => {

  const WIDTH = 50
  const HEIGHT = 50

  let image: HTMLImageElement

  beforeEach((done) => {
    Utils.preload({
      source: [t.WHITE50x50],
      complete: (result) => {
        image = result[0]
        done()
      }
    })
  })

  describe('#naturalSize', () => {
    it ('resolves to naturalWidth and naturalHeight', () => {
      const result = Utils.naturalSize({ naturalWidth: WIDTH, naturalHeight: HEIGHT } as any)
      expect(result.width).toBe(WIDTH)
      expect(result.height).toBe(HEIGHT)
    })

    it ('resolves to width and height from preloaded src', () => {
      const result = Utils.naturalSize({ src: t.WHITE50x50 } as any)
      expect(result.width).toBe(WIDTH)
      expect(result.height).toBe(HEIGHT)
    })
  })
})


================================================
FILE: src/utils/naturalSize.ts
================================================
let img: HTMLImageElement

/**
 * gets the original width and height of an image element
 */
export function naturalSize(image: HTMLImageElement) {
  // for browsers that support naturalWidth and naturalHeight properties
  if (image.naturalWidth) {
    return {
      height: image.naturalHeight,
      width: image.naturalWidth
    }
  }

  // browsers that do not support naturalWidth and naturalHeight properties have to fall back to the width and
  // height properties. However, the image might have a css style applied so width and height would return the
  // css size. To avoid that create a new Image object that is free of css rules and grab width and height
  // properties
  //
  // assume that the src has already been downloaded, so no onload callback is needed.
  img = img || new Image()
  img.crossOrigin = image.crossOrigin
  img.src = image.src
  return {
    height: img.height,
    width: img.width
  }
}


================================================
FILE: src/utils/preload.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'
import * as Utils from '../utils'

describe('SpriteSpin.Utils', () => {

  describe('#preload', () => {

    const source = [t.RED40x30, t.GREEN40x30, t.BLUE40x30]

    function expectArrayOfImages(input, output) {
      expect(Array.isArray(output)).toBe(true)
      expect(output.length).toBe(input.length)
      expect(output.every((it) => it instanceof Image)).toBe(true)
    }

    it ('accepts string input', (done) => {
      Utils.preload({
        source: t.RED40x30,
        initiated: (result) => {
          expectArrayOfImages([t.RED40x30], result)
          done()
        }
      })
    })

    it ('reports array of Image elements when initiated', (done) => {
      Utils.preload({
        source: source,
        initiated: (result) => {
          expectArrayOfImages(source, result)
          done()
        }
      })
    })

    it ('reports array of Image elements on complete', (done) => {
      Utils.preload({
        source: source,
        complete: (result) => {
          expectArrayOfImages(source, result)
          done()
        }
      })
    })

    it ('reports progress for each image', (done) => {
      let count = 0
      Utils.preload({
        source: source,
        progress: () => { count++ },
        complete:  () => {
          expect(count).toBe(source.length)
          done()
        }
      })
    })

    it ('completes if preload count is reached', (done) => {
      let count = 0
      const preloadCount = 2
      Utils.preload({
        source: source,
        preloadCount: preloadCount,
        progress: () => { count++ },
        complete: (result) => {
          expect(count).toBe(preloadCount)
          done()
        }
      })
    })
  })
})


================================================
FILE: src/utils/preload.ts
================================================
function indexOf(element: any, arr: any[]) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === element) {
      return i
    }
  }
}

function noop() {
  //
}

export interface PreloadOptions {
  source: string | string[]
  crossOrigin?: string
  preloadCount?: number
  initiated?: (images: HTMLImageElement[]) => void
  progress?: (p: PreloadProgress) => void
  complete?: (images: HTMLImageElement[]) => void
}

export interface PreloadProgress {
  // The image index that currently has been loaded
  index: number
  // The number of images that have been loaded so far
  loaded: number
  // The total number of images to load
  total: number
  // Percentage value
  percent: number
}

export function preload(opts: PreloadOptions) {
  let src: string[]
  const input = opts.source
  src = typeof input === 'string' ? [input] : input
  // const src: string[] =  ? [opts.source] : opts.source

  const images = []
  const targetCount = (opts.preloadCount || src.length)
  const onInitiated = opts.initiated || noop
  const onProgress = opts.progress || noop
  const onComplete = opts.complete || noop

  let count = 0
  let completed = false
  let firstLoaded = false

  const tick = function () { // tslint:disable-line
    count += 1

    onProgress({
      index: indexOf(this, images),
      loaded: count,
      total: src.length,
      percent: Math.round((count / src.length) * 100)
    })

    firstLoaded = firstLoaded || (this === images[0])

    if (firstLoaded && !completed && (count >= targetCount)) {
      completed = true
      onComplete(images)
    }
  }

  for (const url of src) {
    const img = new Image()
    // push result
    images.push(img)
    // https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
    img.crossOrigin = opts.crossOrigin
    // bind logic, don't care about abort/errors
    img.onload = img.onabort = img.onerror = tick
    // begin load
    img.src = url
  }

  onInitiated(images)
}


================================================
FILE: src/utils/sourceArray.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'
import * as Utils from '../utils'

describe('SpriteSpin.Utils', () => {

  describe('#sourceArray', () => {
    it ('generates array of urls', () => {
      const output = Utils.sourceArray('http://example.com/image_{frame}.jpg', { frame: [1, 3] })
      expect(output[0]).toBe('http://example.com/image_01.jpg')
      expect(output[1]).toBe('http://example.com/image_02.jpg')
      expect(output[2]).toBe('http://example.com/image_03.jpg')
    })

    it ('accepts "digits" option', () => {
      const output = Utils.sourceArray('http://example.com/image_{frame}.jpg', { frame: [1, 3], digits: 3 })
      expect(output[0]).toBe('http://example.com/image_001.jpg')
      expect(output[1]).toBe('http://example.com/image_002.jpg')
      expect(output[2]).toBe('http://example.com/image_003.jpg')
    })

    it ('accepts "lane" option', () => {
      const output = Utils.sourceArray('http://example.com/image_{lane}x{frame}.jpg', { frame: [1, 2], lane: [1, 2] })
      expect(output[0]).toBe('http://example.com/image_01x01.jpg')
      expect(output[1]).toBe('http://example.com/image_01x02.jpg')
      expect(output[2]).toBe('http://example.com/image_02x01.jpg')
      expect(output[3]).toBe('http://example.com/image_02x02.jpg')
    })
  })
})


================================================
FILE: src/utils/sourceArray.ts
================================================
function padNumber(num: number, length: number, pad: string): string {
  let result = String(num)
  while (result.length < length) {
    result = String(pad) + result
  }
  return result
}

/**
 * Options for {@link sourceArray} function
 */
export interface SourceArrayOptions {
  /**
   * Minimum number of digits
   */
  digits?: number
  /**
   * Start and end frame numbers
   */
  frame?: number[]
  /**
   * Start and end lane numbers
   */
  lane?: number[]
  /**
   * Variable to be replaced by a frame number e.g. '{frame}'
   */
  framePlacer?: string
  /**
   * Variable to be replaced by a lane number e.g. '{lane}'
   */
  lanePlacer?: string
}

/**
 * Generates an array of source strings
 *
 * @remarks
 * Takes a template string and generates an array of strings by interpolating {lane} and {frame} placeholders.
 *
 * ```
 * sourceArray('http://example.com/image_{frame}.jpg, { frame: [1, 3], digits: 2 })
 * // gives:
 * // [ 'http://example.com/image_01.jpg', 'http://example.com/image_02.jpg', 'http://example.com/image_03.jpg' ]
 *
 * sourceArray('http://example.com/image_FRAME.jpg, { frame: [1, 3], digits: 2, framePlacer: 'FRAME' })
 * // gives:
 * // [ 'http://example.com/image_01.jpg', 'http://example.com/image_02.jpg', 'http://example.com/image_03.jpg' ]
 * ```
 *
 * @param template - The template string
 * @param opts - Interpolation options
 *
 * @public
 */
export function sourceArray(template: string, opts: SourceArrayOptions) {
  const digits = opts.digits || 2
  const lPlacer = opts.lanePlacer || '{lane}'
  const fPlacer = opts.framePlacer || '{frame}'

  let fStart = 0
  let fEnd = 0
  if (opts.frame) {
    fStart = opts.frame[0]
    fEnd = opts.frame[1]
  }
  let lStart = 0
  let lEnd = 0
  if (opts.lane) {
    lStart = opts.lane[0]
    lEnd = opts.lane[1]
  }
  const result = []
  for (let lane = lStart; lane <= lEnd; lane += 1) {
    for (let frame = fStart; frame <= fEnd; frame += 1) {
      result.push(template
        .replace(lPlacer, padNumber(lane, digits, '0'))
        .replace(fPlacer, padNumber(frame, digits, '0'))
      )
    }
  }
  return result
}


================================================
FILE: src/utils/utils.test.ts
================================================
import * as SpriteSpin from '..'
import * as t from '../lib.test'
import * as Utils from '../utils'

describe('SpriteSpin.Utils', () => {

  describe('#toArray', () => {
    it ('wraps string to array', () => {
      const input = 'foo'
      const output = Utils.toArray(input)
      expect(Array.isArray(output)).toBe(true)
      expect(output[0]).toBe(input)
    })

    it ('doesnt transform arrays', () => {
      const input = ['foo']
      const output = Utils.toArray(input)
      expect(output).toBe(input)
    })
  })

  describe('#clamp', () => {
    it ('clamps to lower bound', () => {
      const min = 10
      const max = 20
      const output = Utils.clamp(5, min, max)
      expect(output).toBe(min)
    })
    it ('clamps to upper bound', () => {
      const min = 10
      const max = 20
      const output = Utils.clamp(25, min, max)
      expect(output).toBe(max)
    })
    it ('preserves inside bounds', () => {
      const min = 10
      const max = 20
      expect(Utils.clamp(min, min, max)).toBe(min)
      expect(Utils.clamp(max, min, max)).toBe(max)
      expect(Utils.clamp(15, min, max)).toBe(15)
    })
  })
})


================================================
FILE: src/utils/utils.ts
================================================
import { namespace } from '../core/constants'

export function noop() {
  // noop
}

function wrapConsole(type: string): (message?: any, ...optionalParams: any[]) => void {
  return console && console[type] ? (...args: any[]) => console.log.apply(console, args) : noop
}

export const log = wrapConsole('log')
export const warn = wrapConsole('warn')
export const error = wrapConsole('error')

export function toArray<T>(value: T | T[]): T[] {
  return Array.isArray(value) ? value : [value]
}

/**
 * clamps the given value by the given min and max values
 */
export function clamp(value: number, min: number, max: number) {
  return (value > max ? max : (value < min ? min : value))
}

/**
 *
 */
export function wrap(value: number, min: number, max: number, size: number) {
  while (value > max) { value -= size }
  while (value < min) { value += size }
  return value
}

/**
 * prevents default action on the given event
 */
export function prevent(e) {
  e.preventDefault()
  return false
}

/**
 * Binds on the given target and event the given function.
 * The SpriteSpin namespace is attached to the event name
 */
export function bind(target: JQuery, event: string, func: (...args: any[]) => any) {
  if (func) {
    target.bind(event + '.' + namespace, (e) => {
      func.apply(target, [e, (target as any).spritespin('data')])
    })
  }
}

/**
 * Unbinds all SpriteSpin events from given target element
 */
export function unbind(target: JQuery): void {
  target.unbind('.' + namespace)
}

/**
 * Checks if given object is a function
 */
export function isFunction(fn: any): boolean {
  return typeof fn === 'function'
}

export function pixelRatio(context) {
  const devicePixelRatio = window.devicePixelRatio || 1
  const backingStoreRatio =
    context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio ||
    context.backingStorePixelRatio || 1
  return devicePixelRatio / backingStoreRatio
}


================================================
FILE: tsconfig.cjs.json
================================================
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "release/cjs",
    "module": "commonjs"
  },
  "exclude": []
}


================================================
FILE: tsconfig.esm2015.json
================================================
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "release/esm2015",
    "module": "es2015",
    "target": "es2015"
  }
}


================================================
FILE: tsconfig.json
================================================
{
  "compileOnSave": false,
  "compilerOptions": {
    "target": "es5",
    "noResolve": true,
    "outDir": "release/src",
    "sourceMap": true,
    "inlineSources": true,
    "declaration": true,
    "newLine": "LF",
    "rootDir": "src",
    "module": "es2015",
    "moduleResolution": "node",
    "alwaysStrict": true,
    "noEmitHelpers": true,
    "typeRoots": [
      "node_modules/@types"
    ],
    "types": [
      "jasmine",
      "jquery",
      "node"
    ]
  },
  "exclude": [
    "src/**/*.test.ts",
    "node_modules",
    "release"
  ]
}


================================================
FILE: tslint.json
================================================
{
  "extends": "tslint:latest",
  "rules": {
    "no-namespace": false,
    "no-internal-module": false,
    "interface-name": [true, "never-prefix"],
    "quotemark": [true, "single", "avoid-escape"],
    "trailing-comma": [true, {
      "multiline": "never",
      "singleline": "never"
    }],
    "semicolon": [true, "never"],
    "member-ordering": [
      false
    ],
    "max-line-length": [
      140
    ],
    "one-variable-per-declaration": [false],
    "object-literal-sort-keys": false,
    "object-literal-key-quotes": [true, "as-needed"],
    "object-literal-shorthand": false,
    "no-reference": false
  }
}
Download .txt
gitextract_2mzudef5/

├── .editorconfig
├── .gitignore
├── .jshintrc
├── .travis.yml
├── LICENSE
├── README.md
├── gulpfile.js
├── karma.conf.js
├── package.json
├── src/
│   ├── api/
│   │   ├── common.test.ts
│   │   ├── common.ts
│   │   ├── fullscreen.test.ts
│   │   ├── fullscreen.ts
│   │   └── index.ts
│   ├── core/
│   │   ├── api.ts
│   │   ├── boot.ts
│   │   ├── constants.ts
│   │   ├── index.ts
│   │   ├── input.ts
│   │   ├── jquery.ts
│   │   ├── layout.ts
│   │   ├── models.ts
│   │   ├── playback.ts
│   │   ├── plugins.ts
│   │   └── state.ts
│   ├── index.ts
│   ├── lib.test.ts
│   ├── plugins/
│   │   ├── index.ts
│   │   ├── input-click.test.ts
│   │   ├── input-click.ts
│   │   ├── input-drag.test.ts
│   │   ├── input-drag.ts
│   │   ├── input-hold.test.ts
│   │   ├── input-hold.ts
│   │   ├── input-move.test.ts
│   │   ├── input-swipe.test.ts
│   │   ├── input-swipe.ts
│   │   ├── input-wheel.ts
│   │   ├── progress.ts
│   │   ├── render-360.test.ts
│   │   ├── render-360.ts
│   │   ├── render-blur.test.ts
│   │   ├── render-blur.ts
│   │   ├── render-ease.test.ts
│   │   ├── render-ease.ts
│   │   ├── render-gallery.test.ts
│   │   ├── render-gallery.ts
│   │   ├── render-panorama.test.ts
│   │   ├── render-panorama.ts
│   │   ├── render-zoom.test.ts
│   │   └── render-zoom.ts
│   ├── spritespin.test.ts
│   └── utils/
│       ├── cursor.ts
│       ├── detectSubsampling.test.ts
│       ├── detectSubsampling.ts
│       ├── index.ts
│       ├── jquery.ts
│       ├── layout.test.ts
│       ├── layout.ts
│       ├── measure.test.ts
│       ├── measure.ts
│       ├── naturalSize.test.ts
│       ├── naturalSize.ts
│       ├── preload.test.ts
│       ├── preload.ts
│       ├── sourceArray.test.ts
│       ├── sourceArray.ts
│       ├── utils.test.ts
│       └── utils.ts
├── tsconfig.cjs.json
├── tsconfig.esm2015.json
├── tsconfig.json
└── tslint.json
Download .txt
SYMBOL INDEX (204 symbols across 36 files)

FILE: gulpfile.js
  function exec (line 15) | function exec(command, cb) {

FILE: karma.conf.js
  constant IS_COVERALLS (line 3) | const IS_COVERALLS = !!process.env.IS_COVERALLS
  constant IS_COVERAGE (line 4) | const IS_COVERAGE = IS_COVERALLS || !!process.env.IS_COVERAGE
  constant IS_TRAVIS (line 5) | const IS_TRAVIS = !!process.env.TRAVIS

FILE: src/api/fullscreen.ts
  type Options (line 4) | interface Options {
  function pick (line 9) | function pick(target, names: string[]): string {
  function unbindChangeEvent (line 54) | function unbindChangeEvent() {
  function bindChangeEvent (line 58) | function bindChangeEvent(callback) {
  function unbindOrientationEvent (line 64) | function unbindOrientationEvent() {
  function bindOrientationEvent (line 67) | function bindOrientationEvent(callback) {
  function requestFullscreenNative (line 72) | function requestFullscreenNative(e) {
  function exitFullscreen (line 77) | function exitFullscreen() {
  function fullscreenEnabled (line 81) | function fullscreenEnabled() {
  function fullscreenElement (line 85) | function fullscreenElement() {
  function isFullscreen (line 89) | function isFullscreen() {
  function toggleFullscreen (line 93) | function toggleFullscreen(data: Data, opts: Options) {
  function requestFullscreen (line 101) | function requestFullscreen(data: Data, opts: Options) {

FILE: src/core/api.ts
  class Api (line 7) | class Api {
    method constructor (line 8) | constructor(public data: Data) { }
  function extendApi (line 16) | function extendApi(methods: { [key: string]: Function }) {

FILE: src/core/boot.ts
  function pushInstance (line 16) | function pushInstance(data: Data) {
  function popInstance (line 22) | function popInstance(data: Data) {
  function eachInstance (line 26) | function eachInstance(cb) {
  function onEvent (line 39) | function onEvent(eventName, e) {
  function onResize (line 49) | function onResize() {
  function applyEvents (line 75) | function applyEvents(data: Data) {
  function applyMetrics (line 108) | function applyMetrics(data: Data) {
  function boot (line 129) | function boot(data: Data) {
  function create (line 170) | function create(options: Options): Data {
  function createOrUpdate (line 234) | function createOrUpdate(options: Options): Data {
  function destroy (line 258) | function destroy(data: Data) {

FILE: src/core/input.ts
  type InputState (line 10) | interface InputState {
  function getInputState (line 35) | function getInputState(data: Data): InputState {
  function updateInput (line 46) | function updateInput(e, data: Data) {
  function resetInput (line 93) | function resetInput(data: Data) {

FILE: src/core/jquery.ts
  function extension (line 6) | function extension(option: string | any, value: any) {

FILE: src/core/layout.ts
  function applyLayout (line 9) | function applyLayout(data: Data) {

FILE: src/core/models.ts
  type Callback (line 3) | type Callback = (e: any, data: Data) => void
  type CallbackOptions (line 10) | interface CallbackOptions {
  type SizeMode (line 41) | type SizeMode = 'original' | 'fit' | 'fill' | 'stretch'
  type RenderMode (line 42) | type RenderMode = 'canvas' | 'image' | 'background'
  type Orientation (line 43) | type Orientation = 'horizontal' | 'vertical'
  type Options (line 50) | interface Options extends CallbackOptions {
  type Data (line 224) | interface Data extends Options {

FILE: src/core/playback.ts
  function getPlaybackState (line 11) | function getPlaybackState(data: Data): PlaybackState {
  type PlaybackState (line 20) | interface PlaybackState {
  function updateLane (line 27) | function updateLane(data: Data, lane: number) {
  function updateAnimationFrame (line 33) | function updateAnimationFrame(data: Data) {
  function updateInputFrame (line 43) | function updateInputFrame(data: Data, frame: number) {
  function updateAnimation (line 50) | function updateAnimation(data: Data) {
  function updateBefore (line 59) | function updateBefore(data: Data) {
  function updateAfter (line 65) | function updateAfter(data: Data) {
  function updateFrame (line 82) | function updateFrame(data: Data, frame?: number, lane?: number) {
  function stopAnimation (line 99) | function stopAnimation(data: Data) {
  function applyAnimation (line 117) | function applyAnimation(data: Data) {
  function startAnimation (line 137) | function startAnimation(data: Data) {

FILE: src/core/plugins.ts
  type SpriteSpinPlugin (line 9) | interface SpriteSpinPlugin extends CallbackOptions {
  function registerPlugin (line 26) | function registerPlugin(name: string, plugin: SpriteSpinPlugin) {
  function registerModule (line 44) | function registerModule(name: string, plugin: SpriteSpinPlugin) {
  function getPlugin (line 55) | function getPlugin(name) {
  function applyPlugins (line 63) | function applyPlugins(data: Data) {
  function fixPlugins (line 79) | function fixPlugins(data: Data) {

FILE: src/core/state.ts
  function getState (line 9) | function getState<T = any>(data: Data, name: string): T {
  function getPluginState (line 26) | function getPluginState<T = any>(data: Data, name: string): T {
  function is (line 39) | function is(data: Data, key: string): boolean {
  function flag (line 51) | function flag(data: Data, key: string, value: boolean) {

FILE: src/lib.test.ts
  function mouseEvent (line 9) | function mouseEvent(name: string, clientX: number, clientY: number) {
  function mouseDown (line 15) | function mouseDown(el: HTMLElement, x, y) {
  function mouseUp (line 18) | function mouseUp(el: HTMLElement, x, y) {
  function mouseLeave (line 21) | function mouseLeave(el: HTMLElement, x, y) {
  function mouseMove (line 24) | function mouseMove(el: HTMLElement, x, y) {
  function touchStart (line 27) | function touchStart(el: HTMLElement, x, y) {
  function touchMove (line 30) | function touchMove(el: HTMLElement, x, y) {
  function touchEnd (line 33) | function touchEnd(el: HTMLElement, x, y) {
  function moveMouse (line 37) | function moveMouse(el: HTMLElement, startX, startY, endX, endY) {
  function moveTouch (line 41) | function moveTouch(el: HTMLElement, startX, startY, endX, endY) {
  function dragMouse (line 45) | function dragMouse(el: HTMLElement, startX, startY, endX, endY) {
  function dragTouch (line 50) | function dragTouch(el: HTMLElement, startX, startY, endX, endY) {
  function getEl (line 56) | function getEl(): HTMLElement {
  function get$El (line 59) | function get$El(): any {

FILE: src/plugins/input-click.ts
  function click (line 6) | function click(e, data: SpriteSpin.Data) {

FILE: src/plugins/input-drag.ts
  type DragState (line 7) | interface DragState {
  function getState (line 17) | function getState(data: SpriteSpin.Data) {
  function getAxis (line 21) | function getAxis(data: SpriteSpin.Data) {
  function onInit (line 31) | function onInit(e, data: SpriteSpin.Data) {
  function dragStart (line 39) | function dragStart(e, data: SpriteSpin.Data) {
  function dragEnd (line 77) | function dragEnd(e, data: SpriteSpin.Data) {
  function drag (line 88) | function drag(e, data: SpriteSpin.Data) {
  function mousemove (line 111) | function mousemove(e, data) {

FILE: src/plugins/input-hold.ts
  type HoldState (line 7) | interface HoldState {
  function getState (line 13) | function getState(data: SpriteSpin.Data) {
  function rememberOptions (line 17) | function rememberOptions(data: SpriteSpin.Data) {
  function restoreOptions (line 24) | function restoreOptions(data: SpriteSpin.Data) {
  function start (line 31) | function start(e, data: SpriteSpin.Data) {
  function stop (line 42) | function stop(e, data: SpriteSpin.Data) {
  function update (line 50) | function update(e, data: SpriteSpin.Data) {
  function onFrame (line 76) | function onFrame(e, data: SpriteSpin.Data) {

FILE: src/plugins/input-swipe.ts
  type SwipeState (line 7) | interface SwipeState {
  function getState (line 12) | function getState(data) {
  function getOption (line 15) | function getOption(data, name, fallback) {
  function init (line 19) | function init(e, data) {
  function start (line 25) | function start(e, data: SpriteSpin.Data) {
  function update (line 32) | function update(e, data: SpriteSpin.Data) {
  function end (line 42) | function end(e, data: SpriteSpin.Data) {

FILE: src/plugins/input-wheel.ts
  function wheel (line 6) | function wheel(e: JQueryMouseEventObject, data: SpriteSpin.Data) {

FILE: src/plugins/progress.ts
  type State (line 6) | interface State {
  function getState (line 17) | function getState(data: SpriteSpin.Data) {
  function onInit (line 22) | function onInit(e, data: SpriteSpin.Data) {
  function onProgress (line 38) | function onProgress(e, data: SpriteSpin.Data) {
  function onLoad (line 48) | function onLoad(e, data: SpriteSpin.Data) {
  function onDestroy (line 52) | function onDestroy(e, data: SpriteSpin.Data) {

FILE: src/plugins/render-360.ts
  function onLoad (line 10) | function onLoad(e, data: SpriteSpin.Data) {
  function onDraw (line 17) | function onDraw(e, data: SpriteSpin.Data) {

FILE: src/plugins/render-blur.ts
  type BlurStep (line 8) | interface BlurStep {
  type BlurState (line 16) | interface BlurState {
  function getState (line 28) | function getState(data) {
  function getOption (line 31) | function getOption(data, name, fallback) {
  function init (line 35) | function init(e, data: SpriteSpin.Data) {
  function onFrame (line 57) | function onFrame(e, data) {
  function trackFrame (line 65) | function trackFrame(data: SpriteSpin.Data) {
  function removeOldFrames (line 85) | function removeOldFrames(frames) {
  function loop (line 97) | function loop(data: SpriteSpin.Data) {
  function killLoop (line 102) | function killLoop(data: SpriteSpin.Data) {
  function applyCssBlur (line 108) | function applyCssBlur(canvas, d) {
  function clearFrame (line 117) | function clearFrame(data: SpriteSpin.Data, state: BlurState) {
  function drawFrame (line 124) | function drawFrame(data: SpriteSpin.Data, state: BlurState, step: BlurSt...
  function tick (line 143) | function tick(data: SpriteSpin.Data) {

FILE: src/plugins/render-ease.ts
  type EaseSample (line 11) | interface EaseSample {
  type EaseState (line 17) | interface EaseState {
  function getState (line 35) | function getState(data) {
  function getOption (line 39) | function getOption(data, name, fallback) {
  function init (line 43) | function init(e, data: SpriteSpin.Data) {
  function update (line 53) | function update(e, data: SpriteSpin.Data) {
  function end (line 60) | function end(e, data: SpriteSpin.Data) {
  function sampleInput (line 101) | function sampleInput(data: SpriteSpin.Data) {
  function killLoop (line 115) | function killLoop(data: SpriteSpin.Data) {
  function loop (line 123) | function loop(data: SpriteSpin.Data) {
  function tick (line 128) | function tick(data: SpriteSpin.Data) {

FILE: src/plugins/render-gallery.ts
  type GalleryState (line 8) | interface GalleryState {
  function getState (line 19) | function getState(data) {
  function getOption (line 23) | function getOption(data, name, fallback) {
  function load (line 27) | function load(e, data: SpriteSpin.Data) {
  function draw (line 63) | function draw(e, data: SpriteSpin.Data) {

FILE: src/plugins/render-panorama.ts
  type PanoramaState (line 8) | interface PanoramaState {
  function getState (line 12) | function getState(data) {
  function onLoad (line 16) | function onLoad(e, data: SpriteSpin.Data) {
  function onDraw (line 43) | function onDraw(e, data: SpriteSpin.Data) {

FILE: src/plugins/render-zoom.test.ts
  function doubleTap (line 7) | function doubleTap(x, y, cb) {

FILE: src/plugins/render-zoom.ts
  type ZoomState (line 8) | interface ZoomState {
  function getState (line 24) | function getState(data) {
  function getOption (line 27) | function getOption(data, name, fallback) {
  function onInit (line 31) | function onInit(e, data: SpriteSpin.Data) {
  function onDestroy (line 52) | function onDestroy(e, data: SpriteSpin.Data) {
  function updateInput (line 60) | function updateInput(e, data: SpriteSpin.Data) {
  function onClick (line 108) | function onClick(e, data: SpriteSpin.Data) {
  function onMove (line 139) | function onMove(e, data: SpriteSpin.Data) {
  function onDraw (line 146) | function onDraw(e, data: SpriteSpin.Data) {
  function toggleZoom (line 196) | function toggleZoom(data) {
  function showZoom (line 210) | function showZoom(data) {
  function hideZoom (line 216) | function hideZoom(data) {
  function wheel (line 222) | function wheel(e: JQueryMouseEventObject, data: SpriteSpin.Data) {

FILE: src/spritespin.test.ts
  function a (line 65) | function a() { /*noop*/ }
  function b (line 66) | function b() { /*noop*/ }
  function a (line 79) | function a() { /*noop*/ }
  function b (line 80) | function b() { /*noop*/ }

FILE: src/utils/cursor.ts
  function getCursorPosition (line 1) | function getCursorPosition(event: any) {

FILE: src/utils/detectSubsampling.ts
  function detectionContext (line 4) | function detectionContext() {
  function detectSubsampling (line 25) | function detectSubsampling(img: HTMLImageElement, width: number, height:...

FILE: src/utils/layout.ts
  type Layoutable (line 3) | interface Layoutable {
  type Layout (line 12) | interface Layout {
  type SizeWithAspect (line 24) | interface SizeWithAspect {
  function getOuterSize (line 33) | function getOuterSize(data: Layoutable): SizeWithAspect {
  function getComputedSize (line 43) | function getComputedSize(data: Layoutable): SizeWithAspect {
  function getInnerSize (line 58) | function getInnerSize(data: Layoutable): SizeWithAspect {
  function getInnerLayout (line 71) | function getInnerLayout(mode: SizeMode, inner: SizeWithAspect, outer: Si...

FILE: src/utils/measure.ts
  type MeasureSheetOptions (line 6) | interface MeasureSheetOptions {
  type SheetSpec (line 16) | interface SheetSpec {
  type SpriteSpec (line 30) | interface SpriteSpec {
  function measure (line 46) | function measure(images: HTMLImageElement[], options: MeasureSheetOption...
  function measureSheet (line 56) | function measureSheet(image: HTMLImageElement, options: MeasureSheetOpti...
  function measureFrames (line 82) | function measureFrames(images: HTMLImageElement[], options: MeasureSheet...
  function measureMutipleSheets (line 94) | function measureMutipleSheets(images: HTMLImageElement[], options: Measu...
  function measureImage (line 111) | function measureImage(image: HTMLImageElement, options: MeasureSheetOpti...
  function findSpecs (line 121) | function findSpecs(metrics: SheetSpec[], frames: number, frame: number, ...

FILE: src/utils/naturalSize.ts
  function naturalSize (line 6) | function naturalSize(image: HTMLImageElement) {

FILE: src/utils/preload.test.ts
  function expectArrayOfImages (line 11) | function expectArrayOfImages(input, output) {

FILE: src/utils/preload.ts
  function indexOf (line 1) | function indexOf(element: any, arr: any[]) {
  function noop (line 9) | function noop() {
  type PreloadOptions (line 13) | interface PreloadOptions {
  type PreloadProgress (line 22) | interface PreloadProgress {
  function preload (line 33) | function preload(opts: PreloadOptions) {

FILE: src/utils/sourceArray.ts
  function padNumber (line 1) | function padNumber(num: number, length: number, pad: string): string {
  type SourceArrayOptions (line 12) | interface SourceArrayOptions {
  function sourceArray (line 56) | function sourceArray(template: string, opts: SourceArrayOptions) {

FILE: src/utils/utils.ts
  function noop (line 3) | function noop() {
  function wrapConsole (line 7) | function wrapConsole(type: string): (message?: any, ...optionalParams: a...
  function toArray (line 15) | function toArray<T>(value: T | T[]): T[] {
  function clamp (line 22) | function clamp(value: number, min: number, max: number) {
  function wrap (line 29) | function wrap(value: number, min: number, max: number, size: number) {
  function prevent (line 38) | function prevent(e) {
  function bind (line 47) | function bind(target: JQuery, event: string, func: (...args: any[]) => a...
  function unbind (line 58) | function unbind(target: JQuery): void {
  function isFunction (line 65) | function isFunction(fn: any): boolean {
  function pixelRatio (line 69) | function pixelRatio(context) {
Condensed preview — 73 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (158K chars).
[
  {
    "path": ".editorconfig",
    "chars": 283,
    "preview": "# EditorConfig is awesome: http://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines with"
  },
  {
    "path": ".gitignore",
    "chars": 93,
    "preview": ".DS_Store\n*.orig\n.idea\n.sass-cache/\n.vscode\n.coveralls.yml\nnode_modules\ncoverage\ndoc\nrelease\n"
  },
  {
    "path": ".jshintrc",
    "chars": 344,
    "preview": "{\n  \"asi\": true,\n  \"bitwise\": true,\n  \"camelcase\": true,\n  \"curly\": true,\n  \"eqeqeq\": true,\n  \"forin\": true,\n  \"immed\": "
  },
  {
    "path": ".travis.yml",
    "chars": 94,
    "preview": "language: node_js\nmatrix:\n  include:\n    - os: osx\naddons:\n  firefox: \"53.0\"\nnode_js:\n  - \"8\"\n"
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "Copyright (c) 2013 Alexander Gräfenstein\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy "
  },
  {
    "path": "README.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "gulpfile.js",
    "chars": 3149,
    "preview": "'use strict'\n\nconst del = require('del')\nconst gulp = require('gulp')\nconst uglify = require('gulp-uglify')\nconst concat"
  },
  {
    "path": "karma.conf.js",
    "chars": 1789,
    "preview": "'use strict'\n\nconst IS_COVERALLS = !!process.env.IS_COVERALLS\nconst IS_COVERAGE = IS_COVERALLS || !!process.env.IS_COVER"
  },
  {
    "path": "package.json",
    "chars": 2399,
    "preview": "{\n  \"name\": \"spritespin\",\n  \"description\": \"jQuery plugin for creating flipbook animations\",\n  \"version\": \"4.1.0\",\n  \"au"
  },
  {
    "path": "src/api/common.test.ts",
    "chars": 5526,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Api#common', () => {\n\n  let data"
  },
  {
    "path": "src/api/common.ts",
    "chars": 2778,
    "preview": "import * as SpriteSpin from '../core'\n\n// tslint:disable:object-literal-shorthand\n// tslint:disable:only-arrow-functions"
  },
  {
    "path": "src/api/fullscreen.test.ts",
    "chars": 99,
    "preview": "import * as SpriteSpin from '..'\n\ndescribe('SpriteSpin.Api#fullscreen', () => {\n  // untestable\n})\n"
  },
  {
    "path": "src/api/fullscreen.ts",
    "chars": 3705,
    "preview": "import { boot, Data, extendApi, namespace, SizeMode } from '../core'\nimport { $ } from '../utils'\n\nexport interface Opti"
  },
  {
    "path": "src/api/index.ts",
    "chars": 40,
    "preview": "import './common'\nimport './fullscreen'\n"
  },
  {
    "path": "src/core/api.ts",
    "chars": 532,
    "preview": "// tslint:disable ban-types\nimport { Data } from './models'\n\n/**\n * @internal\n */\nexport class Api {\n  constructor(publi"
  },
  {
    "path": "src/core/boot.ts",
    "chars": 6504,
    "preview": "import * as Utils from '../utils'\nimport { callbackNames, defaults, eventNames, eventsToPrevent, namespace } from './con"
  },
  {
    "path": "src/core/constants.ts",
    "chars": 3298,
    "preview": "import { Options } from './models'\n\n/**\n * The namespace that is used to bind functions to DOM events and store the data"
  },
  {
    "path": "src/core/index.ts",
    "chars": 243,
    "preview": "export * from './api'\nexport * from './boot'\nexport * from './constants'\nexport * from './input'\nexport * from './layout"
  },
  {
    "path": "src/core/input.ts",
    "chars": 2597,
    "preview": "import { getCursorPosition } from '../utils'\nimport { Data } from './models'\nimport { getState } from './state'\n\n/**\n * "
  },
  {
    "path": "src/core/jquery.ts",
    "chars": 887,
    "preview": "import { $ } from '../utils'\nimport { Api } from './api'\nimport { createOrUpdate, destroy } from './boot'\nimport { names"
  },
  {
    "path": "src/core/layout.ts",
    "chars": 1593,
    "preview": "import * as Utils from '../utils'\nimport { Data } from './models'\n\n/**\n * Applies css attributes to layout the SpriteSpi"
  },
  {
    "path": "src/core/models.ts",
    "chars": 6075,
    "preview": "import { PreloadProgress, SheetSpec } from '../utils'\n\nexport type Callback = (e: any, data: Data) => void\n\n/**\n * Addit"
  },
  {
    "path": "src/core/playback.ts",
    "chars": 3498,
    "preview": "import { clamp, wrap } from '../utils'\nimport { namespace } from './constants'\nimport { Data } from './models'\nimport { "
  },
  {
    "path": "src/core/plugins.ts",
    "chars": 2347,
    "preview": "import { error, warn } from '../utils'\nimport { Callback, CallbackOptions, Data } from './models'\n\n/**\n * Describes a Sp"
  },
  {
    "path": "src/core/state.ts",
    "chars": 1336,
    "preview": "import { Data } from './models'\n\n/**\n * Gets a state object by name.\n * @internal\n * @param data - The SpriteSpin instan"
  },
  {
    "path": "src/index.ts",
    "chars": 1056,
    "preview": "export * from './core'\nexport { sourceArray } from './utils'\n\nimport {\n  $,\n  bind,\n  clamp,\n  detectSubsampling,\n  erro"
  },
  {
    "path": "src/lib.test.ts",
    "chars": 2910,
    "preview": "import { $ } from './utils'\n\nexport const WHITE40x30 = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAQAAAD01J"
  },
  {
    "path": "src/plugins/index.ts",
    "chars": 277,
    "preview": "import './input-click'\nimport './input-drag'\nimport './input-hold'\nimport './input-swipe'\nimport './input-wheel'\nimport "
  },
  {
    "path": "src/plugins/input-click.test.ts",
    "chars": 2234,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#input-click', () => {\n\n "
  },
  {
    "path": "src/plugins/input-click.ts",
    "chars": 702,
    "preview": "import * as SpriteSpin from '../core'\n\n(() => {\n\nconst NAME = 'click'\nfunction click(e, data: SpriteSpin.Data) {\n  if (d"
  },
  {
    "path": "src/plugins/input-drag.test.ts",
    "chars": 3870,
    "preview": "import * as SpriteSpin from '../'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#input-drag', () => {\n\n "
  },
  {
    "path": "src/plugins/input-drag.ts",
    "chars": 3798,
    "preview": "import * as SpriteSpin from '../core'\n\n(() => {\n\nconst NAME = 'drag'\n\ninterface DragState {\n  startAt?: number\n  endAt?:"
  },
  {
    "path": "src/plugins/input-hold.test.ts",
    "chars": 2614,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#input-hold', () => {\n\n  "
  },
  {
    "path": "src/plugins/input-hold.ts",
    "chars": 2259,
    "preview": "import * as SpriteSpin from '../core'\n\n(() => {\n\nconst NAME = 'hold'\n\ninterface HoldState {\n  frameTime: number\n  animat"
  },
  {
    "path": "src/plugins/input-move.test.ts",
    "chars": 3428,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#input-move', () => {\n\n  "
  },
  {
    "path": "src/plugins/input-swipe.test.ts",
    "chars": 1387,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#input-swipe', () => {\n\n "
  },
  {
    "path": "src/plugins/input-swipe.ts",
    "chars": 1826,
    "preview": "import * as SpriteSpin from '../core'\n\n(() => {\n\nconst NAME = 'swipe'\n\ninterface SwipeState {\n  fling: number\n  snap: nu"
  },
  {
    "path": "src/plugins/input-wheel.ts",
    "chars": 536,
    "preview": "import * as SpriteSpin from '../core'\n\n(() => {\n\nconst NAME = 'wheel'\nfunction wheel(e: JQueryMouseEventObject, data: Sp"
  },
  {
    "path": "src/plugins/progress.ts",
    "chars": 1425,
    "preview": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\ninterface State {\n  stage: JQuery<HTM"
  },
  {
    "path": "src/plugins/render-360.test.ts",
    "chars": 2210,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport { $ } from '../utils'\n\ndescribe('SpriteSpin.Plu"
  },
  {
    "path": "src/plugins/render-360.ts",
    "chars": 2222,
    "preview": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst floor = Math.floor\n\nconst NAME "
  },
  {
    "path": "src/plugins/render-blur.test.ts",
    "chars": 880,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#render-blur', () => {\n\n "
  },
  {
    "path": "src/plugins/render-blur.ts",
    "chars": 4399,
    "preview": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst NAME = 'blur'\n\ninterface BlurSt"
  },
  {
    "path": "src/plugins/render-ease.test.ts",
    "chars": 990,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#render-ease', () => {\n\n "
  },
  {
    "path": "src/plugins/render-ease.ts",
    "chars": 3343,
    "preview": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst max = Math.max\nconst min = Math"
  },
  {
    "path": "src/plugins/render-gallery.test.ts",
    "chars": 1810,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#render-gallery', () => {"
  },
  {
    "path": "src/plugins/render-gallery.ts",
    "chars": 2557,
    "preview": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst NAME = 'gallery'\n\ninterface Gal"
  },
  {
    "path": "src/plugins/render-panorama.test.ts",
    "chars": 2099,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\n\ndescribe('SpriteSpin.Plugins#render-panorama', () => "
  },
  {
    "path": "src/plugins/render-panorama.ts",
    "chars": 1793,
    "preview": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst NAME = 'panorama'\n\ninterface Pa"
  },
  {
    "path": "src/plugins/render-zoom.test.ts",
    "chars": 1668,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport { $ } from '../utils'\n\ndescribe('SpriteSpin.Plu"
  },
  {
    "path": "src/plugins/render-zoom.ts",
    "chars": 6522,
    "preview": "import * as SpriteSpin from '../core'\nimport * as Utils from '../utils'\n\n(() => {\n\nconst NAME = 'zoom'\n\ninterface ZoomSt"
  },
  {
    "path": "src/spritespin.test.ts",
    "chars": 6434,
    "preview": "import * as SpriteSpin from './core'\nimport * as t from './lib.test'\nimport { $ } from './utils'\n\ndescribe('SpriteSpin',"
  },
  {
    "path": "src/utils/cursor.ts",
    "chars": 533,
    "preview": "export function getCursorPosition(event: any) {\n  let touches = event.touches\n  let source = event\n\n  // jQuery Event no"
  },
  {
    "path": "src/utils/detectSubsampling.test.ts",
    "chars": 801,
    "preview": "import * as t from '../lib.test'\nimport * as Utils from './index'\n\ndescribe('SpriteSpin.Utils', () => {\n\n  const WIDTH ="
  },
  {
    "path": "src/utils/detectSubsampling.ts",
    "chars": 1587,
    "preview": "let canvas: HTMLCanvasElement\nlet context: CanvasRenderingContext2D\n\nfunction detectionContext() {\n  if (context) {\n    "
  },
  {
    "path": "src/utils/index.ts",
    "chars": 247,
    "preview": "export * from './jquery'\nexport * from './cursor'\nexport * from './detectSubsampling'\nexport * from './layout'\nexport * "
  },
  {
    "path": "src/utils/jquery.ts",
    "chars": 88,
    "preview": "export const $: JQueryStatic<HTMLElement> = (window as any).jQuery || (window as any).$\n"
  },
  {
    "path": "src/utils/layout.test.ts",
    "chars": 4436,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpi"
  },
  {
    "path": "src/utils/layout.ts",
    "chars": 2870,
    "preview": "import { SizeMode } from '../core/models'\n\nexport interface Layoutable {\n  width?: number\n  height?: number\n  frameWidth"
  },
  {
    "path": "src/utils/measure.test.ts",
    "chars": 4221,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpi"
  },
  {
    "path": "src/utils/measure.ts",
    "chars": 3812,
    "preview": "import { detectSubsampling } from './detectSubsampling'\nimport { naturalSize } from './naturalSize'\n/**\n *\n */\nexport in"
  },
  {
    "path": "src/utils/naturalSize.test.ts",
    "chars": 887,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpi"
  },
  {
    "path": "src/utils/naturalSize.ts",
    "chars": 926,
    "preview": "let img: HTMLImageElement\n\n/**\n * gets the original width and height of an image element\n */\nexport function naturalSize"
  },
  {
    "path": "src/utils/preload.test.ts",
    "chars": 1774,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpi"
  },
  {
    "path": "src/utils/preload.ts",
    "chars": 1961,
    "preview": "function indexOf(element: any, arr: any[]) {\n  for (let i = 0; i < arr.length; i++) {\n    if (arr[i] === element) {\n    "
  },
  {
    "path": "src/utils/sourceArray.test.ts",
    "chars": 1313,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpi"
  },
  {
    "path": "src/utils/sourceArray.ts",
    "chars": 2116,
    "preview": "function padNumber(num: number, length: number, pad: string): string {\n  let result = String(num)\n  while (result.length"
  },
  {
    "path": "src/utils/utils.test.ts",
    "chars": 1144,
    "preview": "import * as SpriteSpin from '..'\nimport * as t from '../lib.test'\nimport * as Utils from '../utils'\n\ndescribe('SpriteSpi"
  },
  {
    "path": "src/utils/utils.ts",
    "chars": 2006,
    "preview": "import { namespace } from '../core/constants'\n\nexport function noop() {\n  // noop\n}\n\nfunction wrapConsole(type: string):"
  },
  {
    "path": "tsconfig.cjs.json",
    "chars": 134,
    "preview": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"release/cjs\",\n    \"module\": \"commonjs\"\n  },\n  \"e"
  },
  {
    "path": "tsconfig.esm2015.json",
    "chars": 143,
    "preview": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"release/esm2015\",\n    \"module\": \"es2015\",\n    \"t"
  },
  {
    "path": "tsconfig.json",
    "chars": 556,
    "preview": "{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"noResolve\": true,\n    \"outDir\": \"release/sr"
  },
  {
    "path": "tslint.json",
    "chars": 626,
    "preview": "{\n  \"extends\": \"tslint:latest\",\n  \"rules\": {\n    \"no-namespace\": false,\n    \"no-internal-module\": false,\n    \"interface-"
  }
]

About this extraction

This page contains the full source code of the giniedp/spritespin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 73 files (144.2 KB), approximately 41.5k tokens, and a symbol index with 204 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!