Piped Motion

缓动库,用法:

Easing.quad 0.2
include PipedMotion
motion(:name).fire @sprite, x: 100, y: [100, :quad]
@sprite.extend Animatable
@sprite.animate x: 100, y: [100, :quad]

motion.rb

# coding: utf-8

##
# Animations in named pipes.
# 
# Usage:
#   include PipedMotion
#   motion [pipe] => #<Motion name=nil> | #<MotionCollection pipes=[]>
#     .fire obj, x: 42, y: [24, :quad] do |progress| ... end
#     .busy?
#     .empty?
#     .pause
#     .resume
#     .skip
#     .abort
#     .cancel
#     .then { ... }
#     .wait *pipes
#     .merge *other_pipes
#   sprite.extend Animatable
#   sprite.animate x: 42, y: [24, :quad]
# 
# Api:
# - module Easing
#   - def self.quad_in_out x
# - module PipedMotion
#   - module Motion
#   - module MotionCollection
#   - def pipes
#   - def motion pipe=nil
#   - def update
#     - @_pipes = {}
# - module Animatable
#   - def tween = motion.fire
#   - def animate = tween self

# - easings.net/zh-cn
# - github.com/gdsmith/jquery.easing/blob/master/jquery.easing.js
module Easing
  extend Math
  include Math

  C1 = 1.70158
  C2 = C1 * 1.525
  C3 = C1 + 1
  C4 = 2 * PI / 3
  C5 = 4 * PI / 9
  N1 = 7.5625
  D1 = 2.75

  BounceOut = -> x {
    case
    when x < 1   / D1 then N1 * x * x
    when x < 2   / D1 then N1 * (x - 1.5   / D1)**2 + 0.75
    when x < 2.5 / D1 then N1 * (x - 2.25  / D1)**2 + 0.9375
    else                   N1 * (x - 2.625 / D1)**2 + 0.984375
    end
  }

  Functions = {
    linear:  -> x { x },
    quad:    -> x { x**2 },
    cubic:   -> x { x**3 },
    quart:   -> x { x**4 },
    quint:   -> x { x**5 },
    sine:    -> x { 1 - cos(x * PI / 2) },
    expo:    -> x { 2**(10 * (x - 1)) },
    circ:    -> x { 1 - sqrt(1 - x**2) },
    elastic: -> x { -2**(-10 * x) * sin((x * 10 - 0.75) * C4) + 1 },
    back:    -> x { C3 * x**3 - C1 * x**2 },
    bounce:  -> x { 1 - BounceOut[1 - x] },
  }

  Functions.each do |name, lb|
    define_method :"#{name}_in" do |x|
      lb[x.to_f]
    end
    define_method :"#{name}_out" do |x|
      1 - lb[(1 - x).to_f]
    end
    define_method :"#{name}_in_out" do |x|
      x < 0.5 ? lb[x * 2.0] / 2 : 1 - lb[(1 - x) * 2.0] / 2
    end
    alias_method(name, :"#{name}_out")
  end

  functions = Functions.keys
  functions.map! { |e| %W( #{e} #{e}_in #{e}_out #{e}_in_out ) }
  module_function *functions.flatten
end

module PipedMotion
  # A pipe containing animations on some object
  class Motion
    attr_accessor :pipes, :name, :pipe, :running
    def initialize pipes, name=nil
      @pipes   = pipes
      @name    = name
      @pipe    = []
      @running = false
    end
    alias busy? running
    def pause
      @running = false
    end
    def resume
      @running = true
    end
    def empty?
      @pipe.empty?
    end
    def update
      return unless busy?
      while true
        if @pipe.first.respond_to? :call
          @pipe.shift.call
          return @running = false if @pipe.empty?
          next
        end
        if @pipe.first.first == :wait
          pipes = @pipe.first[1]
          return unless pipes.all? { |s| s.values.flatten.all? &:empty? }
          @pipe.shift
          return @running = false if @pipe.empty?
          next
        end
        break
      end
      obj, t, n, options, blk = *@pipe.first
      t += 1
      blk.call(t.to_f / n) if blk and blk.respond_to? :call
      options.each do |k, from, to|
        if to.respond_to? :call
          v = to.call obj, t
        else
          if to.is_a? Array
            d, f = *to
            i = Easing.public_send(f, t / n.to_f) * (d - from)
          else
            d = to
            i = (to - from).to_f * t / n
          end
          v = t == n ? d : (from + i).ceil
        end
        access obj, k, v
      end if obj
      if t == n
        @pipe.shift
        if @pipe.empty?
          @running = false
          return
        end
        unless @pipe.first.respond_to? :call
          @pipe.first[3].map! { |k, f, t| [k, access(obj, k), t] }
        end
      else
        @pipe.first[1] = t
      end
    end
    def fire obj, duration, options={}, &blk
      options = options.map { |k, v| [k, access(obj, k), v] }
      @pipe << [obj, 0, duration, options, blk]
      @running = true
      self
    end
    def then &blk
      @pipe << blk if blk
      @running = true
      self
    end
    def wait *pipes
      @pipe << [:wait, pipes]
      @running = true
      self
    end
    def skip
      return if @pipe.empty?
      obj, *, options = *@pipe.last
      options.each { |k, _, to| access obj, k, to.is_a?(Array) ? to[0] : to }
      @pipe.shift
      @running = false
      self
    end
    def abort
      @pipe.clear
      @running = false
      self
    end
    def cancel
      return if @pipe.empty?
      obj, *, options = *@pipe.first
      options.each { |k, from, _| access obj, k, from }
      @pipe.shift
      @running = false
      self
    end
    def merge other=[]
      case other
      when Motion
        @pipe.concat other.pipe
        if other.name.nil?
          @pipes[nil].delete other
        else
          @pipes.delete other.name
        end
      when Symbol
        merge @pipes[other]
      when Array
        other.each { |name| merge name }
      end
      self
    end
    def inspect
      "#<Motion #{@pipe[0].inspect}>"
    end
    private
    def access obj, key, value=nil
      if obj.respond_to? :[] and obj.respond_to? :[]=
        value.nil? ? obj[key] : obj[key] = value
      elsif key.is_a? Symbol or key.is_a? String
        if key[0] == ?@
          if value.nil?
            obj.instance_variable_get key
          else
            obj.instance_variable_set key, value
          end
        else
          if value.nil?
            obj.public_send key
          else
            obj.public_send :"#{key}=", value
          end
        end
      else
        raise "can't access #{key.inspect} in #{obj.inspect}"
      end
    end
  end
  # Pipes.
  class MotionCollection
    attr_accessor :pipes, :collection
    def initialize pipes, collection
      @pipes      = pipes
      @collection = collection
    end
    def merge other=[]
      case other
      when MotionCollection
        other = other.collection
      when Symbol
        other = [other]
      end
      (@collection + other).inject { |a, b| a.merge b }
    end
    def - other=[]
      case other
      when MotionCollection
        other = other.collection
      when Symbol
        other = [other]
      end
      MotionCollection.new(@pipes, @collection - other)
    end
    %w( pause resume skip abort cancel ).each do |name|
      define_method name do
        @collection.each &name.to_sym
        self
      end
    end
    def inspect
      "#<MotionCollection #{@collection.map(&:name).inspect}>"
    end
  end

  def motion pipe=nil
    @_pipes ||= { nil => [] }
    case pipe
    when nil
      Motion.new(@_pipes).tap { |m| @_pipes[nil] << m }
    when :all
      MotionCollection.new @_pipes, @_pipes.values.flatten
    when :singleton
      MotionCollection.new @_pipes, @_pipes[nil]
    when Symbol
      @_pipes[pipe] ||= Motion.new @_pipes, pipe
    when Array
      MotionCollection.new @_pipes, pipe.map { |name| @_pipes[name] }
    else
      raise "can't create motion with argument #{pipe.inspect}"
    end
  end

  def pipes
    @_pipes ||= { nil => [] }
  end

  def update(*)
    super if defined? super
    return unless @_pipes
    @_pipes.values.flatten.each &:update
    @_pipes.delete_if { |n, m| !n.nil? && m.empty? }
    @_pipes[nil].delete_if &:empty?
  end
end

module Animatable
  include PipedMotion
  def tween *args, &blk
    motion.fire *args, &blk
  end
  def animate *args, &blk
    tween self, *args, &blk
  end
end