Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Smooth scrolling of image tiles on large grid?

Long time listener, first time caller here.

Ok, I apologize for the length of this post, but I want to be very clear on what I'm trying to do and what I've tried to hopefully guide your answers.

The Goal: I am trying to create a grid of tiles (images) 100 x 100 and I will display only part of the grid at a time because the images are 240 x 120. By clicking buttons (note: no need for mouse scrolling, zooming, etc.), the user will scroll around horizontally and vertically.

As an analogy, take a checkers board that is set up to play and you are looking down on it. It would be quite simple to display, requiring only a few base images and putting them on a grid. Now, consider that you only wanted to show the bottom 1/3 of the board. Then when a user clicks up, they move their view to the middle and another click takes them to the top 1/3 of the board. This is a simple example of what I'm trying to accomplish except on a much larger grid with a smaller view / move.

What I've tried:

  1. First I wrote the code in PHP to draw the full 100x100 grid and everything looked great
  2. Then I altered the code to actually write files for each square, saving them as X-Y.png
  3. I got sucked into a world of map tools and zoomify type tools
  4. I had some partial success writing my own solution

Map tools / Zoomify Fail:

After getting the full map and tiles, I tried to figure out how the heck to scroll part of this window only. A light bulb went off: "what about something like how google maps does things!" That's when trouble started and I got sucked down the rabbit hole a bit researching all these map tools that really got me nowhere because, quite simply, they seem to be made only for displaying geographic maps.

I did, however, get led to Zoomify at one point or another and I thought it could really be an option. The first problem was I couldn't seem to get it to take my own tiles without one shot of the full image, so I tried every screen capture program under the sun to try getting a full shot of my browser with the full grid to let zoomify make them. Let's just say, that didn't work, but i tried with reduced size. It sort of worked, but lost a lot of quality and I realized that zoomify doesn't even really accomplish what I need because 1. the scroll is not that smooth; and 2. these images will eventually contain some links connected with their X-Y coords, which means I need control over how much is scrolled each time the up, down, left, right arrows will be clicked.

My Not so failed attempt to do it myself

Ok, then I got back to the basics and I threw the grid in a DIV with overflow:hidden on the CSS. Great, now I had a window. I didn't want to load all of these images at once, though, so I drew only part of the grid in absolutely positioned DIVs with unique ID's (e.g. Tile_1_1). This was all looking great, but now I needed to make it appear that you are scrolling around the grid as you click the arrow buttons, so I threw it up to a javascript function for this. I had the javascript calculate the new X and new Y and swap the Image source of all the tiles since they are all named after their X/Y coordinates on the grid. Actually, this totally worked and now the map is scrolling. The problem is, it's a bit choppy to just change the image sources. There is no illusion that we are moving around this grid as much as the content is just instantly changing. and THIS my friends is where I need our help.

Where did I go wrong? Was I on the right track with this latest attempt? Do I need to totally rethink this or is there some simple fix for moving the tiles a little more elegantly than swapping the source?

Please keep in mind that I've gotten quite good with php, css, etc., but I never really invested much time in javascript, so you might need to do a little more explaining in that area.

P.S. this was the closest thing I could find on these boards, but it never really got to a clear answer: Tile scrolling / preloading (Google Maps style) of HTML layers with Ajax

Update: Solution Used

I liked both Cobby and pseudosavant's answers. Pseudosavant's solution was really right up my ally with letting CSS do most of the hard work, however I went with Cobby's solution. I thought it would give me a little more control and because I'm actually using a diamond grid, I could wrap my head around it better (not saying the other one wouldn't necessarily work also).

Here is what I did. I know the code is a little crude and needs some cleanup, but maybe it can help someone.

First I needed to find the relationship between my X and Y axis, so I drew myself a little diagram.

enter image description here

I quickly noticed that more X meant starting the diamond both farther left and higher to get the right coordinates in the center of the view (user defined start) and then offset by the Y coordinates to take off some of the negative left and lower the starting point.

Then, the PHP to draw the grid looks something like this ($xcoord and $ycoord come from a form):

//draw the grid
//Offset leftStart and topStart by Xcoord
$leftStart = 360 - ($xcoord * 120);
$topStart = 360 - ($xcoord * 60);

//Offset leftStart and topStart by Ycoord
$leftStart += ($ycoord * 120);
$topStart -= ($ycoord * 60);

for($y = 1; $y <= 99; $y++) { //y min to y max
    $left = $leftStart;
    $top = $topStart;
    for($x = 1; $x <= 99; $x++) { //x min to x max
       //I only want to draw part of the square
       if(($x < ($xcoord + 6)  && $x > ($xcoord - 6)) && ($y < ($ycoord + 6)  && $y > ($ycoord - 6))) {
          //Put out the image - this is how i needed mine formated
          echo "\t<div class=\"move\" id=\"T" . $x . "_" . $y . "\" style='position:absolute; left:" . $left . "px; top:" . $top . "px;'>\n";
          echo "\t\t<img src=\"./<path to your image>" . $x . "-" . $y . ".gif\">\n";
          echo "\t</div>\n";
       }
       $left = $left + 120;
       $top = $top + 60;
    }
$leftStart = $leftStart - 120;
$topStart = $topStart + 60;
}

Throw a DIV around that with overflow:hidden and give it an ID (mine is #tileView). OK, now you have a diamond grid drawn inside your view and now we need to start moving it!

This is how my jquery looks. Sorry if it needs improvement, I just learned Jquery and Ajax yesterday.

<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
$(document).ready(function(){
    $("#top").click(function(){
        var y = document.getElementById('ycoord').value;
        var x = document.getElementById('xcoord').value;
        $(".move").animate({"left": "-=120px", "top": "+=60px"}, 500);
        changeImg(x, y, 'up');
        $("#ycoord").val(parseInt( y ) - 1);
    });     
    $("#bottom").click(function(){
        var y = document.getElementById('ycoord').value;
        var x = document.getElementById('xcoord').value;
        $(".move").animate({"left": "+=120px", "top": "-=60px"}, 500);
        changeImg(x, y, 'down');
        $("#ycoord").val(parseInt( y ) + 1);
    });     
    $("#left").click(function(){
        var y = document.getElementById('ycoord').value;
        var x = document.getElementById('xcoord').value;
        $(".move").animate({"left": "+=120px", "top": "+=60px"}, 500);
        changeImg(x, y, 'left');
        $("#xcoord").val(parseInt( x ) - 1);
    });
     $("#right").click(function(){
        var y = document.getElementById('ycoord').value;
        var x = document.getElementById('xcoord').value;
        $(".move").animate({"left": "-=120px", "top": "-=60px"}, 500);
        changeImg(x, y, 'right');
        $("#xcoord").val(parseInt( x ) + 1);
    });

function changeImg(x, y, move){
    $.ajax({
        type: "POST",
        url: "addImage.php",
        data: 'x=' + x + '&y=' + y + '&move=' + move,  
        cache: false,
        success: function(html){
            $(html).appendTo($("#tileView"));
        }
    });
    $.ajax({
        type: "POST",
        url: "removeImage.php",
        data: 'x=' + x + '&y=' + y + '&move=' + move,  
        cache: false,
        success: function(xml){             
            $("removeTile",xml).each(function(id) {    
            removeTile = $("removeTile",xml).get(id);
            removeID = $("tileID",removeTile).text();
            $("#" + removeID).remove();
        });
        }
    });
}
});
</script>

The ajax calls addImage.php to add a new row on the right spot and deleteImage.php to delete the row that's now out of view (i found without deleting those rows that things started running pretty slow).

In add image you just figure out the new position and output 's for the html to add in the page. For example part might look like this (but set the variables depending on move then go through the loop):

if($_REQUEST["move"] == "up") {
    $xmin = $x - 5;
    $xmax = $x + 5;
    $y = $y - 6;

    $left = 360;
    $top = -360;

    for($i = $xmin; $i <= $xmax; $i++) {
        $id = "T" . $i . "_" . $y;
        $newTiles .= "<div class='move' style='position:absolute; left:" . $left . "px; top:" . $top . "px;' id='$id'><img src='./Map/TileGroup1/1-$i-$y.gif'></div>\n";
        $left += 120;
        $top += 60;
    }
}

then just echo $newTiles out for your script to use.

Delete is similar, just putting out in xml the DIV ID's to delete because i couldn't get each() to work otherwise.

if($_REQUEST["move"] == "up") {
    $xmin = $x - 5;
    $xmax = $x + 5;
    $y = $y + 5;

    echo "<?xml version=\"1.0\"?>\n";  
    echo "<response>\n";  
    for($i = $xmin; $i <= $xmax; $i++) {
        echo "\t<removeTile>\n";
        echo "\t\t<tileID>T" . $i . "_" . $y . "</tileID>\n";
        echo "\t</removeTile>\n";
    }
    echo "</response>"; 
}

Hope this helps someone. Thank you all for the ideas and support!

like image 485
MikeCruz13 Avatar asked Jan 31 '12 01:01

MikeCruz13


3 Answers

Here is the solution I have used before.

  1. The tiles should be a <div> that is set to be inline-block with the <img> inside it. Control the width and height of the title via the <div>. *It is important that there isn't an white-space between the <div> elements otherwise the tiles won't be butted up next to each other.
  2. The tiles should be in a #container <div> that has line-height 0. The container should be set to some multiple of the tile sizes as the tiles will automatically line wrap. So 20 tiles could be a 4x5 or 5x4 depending on the size of the container.
  3. The #container should be in another <div> with overflow: hidden that I call the viewport. You can scroll the #viewport using jQuery and .scrollLeft .scrollTop.

Here is a jsfiddle example: http://jsfiddle.net/pseudosavant/b4hSV/

like image 82
pseudosavant Avatar answered Oct 02 '22 14:10

pseudosavant


I think you need to go back to the approach of the whole grid being nested in a DIV with overflow:hidden but use JavaScript to figure out which images should be visible and only load those images in as needed, then load in additional relevant images as the user scrolls to that area.

Scrolling of course should just manipulate the left and top CSS properties for your grid to move it with in the containing overflow: hidden DIV. In your case, it would be easiest to being by just changing the positioning CSS properties in increments of your tile size (so you never have portions of tiles showing).

From a programming standpoint this is a bit of a pain to do but it sounds like you've already got a method to calculate which ones are/should be displaying?

With this method, it will have that initial glitch as images are being loaded for the first time, but subsequent moving of the grid won't have to reload the same images. Once you've got this working, you can use jQuery or similar to animate the left and top properties to give a smooth scrolling effect.

Then, if you want, you can build on this further and allow partial scrolling of tiles and click-and-drag scrolling like Google Maps. Also if you want to work on performance, I'd recommending setting up an nginx server that listens on a few sub-domains of your site to serve up the tile images.

EDIT: I recommend animating scrollLeft and scrollTop on the container div (that has overflow: hidden) instead, will give much better performance/smoother scrolling.


There is a neat program called TileMill, while the OP deidn't use it it could be applicable for other people.

like image 40
Cobby Avatar answered Oct 02 '22 16:10

Cobby


You can only give a smooth animation experience if your animation is time-based rather than position/frame count based. I would start by looking at requestAnimationFrame (or proper, time-based polyfills for it) and devise your animation/positioning calculations to be time-based. The question would be:

given time t what should the x and y scroll offsets of my map be?

Then think of using the illusion of having a viewport into a massive tile set, rather than actually having a massive tile set that uses overflow: hidden to peek through a small viewport. You don't want to exhaust the DOM with too many tiles.

Let's say your viewport allows for the viewing of 4 x 4 tiles, which makes up for a maximum of 5 x 5 tiles considering partially displayed tiles when the tile offset don't snap to your viewport (which will be the case most of time). 5 x 5 tiles will be all you'll ever needm regardless of how many tiles your map actually has. Here's a sketch:

http://jsfiddle.net/6uRHD/

The task will be to:

  1. Calculate acceleration / velocity / position based on user actions (scroll, nudge with damping) and then compute the position at time t of your animation.
  2. Calculate which tiles fall into the 5 x 5 tile set based on the position and also calculate the viewport offset for these tiles.
like image 40
Ates Goral Avatar answered Oct 02 '22 15:10

Ates Goral