twolfson / grunt-spritesmith

Grunt task for converting a set of images into a spritesheet and corresponding CSS variables
MIT License
1.14k stars 92 forks source link

background-position offsets #142

Closed siiron closed 9 years ago

siiron commented 9 years ago

First of all – great job on spritesmith! I totally dig it, and currently I’m rewriting our current compass-generated sprites to use grunt-spritesmith instead.

I’m still a bit confused about position overrides.

I'm struggling to figure out how to override the generated values for natural background-position. By that I mean moving the position relative of the generated values. I'm used to Compass' mixin sprite-background-position. This can take parameters for offset, but trying this with the mixin sprite-position in grunt-spritesmith:

@include sprite-position($icon-foo, 1px, 3px);

gives me the following error: "Mixin sprite-position takes 1 argument but 3 were passed".

Usually the generated sprite-position is fine, but in some cases I find the need to adjust it by a pixel or two. And often there is only need to do this for a specific icon where other icons in the sprite is fine as they are.

The most common use-case I can think of is when you have an element, e.g. an input field. You'd like to show an icon inside the input and on the right. With the algorithm set to top-down, you'd get a nice vertical column-shaped spritemap. Since the width of the element should not be controlled by the generated sprite (not generating values for width/ height), you'd use:

@include sprite-image($icon-foo);

If you'd add:

@include sprite-position($icon-foo);

the icon is positioned at the left in the input-field. Not quite what you'd want. You'd like it on the right.

I'd expect something like this:

@include sprite-position($icon-foo, 100%, $icon-foo-offset-y)

100% represents the x-position override (100% to the right), and the $icon-foo-offset-y uses the dynamic generated y-position.

How can this be done?

twolfson commented 9 years ago

The quick answer is sprite-position in spritesmith is unintentionally similarly named. While compass was part of the inspiration for spritesmith (i.e. making spritesheets sane), we did the CSS architecture independently =/

That being said, I am interested in learning about your use case more. I am going to perform some research to see how compass implements this feature.

twolfson commented 9 years ago

Alright, I am done with my research. Unfortunately, compass relies on folders so I cannot point you to my work. As a result, I will attempt to compensate here.

I used 2 test sprites as our images, located in images/my-icons/ (e.g. images/my-icons/sprite1.png):

https://github.com/Ensighten/grunt-spritesmith/blob/5.0.0/src-test/test_files/sprite1.png

https://github.com/Ensighten/grunt-spritesmith/blob/5.0.0/src-test/test_files/sprite3.png

I defined a sass/screen.scss via compass init. Its contents were:

@import "compass/utilities/sprites";
@import "my-icons/*.png";

.sprite1 {
  @include my-icons-sprite(sprite1);
}

.sprite3 {
  @include my-icons-sprite(sprite3);
}

Upon compilation, it's output was:

my-icons-sc7a02fc51a

/* line 56, my-icons/*.png */
.my-icons-sprite, .sprite1, .sprite3 {
  background-image: url('/images/my-icons-sc7a02fc51a.png');
  background-repeat: no-repeat;
}

/* line 4, ../sass/screen.scss */
.sprite1 {
  background-position: 0 0;
}

/* line 8, ../sass/screen.scss */
.sprite3 {
  background-position: 0 -50px;
}

When I moved to using my-icons-background-position:

@import "compass/utilities/sprites";
@import "my-icons/*.png";

.sprite1 {
  @include my-icons-sprite(sprite1);
  @include my-icons-sprite-position(sprite1, 20px, 20px);
}

.sprite3 {
  @include my-icons-sprite(sprite3);
  @include my-icons-sprite-position(sprite1, 20px, 20px);
}

I received the same spritesheet with the following CSS:

/* line 56, my-icons/*.png */
.my-icons-sprite, .sprite1, .sprite3 {
  background-image: url('/images/my-icons-sc7a02fc51a.png');
  background-repeat: no-repeat;
}

/* line 4, ../sass/screen.scss */
.sprite1 {
  background-position: 0 0;
  background-position: 20px 20px;
}

/* line 9, ../sass/screen.scss */
.sprite3 {
  background-position: 0 -50px;
  background-position: 20px 20px;
}
twolfson commented 9 years ago

Now that we are on the same page, we can reflect on our options. I am leaning towards not adding this feature for a few reasons:

As a consequence of these reasons, I am deciding to not implement the feature. The suggested workaround will be using the offset-* variables you mentioned:

https://github.com/twolfson/spritesheet-templates/blob/10.0.0/test/expected_files/scss.scss

https://github.com/twolfson/spritesheet-templates/blob/10.0.0/test/expected_files/scss.scss#L73-L75

// Compass flavor
@include my-icons-sprite-position(sprite1, 20px, 20px);

// spritesmith equvialent
background-position: 20px 20px;

// or with custom variables
background-position: $sprite1-offset-x $sprite1-offset-y;
twolfson commented 9 years ago

I should clarify on the point I alluded to about "padding" vs "other sprites". To push a sprite down with some whitespace, we typically suggest including this as a part of the sprite.

It costs a few extra bytes but makes everything sane in terms of CSS (e.g. no browser specific padding fidgeting). Additionally, it allows us to use non-linear packing algorithms which saves much more bytes.

siiron commented 9 years ago

Thank you for looking in to this.

Here is a little more detail about my issue (using a top-down algorithm with padding of 12px between the icons in the sprite):

Using the following Sass:

@include sprite-image($icon-foo);
@include sprite-position($icon-foo);
background-repeat: no-repeat;

gives me the following CSS:

background-image: url(sprite.png);
background-position: 0px -108px;
background-repeat: no-repeat;

This looks like this:

icon-left

As you can see, I'd like to have the icon placed to right and a couple of pixels down.

I guess it's ok to just insert the overrides by either redefining the variabel or use Sass' calculations to calculate the adjustment relative to the original value of the generated offset:

@include sprite-image($icon-foo);
background-position: 100% $icon-foo-offset-y + 2;
background-repeat: no-repeat;

This would generate the following CSS:

background-image: url(sprite.png);
backround-position: 100% -106px;
background-repeat: no-repeat;

Problem solved I guess.

icon-right

My main concern was to hand-code these values and not being able to dynamically calculate them if the sprite changes (adding or deleting icons in the icon-set).

The x-position 100% is still hard coded, but the y-position will always nudge the icon 2px down, even if the original value changes.

What I really ended up doing, was to use css calc to add some pixels to the 100% x-position. This way I could compensate for the exra pixels the icon had to its right (as part of the spritesheet):

@include sprite-image($icon-foo);
background-position: calc(100% + 4px) $icon-foo-offset-y + 2;
background-repeat: no-repeat;

This got me this CSS:

background-image: url(sprite.png);
backround-position: calc(100% + 4px) -106px;
background-repeat: no-repeat;

And this result:

icon-right-2

Voila!

twolfson commented 9 years ago

Ah, thanks for the clarification on your use case =)

One thing to note is calc has problems with IE9 and no support in IE8/below:

http://caniuse.com/#search=calc

If you need to support those browser, I suggest creating 2 separate spritesheets (via 2 grunt tasks), 1 exclusively for image background images (which hopefully are the same width) and 1 for everything else.

The background images can be vertically stacked as they require but the others can be packed more efficiently via the binary-tree algorithm.

One more option we can consider is using a top-down algorithm but that right-aligns all images. This can be done by specifying an object to the algorithm option:

https://github.com/Ensighten/grunt-spritesmith/tree/5.0.0#documentation

https://github.com/twolfson/layout/blob/2.2.0/lib/algorithms/top-down.algorithm.js

{
  algorithm: {
    sort: function (items) {
      // Sort the items by their height
      items.sort(function (a, b) {
        return a.height - b.height;
      });
      return items;
    },
    placeItems: function (items) {
      // Find the maximum width
      var maxWidth = items.reduce(function (maxWidth, item) {
        if (item.width > maxWidth) {
          return item.width;
        }
        return maxWidth;
      }, 0);

      // Iterate over each of the items
      var y = 0;
      items.forEach(function (item) {
        // Update the y to the current height
        item.x = maxWidth - item.width;
        item.y = y;

        // Increment the y by the item's height
        y += item.height;
      });

      // Return the items
      return items;
    };
  }
}