import 'es6-promise/auto' ; import imageLoaded from 'image-loaded' ; import raf from 'raf' ; import { Animation , AnimationOptions , Frame , SpriteSheet , SpriteSheetOptions } from './types' ; const playheadDefaults = { play : true , delay : 50 , tempo : 1 , run : - 1 , reversed : false , script : [ ] , lastTime : 0 , nextDelay : 0 , currentSprite : 1 , currentFrame : - 1 , onPlay : null , onStop : null , onFrame : null , onOutOfView : null } ; class Spriteling { public spriteSheet : SpriteSheet = { loaded : false , url : null , cols : null , rows : null , cutOffFrames : 0 , top : null , bottom : null , left : null , right : null , startSprite : 1 , downsizeRatio : 1 , totalSprites : 0 , sheetWidth : 0 , sheetHeight : 0 , frameWidth : 0 , frameHeight : 0 , animations : { } , onLoaded : null } ; private playhead : Animation ; private readonly element : HTMLElement ; private debug : boolean ; private loadingPromise : Promise < void > ; param { object } options param { HTMLElement | string } element param { boolean } debug constructor ( options , element ? , debug = false ) { if ( element ) { this . element = typeof element === 'string' ? document . querySelector ( element ) as HTMLElement : element ; } if ( ! this . element ) { if ( typeof this . element !== 'undefined' ) { this . log ( 'warn' , `element "${ element }" not found, created new element instead` ) ; } this . element = document . createElement ( 'div' ) ; document . body . appendChild ( this . element ) ; } this . element . className += ' spriteling' ; this . spriteSheet = { ... this . spriteSheet , ... options } ; this . playhead = { ... playheadDefaults } ; this . debug = debug || false ; if ( ! options . cols ) { this . log ( 'error' , 'options.cols not set' ) ; } if ( ! options . rows ) { this . log ( 'error' , 'options.rows not set' ) ; } if ( ! options . url ) { const elementStyle = window . getComputedStyle ( this . element ) ; const cssBackgroundImage = elementStyle . getPropertyValue ( 'background-image' ) ; if ( ! cssBackgroundImage || cssBackgroundImage === 'none' ) { this . log ( 'error' , 'no spritesheet image found, please specify it with options.url or set as css background' ) ; } else { this . spriteSheet . url = cssBackgroundImage . replace ( /"/g , '' ) . replace ( /url\(|\)$/ig , '' ) ; } } this . loadingPromise = this . loadSpriteSheet ( ) . then ( ( ) => { this . spriteSheet . loaded = true ; if ( this . spriteSheet . startSprite > 1 && this . spriteSheet . startSprite <= this . spriteSheet . totalSprites ) { this . drawFrame ( { sprite : this . spriteSheet . startSprite } ) ; } if ( typeof this . spriteSheet . onLoaded === 'function' ) { this . spriteSheet . onLoaded ( ) ; } } ) ; } param { number } spriteNumber public async showSprite ( spriteNumber ) { await this . loadingPromise ; this . playhead . play = false ; this . drawFrame ( { sprite : spriteNumber } ) ; } returns { number } public currentSprite ( ) { return this . playhead . currentSprite ; } param { string } name param { Frame [ ] } script public addScript ( name , script ) { this . spriteSheet . animations [ name ] = script ; } param { string | Animation } scriptName param { Animation } options returns { boolean } public async play ( scriptName ? , options ? ) { await this . loadingPromise ; if ( ! scriptName && ! options ) { if ( ! this . playhead . play ) { if ( this . playhead . run === 0 ) { this . playhead . run = 1 ; } this . playhead . play = true ; } } else { let animationScript ; let animationOptions = { } ; if ( typeof scriptName === 'string' && ! options ) { if ( this . spriteSheet . animations [ scriptName ] ) { this . log ( 'info' , `playing animation "${ scriptName }"` ) ; animationScript = this . spriteSheet . animations [ scriptName ] ; } else { this . log ( 'warn' , `animation "${ scriptName }" not found` ) ; } } else if ( typeof scriptName === 'string' && typeof options === 'object' ) { animationScript = this . spriteSheet . animations [ scriptName ] ; animationOptions = options ; } else if ( typeof scriptName === 'object' && ! options ) { animationScript = scriptName . script || this . playhead . script ; animationOptions = scriptName ; } if ( ! animationScript ) { this . log ( 'info' , `playing animation "all"` ) ; animationScript = this . spriteSheet . animations . all ; } let currentFrame = - 1 ; if ( animationOptions . reversed ) { currentFrame = animationScript . length ; } this . playhead = { ... playheadDefaults , ... { script : animationScript } , ... animationOptions , ... { currentFrame } } ; } if ( this . playhead . run !== 0 && this . playhead . play ) { this . loop ( ) ; } if ( typeof this . playhead . onPlay === 'function' ) { this . playhead . onPlay ( ) ; } } returns { boolean } public isPlaying ( ) { return this . playhead . play ; } param { number } tempo public async setTempo ( tempo ) { await this . loadingPromise ; this . playhead . tempo = tempo ; } returns { number } public getTempo ( ) { return this . playhead . tempo ; } returns { boolean } public async next ( ) { await this . loadingPromise ; this . playhead . currentFrame += 1 ; if ( this . playhead . currentFrame === this . playhead . script . length ) { this . playhead . run -= 1 ; this . playhead . currentFrame = 0 ; } if ( this . playhead . play && this . playhead . run === 0 ) { this . stop ( ) ; } else { const frame = this . playhead . script [ this . playhead . currentFrame ] ; this . drawFrame ( frame ) ; } } returns { boolean } public async previous ( ) { await this . loadingPromise ; this . playhead . currentFrame -= 1 ; if ( this . playhead . currentFrame < 0 ) { this . playhead . currentFrame = this . playhead . script . length - 1 ; this . playhead . run -= 1 ; } if ( this . playhead . play && this . playhead . run === 0 ) { this . stop ( ) ; } else { const frame = this . playhead . script [ this . playhead . currentFrame ] ; this . drawFrame ( frame ) ; } } param frameNumber returns { boolean } public async goTo ( frameNumber ) { await this . loadingPromise ; const baseNumber = Math . floor ( frameNumber / this . playhead . script . length ) ; frameNumber = Math . floor ( frameNumber - ( baseNumber * this . playhead . script . length ) ) ; this . playhead . currentFrame = frameNumber ; const frame = this . playhead . script [ this . playhead . currentFrame ] ; if ( frame ) { this . log ( 'info' , `frame: ${ this . playhead . currentFrame }, sprite: ${ frame . sprite }` ) ; this . drawFrame ( frame ) ; } } /**
     * Reverse direction of play
     */ public async reverse ( ) { await this . loadingPromise ; this . playhead . reversed = ! this . playhead . reversed ; } returns { boolean } public isReversed ( ) { return this . playhead . reversed ; } /**
     * Stop the animation
     */ public async stop ( ) { await this . loadingPromise ; this . playhead . play = false ; if ( typeof this . playhead . onStop === 'function' ) { this . playhead . onStop ( ) ; } } /**
     * Reset playhead to first frame
     */ public async reset ( ) { await this . loadingPromise ; this . goTo ( 0 ) ; } /**
     * Removes the element and kills the animation loop
     */ public destroy ( ) { this . playhead . play = false ; this . element . parentNode . removeChild ( this . element ) ; } /**
     * Load the spritesheet and position it correctly
     */ private loadSpriteSheet ( ) { return new Promise ( ( resolve ) => { const preload = new Image ( ) ; preload . src = this . spriteSheet . url ; imageLoaded ( preload , ( ) => { const sheet = this . spriteSheet ; const element = this . element ; this . log ( 'info' , `loaded: ${ sheet . url }, sprites ${ sheet . cols } x ${ sheet . rows }` ) ; sheet . sheetWidth = preload . width ; sheet . sheetHeight = preload . height ; sheet . frameWidth = sheet . sheetWidth / sheet . cols / sheet . downsizeRatio ; sheet . frameHeight = sheet . sheetHeight / sheet . rows / sheet . downsizeRatio ; sheet . totalSprites = ( sheet . cols * sheet . rows ) - sheet . cutOffFrames ; if ( sheet . frameWidth % 1 !== 0 ) { this . log ( 'error' , `frameWidth ${ sheet . frameWidth } is not a whole number` ) ; } if ( sheet . frameHeight % 1 !== 0 ) { this . log ( 'error' , `frameHeight ${ sheet . frameHeight } is not a whole number` ) ; } element . style . position = 'absolute' ; element . style . width = `${ sheet . frameWidth }px` ; element . style . height = `${ sheet . frameHeight }px` ; element . style . backgroundImage = `url(${ sheet . url })` ; element . style . backgroundPosition = '0 0' ; if ( sheet . downsizeRatio > 1 ) { element . style . backgroundSize = `${ sheet . sheetWidth / sheet . downsizeRatio }px ${ sheet . sheetHeight / sheet . downsizeRatio }px` ; } if ( sheet . top !== null ) { if ( sheet . top === 'center' ) { element . style . top = '50%' ; element . style . marginTop = `${ sheet . frameHeight / 2 * - 1 }px` ; } else { element . style . top = `${ sheet . top }px` ; } } if ( sheet . right !== null ) { element . style . right = `${ sheet . right }px` ; } if ( sheet . bottom !== null ) { element . style . bottom = `${ sheet . bottom }px` ; } if ( sheet . left !== null ) { if ( sheet . left === 'center' ) { element . style . left = `${ sheet . left }px` ; element . style . marginLeft = `${ sheet . frameWidth / 2 * - 1 }px` ; } else { element . style . left = `${ sheet . left }px` ; } } this . autoScript ( ) ; const animationOptions = { script : sheet . animations . all } ; this . playhead = { ... playheadDefaults , ... animationOptions } ; resolve ( ) ; } ) ; } ) ; } /**
     * Generate a linear script based on the spritesheet itself
     */ private autoScript ( ) { const script = [ ] ; for ( let i = 0 ; i < this . spriteSheet . totalSprites ; i ++ ) { script [ i ] = { sprite : ( i + 1 ) } ; } this . addScript ( 'all' , script ) ; } /**
     * The animation loop
     */ private loop = ( time = 0 ) => { const requestFrameId = raf ( this . loop ) ; const playhead = this . playhead ; if ( ! this . element || ! this . spriteSheet . loaded ) { return ; } if ( ( time - playhead . lastTime ) >= playhead . nextDelay ) { this . render ( time ) ; } if ( ! playhead . play ) { raf . cancel ( requestFrameId ) ; return ; } } ; private async render ( time ) { const element = this . element ; const playhead = this . playhead ; if ( element . offsetParent !== null && this . inViewport ( ) ) { if ( playhead . run !== 0 ) { if ( playhead . reversed ) { await this . previous ( ) ; } else { await this . next ( ) ; } const frame = playhead . script [ playhead . currentFrame ] ; this . log ( 'info' , `run: ${ playhead . run }, frame` , frame ) ; playhead . lastTime = time ; } } else { if ( typeof playhead . onOutOfView === 'function' ) { playhead . onOutOfView ( ) ; } } } /**
     * Draw a single frame
     */ private drawFrame ( frame ) { const sheet = this . spriteSheet ; const playhead = this . playhead ; const element = this . element ; this . playhead . nextDelay = frame . delay ? frame . delay : this . playhead . delay ; this . playhead . nextDelay /= this . playhead . tempo ; if ( frame . sprite !== playhead . currentSprite ) { const rect = element . getBoundingClientRect ( ) ; const row = Math . ceil ( frame . sprite / sheet . cols ) ; const col = frame . sprite - ( ( row - 1 ) * sheet . cols ) ; const bgX = ( ( col - 1 ) * sheet . frameWidth ) * - 1 ; const bgY = ( ( row - 1 ) * sheet . frameHeight ) * - 1 ; if ( row > sheet . rows || col > sheet . cols ) { this . log ( 'warn' , `position ${ frame . sprite } out of bound` ) ; } playhead . currentSprite = frame . sprite ; element . style . backgroundPosition = `${ bgX }px ${ bgY }px` ; if ( frame . top ) { element . style . top = `${ rect . top + frame . top }px` ; } if ( frame . right ) { element . style . right = `${ rect . right + frame . right }px` ; } if ( frame . bottom ) { element . style . bottom = `${ rect . bottom + frame . bottom }px` ; } if ( frame . left ) { element . style . left = `${ rect . left + frame . left }px` ; } } if ( typeof playhead . onFrame === 'function' ) { playhead . onFrame ( playhead . currentFrame ) ; } return true ; } returns { boolean } private inViewport ( ) { const sheet = this . spriteSheet ; const rect = this . element . getBoundingClientRect ( ) ; return ( rect . top + sheet . frameHeight >= 0 && rect . left + sheet . frameWidth >= 0 && rect . bottom - sheet . frameHeight <= ( window . innerHeight || document . documentElement . clientHeight ) && rect . right - sheet . frameWidth <= ( window . innerWidth || document . documentElement . clientWidth ) ) ; } param level param message private private log ( level , ... message ) { if ( typeof console === 'undefined' || ( level === 'info' && ! this . debug ) ) { return ; } console [ level ] ( `Spriteling` , ... message ) ; } } export default Spriteling ;