twolfson / spritesheet-templates

Convert spritesheet data into CSS or CSS pre-processor data
The Unlicense
107 stars 48 forks source link

Make registering handlebars helper available. #40

Closed teejayhh closed 9 years ago

teejayhh commented 9 years ago

Hello, I am using this project with spritesmith and it has been great so far. I have the following requirement and I hope we could incorporate this into your project. I need to be able to register handlebarHelper which I can use in my mustache templates. I always have the issue that sprite generators require an even amount of images to great normal/ and retina spritesheets. I want to be able to create normal and retina sprite sheets independent from each other to create %placeholder for each sprite.

%s-arrow-down {
    background-image: url(../../img/spritessmith/spritesheet1x.png);
    background-position: 0px 0px;
    width: 40px;
    height: 40px;
    background-size: 120px 80px;
}

and for retina i would like to do this

%r-arrow-down {
    background-image: url(../../img/spritessmith/spritesheet2x.png);
    background-position: 0px 0px;
    width: 40px; // half of the real retina resolution  {{half px.width}}
    height: 40px; // half of the real retina resolution
    background-size: 120px 80px; // half of the real retina resolution
}

updated. -> sorry my fault that is correct now !

now you can create a mixin which does the following

@mixin sprite(name) {
     @extend %s-{#name};
     media bla bla {
           @extend %r-{#name} !optional;
     }
}

the benefit is that I don't need the same amount of images and we have a fallback to normal in any case. I hacked your project to register a "half" helper which can be used in the templates. I could think of all sorts of things where helper could be beneficial. You could add this to the options like options.handleBarHelpers : [{name:'helpername1',func:function('value'){}}] and then iterate through the array and register the helper with handlebars.registerHelper(template.name,template.func).

Cheers Thomas

twolfson commented 9 years ago

During the development of moving to handlebars, I thought we were exposing the ability to register new helpers via handlebars itself but I am realizing that handlebars is being buried away.

I would prefer users to somehow invoke handlebars.registerHelper since it will avoid confusion as the helpers always become global. However, your idea is definitely good one. I will revisit this by the end of next weekend.

In the interim, I don't fully grasp your example. With retina sprites, the background size/width/height should be the same as their normal sprite equivalents. But the dimensions you are providing are half of the normal ones and 1/4 of the typical retina dimensions. Can you explain why that is?

teejayhh commented 9 years ago

Ok here is how I understand retina. When I work on a project I look at it always from the 1x perspective. A layout can be 320px or let's say 1024 px wide. If I throw in a retina screen everything becomes tiny IF I don't change the zoom to bring us back to 1x levels. Thankfully we don't have to worry about it as our operating system usually takes care of this. Retina means that the density of pixel increases to a point where we can't see any px at 1x dimensions. In terms of our design or layout a pixel doesn't automatically translate into 2px when I use a retina screen, it stays the same. That has implications to sprite handling. Let's I have a div which should become an icon than we usually apply a background on it in 1x size, eg 32 px. Now our element needs to receive the dimensions of 32x32px as well otherwise the icon wouldn't show properly. On a retina screen the with and height stays the same but our image becomes blurry because it gets blown up due to density change. To counteract it we have to use a higher resolution image twice size of normal. By do that our image becomes suddenly cropped in our div container. Now we need to adjust the background-size to half its original retina resolution to match the dimensions of our 1x element and voila our icon appears nice and sharp on a retina screen. This is how I understand the retina issue. My example above reflects that understanding. I hope that makes sense.

teejayhh commented 9 years ago

I just realized I did a little mistake with the widths in my example (which I adjusted) -> it has to match the 1x properties but i still need to half them :)

teejayhh commented 9 years ago

let me share a little code with you. As is said before I am using gruntsmith which uses your project, so this example starts from the gruntsmith config :) The code below is my take on the retina issue and by no means the best solution but a good one for me. I try to use placeholders rather than classes in my scss files. classes create output when imported into multiple public scss files which gets duplicated all over again. The answer to this problem are %placeholder. Creating a placeholder for each icon has an added benefit that we can now extend the placeholder in case we want to use the same icon in different places without add class to the dom! I dont like to hack stuff which is why I did the following to do what I wanted initially. It sort of disables most of your code and takes a short cut to get to the finish line.

module.exports = function(grunt) {

    var handlebars = require('./bower_components/handlebars/handlebars');
    var fs = require('fs');

    // Project configuration.
    grunt.initConfig({
        sprite:{
            x1: {
                cssTemplate: function (data) {
                    var fileData = fs.readFileSync('scss/spritesmith/spritesmith-retina-mixins.template.mustache','utf8');
                    var handlebarstr = handlebars.compile(fileData);
                    return handlebarstr(data);
                },
                src: 'img/spritessmith/1x/*.png',
                dest: 'img/spritessmith/spritesheet1x.png',
                destCss: 'scss/spritesmith/_sprites1x.scss',
                cssSpritesheetName:'sprites1x',
                imgPath: "/app/img/spritessmith/spritesheet1x.png"
            }
            ,x2: {
                cssTemplate: function (data) {
                    var fileData = fs.readFileSync('scss/spritesmith/spritesmith-retina-mixins.template2.mustache','utf8');
                    handlebars.registerHelper('divide',function(value){
                        return parseInt(value) / 2 + 'px';
                    });
                    var handlebarstr = handlebars.compile(fileData);
                    return handlebarstr(data);
                },
                src: 'img/spritessmith/2x/*.png',
                dest: 'img/spritessmith/spritesheet2x.png',
                destCss: 'scss/spritesmith/_sprites2x.scss',
                cssSpritesheetName:'sprites2x',
                imgPath: "/app/img/spritessmith/spritesheet2x.png"
            }
        }

    });

    grunt.loadNpmTasks('grunt-spritesmith');
    grunt.registerTask('default', ['sprite']);
};

here is my 2x template ->

${{spritesheet_name}}: (
    {{#sprites}}
    {{name}}: (
        offset-x: {{divide px.offset_x}},offset-y: {{divide px.offset_y}},width: {{divide px.width}},height: {{divide px.height}},total-width: {{divide px.total_width}},total-height: {{divide px.total_height}},image: '{{{escaped_image}}}'
    ),
    {{/sprites}}
);
{{#each sprites}}
%{{../spritesheet_name}}-{{name}} {
    @media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx) {
        background-image: url({{{escaped_image}}});
        background-position: {{divide px.offset_x}} {{divide px.offset_y}};
        background-size: {{divide px.total_width}} {{divide px.total_height}};
    }
}
{{/each}}

@mixin {{spritesheet_name}}-size($image) {
    background-size: map-get(map-get(${{spritesheet_name}}, $image), 'total-width') map-get(map-get(${{spritesheet_name}}, $image), 'total-height');
}
@mixin {{spritesheet_name}}-width($image) {
    width: map-get(map-get(${{spritesheet_name}}, $image), 'width');
}
@mixin {{spritesheet_name}}-height($image) {
    height: map-get(map-get(${{spritesheet_name}}, $image), 'height');
}
@mixin {{spritesheet_name}}-position($image) {
    background-position: map-get(map-get(${{spritesheet_name}}, $image), 'offset-x') map-get(map-get(${{spritesheet_name}}, $image), 'offset-y');
}
@mixin {{spritesheet_name}}-image($image) {
    background-image: url(map-get(map-get(${{spritesheet_name}}, $image), 'image'));
}
@mixin {{spritesheet_name}}($image, $size: true) {
    @include {{spritesheet_name}}-image($image);
    @include {{spritesheet_name}}-position($image);
    @include {{spritesheet_name}}-size($image);
    background-repeat: no-repeat;
}

I skip the 1x template as its the same without the media query and width and hight applied. To use this you need one mixin

@import "../spritesmith/sprites1x";
@import "../spritesmith/sprites2x";

@mixin retina-sprite($name,$suffix:"sprites"){
    @extend %#{$suffix}1x-#{$name};
    @extend %#{$suffix}2x-#{$name} !optional;
}

If you now need to support 3x retina images like on the new Imap you can create a new grunt task which deals with the 3x images and the handlebar helper deals with the 3x conversions. We used compass for sprite generation in pretty much all our projects and it was chore, its just not fast enough and I believe spriting doesnt belong in the same package of the sass compiler. its suppose to compile sass and thats it.Thats why I love this external grunt solution.

twolfson commented 9 years ago

Your updated original example, now has the same values that the retina ability of grunt-spritesmith already performs. I might be missing something but what is wrong with using the existing variables and functions?

Here is a documentation on our retina parameters:

https://github.com/Ensighten/grunt-spritesmith/tree/4.5.3#retina-parameters

An example usage for retina:

https://github.com/Ensighten/grunt-spritesmith/tree/4.5.3#retina-spritesheet

An example of the SCSS template:

https://github.com/twolfson/spritesheet-templates/tree/9.4.1#scss_retina

An article explaining everything more in depth:

http://twolfson.com/2015-04-21-retina-sprites-are-here!

teejayhh commented 9 years ago

as i said before I don't like the way that I am forced to have an equal amount of sprites for 1x and 2x. especially during the dev period things change at a rapid pace and some asset extraction lags behind and those situations create delays. The retina gruntsmith solution requires exactly that. I also wanted to create placeholder which actually exist in my scss file. Sure I can run an each loop which runs through the sprite array and creates them on the fly, but its expensive and i dont want to do that every time I change something in one of my scss files. Creating existing placeholders takes a bit of the burden of the compiler, and fast compiling performance sits pretty high on my main requirements list. Thats pretty much the reason why I did this way. I hope it makes sense

twolfson commented 9 years ago

Sorry, I must have missed the mention of frustration with equal sprites for 1x and 2x sprites. You seem to be bending over backwards for this solution. We strongly recommend to not regenerate sprites as part of the normal build process due to its cost; it should be reserved only when the sprites change as the results will always be the same.

Have you considered committing the generated spritesheets to the repo (as people normally done when managing them by hand) or leveraging grunt-newer?

https://github.com/tschaub/grunt-newer

If you take that approach, then you can generate placeholder upscaled images (as you mentioned) via a grunt task and only generate them when your assets change:

https://github.com/excellenteasy/grunt-image-resize

I am still going to be adding handlebars helper support. However, I would also like to get you in a less painful configuration =)

teejayhh commented 9 years ago

Hey twolfson thx for thinking this through. My example here is made due to very specific requirements which unfortunately couldn't be met by the package. Believe me I tried the retina solution and because of that I put this example together. I know, calling the task on the grunt call isn't super efficient. Setting up a file watcher should work too and newer is definitely a way to go as well. And on a side note, the configuration isn't painful at all :).

twolfson commented 9 years ago

Sorry for the slow reply. I was more talking about the effects of the configuration (e.g. performance) than the configuration itself. I will be taking a look at adding in helper registration to handlebars by the end of next weekend.

twolfson commented 9 years ago

We went with a format similar to the originally proposed data. There was an option to expose a function on grunt-spritesmith as a library but that breaks grunt conventions.

We allow helper registration via cssHandlebarsHelpers which is an object that is a key-value pair mapping of names to their helper functions. For your case, we would use:

sprite:{
    x1: {
        cssTemplate: 'scss/spritesmith/spritesmith-retina-mixins.template.mustache',
        src: 'img/spritessmith/1x/*.png',
        dest: 'img/spritessmith/spritesheet1x.png',
        destCss: 'scss/spritesmith/_sprites1x.scss',
        cssSpritesheetName:'sprites1x',
        imgPath: "/app/img/spritessmith/spritesheet1x.png"
    },
    x2: {
            cssTemplate: 'scss/spritesmith/spritesmith-retina-mixins.template2.mustache',
            src: 'img/spritessmith/2x/*.png',
            dest: 'img/spritessmith/spritesheet2x.png',
            destCss: 'scss/spritesmith/_sprites2x.scss',
            cssSpritesheetName:'sprites2x',
            imgPath: "/app/img/spritessmith/spritesheet2x.png",
            cssHandlebarsHelpers: {
                divide: function (value) {
                    return parseInt(value) / 2 + 'px';
                }
            }
        }
    }
}

This has been released in grunt-spritesmith@4.6.0 and spritesheet-templates@9.5.0. A gulp.spritesmith release will be occurring shortly.