アフィン変換の成分の変化を目で見て感じるプログラムをつくった。
スペースでリセット。
左右キーで成分選択。
上下キーでボリュームの調整。

#!/usr/bin/env ruby

require 'sdl'
require 'cairo'

class Sprite
  attr_accessor :x, :y, :alive, :enabled, :idx, :info
  attr_writer :draw, :matrix

  def initialize
    @x, @y = 0, 0
    @alive = true
    @enabled = true
    @idx = 0
    @info = {}
    @matrix = Cairo::Matrix.identity
  end

  def renew(cr)
    return if !@enabled
    cr.matrix = @matrix * Cairo::Matrix.translate(@x, @y)
    @draw[cr]
  end
end

class Phase
  attr_accessor :handles, :task

  def initialize(w, h, info=nil)
    @image = Cairo::ImageSurface.new(w, h)
    @info = info
    @sprites = []
    @handles = {}
  end

  def addSprite
    @sprites << Sprite.new
    yield(@sprites.last)
    @sprites.last
  end

  def renew(drawFlag=true)
    @task[] if @task
    if drawFlag
      cr = Cairo::Context.new(@image)
      @sprites.reject!{|obj| !obj.alive}
      @sprites.each {|obj| obj.renew(cr)}
      @image
    else
      nil
    end
  end

  def layerSort
    @sprites.sort!{|a, b| a.idx <=> b.idx}
  end
end

SDL.init(SDL::INIT_VIDEO)
SDL::WM.set_caption("Affine Test","")
w, h = 500, 500
screen = SDL::Screen.open(w, h, 32, 0)
phase = Phase.new(w, h)

element = {
  :xx => 1.0,
  :yx => 0.0,
  :xy => 0.0,
  :yy => 1.0
}
key = [:xx, :yx, :xy, :yy]
idx = 0
dv = 0

phase.addSprite do |obj|
  obj.draw = proc do |cr|
    cr.set_source_color(Cairo::Color::BLACK)
    cr.rectangle(0, 0, w, h)
    cr.fill
  end
end
phase.addSprite do |obj|
  obj.draw = proc do |cr|
    cr.set_source_color(Cairo::Color::ORANGE)
    (1..3).each do |i|
      cr.move_to(0, i * w / 4)
      cr.line_to(h, i * w / 4)
      cr.move_to(i * h / 4, 0)
      cr.line_to(i * h / 4, w)
    end
    cr.stroke
  end
end
box = phase.addSprite do |obj|
  obj.x, obj.y = w / 2, h / 2
  obj.draw = proc do |cr|
    cr.set_source_color(Cairo::Color::WHITE)
    cr.rectangle(-w / 8, -h / 8, w / 4, h / 4)
    cr.fill
    cr.set_source_color(Cairo::Color::RED)
    cr.move_to(0, -w / 8)
    cr.line_to(0, w / 8)
    cr.move_to(-h / 8, 0)
    cr.line_to(h / 8, 0)
    cr.stroke
    cr.set_source_color(Cairo::Color::BLACK)
    cr.font_size = 150
    cr.move_to(-47, 55)
    cr.show_text("R")
  end
end
star = phase.addSprite do |obj|
  obj.x, obj.y = w / 2, h / 2
  a= []
  r = 20
  tx = w / 4
  (-90..269).step(360 / 5) do |deg|
    rad= deg * Math::PI / 180
    a << [Math::cos(rad) * r + tx, Math::sin(rad) * r]
  end
  vector = []
  (a * 2).inject(true) do |b, v|
    vector << v if b
    b ^= true
  end
  obj.draw = proc do |cr|
    cr.set_source_color(Cairo::Color::GREEN)
    cr.set_line_width(3)
    flag = true
    vector.each do |x, y|
      if flag
        cr.move_to(x, y)
      else
        cr.line_to(x, y)
      end
      flag = false
    end
    cr.close_path
    cr.stroke
  end
end
font_size = 15
phase.addSprite do |obj|
  obj.x, obj.y = 5, h - 55
  obj.draw = proc do |cr|
    cr.set_source_color(Cairo::Color::WHITE)
    cr.font_size = font_size
    cr.move_to(0, font_size)
    cr.show_text("Reset : Space Key | Choice : Left or Right Key | Value : Up or Down key")
  end
end
phase.addSprite do |obj|
  obj.x, obj.y = 5, h - 35
  obj.draw = proc do |cr|
    cr.set_source_color(Cairo::Color::WHITE)
    cr.font_size = font_size
    cr.move_to(0, font_size)
    cr.show_text("Cairo::Matrix.new(")
    if idx == 0
      cr.set_source_color(Cairo::Color::RED)
    else
      cr.set_source_color(Cairo::Color::WHITE)
    end
    cr.show_text("%f, " % element[:xx])
    if idx == 1
      cr.set_source_color(Cairo::Color::RED)
    else
      cr.set_source_color(Cairo::Color::WHITE)
    end
    cr.show_text("%f, " % element[:yx])
    if idx == 2
      cr.set_source_color(Cairo::Color::RED)
    else
      cr.set_source_color(Cairo::Color::WHITE)
    end
    cr.show_text("%f, " % element[:xy])
    if idx == 3
      cr.set_source_color(Cairo::Color::RED)
    else
      cr.set_source_color(Cairo::Color::WHITE)
    end
    cr.show_text("%f, " % element[:yy])
    cr.set_source_color(Cairo::Color::WHITE)
    cr.show_text("0.0, 0.0)")
  end
end
phase.task = proc do
  element[key[idx]] += dv * 0.1
  box.matrix = star.matrix = Cairo::Matrix.new(element[:xx], element[:yx], element[:xy], element[:yy], 0, 0)#.invert
end
phase.handles[SDL::Event::KeyDown] = proc do |e|
  case e.sym
  when SDL::Key::UP
    dv += 1
  when SDL::Key::DOWN
    dv -= 1
  when SDL::Key::LEFT
    idx -= 1 if idx > 0
  when SDL::Key::RIGHT
    idx += 1 if idx < 3
  when SDL::Key::SPACE
    element = {
      :xx => 1.0,
      :yx => 0.0,
      :xy => 0.0,
      :yy => 1.0
    }
  when SDL::Key::ESCAPE
    exit
  end
end
phase.handles[SDL::Event::KeyUp] = proc do |e|
  case e.sym
  when SDL::Key::UP
    dv -= 1
  when SDL::Key::DOWN
    dv += 1
  end
end
phase.handles[SDL::Event::Quit] = proc{exit}
loop do
  while e = SDL::Event.poll
    if handle = phase.handles[e.class]
      handle[e]
    end
  end
  image = phase.renew
  surface = SDL::Surface.new_from(image.data, w, h, 32, image.stride, 0, 0, 0, 0)
  screen.put(surface,0,0)
  screen.flip
  SDL.delay(20)
end