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'
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
# from a keyword argument hash that accounts for underscores/dash equivalence
@ -12,111 +12,253 @@ module Compass::SassExtensions::Functions::Sprites
end
end
def generate_sprite_image(uri, kwargs = {})
kwargs.extend VariableReader
class Sprite < Sass::Script::Literal
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)
last_spacing = 0
width = 0
height = 0
# Get image metadata
Compass::Sprites.discover_sprites(uri.value).each do |file|
Compass::Sprites.compute_image_metadata! file, path, name
new(Compass::Sprites.discover_sprites(uri.value), path, name, context, kwargs)
end
images = Compass::Sprites.sprites(path, name)
# Calculation
images.each do |image|
current_spacing = number_from_var(kwargs, "#{image[:name]}-spacing", 0)
if height > 0
height += [current_spacing, last_spacing].max
end
image[:y] = height
height += image[:height]
last_spacing = current_spacing
width = image[:width] if image[:width] > width
def initialize(image_names, path, name, context, options)
@image_names, @path, @name, @options = image_names, path, name, options
@images = nil
@width = nil
@height = nil
@evaluation_context = context
validate!
compute_image_metadata!
end
# Generation
output_png = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)
images.each do |image|
input_png = ChunkyPNG::Image.from_file(image[:file])
def sprite_names
image_names.map{|f| Compass::Sprites.sprite_name(f) }
end
position = kwargs.get_var("#{image[:name]}-position") || Sass::Script::Number.new(0, ["%"])
if position.unit_str == "%"
image[:x] = (width - image[:width]) * (position.value / 100)
def validate!
for sprite_name in sprite_names
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
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
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
else
"no-repeat"
end
if repeat == "no-repeat"
output_png.replace input_png, image[:x], image[:y]
end
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
x = image[:x] - (image[:x] / image[:width]).ceil * image[:width]
x = image[:left] - (image[:left] / image[:width]).ceil * image[:width]
while x < width do
output_png.replace input_png, x, image[:y]
output_png.replace input_png, x, image[:top]
x += image[:width]
end
end
end
output_png.save File.join(File.join(Compass.configuration.images_path, "#{path}.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}")
output_png
end
def sprite_url(uri)
path, name = Compass::Sprites.path_and_name(uri.value)
image_url(Sass::Script::String.new("#{path}.png"))
# The on-the-disk filename of the sprite
def filename
File.join(File.join(Compass.configuration.images_path, "#{path}.png"))
end
def sprite_position(uri, x_shift = SASS_NULL, y_shift = SASS_NULL, depricated_1 = nil, depricated_2 = nil)
check_spacing_deprecation uri, depricated_1, depricated_2
path, name = Compass::Sprites.path_and_name(uri.value)
image_name = File.basename(uri.value, '.png')
image = Compass::Sprites.sprites(path, name).detect{ |image| image[:name] == image_name }
# 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
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 == "%"
x = x_shift.to_s
x = x_shift # CE: Shouldn't this be a percentage of the total width?
else
x = x_shift.value - image[:x]
x = "#{x}px" unless x == 0
x = x_shift.value - image[:left]
x = Sass::Script::Number.new(x, x == 0 ? [] : ["px"])
end
y = y_shift.value - image[:y]
y = "#{y}px" unless y == 0
Sass::Script::String.new("#{x} #{y}")
y = y_shift.value - image[:top]
y = Sass::Script::Number.new(y, y == 0 ? [] : ["px"])
Sass::Script::List.new([x, y],:space)
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

View File

@ -4,66 +4,41 @@ module Compass
attr_accessor :path
class << self
def reset
@@sprites = {}
end
def path_and_name(uri)
if uri =~ %r{((.+/)?(.+))/(.+?)\.png}
[$1, $3, $4]
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)
glob = File.join(Compass.configuration.images_path, uri)
Dir.glob(glob).sort
end
def compute_image_metadata!(file, path, name)
width, height = Compass::SassExtensions::Functions::ImageSize::ImageProperties.new(file).size
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
def sprite_name(file)
File.basename(file, '.png')
end
def images
Compass::Sprites.sprites(self.path, self.name, true)
end
def find_relative(*args)
nil
end
def find(uri, options)
if uri =~ /\.png$/
self.path, self.name = Compass::Sprites.path_and_name(uri)
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)
end
end
def content_for_images(uri, name, images)
<<-SCSS
@import "compass/utilities/sprites/base";
// General Sprite Defaults
// You can override them before you import this file.
$#{name}-sprite-base-class: ".#{name}-sprite" !default;
@ -82,50 +57,46 @@ $#{name}-#{sprite_name}-repeat: $#{name}-repeat !default;
SCSS
end.join}
// All sprites should extend this class
// The #{name}-sprite mixin will do so for you.
\#{$#{name}-sprite-base-class} {
background: generate-sprite-image("#{uri}",
$#{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")}) 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
// based on the size of the original image.
@mixin #{name}-sprite-dimensions($sprite) {
height: image-height("#{name}/\#{$sprite}.png");
width: image-width("#{name}/\#{$sprite}.png");
@mixin #{name}-sprite-dimensions($name) {
@include sprite-dimensions($#{name}-sprite, $name)
}
// Move the background position to display the sprite.
@mixin #{name}-sprite-position($sprite, $x: 0, $y: 0) {
background-position: sprite-position("#{path}/\#{$sprite}.png", $x, $y);
@mixin #{name}-sprite-position($name, $offset-x: 0, $offset-y: 0) {
@include sprite-position($#{name}-sprite, $name, $offset-x, $offset-y)
}
// 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.
@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};
@include #{name}-sprite-position($sprite, $x, $y);
@if $dimensions {
@include #{name}-sprite-dimensions($sprite);
}
@include sprite($#{name}-sprite, $name, $dimensions, $offset-x, $offset-y)
}
@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.
@mixin all-#{name}-sprites {
#{images.map do |sprite_name|
<<-SCSS
.#{name}-#{sprite_name} {
@include #{name}-sprite("#{sprite_name}");
}
SCSS
end.join}
@mixin all-#{name}-sprites($dimensions: $#{name}-sprite-dimensions) {
@include #{name}-sprites(#{images.join(" ")}, $dimensions);
}
SCSS
SCSS
end
def key(uri, options)

View File

@ -10,7 +10,6 @@ describe Compass::Sprites do
FileUtils.cp_r @images_src_path, @images_tmp_path
Compass.configuration.images_path = @images_tmp_path
Compass.configure_sass_plugin!
Compass::Sprites.reset
end
after :each do
@ -247,19 +246,21 @@ describe Compass::Sprites do
it "should use position adjustments in functions" do
css = render <<-SCSS
$squares-position: 100%;
@import "squares/*.png";
$squares-sprite: sprite("squares/*.png", $position: 100%);
.squares-sprite {
background: $squares-sprite no-repeat;
}
.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 {
background-position: sprite-position("squares/ten-by-ten.png", 4px);
background-position: sprite-position($squares-sprite, ten-by-ten, 4px);
}
.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
css.should == <<-CSS
@ -289,15 +290,15 @@ describe Compass::Sprites do
@import "squares/*.png";
.adjusted-percentage {
@include squares-sprite("ten-by-ten", $x: 100%);
@include squares-sprite("ten-by-ten", $offset-x: 100%);
}
.adjusted-px-1 {
@include squares-sprite("ten-by-ten", $x: 4px);
@include squares-sprite("ten-by-ten", $offset-x: 4px);
}
.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
css.should == <<-CSS
@ -344,89 +345,46 @@ describe Compass::Sprites do
image_md5('squares.png').should == '0187306f3858136feee87d3017e7f307'
end
it "should use the sprite-image and sprite-url function as in lemonade" 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");
it "should provide a nice errors for lemonade's old users" do
proc do
render <<-SCSS
.squares {
background: sprite-url("squares/*.png") no-repeat;
}
SCSS
css.should == <<-CSS
.squares-sprite {
background: url('/squares.png') no-repeat;
end.should raise_error Sass::SyntaxError,
%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
.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;
}
.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
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