# frozen_string_literal: true require 'observer' # 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. # # 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 folder names that will be rendered, from back to front. # # @example # 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(&:path).reject(&:empty?) images = Magick::ImageList.new(*image_paths) renderer.background_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 Regexp.last_match(1), *args return when /^(\w+)=$/ set_input Regexp.last_match(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 !@groups[input_name].nil? @groups[input_name].send_updates(input_value) elsif chosen_layer = @layers.find { |layer| layer.name == input_name } chosen_layer.update input_value end end end # @private 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 # @private 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 = begin 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 "#{Regexp.last_match(1)}#{Regexp.last_match(2)}" else select_default end 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 "#{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