# Linus
# version 1
# for Shoes (tested on 0.r811 on WinXP)
# lets you create simple animations using lines, key frames and interpolation
# written by chiisaitsu in July 2008 <chiisaitsu@gmail.com>

require 'enumerator'
require 'yaml'

FRAMES_PER_SEC        = 24

VERTEX_HOVER_DISTANCE = 10

VERTEX_SIZE           = 6
HOVER_VERTEX_SIZE     = 11

CANVAS_COLOR          = '#111'..'#333'
MENU_FLOW_COLOR       = '#bbb'
MENUITEM_FLOW_COLOR   = '#ddd'
MENU_SEPARATOR_COLOR  = '#aaa'
STATUS_TEXT_COLOR     = '#888'
LINE_COLOR            = '#0f0'
DRAWING_LINE_COLOR    = '#ff0'
VERTEX_COLOR          = '#888'
HOVER_VERTEX_COLOR    = '#f00'

APP_NAME =    'Linus'
APP_VERSION = '1'

# the top-level namespace is Shoes??
class ::Array

  def bininsert item, &block
    insert(binsearch(item, &block)[:index], item)
    self
  end

  def binsearch target, &block
    lo= 0
    hi= self.size - 1
    not_found= { :found => false, :index => self.size, :elt => nil }
    while lo <= hi
      mid= (hi + lo) / 2
      comp= (block_given?? yield(self[mid], target) : self[mid] <=> target)
      case comp
      when 0
        return { :found => true, :index => mid, :elt => self[mid] }
      when -1
        lo= mid + 1
      when 1
        hi= mid - 1
        not_found[:index]= mid
        not_found[:elt]= self[mid]
      else
        raise "comparison yielded #{comp.inspect}"
      end
    end
    not_found
  end

  def x
    self[-2]
  end

  def x= val
    self[-2]= val
  end

  def y
    self[-1]
  end

  def y= val
    self[-1]= val
  end

end


class Poly

  attr_reader :keyframes

  def add_keyframe frame
    search= search_keyframes(frame)
    if search[:found]
      search[:elt]
    else
      keyframe= { :index => frame, :vertices => dup_vertices(frame) }
      @keyframes.bininsert(keyframe) { |kf1, kf2| kf1[:index] <=> kf2[:index] }
      keyframe
    end
  end

  def add_vertex vertex, frame
    search= search_keyframes(frame)
    if search[:found]
      search[:elt][:vertices] << vertex
    else
      add_keyframe(frame)[:vertices] << vertex
    end
  end

  def delete_keyframe frame
    search= search_keyframes(frame)
    if search[:found]
      @keyframes.delete_at(search[:index])
      true
    else
      false
    end
  end

  def draw shoes, frame, show_vertices
    verts= vertices(frame)
    verts.each_cons(2) do |vert1, vert2|
      draw_line(shoes, vert1, vert2)
      draw_vertex(shoes, vert1) if show_vertices
    end
    draw_vertex(shoes, verts.last) if show_vertices
  end

  def initialize keyframes=[]
    @keyframes= keyframes
  end

  def search_keyframes frame
    @keyframes.binsearch(frame) { |kf, f| kf[:index] <=> f }
  end

  def vertices frame
    search= search_keyframes(frame)
    if search[:found]
      search[:elt][:vertices]
    else
      interp_vertices(frame,
                      search[:index] == 0 ? nil : @keyframes[search[:index]-1],
                      @keyframes[search[:index]])
    end
  end

  private

  def draw_line shoes, vertex1, vertex2
    shoes.stroke(LINE_COLOR)
    shoes.line(vertex1.x, vertex1.y, vertex2.x, vertex2.y)
  end

  def draw_vertex shoes, vertex
    shoes.nostroke
    if vertex == shoes.hover_vertex
      color= HOVER_VERTEX_COLOR
      size= HOVER_VERTEX_SIZE
    else
      color= VERTEX_COLOR
      size= VERTEX_SIZE
    end
    shoes.fill(color)
    shoes.oval(vertex.x - (size / 2.0), vertex.y - (size / 2.0), size, size)
  end

  def dup_vertices frame
    id_hash= {}
    vertices(frame).map do |v|
      id_hash[v.__id__]= v.dup if id_hash[v.__id__].nil?
      id_hash[v.__id__]
    end
  end

  def interp_vertices frame, prev_keyframe, next_keyframe
    if prev_keyframe && next_keyframe
      interped_verts= []
      zipped_verts= prev_keyframe[:vertices].zip(next_keyframe[:vertices])
      percent= ((frame - prev_keyframe[:index]).to_f /
                (next_keyframe[:index] - prev_keyframe[:index]).to_f)
      zipped_verts.each do |pkf_vert, nkf_vert|
        x= ((1 - percent) * pkf_vert.x) + (percent * nkf_vert.x)
        y= ((1 - percent) * pkf_vert.y) + (percent * nkf_vert.y)
        interped_verts << [x, y]
      end
      interped_verts
    elsif prev_keyframe
      prev_keyframe[:vertices]
    else
      []
    end
  end

end


Shoes.app(:title => "#{APP_NAME} #{APP_VERSION}") do

  def animate_polys
    if @state == :animating
      @state= :pointing
    else
      @current_frame= 1
      @state= :animating
    end
  end

  def check_click
    click do |button, x, y|
      return if y < menubar_height
      case @state
      when :pointing
        case button
        when 1
          if @hover_vertex
            @state= :dragging_vertex
            @hover_poly.add_keyframe(@current_frame)
            check_hover
          else
            @state= :drawing_line
            @polys << Poly.new
            @polys.last.add_vertex([x, y], @current_frame)
          end
        when 2
          @polys.reverse_each do |poly|
            if poly.delete_keyframe(@current_frame)
              @polys.delete(poly) if poly.keyframes.empty?
              break
            end
          end
        end
      when :drawing_line
        case button
        when 1
          if @hover_vertex &&
              @hover_vertex != @polys.last.vertices(@current_frame).last
            @polys.last.add_vertex(@hover_vertex, @current_frame)
          else
            @polys.last.add_vertex([x, y], @current_frame)
          end
        when 2
          @state= :pointing
        end
      end
    end
  end

  def check_hover
    @hover_poly= nil
    @hover_vertex= nil
    min_dist= 1.0 / 0 # Infinity
    @polys.each do |poly|
      poly.vertices(@current_frame).each do |vertex|
        dist= Math.sqrt((vertex.x - mouse.x) ** 2 + (vertex.y - mouse.y) ** 2)
        if dist <= VERTEX_HOVER_DISTANCE && dist < min_dist
          min_dist= dist
          @hover_poly= poly
          @hover_vertex= vertex
        end
      end
    end
  end

  def check_release
    release { @state= :pointing if @state == :dragging_vertex }
  end

  def hover_vertex
    @hover_vertex
  end

  def keyframe? frame
    if @polys.empty?
      false
    else
      @polys.any? { |p| p.keyframes.map { |k| k[:index] }.include? frame }
    end
  end

  def last_keyframe
    if @polys.empty?
      1
    else
      @polys.map { |p| p.keyframes.map { |k| k[:index] }.max }.max
    end
  end

  def load filename=nil
    filename ||= ask_open_file
    unless filename.nil?
      begin
        reset
        YAML.load_file(filename).each do |poly_keyframes|
          @polys << Poly.new(poly_keyframes)
        end
        animate_polys
      rescue Exception => exc
        alert("Error loading file:\n#{exc}")
      end
    end
  end

  def menubar *menus
    @menu_flow= flow { background(MENU_FLOW_COLOR) }
    menu_links= []
    menuitem_flows= []
    selected_menu= nil
    select_menu= lambda do |menu|
      if menu != selected_menu
        unless selected_menu.nil?
          menuitem_flows[selected_menu].hide
          menu_links[selected_menu].replace(menus[selected_menu].first)
        end
        @menuitem_flow= menuitem_flows[menu]
        @menuitem_flow.show
        selected_menu= menu
        menu_links[menu].replace(strong(menus[menu].first))
      end
    end
    menus.each_with_index do |menu, menu_index|
      menu_link= link(menu.first) { select_menu.call(menu_index) }
      @menu_flow.append { para(menu_link) }
      menu_links.push(menu_link)
    end
    menus.each do |menu|
      menuitem_flows.push(flow { background(MENUITEM_FLOW_COLOR) })
      menuitem_flows.last.hide
      menu[1..-1].each do |menuitem|
        case menuitem
        when Proc
          menuitem_flows.last.append { menuitem.call }
        when Array
          menuitem_link= link(menuitem[0]) { menuitem[1].call }
          menuitem_flows.last.append { para(menuitem_link) }
        when nil
          menuitem_flows.last.append do
            para('  |  ', :stroke => MENU_SEPARATOR_COLOR)
          end
        end
      end
    end
    select_menu.call(menus.size - 1)
  end

  def menubar_height
    @menu_flow.height + @menuitem_flow.height
  end

  def next_keyframe
    nex= nil
    @polys.each do |poly|
      search= poly.search_keyframes(@current_frame)
      pnex= if search[:found]
              if search[:index] + 1 < poly.keyframes.size
                poly.keyframes[search[:index] + 1][:index]
              else
                nil
              end
            elsif search[:index] < poly.keyframes.size
              poly.keyframes[search[:index]][:index]
            else
              nil
            end
      nex= pnex if pnex && (nex.nil? || pnex < nex)
    end
    nex || @current_frame
  end

  def paint_canvas
    @canvas.clear do
      @polys.each { |p| p.draw(self, @current_frame, @state != :animating) }
      if @state == :drawing_line
        stroke(DRAWING_LINE_COLOR)
        last_vert= @polys.last.vertices(@current_frame).last
        if @hover_vertex && @hover_vertex != last_vert
          line(last_vert.x, last_vert.y, @hover_vertex.x, @hover_vertex.y)
        else
          line(last_vert.x, last_vert.y, mouse.x, mouse.y)
        end
      end
    end
  end

  def prev_keyframe
    prev= nil
    @polys.each do |poly|
      search= poly.search_keyframes(@current_frame)
      pprev= (search[:index] > 0 ?
              poly.keyframes[search[:index] - 1][:index] :
              nil)
      prev= pprev if pprev && (prev.nil? || prev < pprev)
    end
    prev || @current_frame
  end

  def reset
    @state= :pointing
    @current_frame= 1
    @polys= []
  end

  def save
    fname= ask_save_file
    unless fname.nil?
      begin
        File.open(fname, 'w') do |file|
          file.puts(@polys.map { |poly| poly.keyframes }.to_yaml)
        end
      rescue Exception => exc
        alert("Error saving file:\n#{exc}")
      end
    end
  end

  def set_status_text
    @current_frame_para.replace(@current_frame.to_s)
    @keyframe_para.replace(keyframe?(@current_frame) ? ' (KEYFRAME)' : ' ')
    @status_text.replace(
      case @state
      when :animating
        'wheeeee!'
      when :dragging_vertex
        "you draggin' real good, mmm hmm"
      when :drawing_line
        'left-click => add another vertex,   right-click => stop'
      when :pointing
        'left-click => start new poly or drag vertex,   ' \
        'right-click => in keyframe, delete last poly'
      end
    )
  end

  ##############

  reset
  background(CANVAS_COLOR)
  @canvas= flow

  menu= [['App',
          ['Save',    lambda { save }],
          ['Load',    lambda { load }],
          nil,
          ['Reset',   lambda { reset }],
          nil,
          ['Quit',    lambda { quit }]],
         ['Frame',
          ['  |<  ',  lambda { @current_frame= 1 }],
          ['  K<  ',  lambda { @current_frame= prev_keyframe }],
          ['  <<  ',
           lambda { @current_frame -= [10, @current_frame - 1].min }],
          ['  <  ',   lambda { @current_frame -= 1 if @current_frame > 1 }],
          ['  >  ',   lambda { @current_frame += 1 }],
          ['  >>  ',  lambda { @current_frame += 10 }],
          ['  >K  ',  lambda { @current_frame= next_keyframe }],
          ['  >|  ',  lambda { @current_frame= last_keyframe }],
          nil,
          ['Animate', lambda { animate_polys }],
          nil,
          lambda { para('Frame ') },
          lambda { @current_frame_para= para('...') },
          lambda { @keyframe_para= para('...') }],
         ['Help',
          lambda { para('Click the links above to change menus. ' \
                        'Try "Examples" and then "shoes."') }]]

  if File.directory? 'examples'
    m= ['Examples']
    Dir.glob('examples/*.yaml').each do |fname|
      m << [File.basename(fname, File.extname(fname)), lambda { load(fname) }]
    end
    menu.insert(-2, m)
  end

  menubar(*menu)

  flow do
    @status_text= para('...', :font => '12px', :stroke => STATUS_TEXT_COLOR)
  end

  animate(FRAMES_PER_SEC) do
    unless @state == :animating
      check_hover unless @state == :dragging_vertex
      check_click
      check_release
    end
    case @state
    when :animating
      if @current_frame >= last_keyframe
        @state= :pointing
      else
        @current_frame += 1
      end
    when :dragging_vertex
      @hover_vertex.x= mouse.x
      @hover_vertex.y= mouse.y
    end
    set_status_text
    paint_canvas
  end

end
