Implementing fluid JS tile interface
Asked Answered
C

3

6

I'm building a photography website, and I want to create a nice "tiled" interface, which will look similar to the interface on new version MSN Money Now (note - the new version of the website can be viewed only on Windows 8 PCs) - http://t.money.msn.com/now/. I tried to implement this in Javascript.

Here is a sample page with prefilled data: http://photoachiever.azurewebsites.net/en

I created Tile groups - each 2 units high, 2 units wide, which can contain either one big square tile, two wide tiles or four small square tiles. Now, because I want the site to be responsive, I wanted to calculate on the fly in Javascript the optimal unit size, so that always 100 % of the space are filled and for wider screens are for example more columns visible and so on. It works the same way on MSN Money website, but there are two important differences:

1) When my images load the first time, I just see them in their highest resultion up until the point where all images are loaded and the JS is executed. The MSN Money web just displays a green area and the images later appear, already resized appropriately. 2) When I resize the window, it is far from fluid and the caluclations and mainly image resizing are very significantly visible. On MSN Money however the resizing is very smooth and even the images seem to just resize without a glitch. Also - they managed to make the fonts resize fluidly.

Could you please explain me, how the MSN Money website achieved these results? I have seen a few similare questions here on Stack Overflow, but they never required the equal width and height of individual tiles, which I really need for my design.

Bonus question: Could you please add some explanations of how to achieve responsive animated reflow of divs? Example found on http://www.brainyquote.com/ - when you change Window size, it reflows all quotes in an animated manner.

Edit: I'm attaching my current code, which is far from correct (preformance is very low and images appear too large first and their size drops a after they all download).

First part of the code (attaches all events to the tiles and adds animation on click):

function attachTileEvents() {
if ($(".tile-flow").size() >= 1) {
    $(window).resize(function () {
        delay(function () {
            resizeTiles();
        }, 100);
    });
    $(document).on("click", ".tile-flow .load-next-page", manualLoadContentDetection);
    $(window).on("scroll", scrollLoadContentDetection);
    $(document).on("touchend", scrollLoadContentDetection);
}
resizeTiles();
$(".tile .contents").each(function () {
    var tile = $(this).parent()[0]
    var mouse = { x: 0, y: 0, down: false };

    var maxRotation = 16;
    var minScale = 0.95;
    var setRotation = function (scaled) {
        //Rotations as percentages 
        var width = tile.offsetWidth;
        var height = tile.offsetHeight;
        var diag = Math.sqrt((width / 2) * (width / 2) + (height / 2) * (height / 2));
        var dist = Math.sqrt((mouse.x - (width / 2)) * (mouse.x - (width / 2)) + (mouse.y - (height / 2)) * (mouse.y - (height / 2)));
        var fract = 1.0;
        if (dist > 0) {
            fract = dist / diag;
        }
        var yRotation = (mouse.x - (width / 2)) / (width / 2);
        var xRotation = (mouse.y - (height / 2)) / (height / 2);

        if (scaled) {
            tile.style.webkitTransform = "rotateX(" + -xRotation * maxRotation + "deg)" + " rotateY(" + yRotation * maxRotation + "deg)" + " scale(" + (minScale + fract * (1 - minScale)) + ")";
            tile.style.mozTransform = "rotateX(" + -xRotation * maxRotation + "deg)" + " rotateY(" + yRotation * maxRotation + "deg)" + " scale(" + (minScale + fract * (1 - minScale)) + ")";
            tile.style.transform = "rotateX(" + -xRotation * maxRotation + "deg)" + " rotateY(" + yRotation * maxRotation + "deg)" + " scale(" + (minScale + fract * (1 - minScale)) + ")";
        } else {
            tile.style.webkitTransform = "rotateX(" + -xRotation * maxRotation + "deg)" + " rotateY(" + yRotation * maxRotation + "deg)";
            tile.style.mozTransform = "rotateX(" + -xRotation * maxRotation + "deg)" + " rotateY(" + yRotation * maxRotation + "deg)";
            tile.style.transform = "rotateX(" + -xRotation * maxRotation + "deg)" + " rotateY(" + yRotation * maxRotation + "deg)";
        }
    }
    var MouseDown = function (e) { mouse.x = e.offsetX; mouse.y = e.offsetY; mouse.down = true; setRotation(true); }
    var MouseUp = function (e) { if (mouse.down) { mouse.down = false; tile.style.webkitTransform = "rotateX(0deg)" + " rotateY(0deg) scale(1.0)"; tile.style.mozTransform = "rotateX(0deg)" + " rotateY(0deg) scale(1.0)"; tile.style.transform = "rotateX(0deg)" + " rotateY(0deg) scale(1.0)"; } }
    var MouseOut = function (e) { mouse.down = false; tile.style.webkitTransform = "rotateX(0deg)" + " rotateY(0deg) scale(1.0)"; tile.style.mozTransform = "rotateX(0deg)" + " rotateY(0deg) scale(1.0)"; tile.style.transform = "rotateX(0deg)" + " rotateY(0deg) scale(1.0)"; }
    var MouseMove = function (e) { mouse.x = e.offsetX; mouse.y = e.offsetY; if (mouse.down == true) { setRotation(false); } }
    $(tile).on("mousemove", MouseMove);
    $(tile).on("mousedown", MouseDown);
    $(tile).on("mouseup", MouseUp);
    $(tile).on("mouseout", MouseOut);
});}

And the main part - resizing:

var TileSizes = { wideWidth: 0, singleWidth: 0, margin: 0 };
function resizeTiles() {
var rowColumnNumber = 2;
var width = $(window).width();
if (width >= 2500) {
    rowColumnNumber = 7;
}
else if (width >= 2000) {
    rowColumnNumber = 6;
} else if (width >= 1600) {
    rowColumnNumber = 5;
} else if (width >= 1280) {
    rowColumnNumber = 4;
} else if (width >= 768) {
    rowColumnNumber = 3;
} else if (width >= 480) {
    rowColumnNumber = 2;
} else {
    rowColumnNumber = 1;
}
var totalWidth = $(".tile-flow").width() - 17; //compensate for the scrollbar
//calculate the margin size : 5% of the flow width
var margin = Math.round(totalWidth * 0.05 / rowColumnNumber);
var wideSize = Math.floor((totalWidth - margin * (rowColumnNumber - 1)) / rowColumnNumber);
var halfSize = Math.floor((wideSize - margin) / 2);
var quaterSize = Math.floor(halfSize * 2.5 / 3);
var heightSize = Math.floor(halfSize * 2 / 2.0);
var doubleHeightSize = heightSize * 2 + margin;
var detailsSize = quaterSize * 2 + margin;
TileSizes.wideWidth = doubleHeightSize;
TileSizes.singleWidth = heightSize;
TileSizes.margin = margin;
$(".big-square-tile").width(doubleHeightSize);
$(".big-square-tile").height(doubleHeightSize);
$(".wide-tile").width(doubleHeightSize);
$(".small-tile").width(halfSize);
$(".tile-flow .col .small-tile:even").css("margin-right", margin);
$(".small-tile").height(heightSize);
$(".wide-tile").height(heightSize);
$(".col").width(doubleHeightSize);
$(".col").css("margin-right", margin);
$(".col:nth-child(" + rowColumnNumber + "n)").css("margin-right", 0);
//all tiles get bottom margin

var how = 0;
$(".wide-tile .contents footer").each(function () {
    if ((how % 4 == 0) || (how % 4 == 1)) {
        $(this).width(TileSizes.singleWidth - 20);
    } else {
        $(this).height(75);
    }
    if (how % 4 == 0) {
        $(this).css("left", TileSizes.wideWidth);
    } else if (how % 4 == 1) {
        $(this).css("left", -TileSizes.singleWidth);
    }
    else if (how % 4 == 2) {
        $(this).css("top", TileSizes.singleWidth);
    } else {
        $(this).css("top", -95);
    }
    how = how + 1;
});

$(".big-square-tile .contents footer").each(function () {
    $(this).height(75);
    if (how % 2 == 0) {
        $(this).css("top", TileSizes.wideWidth);
    } else {
        $(this).css("top", -95);
    }
    how = how + 1;
});

$(".small-tile .contents footer").each(function () {
    $(this).width(TileSizes.singleWidth - 20);
    $(this).height(TileSizes.singleWidth - 20);
    if (how % 4 == 0) {
        $(this).css("left", TileSizes.singleWidth);
    } else if (how % 4 == 1) {
        $(this).css("left", -TileSizes.singleWidth);
    }
    else if (how % 4 == 2) {
        $(this).css("top", TileSizes.singleWidth);
    } else {
        $(this).css("top", -TileSizes.singleWidth);
    }
    how = how + 1;
});

$(".tile").css("margin-bottom", margin);
//resize images    
var imageList = Array();
$(".big-square-tile img").each(function () {
    imageList.push($(this));
    var img = new Image();
    img.onload = function () {
        var originalHeight = this.height;
        var originalWidth = this.width;
        var index = parseInt(this.id.replace("RESIZINGBIG", ""));
        if (originalHeight > originalWidth) {
            imageList[index].css("height", "auto");
            imageList[index].css("width", "100%");
        } else {
            imageList[index].css("height", "100%");
            imageList[index].css("width", "auto");
        }
    }
    img.id = "RESIZINGBIG" + (imageList.length - 1);
    img.src = $(this).attr('src');
});

$(".small-tile img").each(function () {
    imageList.push($(this));
    var img = new Image();
    img.onload = function () {
        var originalHeight = this.height;
        var originalWidth = this.width;
        var index = parseInt(this.id.replace("RESIZINGSMALL", ""));
        if (originalHeight > originalWidth) {
            imageList[index].css("height", "auto");
            imageList[index].css("width", "100%");
        } else {
            imageList[index].css("height", "100%");
            imageList[index].css("width", "auto");
        }
    }
    img.id = "RESIZINGSMALL" + (imageList.length - 1);
    img.src = $(this).attr('src');
});

$(".wide-tile img").each(function () {
    $(this).css("height", "auto");
    $(this).css("width", "100%");
});}

And here is a sample of how the HTML code looks now:

<div class="tile-flow">
    <div class="tile-row">
        <div class="col">
            <div class="tile big-square-tile">
                <div class="contents">
                    <img src="~/Images/Test/5.jpg" />
                    <footer>
                        <h1>Test</h1>
                        <span class="author">by Test</span>
                    </footer>
                </div>
            </div>
        </div>
        <div class="col">
            <div class="tile small-tile">
                <div class="contents">
                    <img src="~/Images/Test/2.jpg" />
                    <footer>
                        <h1>Test</h1>
                        <span class="author">by Test</span>
                    </footer>
                </div>
            </div>
            <div class="tile small-tile">
                <div class="contents">
                    <img src="~/Images/Test/3.jpg" />
                    <footer>
                        <h1>Test</h1>
                        <span class="author">by Test</span>
                    </footer>
                </div>
            </div>
            <div class="tile wide-tile">
                <div class="contents">
                    <img src="~/Images/Test/4.jpg" />
                    <footer>
                        <h1>Test</h1>
                        <span class="author">by Test</span>
                    </footer>
                </div>
            </div>
        </div>
        <div class="col">
            <div class="tile big-square-tile">
                <div class="contents">
                    <img src="~/Images/Test/6.jpg" />
                    <footer>
                        <h1>Test</h1>
                        <span class="author">by Test</span>
                    </footer>
                </div>

            </div>
        </div>
        <div class="col">
            <div class="tile wide-tile">
                <div class="contents">
                    <img src="~/Images/Test/1.jpg" />
                    <footer>
                        <h1>Test</h1>
                        <span class="author">by Test</span>
                    </footer>
                </div>
            </div>
            <div class="tile wide-tile">
                <div class="contents">
                    <img src="~/Images/Test/7.jpg" />
                    <footer>
                        <h1>Test</h1>
                        <span class="author">by Test</span>
                    </footer>
                </div>
            </div>
        </div>
</div>
</div>   
Champlain answered 18/4, 2013 at 14:8 Comment(1)
might look there for some profiling lessons: discover-devtools.codeschool.com additionally google io 2012 had some sessions on performance optimizationsNoachian
N
7

If I were you I would use Isotope for the basic layout and add the slide shows and click events along side it. You can insert most any content you like. jQuery Isotope.

Updated Working Model

Full page result

JS

$(function () {

    var $container = $('#container');

    $container.imagesLoaded(function () {
        $container.isotope({
            itemSelector: '.photo'
        });
    });
});


var $container = $('#container');
// initialize Isotope
$container.isotope({
    // options...
    resizable: false, // disable normal resizing
    // set columnWidth to a percentage of container width
    masonry: {
        columnWidth: $container.width() / 5
    }
});

// update columnWidth on window resize
$(window).smartresize(function () {
    $container.isotope({
        // update columnWidth to a percentage of container width
        masonry: {
            columnWidth: $container.width() / 5
        }
    });
});

//click function

    $(function () {
        $('.photo').click(function () {
            $(this).toggleClass('red');
        });
    });

//hover function

    $(function () {
        $('#photo1').hover(function () {
            $('#info1').fadeToggle();
        });
    });

Proof of concept- Animations inside Isotope

Note this animation is total kludge fine tune before using.

 function animatie() {
     var d = 0;
     for (var i = 0; i < 3; ++i) {
         var b = "#info" + i;
         $(b).css('background', 'silver');
         $(b).hide().delay(d).slideDown(1000).delay(3000).slideUp(1000);
         d += 5000;
     }
 }
 animatie();
 window.setInterval(animatie, 15000);

 $(function () {
     for (var i = 0; i < 3; ++i) {
         var z = '.box' + i;
         var divHeight = $(z).height();
         $(z).css('max-height', divHeight + 'px');
         $(z).css('max-height', divHeight + 'px');
         $(z).css('overflow', 'hidden');
     }
 });
 $(window).resize(function () {
     for (var i = 0; i < 3; ++i) {
         var z = '.box' + i;
         var divHeight = $(z).height();
         $(z).css('max-height', divHeight + 'px');
         $(z).css('overflow', 'hidden');
     }
 });

This is a very cool plugin for layout, sorting, and filtering. It will give you the tiles and animations as basic features.

Fluid Isotope

Images Loaded Plugin

Infinite Scroll

Added animation inside Isotope, Check Out the updated jsFiddles above

Nellanellda answered 23/4, 2013 at 0:8 Comment(11)
Thank you for this suggestion, it looks nice, but I'm still looking for a solution I could implement myself, without the overhead this big library has. I also require a tile animation - the tile has two sides, the second is always revealed after a timeout, slides from one of the sides of the tile.Champlain
The plugin also seems to have a little bit of problems with resizing and does not behave well on mobile devices. For these reasons, I'm more interested in a way to improve my current code or rebuild it so that the features I need will be there.Champlain
@MZetko Can you include related CSS and images, and or put together a jsFiddle of what you have so farNellanellda
Basically - what would be sufficient for me is to have those images load somehow later to stop them from showing up big the first moment and then find a way to get rid of the flicker that happens when the window is resized.Champlain
Yes, that gets me rid of the flickering, thank you very much. And do you have some idea about resizing flicker?Champlain
@MZetko What do you mean? I'm not seeing a flicker of the jsfiddles when they're re-sized.Nellanellda
Sorry sir, it was probably some kind of browser issue, now it works well. The only two things left I'm concered about are - will this approach work with my animations (from the sample) and second - when I resize the Window, in a particular resolution point, the images break up into two columns, without a reason. It does not look very well in that layout. I have noticed similar behavior on the Isotope webiste. Is that something that can be prevented?Champlain
@MZetko the 2 column issue was my mistake in the css, sorry about that. I had a 15px margin on .photo that needed to be removed. As far as your animations go, have you tried just dropping them in to the .photo div?Nellanellda
@MZetko Updated answer, with basic animation check out jsFiddle.Nellanellda
Thank you very much for your extensive help on this :-) .Champlain
@MZetko Its all good, I enjoy a challenge from time to time. I learned a lot from the exercise.Nellanellda
P
1

@MZetko I understand and respect your desire to implement it on your on.

@apaul34208 pointed in the right direction when he suggested Isotope. That's the term used for this kind of layout grid. You don't have to use jQuery, but it would be useful to look at how it does what it does to get it done... :)

I'm currently implementing a Wordpress site with the Studiofolio template, and even though I think it would have been fun to do it myself, I'm happy I spent those bucks on it. Now I can finish setting it up and go on to the next project. Cheers!

Philbrook answered 25/4, 2013 at 22:6 Comment(2)
I just noticed you'd like to have tile animation and are concerned with good support on mobile devices. This WP template is good at both, so take a look at the demo to see how it's done.Bollworm
I certainly like both suggestions, but I'm still more interested in a solution I could have full control over... This is not a project I'm doing for someone, but something I want to do right and as well as I possibly can.Champlain
H
1

1) Loading images

You should hide images by default. You can do this by setting their dimensions to 1px
(using display:none may cause loading troubles). At the end of your css:

div.tile img { width:1px; height:1px }

This will be overwritten by your tile specific styles on a per element basis after your calculations are done.

To have a background while they are loading you have to use a background color other than white :-)
(e.g. look at your div.wide-tile rule). At the end of your css:

.col .tile { background-color: #2440b2; }

This will have a greater specificity than your white backgrounds so it will override them.

To avoid flicker I would hide all the tiles until their initial position is know (I didn't modify your js).

[working demo] (make sure to use an empty cache)

2) Reisizing with animation

Basically you have to give up using floats for this to work and use absolutely positioned elements instead (floats cannot be animated).

After this you can simply use CSS transitions on the tiles (top, left) so when you recalculate their positions inside a resize event handler they will slide to their new positions with a nice animation.

-webkit-transition: left .5s, top .5s;
-moz-transition: left .5s, top .5s;
transition: left .5s, top .5s;

[working demo]

Hirsh answered 28/4, 2013 at 9:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.