Monday, November 22, 2010

Scrolling in JavaScript

With the app I've been working on I'd like to be able to have a map that is larger than what fits on the screen for the icons to move around in. This can be done straightforwardly by putting scrollbars around the canvas element. In addition, I'd like for the window to always show the icon while it is moving, which means it may have to autoscroll. Here's how I did that.

HTML Scroll Bars
Here is what the HTML looks like that contains the canvas:
<div id="windowContainer">
  <canvas class="gridLayer" id="gridLayer" width="800" height="500"></canvas>
  <canvas class="gridLayer" id="drawLayer" width="800" height="500"></canvas>
  <canvas class="gridLayer" id="iconLayer" width="800" height="500"></canvas>
  <canvas class="gridLayer" id="controlLayer" width="800" height="500"></canvas>
</div>
and here is what the styles for these CSS classes look like:
#windowContainer {
  position: relative;
  width : 500px;
  height: 300px;
  overflow: auto;
}
.gridLayer {
  position: absolute;
  top: 0px;
  left: 0px;
}
There are two keys to making this work. First the surrounding container (windowContainer) has smaller dimensions (500x300) than the canvas (800x500). The second is the style overflow: auto which is applied to the windowContainer. This makes the scroll bars appear since the contained content (the canvases) are larger than the windowContainer.

You may have noticed that I applied the width and height attributes directly to the canvas object rather than to the style. The reason is that the width and height define the logical dimensions of the canvas, which I use throughout the code, while the style sheet defines the size it appears on the screen. Since I want those two to be the same, I define the width/height on each tag rather than on the CSS style.

JavaScript Scrolling
To make sure that a moving icon is always in view, I created a WindowScroller object. This object takes an icon that is to be kept in view and the window with the scroll bars. I want the icon to be partially centered, so this object also has a border which is how far the icon should be kept from the edge of the window. The WindowScroller object is responsible for scrolling the window appropriately anytime the icon moves.

The challenge I had was how to detect when the icon has moved. Using my Java instincts, I was going to have an IconMoveListener of which the WindowScroller would be one, and each Icon would have a set of these listeners that have subscribed to watch for move events. There are two problems with this approach. The first is that it is more complicated than what I need. The second is that it is buggy. If you'll recall, I had made the decision to decouple the logical moving of the icon from the drawing of the icon. If I scroll the window when the icon is moved, the icon will appear to move again when it is redrawn, and this causes for very jittery movement. To solve this problem I started to come up with more complicated approaches, but then a simple solution occurred to me.

Rewrite Draw
I really don't care when an Icon is logically moved, all I care about is that anytime it is drawn, it should be visible. To accomplish this I overwrote the icon's draw method with one that scrolls. Here's what it looks like:
  1 function WindowScroller(icon, window, border) {
  2   var origDraw = icon.draw; 
  3   icon.draw = function() { 
  4     var left = window.scrollLeft(); 
  5     left = Math.min(left, icon.realPoint.x - border); 
  6     left = Math.max(left, icon.realPoint.x - window.width() + icon.iconLayer.grid.size + border); 
  7  
  8     var top = window.scrollTop(); 
  9     top = Math.min(top, icon.realPoint.y - border); 
 10     top = Math.max(top, icon.realPoint.y - window.height() + icon.iconLayer.grid.size + border); 
 11  
 12     window.scrollLeft(left); 
 13     window.scrollTop(top); 
 14     origDraw.call(icon); 
 15   } 
 16 }

On line 2 I save the original draw method.  Line 3 saves the new draw method. On line 5 I make sure that the left point of the window is far enough left to include the icon and the border. On line 6 I make sure that the left point of the window is also far enough right to include the icon and the border. Lines 8-10 make the same calculation but with the top and bottom of the window. Lines 12 and 13 actually scroll the window and line 14 calls the original draw method.

The scrollLeft and scrollTop methods that are called on lines 12 and 13 are jQuery functions. I use jQuery for this to increase the cross-browser compatibility.

Why a Class With No Methods?
One thing that strikes me as funny is that I've created a class that just has a single method - its constructor. This smells like it should be a method in another class rather than its own class. I originally had it in its own class because I assumed I would need other helper methods. I've left it in its own class because I don't know where to put it. It doesn't belong in the Icon class because the Icon really shouldn't know about the window the canvas is in or things like that. None of the other classes feel appropriate either, since it doesn't use any other class. Hence the decision to leave it in its own class. Maybe at a future date either more functionality will go in this class or a better class to fold this in to will present itself.

Demo
Well, without further ado, here is the demo. I suggest scrolling to the right and clicking on an empty grid square and seeing what happens.

       

No comments: