Change the sprite utilities to create a sprite literal object that can retain information about the sprite for use in other function calls. This avoids the need for global storage and allows multiple sprites constructed from the same sprite folder with different values.

This commit is contained in:
Chris Eppstein 2010-11-30 16:55:53 -08:00
parent 0354dd92a5
commit 5567e7b13f
4 changed files with 330 additions and 230 deletions

View File

@ -0,0 +1,29 @@
// Set the width and height of an element to the original
// dimensions of an image before it was included in the sprite.
@mixin sprite-dimensions($sprite, $name) {
height: image-height(sprite-file($sprite, $name));
width: image-width(sprite-file($sprite, $name));
}
// Set the background position of the given `$sprite` to display the
// image of the given `$name`. You can move the image relative to its
// natural position by passing `$offset-x` and `$offset-y`.
@mixin sprite-position($sprite, $name, $offset-x: 0, $offset-y: 0) {
background-position: sprite-position($sprite, $name, $offset-x, $offset-y);
}
@mixin sprite($sprite, $name, $dimensions: false, $offset-x: 0, $offset-y: 0) {
@include sprite-position($sprite, $name, $offset-x, $offset-y);
@if $dimensions {
@include sprite-dimensions($sprite, $name);
}
}
@mixin sprites($sprite, $sprite-names, $base-class: false, $dimensions: false) {
@each $sprite-name in $sprite-names {
.#{sprite-name($sprite)}-#{$sprite-name} {
@if $base-class { @extend #{$base-class}; }
@include sprite($sprite, $sprite-name, $dimensions);
}
}
}

View File

@ -1,7 +1,7 @@
require 'chunky_png' require 'chunky_png'
module Compass::SassExtensions::Functions::Sprites module Compass::SassExtensions::Functions::Sprites
SASS_NULL = Sass::Script::Number::new(0) ZERO = Sass::Script::Number::new(0)
# Provides a consistent interface for getting a variable in ruby # Provides a consistent interface for getting a variable in ruby
# from a keyword argument hash that accounts for underscores/dash equivalence # from a keyword argument hash that accounts for underscores/dash equivalence
@ -12,111 +12,253 @@ module Compass::SassExtensions::Functions::Sprites
end end
end end
def generate_sprite_image(uri, kwargs = {}) class Sprite < Sass::Script::Literal
kwargs.extend VariableReader
attr_accessor :image_names, :path, :name, :options
attr_accessor :images, :width, :height
def self.from_uri(uri, context, kwargs)
path, name = Compass::Sprites.path_and_name(uri.value) path, name = Compass::Sprites.path_and_name(uri.value)
last_spacing = 0 new(Compass::Sprites.discover_sprites(uri.value), path, name, context, kwargs)
width = 0
height = 0
# Get image metadata
Compass::Sprites.discover_sprites(uri.value).each do |file|
Compass::Sprites.compute_image_metadata! file, path, name
end end
images = Compass::Sprites.sprites(path, name) def initialize(image_names, path, name, context, options)
@image_names, @path, @name, @options = image_names, path, name, options
# Calculation @images = nil
images.each do |image| @width = nil
current_spacing = number_from_var(kwargs, "#{image[:name]}-spacing", 0) @height = nil
if height > 0 @evaluation_context = context
height += [current_spacing, last_spacing].max validate!
end compute_image_metadata!
image[:y] = height
height += image[:height]
last_spacing = current_spacing
width = image[:width] if image[:width] > width
end end
# Generation def sprite_names
output_png = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT) image_names.map{|f| Compass::Sprites.sprite_name(f) }
images.each do |image| end
input_png = ChunkyPNG::Image.from_file(image[:file])
position = kwargs.get_var("#{image[:name]}-position") || Sass::Script::Number.new(0, ["%"]) def validate!
if position.unit_str == "%" for sprite_name in sprite_names
image[:x] = (width - image[:width]) * (position.value / 100) unless sprite_name =~ /\A#{Sass::SCSS::RX::IDENT}\Z/
raise Sass::SyntaxError, "#{sprite_name} must be a legal css identifier"
end
end
end
# Calculates the overal image dimensions
# collects image sizes and input parameters for each sprite
def compute_image_metadata!
@images = []
@width = 0
image_names.each do |file|
relative_file = file.gsub(Compass.configuration.images_path+"/", "")
width, height = Compass::SassExtensions::Functions::ImageSize::ImageProperties.new(file).size
sprite_name = Compass::Sprites.sprite_name(relative_file)
@width = [@width, width].max
@images << {
:name => sprite_name,
:file => file,
:relative_file => relative_file,
:height => height,
:width => width,
:repeat => repeat_for(sprite_name),
:spacing => spacing_for(sprite_name),
:position => position_for(sprite_name)
}
end
@images.each_with_index do |image, index|
if index == 0
image[:top] = 0
else else
image[:x] = position.value last_image = @images[index-1]
image[:top] = last_image[:top] + last_image[:height] + [image[:spacing], last_image[:spacing]].max
end
if image[:position].unit_str == "%"
image[:left] = (@width - image[:width]) * (image[:position].value / 100)
else
image[:left] = image[:position].value
end
end
@height = @images.last[:top] + @images.last[:height]
end end
repeat = if (var = kwargs.get_var("#{image[:name]}-repeat")) def position_for(name)
options.get_var("#{name}-position") || options.get_var("position") || Sass::Script::Number.new(0, ["px"])
end
def repeat_for(name)
if (var = options.get_var("#{name}-repeat"))
var.value
elsif (var = options.get_var("repeat"))
var.value var.value
else else
"no-repeat" "no-repeat"
end end
if repeat == "no-repeat" end
output_png.replace input_png, image[:x], image[:y]
def spacing_for(name)
(options.get_var("#{name}-spacing") ||
options.get_var("spacing") ||
ZERO).value
end
def image_for(name)
@images.detect{|img| img[:name] == name}
end
# Calculate the size of the sprite
def size
[width, height]
end
# Generate a sprite image if necessary
def generate
if generation_required?
save!(construct_sprite)
end
end
def generation_required?
!File.exists?(filename) || outdated?
end
# Returns a PNG object
def construct_sprite
output_png = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)
images.each do |image|
input_png = ChunkyPNG::Image.from_file(image[:file])
if image[:repeat] == "no-repeat"
output_png.replace input_png, image[:left], image[:top]
else else
x = image[:x] - (image[:x] / image[:width]).ceil * image[:width] x = image[:left] - (image[:left] / image[:width]).ceil * image[:width]
while x < width do while x < width do
output_png.replace input_png, x, image[:y] output_png.replace input_png, x, image[:top]
x += image[:width] x += image[:width]
end end
end end
end end
output_png.save File.join(File.join(Compass.configuration.images_path, "#{path}.png")) output_png
sprite_url(uri)
end
Sass::Script::Functions.declare :generate_sprite_image, [:uri], :var_kwargs => true
def sprite_image(uri, x_shift = SASS_NULL, y_shift = SASS_NULL, depricated_1 = nil, depricated_2 = nil)
check_spacing_deprecation uri, depricated_1, depricated_2
url = sprite_url(uri)
position = sprite_position(uri, x_shift, y_shift)
Sass::Script::String.new("#{url} #{position}")
end end
def sprite_url(uri) # The on-the-disk filename of the sprite
path, name = Compass::Sprites.path_and_name(uri.value) def filename
image_url(Sass::Script::String.new("#{path}.png")) File.join(File.join(Compass.configuration.images_path, "#{path}.png"))
end end
def sprite_position(uri, x_shift = SASS_NULL, y_shift = SASS_NULL, depricated_1 = nil, depricated_2 = nil) # saves the sprite for later retrieval
check_spacing_deprecation uri, depricated_1, depricated_2 def save!(output_png)
path, name = Compass::Sprites.path_and_name(uri.value) output_png.save filename
image_name = File.basename(uri.value, '.png') end
image = Compass::Sprites.sprites(path, name).detect{ |image| image[:name] == image_name }
# All the full-path filenames involved in this sprite
def image_filenames
image_names.map do |image_name|
File.join(File.join(Compass.configuration.images_path, image_name))
end
end
# Checks whether this sprite is outdated
def outdated?
last_update = self.mtime
image_filenames.each do |image|
return true if File.mtime(image) > last_update
end
false
end
def mtime
File.mtime(filename)
end
def inspect
to_s
end
def to_s(options = self.options)
sprite_url(self).value
end
def method_missing(meth, *args, &block)
if @evaluation_context.respond_to?(meth)
@evaluation_context.send(meth, *args, &block)
else
super
end
end
end
def sprite(uri, kwargs = {})
kwargs.extend VariableReader
Sprite.from_uri(uri, self, kwargs)
end
Sass::Script::Functions.declare :sprite, [:uri], :var_kwargs => true
def sprite_image(sprite, image = nil, x_shift = ZERO, y_shift = ZERO)
unless sprite.is_a?(Sprite)
missing_sprite!("sprite-image")
end
unless image && image.is_a?(Sass::Script::String)
raise Sass::SyntaxError, %Q(The second argument to sprite-image must be a sprite name. See http://beta.compass-style.org/help/tutorials/spriting/ for more information.)
end
url = sprite_url(sprite)
position = sprite_position(sprite, image, x_shift, y_shift)
Sass::Script::List.new([url, position], :space)
end
def sprite_name(sprite)
unless sprite.is_a?(Sprite)
missing_sprite!("sprite-name")
end
Sass::Script::String.new(sprite.name)
end
def sprite_file(sprite, image_name)
unless sprite.is_a?(Sprite)
missing_sprite!("sprite-file")
end
if image = sprite.image_for(image_name.value)
Sass::Script::String.new(image[:relative_file])
else
missing_image!(sprite, image_name)
end
end
def sprite_url(sprite)
unless sprite.is_a?(Sprite)
missing_sprite!("sprite-url")
end
sprite.generate
image_url(Sass::Script::String.new("#{sprite.path}.png"))
end
def missing_image!(sprite, image_name)
raise Sass::SyntaxError, "No image called #{image_name} found in sprite #{sprite.path}/#{sprite.name}. Did you mean one of: #{sprite.sprite_names.join(", ")}"
end
def missing_sprite!(function_name)
raise Sass::SyntaxError, %Q(The first argument to #{function_name} must be a sprite. See http://beta.compass-style.org/help/tutorials/spriting/ for more information.)
end
def sprite_position(sprite, image_name = nil, x_shift = ZERO, y_shift = ZERO)
unless sprite.is_a?(Sprite)
missing_sprite!("sprite-position")
end
unless image_name && image_name.is_a?(Sass::Script::String)
raise Sass::SyntaxError, %Q(The second argument to sprite-image must be a sprite name. See http://beta.compass-style.org/help/tutorials/spriting/ for more information.)
end
image = sprite.image_for(image_name.value)
unless image
missing_image!(sprite, image_name)
end
if x_shift.unit_str == "%" if x_shift.unit_str == "%"
x = x_shift.to_s x = x_shift # CE: Shouldn't this be a percentage of the total width?
else else
x = x_shift.value - image[:x] x = x_shift.value - image[:left]
x = "#{x}px" unless x == 0 x = Sass::Script::Number.new(x, x == 0 ? [] : ["px"])
end end
y = y_shift.value - image[:y] y = y_shift.value - image[:top]
y = "#{y}px" unless y == 0 y = Sass::Script::Number.new(y, y == 0 ? [] : ["px"])
Sass::Script::String.new("#{x} #{y}") Sass::Script::List.new([x, y],:space)
end end
private
def number_from_var(kwargs, var_name, default_value)
if number = kwargs.get_var(var_name)
assert_type number, :Number
number.value
else
default_value
end
end
def check_spacing_deprecation(uri, spacing_before, spacing_after)
if spacing_before or spacing_after
path, name, image_name = Compass::Sprites.path_and_name(uri.value)
message = %Q(Spacing parameter is deprecated. ) +
%Q(Please add `$#{name}-#{image_name}-spacing: #{spacing_before};` ) +
%Q(before the `@import "#{path}/*.png";` statement.)
raise Compass::Error, message
end
end
end end

View File

@ -4,66 +4,41 @@ module Compass
attr_accessor :path attr_accessor :path
class << self class << self
def reset
@@sprites = {}
end
def path_and_name(uri) def path_and_name(uri)
if uri =~ %r{((.+/)?(.+))/(.+?)\.png} if uri =~ %r{((.+/)?(.+))/(.+?)\.png}
[$1, $3, $4] [$1, $3, $4]
end end
end end
def sprites(path, name, create = false)
if !defined?(@@sprites) || @@sprites.nil?
@@sprites = {}
end
index = "#{path}/#{name}"
images = @@sprites[index]
if images
images
elsif create
images = @@sprites[index] = []
else
raise Compass::Error, %Q(`@import` statement missing. Please add `@import "#{path}/*.png";`.)
end
end
def discover_sprites(uri) def discover_sprites(uri)
glob = File.join(Compass.configuration.images_path, uri) glob = File.join(Compass.configuration.images_path, uri)
Dir.glob(glob).sort Dir.glob(glob).sort
end end
def compute_image_metadata!(file, path, name) def sprite_name(file)
width, height = Compass::SassExtensions::Functions::ImageSize::ImageProperties.new(file).size File.basename(file, '.png')
sprite_name = File.basename(file, '.png');
unless sprite_name =~ /\A#{Sass::SCSS::RX::IDENT}\Z/
raise Sass::SyntaxError, "#{sprite_name} must be a legal css identifier"
end
sprites(path, name, true) << {
:name => sprite_name,
:file => file,
:height => height,
:width => width
}
end
end end
def images end
Compass::Sprites.sprites(self.path, self.name, true)
def find_relative(*args)
nil
end end
def find(uri, options) def find(uri, options)
if uri =~ /\.png$/ if uri =~ /\.png$/
self.path, self.name = Compass::Sprites.path_and_name(uri) self.path, self.name = Compass::Sprites.path_and_name(uri)
options.merge! :filename => name, :syntax => :scss, :importer => self options.merge! :filename => name, :syntax => :scss, :importer => self
image_names = Compass::Sprites.discover_sprites(uri).map{|i| File.basename(i, '.png')} sprite_files = Compass::Sprites.discover_sprites(uri)
image_names = sprite_files.map {|i| Compass::Sprites.sprite_name(i) }
Sass::Engine.new(content_for_images(uri, name, image_names), options) Sass::Engine.new(content_for_images(uri, name, image_names), options)
end end
end end
def content_for_images(uri, name, images) def content_for_images(uri, name, images)
<<-SCSS <<-SCSS
@import "compass/utilities/sprites/base";
// General Sprite Defaults // General Sprite Defaults
// You can override them before you import this file. // You can override them before you import this file.
$#{name}-sprite-base-class: ".#{name}-sprite" !default; $#{name}-sprite-base-class: ".#{name}-sprite" !default;
@ -82,48 +57,44 @@ $#{name}-#{sprite_name}-repeat: $#{name}-repeat !default;
SCSS SCSS
end.join} end.join}
// All sprites should extend this class $#{name}-sprite: sprite("#{uri}",
// The #{name}-sprite mixin will do so for you.
\#{$#{name}-sprite-base-class} {
background: generate-sprite-image("#{uri}",
#{images.map do |sprite_name| #{images.map do |sprite_name|
%Q{ $#{sprite_name}-position: $#{name}-#{sprite_name}-position, %Q{ $#{sprite_name}-position: $#{name}-#{sprite_name}-position,
$#{sprite_name}-spacing: $#{name}-#{sprite_name}-spacing, $#{sprite_name}-spacing: $#{name}-#{sprite_name}-spacing,
$#{sprite_name}-repeat: $#{name}-#{sprite_name}-repeat} $#{sprite_name}-repeat: $#{name}-#{sprite_name}-repeat}
end.join(",\n")}) no-repeat; end.join(",\n")});
// All sprites should extend this class
// The #{name}-sprite mixin will do so for you.
\#{$#{name}-sprite-base-class} {
background: $#{name}-sprite no-repeat;
} }
// Use this to set the dimensions of an element // Use this to set the dimensions of an element
// based on the size of the original image. // based on the size of the original image.
@mixin #{name}-sprite-dimensions($sprite) { @mixin #{name}-sprite-dimensions($name) {
height: image-height("#{name}/\#{$sprite}.png"); @include sprite-dimensions($#{name}-sprite, $name)
width: image-width("#{name}/\#{$sprite}.png");
} }
// Move the background position to display the sprite. // Move the background position to display the sprite.
@mixin #{name}-sprite-position($sprite, $x: 0, $y: 0) { @mixin #{name}-sprite-position($name, $offset-x: 0, $offset-y: 0) {
background-position: sprite-position("#{path}/\#{$sprite}.png", $x, $y); @include sprite-position($#{name}-sprite, $name, $offset-x, $offset-y)
} }
// Extends the sprite base class and set the background position for the desired sprite. // Extends the sprite base class and set the background position for the desired sprite.
// It will also apply the image dimensions if $dimensions is true. // It will also apply the image dimensions if $dimensions is true.
@mixin #{name}-sprite($sprite, $dimensions: $#{name}-sprite-dimensions, $x: 0, $y: 0) { @mixin #{name}-sprite($name, $dimensions: $#{name}-sprite-dimensions, $offset-x: 0, $offset-y: 0) {
@extend \#{$#{name}-sprite-base-class}; @extend \#{$#{name}-sprite-base-class};
@include #{name}-sprite-position($sprite, $x, $y); @include sprite($#{name}-sprite, $name, $dimensions, $offset-x, $offset-y)
@if $dimensions {
@include #{name}-sprite-dimensions($sprite);
} }
@mixin #{name}-sprites($sprite-names, $dimensions: $#{name}-sprite-dimensions) {
@include sprites($#{name}-sprite, $sprite-names, $#{name}-sprite-base-class, $dimensions)
} }
// Generates a class for each sprited image. // Generates a class for each sprited image.
@mixin all-#{name}-sprites { @mixin all-#{name}-sprites($dimensions: $#{name}-sprite-dimensions) {
#{images.map do |sprite_name| @include #{name}-sprites(#{images.join(" ")}, $dimensions);
<<-SCSS
.#{name}-#{sprite_name} {
@include #{name}-sprite("#{sprite_name}");
}
SCSS
end.join}
} }
SCSS SCSS
end end

View File

@ -10,7 +10,6 @@ describe Compass::Sprites do
FileUtils.cp_r @images_src_path, @images_tmp_path FileUtils.cp_r @images_src_path, @images_tmp_path
Compass.configuration.images_path = @images_tmp_path Compass.configuration.images_path = @images_tmp_path
Compass.configure_sass_plugin! Compass.configure_sass_plugin!
Compass::Sprites.reset
end end
after :each do after :each do
@ -247,19 +246,21 @@ describe Compass::Sprites do
it "should use position adjustments in functions" do it "should use position adjustments in functions" do
css = render <<-SCSS css = render <<-SCSS
$squares-position: 100%; $squares-sprite: sprite("squares/*.png", $position: 100%);
@import "squares/*.png"; .squares-sprite {
background: $squares-sprite no-repeat;
}
.adjusted-percentage { .adjusted-percentage {
background-position: sprite-position("squares/ten-by-ten.png", 100%); background-position: sprite-position($squares-sprite, ten-by-ten, 100%);
} }
.adjusted-px-1 { .adjusted-px-1 {
background-position: sprite-position("squares/ten-by-ten.png", 4px); background-position: sprite-position($squares-sprite, ten-by-ten, 4px);
} }
.adjusted-px-2 { .adjusted-px-2 {
background-position: sprite-position("squares/twenty-by-twenty.png", -3px, 2px); background-position: sprite-position($squares-sprite, twenty-by-twenty, -3px, 2px);
} }
SCSS SCSS
css.should == <<-CSS css.should == <<-CSS
@ -289,15 +290,15 @@ describe Compass::Sprites do
@import "squares/*.png"; @import "squares/*.png";
.adjusted-percentage { .adjusted-percentage {
@include squares-sprite("ten-by-ten", $x: 100%); @include squares-sprite("ten-by-ten", $offset-x: 100%);
} }
.adjusted-px-1 { .adjusted-px-1 {
@include squares-sprite("ten-by-ten", $x: 4px); @include squares-sprite("ten-by-ten", $offset-x: 4px);
} }
.adjusted-px-2 { .adjusted-px-2 {
@include squares-sprite("twenty-by-twenty", $x: -3px, $y: 2px); @include squares-sprite("twenty-by-twenty", $offset-x: -3px, $offset-y: 2px);
} }
SCSS SCSS
css.should == <<-CSS css.should == <<-CSS
@ -344,89 +345,46 @@ describe Compass::Sprites do
image_md5('squares.png').should == '0187306f3858136feee87d3017e7f307' image_md5('squares.png').should == '0187306f3858136feee87d3017e7f307'
end end
it "should use the sprite-image and sprite-url function as in lemonade" do it "should provide a nice errors for lemonade's old users" do
css = render <<-SCSS proc do
@import "squares/*.png"; render <<-SCSS
.squares {
.squares-1 { background: sprite-url("squares/*.png") no-repeat;
background: sprite-image("squares/twenty-by-twenty.png") no-repeat;
}
.squares-2 {
background: sprite-image("squares/twenty-by-twenty.png", 100%) no-repeat;
}
.squares-3 {
background: sprite-image("squares/twenty-by-twenty.png", -4px, 3px) no-repeat;
}
.squares-4 {
background-image: sprite-url("squares/twenty-by-twenty.png");
}
.squares-5 {
background-image: sprite-url("squares/*.png");
} }
SCSS SCSS
css.should == <<-CSS end.should raise_error Sass::SyntaxError,
.squares-sprite { %q(The first argument to sprite-url must be a sprite. See http://beta.compass-style.org/help/tutorials/spriting/ for more information.)
background: url('/squares.png') no-repeat; proc do
render <<-SCSS
.squares {
background: sprite-image("squares/twenty-by-twenty.png") no-repeat;
} }
SCSS
end.should raise_error Sass::SyntaxError,
%q(The first argument to sprite-image must be a sprite. See http://beta.compass-style.org/help/tutorials/spriting/ for more information.)
proc do
render <<-SCSS
@import "squares/*.png";
.squares-1 { .squares {
background: sprite-position("squares/twenty-by-twenty.png") no-repeat;
}
SCSS
end.should raise_error Sass::SyntaxError,
%q(The first argument to sprite-position must be a sprite. See http://beta.compass-style.org/help/tutorials/spriting/ for more information.)
end
it "should work even if @import is missing" do
actual_css = render <<-SCSS
.squares {
background: sprite-image(sprite("squares/*.png"), twenty-by-twenty) no-repeat;
}
SCSS
actual_css.should == <<-CSS
.squares {
background: url('/squares.png') 0 -10px no-repeat; background: url('/squares.png') 0 -10px no-repeat;
} }
.squares-2 {
background: url('/squares.png') 100% -10px no-repeat;
}
.squares-3 {
background: url('/squares.png') -4px -7px no-repeat;
}
.squares-4 {
background-image: url('/squares.png');
}
.squares-5 {
background-image: url('/squares.png');
}
CSS CSS
end end
it "should raise deprecation errors for lemonade's spacing syntax" do
proc do
render <<-SCSS
@import "squares/*.png";
.squares {
background: sprite-image("squares/twenty-by-twenty.png", 0, 0, 11px) no-repeat;
}
SCSS
end.should raise_error Compass::Error,
%q(Spacing parameter is deprecated. Please add `$squares-twenty-by-twenty-spacing: 11px;` before the `@import "squares/*.png";` statement.)
proc do
render <<-SCSS
@import "squares/*.png";
.squares {
background: sprite-position("squares/twenty-by-twenty.png", 0, 0, 11px) no-repeat;
}
SCSS
end.should raise_error Compass::Error,
%q(Spacing parameter is deprecated. Please add `$squares-twenty-by-twenty-spacing: 11px;` before the `@import "squares/*.png";` statement.)
end
it "should raise an error if @import is missing" do
proc do
render <<-SCSS
.squares {
background: sprite-image("squares/twenty-by-twenty.png") no-repeat;
}
SCSS
end.should raise_error Compass::Error,
%q(`@import` statement missing. Please add `@import "squares/*.png";`.)
end
end end