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 adcfcf4556
commit 027ebdd8c4
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
path, name = Compass::Sprites.path_and_name(uri.value)
last_spacing = 0
width = 0
height = 0
# Get image metadata attr_accessor :image_names, :path, :name, :options
Compass::Sprites.discover_sprites(uri.value).each do |file| attr_accessor :images, :width, :height
Compass::Sprites.compute_image_metadata! file, path, name
def self.from_uri(uri, context, kwargs)
path, name = Compass::Sprites.path_and_name(uri.value)
new(Compass::Sprites.discover_sprites(uri.value), path, name, context, kwargs)
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])
def validate!
position = kwargs.get_var("#{image[:name]}-position") || Sass::Script::Number.new(0, ["%"]) for sprite_name in sprite_names
if position.unit_str == "%" unless sprite_name =~ /\A#{Sass::SCSS::RX::IDENT}\Z/
image[:x] = (width - image[:width]) * (position.value / 100) raise Sass::SyntaxError, "#{sprite_name} must be a legal css identifier"
else end
image[:x] = position.value
end end
end
repeat = if (var = kwargs.get_var("#{image[:name]}-repeat"))
# 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
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
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]
else def spacing_for(name)
x = image[:x] - (image[:x] / image[:width]).ceil * image[:width] (options.get_var("#{name}-spacing") ||
while x < width do options.get_var("spacing") ||
output_png.replace input_png, x, image[:y] ZERO).value
x += image[:width] end
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
end end
output_png.save File.join(File.join(Compass.configuration.images_path, "#{path}.png"))
def generation_required?
sprite_url(uri) !File.exists?(filename) || outdated?
end end
Sass::Script::Functions.declare :generate_sprite_image, [:uri], :var_kwargs => true
# Returns a PNG object
def sprite_image(uri, x_shift = SASS_NULL, y_shift = SASS_NULL, depricated_1 = nil, depricated_2 = nil) def construct_sprite
check_spacing_deprecation uri, depricated_1, depricated_2 output_png = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)
url = sprite_url(uri) images.each do |image|
position = sprite_position(uri, x_shift, y_shift) input_png = ChunkyPNG::Image.from_file(image[:file])
Sass::Script::String.new("#{url} #{position}") if image[:repeat] == "no-repeat"
end output_png.replace input_png, image[:left], image[:top]
else
def sprite_url(uri) x = image[:left] - (image[:left] / image[:width]).ceil * image[:width]
path, name = Compass::Sprites.path_and_name(uri.value) while x < width do
image_url(Sass::Script::String.new("#{path}.png")) output_png.replace input_png, x, image[:top]
x += image[:width]
end
end
end
output_png
end
# The on-the-disk filename of the sprite
def filename
File.join(File.join(Compass.configuration.images_path, "#{path}.png"))
end
# saves the sprite for later retrieval
def save!(output_png)
output_png.save filename
end
# 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 end
def sprite_position(uri, x_shift = SASS_NULL, y_shift = SASS_NULL, depricated_1 = nil, depricated_2 = nil) def sprite(uri, kwargs = {})
check_spacing_deprecation uri, depricated_1, depricated_2 kwargs.extend VariableReader
path, name = Compass::Sprites.path_and_name(uri.value) Sprite.from_uri(uri, self, kwargs)
image_name = File.basename(uri.value, '.png') end
image = Compass::Sprites.sprites(path, name).detect{ |image| image[:name] == image_name } 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 end
def images def find_relative(*args)
Compass::Sprites.sprites(self.path, self.name, true) 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,50 +57,46 @@ $#{name}-#{sprite_name}-repeat: $#{name}-repeat !default;
SCSS SCSS
end.join} end.join}
$#{name}-sprite: sprite("#{uri}",
#{images.map do |sprite_name|
%Q{ $#{sprite_name}-position: $#{name}-#{sprite_name}-position,
$#{sprite_name}-spacing: $#{name}-#{sprite_name}-spacing,
$#{sprite_name}-repeat: $#{name}-#{sprite_name}-repeat}
end.join(",\n")});
// All sprites should extend this class // All sprites should extend this class
// The #{name}-sprite mixin will do so for you. // The #{name}-sprite mixin will do so for you.
\#{$#{name}-sprite-base-class} { \#{$#{name}-sprite-base-class} {
background: generate-sprite-image("#{uri}", background: $#{name}-sprite no-repeat;
#{images.map do |sprite_name|
%Q{ $#{sprite_name}-position: $#{name}-#{sprite_name}-position,
$#{sprite_name}-spacing: $#{name}-#{sprite_name}-spacing,
$#{sprite_name}-repeat: $#{name}-#{sprite_name}-repeat}
end.join(",\n")}) 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
def key(uri, options) def key(uri, options)

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
@import "squares/*.png";
.squares-1 {
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
css.should == <<-CSS
.squares-sprite {
background: url('/squares.png') no-repeat;
}
.squares-1 {
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
end
it "should raise deprecation errors for lemonade's spacing syntax" do
proc do proc do
render <<-SCSS render <<-SCSS
@import "squares/*.png";
.squares { .squares {
background: sprite-image("squares/twenty-by-twenty.png", 0, 0, 11px) no-repeat; background: sprite-url("squares/*.png") no-repeat;
} }
SCSS SCSS
end.should raise_error Compass::Error, end.should raise_error Sass::SyntaxError,
%q(Spacing parameter is deprecated. Please add `$squares-twenty-by-twenty-spacing: 11px;` before the `@import "squares/*.png";` statement.) %q(The first argument to sprite-url must be a sprite. See http://beta.compass-style.org/help/tutorials/spriting/ for more information.)
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 proc do
render <<-SCSS render <<-SCSS
.squares { .squares {
background: sprite-image("squares/twenty-by-twenty.png") no-repeat; background: sprite-image("squares/twenty-by-twenty.png") no-repeat;
} }
SCSS SCSS
end.should raise_error Compass::Error, end.should raise_error Sass::SyntaxError,
%q(`@import` statement missing. Please add `@import "squares/*.png";`.) %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 {
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;
}
CSS
end end
end end