UNPKG

act

Version:

An animation engine for the canvas and dom

623 lines (517 loc) 18.4 kB
# act # ==================================================================== # Transaction based animation for dom and canvas # # ### Usage # # #### act # _ = require 'underscore' # act.function # --------------------------------------------------------------------- _actInstanceID = 1 actGenerator = -> # act.function # --------------------------------------------------------------------- act = (obj, dest, options = {}) -> scheduler = act._scheduler if obj instanceof Implicit obj = obj._obj if options.animator task = new Task obj, dest, act, options scheduler.addTask task else for k of dest task = new Task obj, (_.pick dest, k), act, options scheduler.addTask task # For debugging, mark each instance if act with a unique ID act.instanceID = _actInstanceID _actInstanceID++ # act.EaseLinear, etc. # --------------------------------------------------------------------- for k, v of Ease act[k] = v act.animator = (name) -> mapAnimatorFromName[name] # act.on, act.off, act.trigger # --------------------------------------------------------------------- _extendEventFunctions act # act.clone # --------------------------------------------------------------------- # Create a seperate act function with seperate scheduler, rate, etc # This does not clone tasks. act.clone = actGenerator # act.rate # --------------------------------------------------------------------- # Rate allows speeding up and slowing down time. # # rate = 2 Twice as fast # rate = 0.5 Half as fast # act.rate = 1 # act.tickInterval # --------------------------------------------------------------------- # Interval in seconds between update ticks (default: 1/60, 60 Hz) act.tickInterval = 1/60 # act._transactionBuilders # --------------------------------------------------------------------- act._transactionBuilders = [] # act.play # --------------------------------------------------------------------- act.play = -> act._forceRenderNextTick = true if not act._playState # Store reference to state to indentity specific start has been stopped act._playState = state = lastTick: _getTime() ticker = -> if act._playState == state # Set up next tick _setTimeout act.tickInterval, ticker # Step all tasks timeCurrent = _getTime() timeElapsed = timeCurrent - state.lastTick changed = act._tick timeElapsed * act.rate state.lastTick = timeCurrent # If any task stepped, render if changed or act._forceRenderNextTick act._forceRenderNextTick = false act.trigger 'render' # Start tick _setTimeout act.tickInterval, ticker # act._tick # --------------------------------------------------------------------- # Tick all your tasks act._tick = (dt) -> act._rootScheduler.tick dt # act.fastForward # --------------------------------------------------------------------- # Fast forward in time act.fastForward = act._tick # act.stop # --------------------------------------------------------------------- act.stop = -> act._playState = null # act.begin # --------------------------------------------------------------------- # Start defining a new transaction act.begin = (options) -> builder = new TransactionBuilder options act._transactionBuilders.push builder builder # act._scheduler # --------------------------------------------------------------------- Object.defineProperty act, '_scheduler', get: -> len = act._transactionBuilders.length if len then act._transactionBuilders[len-1] else act._rootScheduler enumerable: false # act.commit # --------------------------------------------------------------------- # Stop defining a transaction and add it to its parent transaction act.commit = () -> builderTop = act._transactionBuilders.pop() scheduler = @_scheduler scheduler.addTask builderTop.transaction() act.renderNextTick = -> act._forceRenderNextTick = true # act.property # --------------------------------------------------------------------- act.property = (obj, key, val, options) -> # Default options to actOptions[k] if (_.isUndefined options) and obj.actOptions options = if _.isFunction obj.actOptions obj.actOptions()[key] else obj.actOptions[key] # Close over animatable value store = value: val setter = (v) -> act store, value: v, options setter._actStore = store Object.defineProperty obj, key, get: -> store.value set: setter act._implicitPropertyStore = (obj, key) -> d = Object.getOwnPropertyDescriptor obj, key return d?.set?._actStore # act.properties # --------------------------------------------------------------------- act.properties = (obj, properties, options) -> for k, v of properties act.property obj, k, v, options?[k] # act.implicit # --------------------------------------------------------------------- act.implicit = (obj, options = {}) -> # Default animation options options = _.clone options if obj.actOptions _.defaults options, (if _.isFunction obj.actOptions then obj.actOptions() else obj.actOptions) for k, v of obj # Null means omit if options[k] == null options[k] = {animator: null} # Undefined means default animator else if options[k] == undefined options[k] = {animator: _defaultInterpolationForValue v} else if _.isString options[k] or _.isFunction options[k] options[k] = {animator: options[k]} else throw new Error "act: animator is not string or function" new Implicit act, obj, options _defaultInterpolationForValue = (v) -> if _.isNumber v then linearAnimator else setAtEndAnimator # _rootScheduler # --------------------------------------------------------------------- # act root scheduler act._rootScheduler = new Scheduler() act.play() # End actGenerator return act # act.Animator # --------------------------------------------------------------------- linearAnimator = (obj, destinations) -> initials = {} finals = {} for k, v of destinations # Handle operators if _.isObject v ops = _.keys v throw (new Error "act: expected only one operator (#{ops}) for key #{k}") unless ops.length == 1 destinations[k] = op: ops[0], value: v[ops[0]] else destinations[k] = op: '=', value: v initials[k] = obj[k] finals[k] = _final initials[k], destinations[k]['op'], destinations[k]['value'] (t) -> changed = {} for k, v of finals changed[k] = _interp initials[k], finals[k], t changed _interp = (a, b, t) -> a * (1.0 - t) + b * t _final = (initial, op, value) -> if op == '==' or op == '=' return value else if op == '/=' return initial / value else if op == '*=' return initial * value else if op == '-=' return initial - value else if op == '+=' return initial + value # setAtEndAnimator # --------------------------------------------------------------------- setAtEndAnimator = (obj, destinations) -> initials = _.pick obj, _.keys(destinations)... finals = _.clone destinations (t) -> _.clone (if t < 1 then initials else finals) # Animator short names # --------------------------------------------------------------------- mapAnimatorFromName = 'setAtEnd': setAtEndAnimator 'linear': linearAnimator # 'type': typeByItemAnimator # Works on arrays and strings # 'typeByWord': typeByWordAnimator # Works on strings only # 'step': stepAnimator # 'color': colorAnimator # 'region': regionAnimator # 'stringTypeWord': stringTypeByWordAnimator # act.Task # --------------------------------------------------------------------- class Task constructor: (@obj, @destination, @act, options = {}) -> for k, v of @destination throw new Error('act: functions cannot be animated') if _.isFunction v if options.duration? and options.duration <= 0 throw new Error 'act: duration must be positive' @duration = options.duration || 1 @started = options.started || -> @completed = options.completed || -> @_easing = options.easing || EaseLinear @_animator = if _.isString options.animator mapAnimatorFromName[options.animator] else options.animator || linearAnimator @startTime = 0 @_elapsed = 0 update: (time) -> # Start if we need to elapsed = time - @startTime if (@_elapsed <= 0) and (elapsed <= 0) return # Haven't started yet if (@_elapsed <= 0) and (elapsed > 0) @_start() # Forwards over start point else if (@_elapsed >= @duration) and (elapsed >= @duration) return # Already completed if (elapsed < 0) elapsed = 0 # Clamp elapsed at start time if (elapsed > @duration) elapsed = @duration # Clamp elapsed at end time # Update the children eased = @_easing (elapsed / @duration) extender = (@_interpolator eased) for k, v of extender if store = act._implicitPropertyStore @obj, k store.value = v else @obj[k] = v if (elapsed >= @duration) @_complete() @_elapsed = elapsed _start: -> @_interpolator = @_animator @obj, @destination, @act finalValue = @_interpolator 1 initialValue = @_interpolator 0 @obj.actBefore? initialValue, finalValue @started?() _complete: -> initialValue = @_interpolator 0 finalValue = @_interpolator 1 @obj.actAfter? initialValue, finalValue @completed?() _maxEndTime = (tasks) -> _.reduce tasks, ((acc,task) -> Math.max acc, (task.startTime + task.duration)), 0 # Easing # --------------------------------------------------------------------- Ease = {} Ease.EaseLinear = EaseLinear = (v) -> v Ease.EaseIn = EaseIn = (v) -> v * v * v Ease.EaseOut = EaseOut = (v) -> 1.0 - EaseIn(1.0 - v) Ease.EaseIn2 = EaseIn2 = (v) -> v * v Ease.EaseOut2 = EaseOut2 = (v) -> 1.0 - EaseIn(1.0 - v) Ease.EaseInOut = EaseInOut = (v) -> if v < 0.5 then EaseIn(v * 2) / 2 else EaseOut(v * 2 - 1.0) / 2 + 0.5 Ease.EaseInOut2 = EaseInOut2 = (v) -> (3 * v * v) - 2 * v * v * v Ease.EaseOutBounce = EaseOutBounce = (v) -> if (v < 1 / 2.75) 7.5625 * v * v else if (v < 2 / 2.75) v -= 1.5 / 2.75 7.5625 * v * v + 0.75 else if (v < 2.5 / 2.75) v -= 2.25 / 2.75 7.5625 * v * v + 0.9375 else v -= 2.625 / 2.75 7.5625 * v * v + 0.984375 # Transaction # --------------------------------------------------------------------- class Transaction constructor: (fields) -> @_serial = fields.serial @_rate = fields.rate throw new Error 'act: rate must be positive' unless @_rate > 0 @_easing = fields.easing @_tasks = fields.tasks @_reverseTasks = (@_tasks.slice 0).reverse() @_started = fields.started @_completed = fields.completed @_elapsed = 0 @duration = (_maxEndTime @_tasks) / @_rate @startTime = 0 update: (time) -> # Start if we need to elapsed = time - @startTime if (@_elapsed <= 0) and (elapsed <= 0) return # Haven't started yet if (@_elapsed <= 0) and (elapsed > 0) @_start() # Forwards over start point else if (@_elapsed >= @duration) and (elapsed >= @duration) return # Already completed if (elapsed < 0) elapsed = 0 # Clamp elapsed at start time if (elapsed > @duration) elapsed = @duration # Clamp elapsed at end time # Update the children tasks = if elapsed >= @_elapsed then @_tasks else @_reverseTasks eased = (@_easing elapsed / @duration) * @duration * @_rate for task in @_tasks task.update eased if (elapsed >= @duration) @_complete() @_elapsed = elapsed # Start calcuates duration and sets up everything _start: -> @_started?() _complete: -> @_completed?() # TransactionBuilder # --------------------------------------------------------------------- _default = (a = b, b) -> a class TransactionBuilder constructor: (fields = {}) -> @rate = _default fields.rate, 1 @started = fields.started || -> @completed = fields.completed || -> @serial = fields.serial || false @easing = fields.easing || EaseLinear @_tasks = [] addTask: (task) -> @_tasks.push task transaction: -> if @serial t = 0 for task in @_tasks task.startTime = t t += task.duration new Transaction rate: @rate started: @started completed: @completed serial: @serial easing: @easing tasks: @_tasks # Scheduler # --------------------------------------------------------------------- # Transactions are added to the scheduler class Scheduler constructor: (options = {}) -> @_tasks = [] @rate = options.rate || 1 @_elapsed = 0 tick: (dt) -> @_elapsed += dt * @rate incompleteTasks = [] changed = @_tasks.length for task in @_tasks task.update @_elapsed # Don't keep tasks that have ended if task.startTime + task.duration >= @_elapsed incompleteTasks.push task @_tasks = incompleteTasks changed # Add addTask: (task) -> task.startTime = @_elapsed @_tasks.push task @rate = 1.0 # Implicit # --------------------------------------------------------------------- # Proxy assignments to create implicit animations class Implicit constructor: (@_act, @_obj, options) -> _defineAnimatedProperty = (k, actOptions) => Object.defineProperty @, k, get: => @_obj[k] set: (v) => @_act @_obj, (_objectFromKeysAndValues k, v), actOptions _definePassthroughProperty = (k) => Object.defineProperty @, k, get: => @_obj[k] set: (v) => @_obj[k] = v _proxyFunction = (k) => @[k] = -> @_obj[k].apply @, arguments # Proxy the given setters, with animation for k, opts of options do (k, opts) => if opts == null or opts.animator == null _definePassthroughProperty k else _defineAnimatedProperty k, opts # Proxy all methods and remaining non-animated properties for k, v of @_obj do (k, v) => # Functions apply as if from the original object if _.isFunction v _proxyFunction k else if not @[k]? # Pass through properties that aren't animated to the original object _definePassthroughProperty k _objectFromKeysAndValues = -> obj = Object.create null i = 0 while i + 1 < arguments.length k = arguments[i] v = arguments[i + 1] obj[k] = v i += 2 obj # _setTimeout # --------------------------------------------------------------------- # Abstract setTimeout to _requestAnimFrame = -> if window? window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame _setTimeout = (timeSeconds, fn) -> if requestAnimFrame = _requestAnimFrame() requestAnimFrame fn else setTimeout fn, timeSeconds * 1000 # _getTime # --------------------------------------------------------------------- # Get current time _getTime = -> (new Date).getTime() * 0.001 # _smoothTicks # --------------------------------------------------------------------- # Smooths ticks _smoothTicks = (simulate, tick, fn) -> return fn ### # TODO: Think about this later return fn unless simulate throw (new Error 'tick must be positive') unless tick > 0 timeElapsed = 0 (dt) -> timeElapsed += dt while timeElapsed > tick fn tick timeElapsed -= tick ### # Array Remove - By John Resig (MIT Licensed) # --------------------------------------------------------------------- Array.prototype.remove = (from, to) -> rest = this.slice((to or from) + 1 or this.length) @length = if from < 0 then this.length + from else from @push.apply this, rest # act # --------------------------------------------------------------------- _extendEventFunctions = (obj) -> throw (new Error 'act: object already has event functions') if obj.on? or obj.off? or obj.trigger? _eventHandlers = {} obj.on = (eventName, cb, context) -> throw (new Error "act.on: eventName is not a string (#{eventName})") unless _.isString eventName throw (new Error "act.on: eventHandler is not a function (eventName: #{eventName}, eventHandler: #{cb})") unless _.isFunction cb eventNames = eventName.split ' ' eventHandler = cb: cb, context: context for name in eventNames if name.length _eventHandlers[name] = [] unless _eventHandlers[name] _eventHandlers[name].push eventHandler -> obj.off eventName, cb, context obj.off = (eventName, cb, context) -> if eventName eventHandlersToChange = {} eventNames = eventName.split ' ' for name in eventNames if name.length and _eventHandlers[name] eventHandlersToChange[name] = _eventHandlers[name] else eventHandlersToChange = _eventHandlers # Match cb and context for name, handlers of eventHandlersToChange _eventHandlers[name] = _.reject handlers, (handler) -> if cb return false unless handler.cb == cb if context return false unless handler.context == context true null obj.trigger = (eventName) -> eventNames = eventName.split ' ' for name in eventNames if name.length and _eventHandlers[name] for handler in _eventHandlers[name] args = Array.prototype.slice.call arguments, 1 handler.cb.apply (handler.context || obj), args null # act # --------------------------------------------------------------------- act = actGenerator() # Version # --------------------------------------------------------------------- act.version = '0.0.6' # Export # --------------------------------------------------------------------- module.exports = act