DHTML 2-D Scrolling Engine - Part II - Smooth Scrolling
Oct 18th, 2007 by gatoloc0
Yup,
Here we are (finally!) with the second part of our scrolling tutorial.
If you got the first part, this should be easy since it can be considered just a ‘fine tuning’ of previous code.
We are going to modify our scrolling engine so it is able to scroll ’smoothly’ just like on most 8-bit video games.
Intro
Smooth scrolling is just that, a scrolling operation where the content been shown by the Viewport is slightly moved onto the desired scrolling direction.
Since a demo is worth a thousand words, here is an example of what is not a smooth scrolling effect (if you missed it from the previous tutorial).
If you can’t wait for a smooth scrolling example, just go to the bottom of this page and click the ‘RunMe’ button, otherwise keep reading…
First try
Ok, now you are asking:
How is it possible to achieve the smooth scrolling effect if my map tiles are 32×32 pixels?
If I need to scroll my map I must do it by 32 pixels!
Ok, this is true. But we can easily think of a solution (of course we can since “we is experts”!).
If 32 pixels are so much for smooth scrolling, we can reduce the tile size (say 4×4) and we are done (!).

Little problem
about this “great” idea.
There are at least two big problems with this method.
The first thing is that managing tiles being so small is graphically insane.
The second really big problem is that our DHTML engine will never be able to handle such a big number of tiles (10×10 VS 80×80)
Oh, my! How on earth it is possible to achieve a decent DHTML scrolling?!
The idea
The solution is relatively simple. We can achieve the same effect by showing the viewer something that “looks like a smooth scrolling” map but “is not”.
We need to change our code a little.
First we increment our map size by two columns and two rows.

Second, we add a little magic.
Now that we have a bigger map consider this image.

This is how our scrolling engine works. From State 0, when the user press the right key, we immediately show the new column on the right side.
This bring us to State 4
Now, we add some intermediate points between State 0 and State 4 obtaining this:

This explains how the smooth scrolling technique is achieved.
State 1
When the user press the right key, instead of immediately showing the new tile column on the right side, as we did on State 4, we Translate our Viewport to the left by a few pixels.
State 2
The Viewport has been translated horizontally.
The cyan area indicated by B is how much our viewport has moved to the left. Note that a portion of the previously grayed area is now visible on the right side while the opposite area on the left side (previously visible) is now hidden.
State 3.
We keep moving our Viewport to the left. Note that A represents how many horizontal pixels we still have before going into State 4.
So, when the user press the right key and B becomes >= than A, we reset the left translation so B becomes 0, like on State 1, and then we switch to State 4.
A working example
To help you understand this concept, try this demo.
As you can see, the smooth scrolling effect is only applied when you press the right key.
The result is a little strange since the grayed area represented in the images above is not hidden.
We obtain this scrolling effect by slightly changing our Previous code.
First we add a new child div called ‘viewport’ that will contain the visible map area.
<div id="mappa" style="position:absolute;left:100px;top:80px;height:100px; width:320px;height:320px;display:block;"> <div id="viewport" style="position:absolute;left:-32px;top:-32px;"></div> </div>
Now, instead of creating our Viewport inside the ‘mappa’ div, as we did before, we use the new ‘viewport’ div.
map.createViewPort('viewport');
While doing smooth scrolling, all translations will be applied to this new div.
Then we modify our Key listener object, introducing smooth scrolling for the right key
/***************************************************************** ******** KEY OBJECT *****************************************************************/ var key=function() { var LEFT=37; var UP=38; var RIGHT=39; var DOWN=40; return { capture:function(e) { switch(e.keyCode) { case LEFT :if (map.x){map.x--;map.paint(map.x,map.y)};break; case RIGHT : if (Math.abs(map.sx)<map.tw){ map.sx-=map.ss; //move the 'viewport' div to the left } else { if (map.x<map.limitx){ map.x++;map.paint(map.x,map.y); //paint new content map.sx=0; //reset X offset }; } map.move(map.sx,map.sy); //this changes the 'viewport' div position break; case UP :if (map.y){map.y--;map.paint(map.x,map.y)};break; case DOWN :if (map.y<map.limity){map.y++;map.paint(map.x,map.y)};break; } } } }()
where map.move is (parent is the ‘viewport’ div):
move: function(sx,sy) { parent.style.left=(_ox+sx)+'px'; parent.style.top=(_oy+sy)+'px'; },
and:
map.tw= TILE_WIDTH map.ss= 2 = scrolling speed map.limitx= WIDTH-VIEWPORT_WIDTH-2 map.sx = current smooth scrolling x map.sy = current smooth scrolling y
Now we can hide the grayed area represented in the images above by applying ‘overflow:hidden’ to the ‘mappa’ div:
<div id="mappa" style="position:absolute;left:100px;top:80px;height:100px; width:320px;height:320px;display:block;overflow:hidden"> <div id="viewport" style="position:absolute;left:-32px;top:-32px;"></div> </div>
And we obtain this (note that smooth scrolling has been also added to the left key)
Ta daaaah! Smooth Scrolling!
We just need to apply the new ’smooth code’ to all cursor keys, adjust some vars and we are ready to go.
Code example I
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <script> /***************************************************************** ******** gfzSprite OBJECT *****************************************************************/ var gfzSprite=function(sId,bx,by,bw,bh) { this.constructor.all[sId]=this; //all collection this.width=bw; this.height=bh; this.background='transparent url('+this.constructor.imageFile+') -'+bx+'px -'+by+'px no-repeat'; } gfzSprite.imageFile=''; gfzSprite.all=[]; /***************************************************************** ******** MAP OBJECT *****************************************************************/ var map=function() { // 0 1 2 3 var spr=['glo2_wall01','glo2_terrain01','glo2_stair01','glo2_wall02', // 4 5 'glo2_terrain02','glo2_door01']; var dat=[ 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3, 3,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,5,0,3, 3,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,3, 3,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,3, 3,0,0,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,3, 3,0,0,0,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,3, 3,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,3, 3,0,0,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,3, 3,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,3, 3,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,3, 3,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,3, 3,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,3, 3,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,3, 3,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,3, 3,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,3, 3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3 ]; var viewport=[]; var WIDTH=20; var HEIGHT=dat.length/WIDTH; var VIEWPORT_WIDTH=10; var VIEWPORT_HEIGHT=10; var TILE_WIDTH=32; var TILE_HEIGHT=32; var parent=null; var SS=4; //scrolling speed var _ox=0,_oy=0; //store viewport position return { ss:SS, tw:TILE_WIDTH, th:TILE_HEIGHT, limitx:WIDTH-VIEWPORT_WIDTH-2, limity:HEIGHT-VIEWPORT_HEIGHT-2, x:0, y:0, sx:TILE_WIDTH, //current smooth scrolling x sy:TILE_HEIGHT, //current smooth scrolling y createViewPort: function(sContainer,sParent) { var container=document.getElementById(sContainer); parent=document.getElementById(sParent); //set global parent var //init container style container.style.width=TILE_WIDTH*VIEWPORT_WIDTH+'px'; container.style.height=TILE_HEIGHT*VIEWPORT_HEIGHT+'px'; container.style.display='block'; container.style.overflow='hidden'; //init parent style parent.style.position='absolute'; parent.style.left=-TILE_WIDTH+'px'; parent.style.top=-TILE_HEIGHT+'px'; _ox=parent.offsetLeft; _oy=parent.offsetTop; var ix=0; var iy=0; var zEnd=VIEWPORT_WIDTH+2 var yEnd=VIEWPORT_HEIGHT+2; for (var z=0;z<zEnd;z++) { for (var y=0;y<yEnd;y++) { var d=document.createElement('div'); var ds=d.style; ds.position='absolute'; ds.left=ix+'px'; ds.top=iy+'px'; ds.display='block'; ds.width=TILE_WIDTH+'px'; ds.height=TILE_HEIGHT+'px'; parent.appendChild(d); ix+=TILE_WIDTH; viewport.push(d); } ix=0; iy+=TILE_HEIGHT; } }, move: function(sx,sy) { parent.style.left=(_ox+sx)+'px'; parent.style.top=(_oy+sy)+'px'; }, paint: function(oriX,oriY) { var idx=0; var yEnd=oriY+VIEWPORT_HEIGHT+2; var xEnd=oriX+VIEWPORT_WIDTH+2; var ga=gfzSprite.all; var mapSprite=spr; var map=dat; for (var y=oriY;y<yEnd;y++) { for (var x=oriX;x<xEnd;x++) { viewport[idx].style.background=ga[mapSprite[map[x+y*WIDTH]]].background; idx++; } } } } }() /***************************************************************** ******** KEY OBJECT *****************************************************************/ var key=function() { var LEFT=37; var UP=38; var RIGHT=39; var DOWN=40; return { capture:function(e) { switch(e.keyCode) { case LEFT: if (map.sx<map.tw){ map.sx+=map.ss; } else { if (map.x){ map.x--;map.paint(map.x,map.y); map.sx=0; }; } map.move(map.sx,map.sy) break; case RIGHT: if (map.sx>-map.tw){ map.sx-=map.ss; } else { if (map.x<map.limitx){ map.x++;map.paint(map.x,map.y); map.sx=0; }; } map.move(map.sx,map.sy) break; case UP: if (map.sy<map.th){ map.sy+=map.ss; } else { if (map.y){ map.y--;map.paint(map.x,map.y); map.sy=0; }; } map.move(map.sx,map.sy) break; case DOWN: if (map.sy>-map.th){ map.sy-=map.ss; } else { if (map.y<map.limity){ map.y++;map.paint(map.x,map.y) map.sy=0; }; } map.move(map.sx,map.sy) break; } } } }() function init() { gfzSprite.imageFile='/wordpress/wp-content/uploads/2007/07/glo2.png'; new gfzSprite('glo2_wall01',107,76,32,32); new gfzSprite('glo2_terrain01',139,108,32,32); new gfzSprite('glo2_wall02',14,143,32,32); new gfzSprite('glo2_terrain02',46,143,32,32); new gfzSprite('glo2_door01',46,177,32,32); new gfzSprite('glo2_stair01',139,236,32,32); map.createViewPort('mappa','viewport'); map.move(map.sx,map.sy); map.paint(0,0); window.onkeydown=function(e){key.capture(e);return false;} } window.onload=init; </script> </head> <body> Use cursor keys to scroll the map. <div id="mappa" style="position:absolute;left:100px;top:80px;"> <div id="viewport"></div> </div> </body> </html>
OK, that’s it. As usual, if there’s something you want to know about the code just ask me.
In the next tutorial I will try to introduce NPC’s and some game basics.
I think this will be interesting…
Ciao!
Hi,
Just discovered your tutorials and I must say, I really like them. It must have take you a lot of effort to put all this usefull info online. I once made a tile scroller in director and was thinking of doing it all over again in javascript. That’s how I found your site.
I was wondering why you use the div background technique and not the element. Did you test the speed difference between divs and canvas? (Would be great to see the difference in case you did
I was wondering how you would solve blocked possitions. If for example you would walk up to a stone, would you then check the next tile for being a stone, or would you add a next level to your array stating that possition xy is blocked?
Best regards,
Westworld
ps I noticed that the two examples on this page didnt run very smooth (quad 2,4 running Firefox 2,0)
Westworld,
Thank you! I really appreciate your comments. Your words repays me for all the hard work :).
About your questions:
Q: I was wondering why you use the div background technique and not the element.
R: Sorry, but I didn’t catch this one.. What do you mean exactly by ‘and not the element’?
Q: Did you test the speed difference between divs and canvas? (Would be great to see the difference in case you did
)
nice point.. i will talk about canvas and collision detection on my next tutorial
Q: I was wondering how you would solve blocked possitions. If for example you would walk up to a stone, would you then check the next tile for being a stone, or would you add a next level to your array stating that possition xy is blocked?
R: He he
Well, yes.. speed varies greatly depending on the graphic card, browser and system config…
The main problem is seen when the viewport coordinates are reset to 0,0 (a little flicker occurs)
I will add a new user configurable scrolling page so we can build a sort of ‘best default settings’.
ciao!
i love your tutorials
keep them coming!
Awesome tutorial! I’m working on a similar project actually, which has smooth scrolling too. I’ve also noticed a lot of slowdown with my animation in firefox 2.0. There have been some articles written about the garbage collector slowing it down, which is apparently fixed in firefox 3. Keep up the good work! Your project is really cool.