digital expertise network :: about the guild :: case studies :: contact the guild :: articles :: second life :: tools

Dealing with images in content management systems (page 4)

Navigation: Page 1 | Page 2 | Page 3 | Page 4 | Page 5 | Samples

(Download the source code for this article)

Client Side Canvas Script

The key point to remember is that the user's selection is just a transparent div element with a border set to a dashed line, and absolutely positioned to appear to float over the canvas image.

The job of the canvas script is to:

  • Set all the elements up in the correct positions on the page
  • Initialise the selection div in a suitable default position and size, at the correct aspectRatio if both ImageWidth and ImageHeight were specified on the control
  • Respond to the user's mouse moving and clicking, such that:
    • When the mouse pointer is near the corners or edges of the selection, the cursor changes to a resize icon indicating a resize can be performed in the appropriate direction
    • When the mouse pointer is inside the selection and away from the edge, the cursor changes to a move icon
    • When the mouse pointer is near the corners or edges of the selection, clicking and dragging will have the effect of resizing the selection in an intuitive natural way
    • When the mouse pointer is inside the selection, clicking and dragging should move the selection.
    • If the user does resize the selection and as aspectRatio is being enforced, the script should ensure that the selection is maintained at the correct aspect ratio even though the user's mouse movements would otherwise change the ratio. This constraining of the selection is familiar from image editing packages – the script should ensure that the selection behaves in an intuitive way as the user drags the edges to resize.
  • Store the position and size of the selection relative to the canvas prior to the page being resubmitted

When the page loads the initialise(..) method is called, with parameter values that the control wrote out in the prerender phase described earlier.

First of all, references to all the html elements we need to manipulate are stored in variables for ease of use:

... oCanvas = document.getElementById(canvasID); oSelection = document.getElementById(selectionBoxID); ...

Then, we resize and position the popupDiv element so that it is a little wider than the canvas image and inset from the top and left of the viewport. We also store the position and size of the canvas itself for later use:

canvasRect = rectangle(oCanvas);

The rectangle function needs some explaining. It is used to represent the position and size of an element without having to continually access its style properties (e.g., oElement.style.left). It also allows us to capture a snapshot of an element that might be changing. This is particularly useful when dealing with the user's selection. Rather than continuously adjusting the size and position of the selection div, we use a more abstract object to represent the dimensions and location of the selection (or any other element):

// returns an object that represents the size and position of the element oBlock function rectangle(oBlock) { return { x: parseInt(oBlock.style.left), y: parseInt(oBlock.style.top), w: parseInt(oBlock.style.width), h: parseInt(oBlock.style.height) }; }

This method returns an object that has w, h, x, and y properties. It is analogous to the System.Drawing.Rectangle we were using earlier. At the beginning of a sequence of operations on the selection, we can obtain a new rectangle object to represent a snapshot of the current selection. We can then scale, move and constrain this representation by altering its x, y, w and h properties and when we're done, call setSelection with our altered rectangle to apply its new position and location to the selection div:

// applies the size and position information in the // rectangle rect to the selection div function setSelection(rect) { oSelection.style.left = rect.x + "px"; oSelection.style.top = rect.y + "px"; oSelection.style.width = rect.w + "px"; oSelection.style.height = rect.h + "px"; }

This means we are not continuously changing the style properties of the selection element itself, which might confuse the issue when resizing.

Back in the initialise(..) method, we continue to set up the page by position and sizing the selection appropriately.

if(iReqdWidth > 0 && iReqdHeight > 0) { bConstrain = true; aspectRatio = iReqdWidth / iReqdHeight; }

bConstrain is a global variable that is checked when the user resizes the selection to see if the selection should be "tweaked" to ensure that the correct aspectRatio is maintained. This demonstrates the use of rectangle:

var selection = rectangle(oSelection); constrain(selection); setSelection(selection);

We store a snapshot of the selection in the variable selection, then call constrain(..), then apply the dimensions of the rectangle back to the "real" selection.

The constrain(..) function will be described shortly.

To finish off the initialisation we hook up some event handlers for mouse events:

// now hook up some event handling: document.onmousemove = move; document.onmouseup = up; document.onmousedown = down; oCanvas.ondrag = function(){return false;} oSelection.ondrag = function(){return false;} document.ondrag = function(){return false;}

We need the mouse event handlers to be on the whole document rather than just the selection or the canvas because in a dragging or resizing operation the user might move the mouse outside of the canvas or even the popupDiv as part of normal use. This is especially true when an aspect ratio is being enforced and the image is being constrained as it's being resized, as the point on the selection the user first grabbed might not stay in sync with the user's mouse as the selection dimensions are constrained by the script. The final three event handlers are essential to cancel any drag events. The browser might otherwise handle these by appearing to select those parts of the page the user moves the mouse over while moving or resizing the selection. This would give a very messy user experience.

Moving the mouse

As the user moves the mouse, the move(e) function will be called. Two global variables, bMoving and bResizing, keep track of whether the script has decided the user is moving or resizing the selection. They will be set in response to a mousedown event. Initially both these will be false, indicating the user is just moving the mouse around. The bulk of the move function is a three condition if statement, the last of which is the "just moving the mouse around" condition. We will come back to the first two, which handle moving and resizing, in a moment.

... else { resizeXMode = ""; resizeYMode = ""; var targetSize = 15; if(p.x >= selection.x && p.x <= (selection.x + selection.w) && p.y >= selection.y && p.y <= (selection.y + selection.h)) { // cursor is inside the selection // default to move behaviour oSelection.style.cursor = "move"; oCanvas.style.cursor = "move"; bCanMove = true; // further checks for hotspots omitted... ...

The two global variables resizeXMode and resizeYMode keep track of what sort of resizing the user would be allowed to perform given the current mouse position. They vary independently. The resizeXMode variable can be either "E"(ast) or "W"(est) and resizeYMode can be either "N"(orth) or "S"(outh). As the user moves the mouse around the script in this else block will set these two variables.

They are both blanked at the start of the block, and then we check to see whether or not the mouse pointer is inside the selection. If so, we set bCanMove to true, indicating that the user would be allowed to initiate a move operation from this point. We also set the cursor to the move symbol. We then further check to see if the user is in one of eight "hotspot" areas from which a resize operation could be started and if so, set the resizeXMode and resizeYMode variables accordingly. We also set the cursor symbol to point in the appropriate direction. The sizes of these hotspots are governed by the targetSize variable, which here is set at 15 pixels.

The rest of this condition is a lot of tedious code that finds out which of these hotspots the cursor is over, and sets the resizeXMode and resizeYMode variables, and the cursor icon, appropriately.

mouseup and mousedown

The down(e) and up() functions handle the mouse being pressed and released. Throughout this script we make heavy use of the positions at which mousemove and mousedown events occur. To make this easier to code against we use a function which returns an object that represents the position at which an event occurred:

// returns an object that represents the position relative to // popupOrigin that event e occurred. function point(e) { if (e.pageX || e.pageY) { this.x = e.pageX; this.y = e.pageY; } else if (e.clientX || e.clientY) { // need to use both document.body and document.documentElement to // cater for various combinations of IE 5/6 in normal and quirksmode this.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; this.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; } // give the point position relative to the popup this.x -= popupOrigin.x; this.y -= popupOrigin.y; }

This can be seen in the down(e) event handler that follows. Note that the first line allows the event to be handled in a cross-browser manner as discussed on quirksmode here: http://www.quirksmode.org/js/events_access.html

function down(e) { if (!e) var e = window.event; downPoint = new point(e); originalRect = rectangle(oSelection); if(bCanMove && resizeXMode == "" && resizeYMode == "") { bMoving = true; } if(resizeXMode != "" || resizeYMode != "") { bResizing = true; } return false; } function up() { bMoving = false; bResizing = false; }

When the user presses and holds the mouse button, we first of all store the position of the mouse at which the down event occurred:

downPoint = new point(e);

This will be used later to determine how far the mouse has moved. We also store the size and position of the selection as a rectangle in the originalRect variable – again, this will be used to determine how much to resize or move the selection later.

If the mouse was over the selection but not in a hotspot (resizeXMode == "" && resizeYMode == ""), then we set bMoving to true. The next time the move function is called in response to further mouse movement, we'll enter the first condition in the "if" statement. However, if the mouse pointer is over a hotspot then we set the global variable bResizing to true. The next time the move function is called in response to user mouse movements the second condition in its "if" statement will be called. This state of affairs continues until the user stops holding the mouse button down. The up event handler couldn't be simpler – it just sets the two globals bMoving and bResizing to false, so that further mouse movements default to the "just moving the mouse around" state discussed already.

Moving is actually the simplest state to handle:

function move(e) { if(bInMove) return; // we're already processing a mousemove event bInMove = true; oCanvas.style.cursor = "auto"; oSelection.style.cursor = "auto"; bCanMove = false; if (!e) var e = window.event; var p = new point(e); var selection = rectangle(oSelection); if(bMoving) { var dx = p.x - downPoint.x; var dy = p.y - downPoint.y; selection.x = 0 + originalRect.x + dx; selection.y = 0 + originalRect.y + dy; setSelection(selection); checkConfine(selection); } ...

As already mentioned, we capture a snapshot of the current selection size and position. Then we find out how far the mouse has moved since the user started moving (in down(e)) and store the difference in dx and dy. Then all we do is set the new selection position to the old one plus the differences, and call setSelection to apply the new selection rectangle to the selection div. We then call checkConfine(..), which disables the OK button and displays a warning message if the selection is anywhere outside the canvas:

function checkConfine(rect) { var msg = ""; if((rect.x) < canvasRect.x) msg += "Selection extends to the left of the canvas. "; if(rect.y < canvasRect.y) msg += "Selection extends above canvas. "; if((rect.x + rect.w) > (canvasRect.x + canvasRect.w)) msg += "Selection extends to the right of the canvas. "; if((rect.y + rect.h) > (canvasRect.y + canvasRect.h)) msg += "Selection extends below the canvas. "; if(msg) { oConfirmButton.disabled = true; oDebugInfo.innerHTML = "WARNING: " + msg; } else { oConfirmButton.disabled = false; oDebugInfo.innerHTML = ""; } }

Navigation: Page 1 | Page 2 | Page 3 | Page 4 | Page 5 | Samples