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
//
target
.empty()
.addClass('spritespin-instance')
.append("")
// 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
/**
* 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(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(data: Data, name: string): T {
const state = getState(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('')
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
}
const template = `
`
function getState(data: SpriteSpin.Data) {
return SpriteSpin.getPluginState(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.$("")
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.$(''))
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.$("")
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 = (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(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
}
}