[SOLUTION] The Golden Fibonacci Ratio (#69)

# Author: Shane Emmons

···

#
# Quiz 69: The Golden Fibonacci Ratio.
#
# I decided for this quiz to try and use the curses library to make
# screen placement easier. I think it turned out well, though I am sure
# there are more clever ways of achieving the same results. Press the
# 'enter' key to advance through each successive iteration of the
# algorithm. Five is the optimal number of times to iterate for
standard
# consoles.
#
# usage: ruby quiz69.rb [optional: number of times to add sqr]

require 'curses'

class Square

    attr_reader :height, :width
    attr_writer :height, :width

    def initialize( screen, length = 1, width = 1 )
        @screen, @height, @width = screen, length, width
    end

    def max_side
        @height >= @width ? @height : @width
    end

    def add_to_screen
        @screen.setpos( 0, 0 )
        ( 0 .. @width + 1 ).each { @screen.addstr( '#' ) }
        ( 1 .. @height ).each do |height|
            @screen.setpos( height, 0 )
            @screen.addstr( '#' )
            @screen.setpos( height, @width + 1 )
            @screen.addstr( '#' )
        end
        @screen.setpos( @height + 1, 0 )
        ( 0 .. @width + 1 ).each { @screen.addstr( '#' ) }
        @screen.getch
    end

end

add_to = :right
sqr = Square.new( Curses::init_screen )
sqr.add_to_screen

times = ARGV[ 0 ] || 5
( 1 .. times.to_i ).each do |x|

    if add_to == :right
        sqr.width += sqr.max_side + 1
    else
        sqr.height += sqr.max_side + 1
    end

    add_to = add_to == :right ? :bottom : :right
    sqr.add_to_screen
    
end

I've never done OpenGL before. So after looking a some examples here's
what I came up with. I did this pretty quick so there are many things
that ought to be fixed (like the mess of global variables), but it gets
the job done. Like the curses submission, pressing enter repeatedly
will add the next iteration. I don't have automatic resizing yet so
after you press enter, to get it to look right you have to resize the
window. Maybe I'll think about that a bit later (suggestions are
welcome on how resizing should be done). Anyways thanks for the quiz!

-----Jay Anderson

require 'opengl'
require 'glut'

class Array
    def rotate!
        push shift
    end
    def rotate
        self.dup.rotate!
    end
end

key_func = lambda do |key,x,y|
    case key
    when ?Q, ?q
        exit 0
    when ?\r, ?\n
        side = $width>$height ? $width : $height
        sq_x, sq_y = $x, $y
        case $add_to.first
        when :right
            sq_x = $x + $width
            $width += side
        when :left
            sq_x = $x - side
            $x = sq_x
            $width += side
        when :top
            sq_y = $y + $height
            $height += side
        when :bottom
            sq_y = $y - side
            $y = sq_y
            $height += side
        end
        $squares << {
            :side => side,
            :x => sq_x,
            :y => sq_y,
            :color => [rand, rand, rand]
        }

        $add_to.rotate!
    end
    GLUT.PostRedisplay
end

reshape_func = lambda do |w,h|
    $screen_width = w
    $screen_height = h
    range = ($width>$height ? $width : $height)
    border = range * 0.05
    left, right = $x-border, $x+range+border
    bottom, top = $y-border, $y+range+border
    aspect = w.to_f/h.to_f
    if w <= h then
        bottom /= aspect
        top /= aspect
    else
        left *= aspect
        right *= aspect
    end
    GL.Viewport(0, 0, w, h)
    GL.MatrixMode GL::PROJECTION
    GL.LoadIdentity
    GLU::Ortho2D(left, right, bottom, top)
    GL.MatrixMode GL::MODELVIEW
end

display_func = lambda do
    GL.Clear GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT

    $squares.each do |s|
        GL.Translate(s[:x], s[:y], 0.0)
        side = s[:side]
        GL.Begin(GL::QUADS)
        GL.Color(*s[:color])
        GL.Vertex(0.0, 0.0)
        GL.Vertex(side, 0.0)
        GL.Vertex(side, side)
        GL.Vertex(0.0, side)
        GL.End
        GL.Translate(-s[:x], -s[:y], 0.0)
    end

    GL.Flush
end

GLUT.Init
GLUT.InitDisplayMode GLUT::SINGLE | GLUT::RGB | GLUT::DEPTH
GLUT.CreateWindow "Fibonacci"
GL.FrontFace GL::CW
GL.Enable(GL::DEPTH_TEST)

$screen_width = GLUT.Get(GLUT::WINDOW_WIDTH)
$screen_height = GLUT.Get(GLUT::WINDOW_HEIGHT)
$squares = []
$squares << {
    :side => 1.0,
    :x => 0.0,
    :y => 0.0,
    :color => [rand, rand, rand]
}
$width = 1.0
$height = 1.0
$x = 0.0
$y = 0.0
$add_to = [:right, :bottom, :left, :top]

GLUT.KeyboardFunc key_func
GLUT.ReshapeFunc reshape_func
GLUT.DisplayFunc display_func

GLUT.MainLoop

Here's my attempt at the quiz. Instead of producing a text output I
tried myself at SVG. The first version I came up with didn't use any
special SVG commands and takes care of finding out the positions for
each square by itself. After seeing the Postscript solution I looked in
the SVG specs for something similar.. the result is the second version,
which seems a bit more unclear for human beings but makes the algorithm
much easier (no more "case @side")

So here they are..

First version with explicit placement of squares:

require 'rubygems'
require 'svg/svg'
require 'memoize'

module GoldenRectangle
        COLORS = ['red', 'green', 'blue', 'yellow']
        SEED_POS = {:x => 0, :y => 0}

        class PreSeedSquare
                def height
                        0
                end

                def x
                        SEED_POS[:x]
                end

                def y
                        SEED_POS[:y]
                end

                def color
                        COLORS[0]
                end

                def parent
                        @parent
                end
        end

        class SeedSquare < PreSeedSquare
                def initialize
                        @parent = PreSeedSquare.new
                end

                def fib(n)
                        a, b = 1, 1
                        n.times { a, b = b, a + b }
                        a
                end

                def size
                        fib(level)
                end

                def level
                        0
                end

                def width
                        size
                end

                alias_method :height, :width

                def side
                        :top
                end

                def color
                        COLORS[1]
                end

                def as_spiral
                        ["M#{x},#{y + height}", "A#{width},#{height} 0
0,1 #{x + width},#{y}"]
                end

                def as_squares
                        [svg_square]
                end

                def svg_square
                        col = self.color
                        SVG::Rect.new(x, y, width, height) { self.style
= SVG::Style.new(:fill => col, :opacity => 0.5) }
                end
        end

        class Square < SeedSquare
                include Memoize

                attr_reader :parent

                def initialize(parent)
                        @parent = parent
                        @grandparent = @parent.parent

                        # without memoization this implementation
wouldn't be feasible
                        [:parent, :color, :size, :side, :x, :y].each
{|f| memoize f }
                end

                class << self
                        alias_method :attach_to, :new
                end

                def as_squares
                        @parent.as_squares << svg_square
                end

                def as_spiral
                        @parent.as_spiral << "A#{width},#{height} 0 0,1
" + spiral_point.join(",")
                end

                def color
                        if @grandparent.parent.nil?
                                COLORS[2]
                        elsif
                                @grandparent.parent.parent.nil?
                                COLORS[3]
                        else
                                (COLORS -
[@parent.parent.parent.parent.color, @parent.parent.parent.color,
@parent.color]).first
                        end
                end

                def level
                        @parent.level + 1
                end

                def side
                        case @parent.side
                        when :right
                                :bottom
                        when :bottom
                                :left
                        when :left
                                :top
                        when :top
                                :right
                        end
                end

                def x
                        case side
                        when :right
                                @parent.x + @parent.width
                        when :bottom
                                @grandparent.x
                        when :left
                                @parent.x - width
                        when :top
                                @parent.x
                        end
                end

                def y
                        case side
                        when :left
                                @grandparent.y
                        when :bottom
                                @parent.y + @parent.height
                        when :top
                                @parent.y - height
                        when :right
                                @parent.y
                        end
                end

                def spiral_point
                        case side
                        when :right
                                [x + width, y + height]
                        when :left
                                [x, y]
                        when :top
                                [x + width, y]
                        when :bottom
                                [x, y + height]
                        end
                end
        end
end

def fixpoint(start, limit, &blk)
        limit.times do
                start = blk.call(start)
        end
        start
end

iterations = ARGV.shift || 17

# the golden rectangle is the fixpoint of the function that attaches a
square
# to a golden-rectangle-approximation
# we start with a 1x1 seed
the_golden_rectangle = fixpoint(GoldenRectangle::SeedSquare.new,
iterations) do |rectangle|
        GoldenRectangle::Square.attach_to rectangle
end

svg = SVG.new('1024', '768', '-550 -300 1024 768')
svg << the_golden_rectangle.as_squares
svg << SVG::Path.new(the_golden_rectangle.as_spiral) {
        self.style = SVG::Style.new(:fill => 'none', :stroke => '#000',
:stroke_width => 1, :stroke_opacity => 1.0)
}
puts svg.to_s

···

###############
Second solution involving rotate & translate

require 'rubygems'
require 'svg/svg'
require 'memoize'

module GoldenRectangle
        COLORS = ['red', 'green', 'blue', 'yellow']
        SEED_POS = {:x => 0, :y => 0}

        class PreSeedSquare
                def height
                        0
                end

                def x
                        SEED_POS[:x]
                end

                def y
                        SEED_POS[:y]
                end

                def color
                        COLORS[0]
                end

                def parent
                        @parent
                end
        end

        class SeedSquare < PreSeedSquare
                def initialize
                        @parent = PreSeedSquare.new
                end

                def fibo(n)
                        a, b = 1, 1
                        n.times { a, b = b, a + b}
                        a
                end

                def size
                        fibo(level)
                end

                def width
                        size
                end

                def level
                        0
                end

                alias_method :height, :width

                def color
                        COLORS[1]
                end

                def as_spiral
                        ["M#{x},#{y + height}", "A#{width},#{height} 0
0,1 #{x + width},#{y}"]
                end

                def as_squares
                        svg_square
                end

                def svg_square
                        col = self.color
                        group = SVG::Group.new
                        group.transform = "rotate(90)
translate(#{-width},#{-height})"
                        group.id = level
                        group << SVG::Rect.new(0, 0, width, height) {
self.style = SVG::Style.new(:fill => col, :opacity => 0.5) }

                        path = ["M#{0},#{0}", "A#{width},#{height} 0
0,0 #{width},#{height}"]
                        group << SVG::Path.new(path) {
                                self.style = SVG::Style.new(:fill =>
'none', :stroke => '#000', :stroke_width => 1, :stroke_opacity => 1.0)
                        }
                        group
                end
        end

        class Square < SeedSquare
                include Memoize

                attr_reader :parent

                def initialize(parent)
                        @parent = parent
                        @grandparent = @parent.parent

                        # without memoization this implementation
wouldn't be feasible
                        [:level, :parent, :color, :size].each {|f|
memoize f }
                end

                class << self
                        alias_method :attach_to, :new
                end

                def as_squares
                        g1 = @parent.as_squares
                        g2 = svg_square
                        g2 << g1
                        g2
                end

                def color
                        if @grandparent.parent.nil?
                                COLORS[2]
                        elsif
                                @grandparent.parent.parent.nil?
                                COLORS[3]
                        else
                                (COLORS -
[@parent.parent.parent.parent.color, @parent.parent.parent.color,
@parent.color]).first
                        end
                end

                def level
                        @parent.level + 1
                end
        end
end

def fixpoint(start, limit, &blk)
        limit.times do
                start = blk.call(start)
        end
        start
end

iterations = ARGV.shift || 17

# the golden rectangle is the fixpoint of the function that attaches a
square
# to a golden-rectangle-approximation
# we start with a 1x1 seed
the_golden_rectangle = fixpoint(GoldenRectangle::SeedSquare.new,
iterations) do |rectangle|
        GoldenRectangle::Square.attach_to rectangle
end

svg = SVG.new('1024', '768', '2500 -1100 1024 768')
svg << the_golden_rectangle.as_squares
svg << SVG::Path.new(the_golden_rectangle.as_spiral) {
        self.style = SVG::Style.new(:fill => 'none', :stroke => '#000',
:stroke_width => 1, :stroke_opacity => 1.0)
}
puts svg.to_s

I fixed a few things and added mouse callbacks (click drag for zoom,
shift-click-drag for panning). I'd still like to figure out how to make
it more OO, but at least my understanding of OpenGL is better.

-----Jay Anderson

usr/bin/ruby

require 'opengl'
require 'glut'

class Array
  def rotate!
    push shift
  end
  def rotate
    self.dup.rotate!
  end
end

mouse_func = lambda do |button,state,x,y|
  shift_down = (GLUT.GetModifiers & GLUT::ACTIVE_SHIFT) != 0
  case button
  when GLUT::LEFT_BUTTON
    case state
    when GLUT::UP
      $mode = :none
    when GLUT::DOWN
      if shift_down then
        $mode = :pan
      else
        $mode = :zoom
      end
      $mouse_x = x
      $mouse_y = y
    end
  when 3 #scroll up
    if state == GLUT::DOWN then
      $zoom *= 1.25
      GLUT.PostRedisplay
    end
  when 4 #scroll down
    if state == GLUT::DOWN then
      $zoom /= 1.25
      GLUT.PostRedisplay
    end
  end
end

motion_func = lambda do |x,y|
  case $mode
  when :zoom
    $zoom *= 1.0 + (y-$mouse_y)/100.0
  when :pan
    $pan_x += (x-$mouse_x)*$units_per_pixel
    $pan_y += ($mouse_y-y)*$units_per_pixel
  when :rotate
  end
  if $mode != :none then
    $mouse_x = x
    $mouse_y = y
    GLUT.PostRedisplay
  end
end

key_func = lambda do |key,x,y|
  case key
  when ?Q, ?q
    GLUT.DestroyWindow($window);
    exit 0
  when ?-
    $zoom /= 2.0
  when ?+
    $zoom *= 2.0
  when ?\r, ?\n
    side = $width>$height ? $width : $height
    sq_x, sq_y = $box_x, $box_y
    case $add_to.first
    when :right
      sq_x = $box_x + $width
      $width += side
    when :left
      sq_x = $box_x - side
      $box_x = sq_x
      $width += side
    when :top
      sq_y = $box_y + $height
      $height += side
    when :bottom
      sq_y = $box_y - side
      $box_y = sq_y
      $height += side
    end
    $squares << {
      :side => side,
      :x => sq_x,
      :y => sq_y,
      :color => [rand, rand, rand]
    }

    $add_to.rotate!
  end
  GLUT.PostRedisplay
end

reshape_func = lambda do |w,h|
  h = 1 if h == 0
  $screen_width = w
  $screen_height = h
  GL.Viewport(0, 0, w, h)
  GL.MatrixMode GL::PROJECTION
  GL.LoadIdentity
  GLU.Perspective(45.0, w.to_f/h.to_f, 0.1, 100.0);
  GL.MatrixMode GL::MODELVIEW
end

display_func = lambda do
  GL.Clear GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT

  GL.LoadIdentity
  GL.Translate($pan_x, $pan_y, -6.0)
  GL.Scale($zoom, $zoom, 0.0)
  $squares.each do |s|
    GL.Translate(s[:x], s[:y], 0.0)
    side = s[:side]
    GL.Begin(GL::QUADS)
    GL.Color(*s[:color])
    GL.Vertex(0.0, 0.0)
    GL.Vertex(side, 0.0)
    GL.Vertex(side, side)
    GL.Vertex(0.0, side)
    GL.End
    GL.Translate(-s[:x], -s[:y], 0.0)
  end

  GLUT.SwapBuffers;
end

GLUT.Init
GLUT.InitDisplayMode(GLUT::DOUBLE | GLUT::RGBA | GLUT::DEPTH)

$screen_width = 640
$screen_height = 480
$units_per_pixel = 1.0/100.0 #TODO: should be determined by screen size
GLUT.InitWindowSize($screen_width, $screen_height)
GLUT.InitWindowPosition(0, 0)

$window = GLUT.CreateWindow "Fibonacci"

GLUT.KeyboardFunc key_func
GLUT.ReshapeFunc reshape_func
GLUT.DisplayFunc display_func
GLUT.MotionFunc motion_func
GLUT.MouseFunc mouse_func
#GLUT.IdleFunc display_func

GL.ClearColor(0.0, 0.0, 0.0, 0.0)
GL.ClearDepth(1.0)
GL.DepthFunc(GL::LESS)
GL.Enable(GL::DEPTH_TEST)
GL.ShadeModel(GL::SMOOTH)

$squares = []
$squares << {
  :side => 1.0,
  :x => 0.0,
  :y => 0.0,
  :color => [rand, rand, rand]
}
$width = 1.0
$height = 1.0
$box_x = 0.0
$box_y = 0.0
$pan_x = 0.0
$pan_y = 0.0
$zoom = 1.0
$mode = :none
$add_to = [:right, :bottom, :left, :top]

GLUT.MainLoop