File: scene.rb

package info (click to toggle)
ruby-gruff 0.6.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, sid
  • size: 852 kB
  • sloc: ruby: 4,929; makefile: 3
file content (209 lines) | stat: -rw-r--r-- 5,643 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209

require "observer"
require File.dirname(__FILE__) + '/base'

##
# A scene is a non-linear graph that assembles layers together to tell a story.
# Layers are folders with appropriately named files (see below). You can group 
# layers and control them together or just set their values individually.
#
# Examples:
#
# * A city scene that changes with the time of day and the weather conditions.
# * A traffic map that shows red lines on streets that are crowded and green on free-flowing ones.
#
# Usage:
# 
#  g = Gruff::Scene.new("500x100", "path/to/city_scene_directory")
#
#  # Define order of layers, back to front
#  g.layers = %w(background haze sky clouds)
#
#  # Define groups that will be controlled by the same input
#  g.weather_group = %w(clouds)
#  g.time_group = %w(background sky)
#
#  # Set values for the layers or groups
#  g.weather = "cloudy"
#  g.time = Time.now
#  g.haze = true
#
#  # Write the final graph to disk
#  g.write "hazy_daytime_city_scene.png"
#
#
# There are several rules that will magically select a layer when possible.
#
# * Numbered files will be selected according to the closest value that is less than the input value.
# * 'true.png' and 'false.png' will be used as booleans.
# * Other named files will be used if the input matches the filename (without the filetype extension).
# * If there is a file named 'default.png', it will be used unless other input values are set for the corresponding layer.

class Gruff::Scene < Gruff::Base
    
  # An array listing the foldernames that will be rendered, from back to front.
  #
  #  g.layers = %w(sky clouds buildings street people)
  #
  attr_reader :layers

  def initialize(target_width, base_dir)
    @base_dir = base_dir
    @groups = {}
    @layers = []    
    super target_width
  end

  def draw
    # Join all the custom paths and filter out the empty ones
    image_paths = @layers.map { |layer| layer.path }.select { |path| !path.empty? }
    images = Magick::ImageList.new(*image_paths)
    @base_image = images.flatten_images
  end

  def layers=(ordered_list)
    ordered_list.each do |layer_name|
      @layers << Gruff::Layer.new(@base_dir, layer_name)
    end
  end

  # Group layers to input values
  #
  #  g.weather_group = ["sky", "sea", "clouds"]
  #
  # Set input values
  #
  #  g.weather = "cloudy"
  #
  def method_missing(method_name, *args)
    case method_name.to_s
    when /^(\w+)_group=$/
      add_group $1, *args
      return
    when /^(\w+)=$/
      set_input $1, args.first
      return
    end
    super
  end

private

  def add_group(input_name, layer_names)
    @groups[input_name] = Gruff::Group.new(input_name, @layers.select { |layer| layer_names.include?(layer.name) })
  end

  def set_input(input_name, input_value)
    if not @groups[input_name].nil?
      @groups[input_name].send_updates(input_value)
    else
      if chosen_layer = @layers.detect { |layer| layer.name == input_name }
        chosen_layer.update input_value
      end
    end
  end
  
end


class Gruff::Group

  include Observable
  attr_reader :name

  def initialize(folder_name, layers)
    @name = folder_name
    layers.each do |layer|
      layer.observe self
    end
  end
  
  def send_updates(value)
    changed
    notify_observers value
  end
  
end


class Gruff::Layer
  
  attr_reader :name
  
  def initialize(base_dir, folder_name)
    @base_dir = base_dir.to_s
    @name = folder_name.to_s
    @filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }.sort
    @selected_filename = select_default
  end
  
  # Register this layer so it receives updates from the group
  def observe(obj)
    obj.add_observer self
  end
  
  # Choose the appropriate filename for this layer, based on the input
  def update(value)
    @selected_filename =  case value.to_s
                          when /^(true|false)$/
                            select_boolean value
                          when /^(\w|\s)+$/
                            select_string value
                          when /^-?(\d+\.)?\d+$/
                            select_numeric value
                          when /(\d\d):(\d\d):\d\d/
                            select_time "#{$1}#{$2}"
                          else
                            select_default
                          end
    # Finally, try to use 'default' if we're still blank
    @selected_filename ||= select_default
  end

  # Returns the full path to the selected image, or a blank string
  def path
    unless @selected_filename.nil? || @selected_filename.empty?
      return File.join(@base_dir, @name, @selected_filename)
    end
    ''
  end

private

  # Match "true.png" or "false.png"
  def select_boolean(value)
    file_exists_or_blank value.to_s
  end

  # Match -5 to _5.png
  def select_numeric(value)
    file_exists_or_blank value.to_s.gsub('-', '_')
  end
  
  def select_time(value)
    times = @filenames.map { |filename| filename.gsub('.png', '') }
    times.each_with_index do |time, index|
      if (time > value) && (index > 0)
        return "#{times[index - 1]}.png"
      end
    end
    return "#{times.last}.png"
  end
  
  # Match "partly cloudy" to "partly_cloudy.png"
  def select_string(value)
    file_exists_or_blank value.to_s.gsub(' ', '_')
  end
  
  def select_default
    @filenames.include?("default.png") ? "default.png" : ''
  end

  # Returns the string "#{filename}.png", if it exists.
  #
  # Failing that, it returns default.png, or '' if that doesn't exist.
  def file_exists_or_blank(filename)
    @filenames.include?("#{filename}.png") ? "#{filename}.png" : select_default
  end
  
end