module Chance::Instance::Spriting
The Spriting
module handles sorting slices into sprites, laying them out within the sprites, and generating the final sprite images.
Public Instance Methods
# File vendor/chance/lib/chance/instance/spriting.rb, line 285 def canvas_for_sprite(sprite) width = sprite[:width] height = sprite[:height] # If we require RMagick, we should have already loaded it, so we don't # need to worry over that at the moment. if sprite[:name] =~ /\.(gif|jpg)/ return Magick::Image.new(width, height) else return ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT) end end
Writes a slice to the target canvas, repeating it as necessary to fill the width/height.
# File vendor/chance/lib/chance/instance/spriting.rb, line 299 def compose_slice_on_canvas(target, slice, x, y, width, height) source_canvas = slice[:canvas] || slice[:file][:canvas] source_width = slice[:sprite_slice_width] source_height = slice[:sprite_slice_height] top = 0 left = 0 # Repeat the pattern to fill the width/height. while top < height do left = 0 while left < width do if target.respond_to?(:replace!) target.replace!(source_canvas, left + x, top + y) else target.composite!(source_canvas, left + x, top + y) end left += source_width end top += source_height end end
Generates the image for the specified sprite, putting it in the sprite's :image property.
# File vendor/chance/lib/chance/instance/spriting.rb, line 250 def generate_sprite(sprite, dst_path) canvas = canvas_for_sprite(sprite) sprite[:canvas] = canvas # If we are padding sprites, we should paint the background something really # obvious & obnoxious. Say, magenta. That's obnoxious. A nice light purple wouldn't # be bad, but magenta... that will stick out like a sore thumb (I hope) if @options[:pad_sprites_for_debugging] magenta = ChunkyPNG::Color.rgb(255, 0, 255) canvas.rect(0, 0, sprite[:width], sprite[:height], magenta, magenta) end sprite[:slices].each do |slice| x = slice[:sprite_slice_x] y = slice[:sprite_slice_y] width = slice[:sprite_slice_width] height = slice[:sprite_slice_height] # If it repeats, it needs to go edge-to-edge in one direction if slice[:repeat] == 'repeat-y' height = sprite[:height] end if slice[:repeat] == 'repeat-x' width = sprite[:width] end compose_slice_on_canvas(canvas, slice, x, y, width, height) end if sprite[:efficiency] != nil and sprite[:efficiency] < 50.0 # SC.logger.warn "Inefficient sprite: '#{dst_path}'.\n The content to wasted space ratio for the sprite is only #{sprite[:efficiency].round(0)}%. This sprite could be optimized by removing overly large slices within it.\n" end end
Performs the spriting process on all of the @slices, creating sprite images in the class's @sprites property and updating the individual slices with a :sprite property containing the identifier of the sprite, and offset properties for the offsets within the image.
# File vendor/chance/lib/chance/instance/spriting.rb, line 21 def generate_sprite_definitions(opts) @sprites = {} group_slices_into_sprites(opts) @sprites.each do |key, sprite| layout_slices_in_sprite sprite, opts end end
Creates a sprite definition with a given name and set of options
# File vendor/chance/lib/chance/instance/spriting.rb, line 55 def get_sprite_named(sprite_name, opts) if @sprites[sprite_name].nil? @sprites[sprite_name] = { :name => sprite_name, :slices => [], :has_generated => false, # The sprite will use horizontal layout under repeat-y, where images # must stretch all the way from the top to the bottom :use_horizontal_layout => opts[:horizontal_layout] } end end
Determines the appropriate sprite for each slice, creating it if necessary, and puts the slice into that sprite. The appropriate sprite may differ based on the slice's repeat settings, for instance.
# File vendor/chance/lib/chance/instance/spriting.rb, line 33 def group_slices_into_sprites(opts) @slices.each do |key, slice| sprite = sprite_for_slice(slice, opts) sprite[:slices] << slice @sprites[sprite[:name]] = sprite end end
Performs the layout operation, laying either up-to-down, or “ (for repeat-y slices) left-to-right.
# File vendor/chance/lib/chance/instance/spriting.rb, line 82 def layout_slices_in_sprite(sprite, opts) # The position is the position in the layout direction. In vertical mode # (the usual) it is the Y position. pos = 0 # Adds some padding that will be painted with a pattern so that it is apparent that # CSS is wrong. # NOTE: though this is only in debug mode, we DO need to make sure it is on a 2px boundary. # This makes sure 2x works properly. padding = @options[:pad_sprites_for_debugging] ? 2 : 0 # The position within a row. It starts at 0 even if we have padding, # because we always just add padding when we set the individual x/y pos. inset = 0 # The length of the row. Length, when layout out vertically (the usual), is the height row_length = 0 # The size is the current size of the sprite in the non-layout direction; # for example, in the usual, vertical mode, the size is the width. # # Usually, this is computed as a simple max of itself and the width of any # given slice. However, when repeating, the least common multiple is used, # and the smallest items are stored as well. size = 1 largest_size = nil used_area = 0 is_horizontal = sprite[:use_horizontal_layout] # Figure out slice width/heights. We cannot rely on slicing to do this for us # because some images may be being passed through as-is. sprite[:slices].each {|slice| # We must find a canvas either on the slice (if it was actually sliced), # or on the slice's file. Otherwise, we're in big shit. canvas = slice[:canvas] || slice[:file][:canvas] # TODO: MAKE A BETTER ERROR. unless canvas throw "Could not sprite image " + slice[:path] + "; if it is not a PNG, make sure you have rmagick installed" end # RMagick has a different API than ChunkyPNG; we have to detect # which one we are using, and use the correct API accordingly. if canvas.respond_to?('columns') slice_width = canvas.columns slice_height = canvas.rows else slice_width = canvas.width slice_height = canvas.height end # slice_length = is_horizontal ? slice_width : slice_height slice_size = is_horizontal ? slice_height : slice_width # When repeating, we must use the least common multiple so that # we can ensure the repeat pattern works even with multiple repeat # sizes. However, we should take into account how much extra we are # adding by tracking the largest size item as well. if slice[:repeat] != "no-repeat" size = size.lcm slice_size else size = [size, slice_size + padding * 2].max end largest_size = size if largest_size.nil? largest_size = [size, largest_size].max slice[:slice_width] = slice_width.to_i slice[:slice_height] = slice_height.to_i } # Sort slices from widest/tallest (dependent on is_horizontal) or is_vertical # NOTE: This means we are technically sorting reversed sprite[:slices].sort! {|a, b| # WHY <=> NO WORK? if is_horizontal b[:slice_height] <=> a[:slice_height] else b[:slice_width] <=> a[:slice_width] end } sprite[:slices].each do |slice| # We must find a canvas either on the slice (if it was actually sliced), # or on the slice's file. Otherwise, we're in big shit. canvas = slice[:canvas] || slice[:file][:canvas] slice_width = slice[:slice_width] slice_height = slice[:slice_height] slice_length = is_horizontal ? slice_width : slice_height slice_size = is_horizontal ? slice_height : slice_width if slice[:repeat] != "no-repeat" or inset + slice_size + padding * 2 > size or not @options[:optimize_sprites] pos += row_length inset = 0 row_length = 0 end # We have extras for manual tweaking of offsetx/y. We have to make sure there # is padding for this (on either side) # # We have to add room for the minimum offset by adding to the end, and add # room for the max by adding to the front. We only care about it in our # layout direction. Otherwise, the slices are flush to the edge, so it won't # matter. if slice[:min_offset_x] < 0 and is_horizontal slice_length -= slice[:min_offset_x] elsif slice[:min_offset_y] < 0 and not is_horizontal slice_length -= slice[:min_offset_y] end if slice[:max_offset_x] > 0 and is_horizontal pos += slice[:max_offset_x] elsif slice[:max_offset_y] > 0 and not is_horizontal pos += slice[:max_offset_y] end slice[:sprite_slice_x] = (is_horizontal ? pos : inset) slice[:sprite_slice_y] = (is_horizontal ? inset : pos) # add padding for x, only if it a) doesn't repeat or b) repeats vertically because it has horizontal layout if slice[:repeat] == "no-repeat" or slice[:repeat] == "repeat-y" slice[:sprite_slice_x] += padding end if slice[:repeat] == "no-repeat" or slice[:repeat] == "repeat-x" slice[:sprite_slice_y] += padding end slice[:sprite_slice_width] = slice_width slice[:sprite_slice_height] = slice_height inset += slice_size + padding * 2 # We pad the row length ONLY if it is a repeat-x, repeat-y, or no-repeat image. # If it is "repeat", we do not pad it, because it should be processed raw. row_length = [slice_length + (slice[:repeat] != "repeat" ? padding * 2 : 0), row_length].max # In 2X, make sure we are aligned on a 2px grid. # We correct this AFTER positioning because we always position on an even grid anyway; # we just may leave that even grid if we have an odd-sized image. We do this after positioning # so that the next loop knows if there is space. if opts[:x2] row_length = (row_length.to_f / 2).ceil * 2 inset = (inset.to_f / 2).ceil * 2 end used_area += slice_size * slice_length end pos += row_length total_area = pos * largest_size if total_area > 300000 # an arbritrarily large sprite size at which we could begin to worry about the efficiency efficiency = (used_area / total_area.to_f) * 100 sprite[:efficiency] = efficiency end sprite[:width] = is_horizontal ? pos : size sprite[:height] = is_horizontal ? size : pos end
Postprocesses the CSS, inserting sprites and defining offsets.
# File vendor/chance/lib/chance/instance/spriting.rb, line 327 def postprocess_css_sprited(opts) # The images should already be sliced appropriately, as we are # called by the css method, which calls slice_images. # We will need the position of all sprites, so generate the sprite # definitions now: generate_sprite_definitions(opts) css = @css.gsub(/_sc_chance:\s*["'](.*?)["']/) {|match| slice = @slices[$1] sprite = sprite_for_slice(slice, opts) output = "background-image: chance_file('#{sprite[:name]}')\n" if slice[:x2] width = sprite[:width] / slice[:proportion] height = sprite[:height] / slice[:proportion] output += "; -webkit-background-size: #{width}px #{height}px\n" output += "; -moz-background-size: #{width}px #{height}px\n" output += "; background-size: #{width}px #{height}px\n" end output } css.gsub!(/-chance-offset:\s?"(.*?)" (-?[0-9]+) (-?[0-9]+)/) {|match| slice = @slices[$1] slice_x = $2.to_i - slice[:sprite_slice_x] slice_y = $3.to_i - slice[:sprite_slice_y] # If it is 2x, we are scaling the slice down by 2, making all of our # positions need to be 1/2 of what they were. if slice[:x2] slice_x /= slice[:proportion] slice_y /= slice[:proportion] end "background-position: #{slice_x}px #{slice_y}px" } css end
# File vendor/chance/lib/chance/instance/spriting.rb, line 372 def sprite_data(opts, dst_path) _render slice_images(opts) generate_sprite_definitions(opts) sprite = @sprites[opts[:name]] # When the sprite is nil, it simply means there weren't any images, # so this sprite is not needed. But build systems may still # expect this file to exist. We'll just make it empty. if sprite.nil? return "" end generate_sprite(sprite, dst_path) if not sprite[:has_generated] ret = sprite[:canvas].to_blob if Chance.clear_files_immediately sprite[:canvas] = nil sprite[:has_generated] = false end ret end
Returns the sprite to use for the given slice, creating the sprite if needed. The sprite could differ based on repeat settings or file type, for instance.
# File vendor/chance/lib/chance/instance/spriting.rb, line 44 def sprite_for_slice(slice, opts) sprite_name = sprite_name_for_slice(slice, opts) get_sprite_named(sprite_name, opts.merge({ :horizontal_layout => slice[:repeat] == "repeat-y" ? true : false })) return @sprites[sprite_name] end
Determines the name of the sprite for the given slice. The sprite by this name may not exist yet.
# File vendor/chance/lib/chance/instance/spriting.rb, line 72 def sprite_name_for_slice(slice, opts) if slice[:repeat] == "repeat" return slice[:path][0..(-1 - File.extname(slice[:path]).length)] + (opts[:x2] ? "@2x" : "") + File.extname(slice[:path]) end return slice[:repeat] + (opts[:x2] ? "@2x" : "") + File.extname(slice[:path]) end
# File vendor/chance/lib/chance/instance/spriting.rb, line 399 def sprite_names(opts={}) _render slice_images(opts) generate_sprite_definitions(opts) @sprites.keys end