Stageplan takes shape

My re-write of the Stageplan app is going well, despite the fact that I have little time to do it. So I'm posting a demo here. It uses Raphael 0.71. I had some difficulties with this, but have solved them, albeit somewhat inelegantly.

Click on an item and drag it onto the "stage" area. When you do, a slider control appears. It allows you to rotate the object. When multiple objects are on the stage, clicking on any one of them selects the item shows its slider control. When you click on the stage, a selected item is deselected and its slider disappears.

I've added an "amplifier" object. I should also add a DI box and a keyboard as these are regularly specified by students in their stageplans. Other items, such as the drumkit and piano, tend to be static (as they are difficult to move). The latter can probably be added to the stage as partly opaque objects.

Here's the demo.

The next step is to figure out how to save objects on the server so that stageplans can be recalled, viewed and edited. Bolt on an authentication system and it'll be done!

Making progress with the Stageplan app

It's been some time since I worked on the application that would allow performance students to draw stage plans for their performances. As I mentioned previously, I'm using Raphael, a Javascript drawing library which uses SVG or VML.

There have been several revisions of Raphael since I last worked on the app, so when I upgraded I found that I could no longer clone image objects. The reason turned out to be that calling myImage.attr("href") no longer returns the URL of the source image. There is now no easy way to do this. Dimitry or one of the other contributors has added a "src" attribute which is easily set (for changing the image source of an object) but not retrieved. Since my cloning method relies on retrieving this value, I was stuck.

When I looked at the Raphael source code, I figured out a way of doing it, but it's clumsy:

myImage.node.getAttributeNS(myImage.svg.xlink, "href");

The app works like this: If you click on a tool, the tool is cloned as an "item". If you let go of the mouse button while it's still over the "palette", the clone is deleted. When you drag the item onto the stage, a slider for controlling its rotation appears. Clicking on the stage "deselects" the item and its slider disappears. Clicking on an item shows its slider with the slider thumb showing the previous degree of rotation. Moving the slider's thumb rotates the item. Clicking and dragging the item moves it around the stage.

I took the opportunity to refactor the code and rewrite almost all of it from scratch. There is now an object for each tool and for each item created, as well as the slider which controls the rotation of the selected object. It's not very nice OO, as there are explicit references to the "palette" and "stage" in Tool and Item methods, particularly for assigning event handlers to items. However, for a reasonably experienced PHP programmer (with a smattering of assembly code, Basic, Pascal and C on the way there), Javascript's lambda functions and closures and the consequent effects on variable scope seem quite weird anyway, so I'm not going to lose any sleep over it!

In a day or two, I'll post a demo of the work done so far.

Reliable dragging and rotation too!

In my last entry, I demonstrated that when a Raphael SVG object is rotated, its coordinate system is also rotated. In fact, when the object is returned to a rotational angle of 0 degrees, it jumps to the location it would have moved to had it not been rotated.

So the issue is, how can one move an object which has been rotated away from its original angle (whatever shape and orientation it had when it was created). I previously considered using trigonometric calculations to adjust both x and y coordinates to make the object move to where it would be if its angle of rotation had been 0 degrees. But I now realise that this would not have worked properly as the distance the object moves when it is rotated away from 0 degrees is the same as it would have moved when it was at 0 degrees' rotation. If my memory of trigonometry serves me correctly, that would require tan, not sin or cos.

I'm sure that, with a little revision, I could work it out. But I suspect that some of those mathematical functions result in a performance hit, especially as they'd have to be performed with every mousemove event. I've no doubt that SVG or VML functions already employ such calculations, even though they're done by the Javascript engine which is most likely written in C or C++ and therefore performs at least an order of magnitude faster than the interpreted Javascript which invokes them. Or maybe such calculations only happen once, when the object is rotated and then a completely different coordinate system is maintained in memory.

Since I understand nothing about Javascript engines, I could be talking completely through my hat.

So I decided to try a simpler approach that I alluded to before: rotate the object back to 0 degrees before moving it and then re-rotate it to whatever angle it was before the move.

I could "undo" the rotation on the mousedown and "redo" the rotation on the mouseup, but the user would see the object in an unrotated state during the mousemove. I decided to so these operations for every mousemove event.

Here's the demo. It has the same "Rotate"/"Unrotate" button under the canvas (scroll down a bit if you can't see it) as before.

If you examine the Javascript, you'll notice there are only a few lines added. I don't notice any performance difference between the previous one and this, even though the Javascript engine is probably working a bit harder. WRONG! There is a noticeable performance hit. At first, I thought there wasn't because I forgot how smooth movement is without the modifications to the previous script. And to the user, it looks as if the object is moving smoothly fairly smoothly, with no blurring due to redraws even though, one assumes, redraws are occurring.

I'm not using a superfast machine either: it's an old IBM R30 running Opensuse 11.0 (Linux) with KDE4 and Firefox 3 (FF3 is required for scalable vector graphics).

So, with this key functionality in place, I can continue developing the "stageplan" application.

Reliable dragging using Raphael

After some weeks of frantic marking and other administrative affairs, I finally had the chance to sit down this week and work on the Javascript-based app using Raphael.

I discovered that one can indeed access drawing objects with the JQuery Javascript library, provided one adds an "id" attribute and refers to the "original" object. So if, for example, one creates an object as follows:

var paper = Raphael("container", 640, 480);
var square = paper.rect(50, 50, 20, 20);
square.attr({
    "fill":"#b34300",
    "id":"square",
});

...one can only access attributes of the object like this:

square.attr("x");

...and not this (using Jquery):

$("#square").attr("x");

I think this reflects the very complicated nature of Raphael and, in fact, SVG or VML drawing. (Take any comments about VML with a grain of salt though because I haven't got any of this to work in IE yet.) At least, the structure of SVG and its child objects seems to be very complex if the Firefox DOM Inspector tool provides any indication!

Anyway, I tried the tricks that I alluded to previously: that is, attaching mousemove and mouseup handlers to the "canvas" rather than the object being clicked and dragged. And it works a treat.

Demo

There is still the problem of rotating the object. It's probably more obvious in this example than in the previous one. Click on the "Rotate" button to rotate the square 45 degrees clockwise. When you drag the mouse straight up or down, the object moves diagonally. When "Unrotate" is clicked, it shifts position as its coordinate system is re-aligned with the canvas's. Rotating any object also rotates its coordinate system.

I don't know if this is a side-effect of Raphael or the underlying SVG or VML. Any ideas?

Drawing in Javascript

In the unlikely event that I have actually redesigned this blog and various tags I've created for each post appear in a sidebar or other peripheral area, and that you have navigated here via the raphael tag, I hope you will not be disappointed that this post has nothing to do with any Renaissance painter. Except, that is, for the fact that the Javascript library to which I refer was probably named after one.

Raphael is a Javascript library for creating and manipulating vector graphics by Dmitry Baranovskiy, a software developer for Atlassian. My use of it is all the more miraculous as I haven't looked at Javascript for about 8 years (version 1.1, I think).

As you may know, I teach sound production at a TAFE institute (that's "Technical and Further Education" for those not aware of vocational training in Australia). My students regularly record performances by music performance students in the Performing Arts department of which we are part. The institute also runs degree courses. There's one in popular music. Students in that course have been providing stage plans for their performances so that my students can plan the recording: microphones, mixer channel and track allocation, and so on. Certain instruments and elements are more or less fixed: grand piano and drums, mainly, but usually instrument amplifiers as well. . I'm trying to get TAFE performance students to do the same.

When I read about Raphael, I immediately thought of a web application in which performance students could draw their stage plans on-line. They could make changes and my students could view the plans.

So I created a mockup with an area for a "palette" of tools and a "stage", and graphics for a microphone, an amplifier and a keyboard&emdash;the most likely mobile objects in a performance. With a library like Jquery, the Javscript for this is fairly simple:

$(document).ready( function() {
    var paper = Raphael("container", 640, 480);
    
    var palette = paper.rect(0,0,640,60);
    palette.attr({fill:"#eee"});
    var stage = paper.rect(0, 60, 640, 420);
    stage.attr({"stroke-width": "0", "fill": "#b38439"});
    
    var mic = paper.image("microphone2.png", 50, 50, 15, 47);
    
    var amp = paper.image("amp.png", 45, 5, 53, 45);
    
    var keys = paper.image("keyboard.png", 95, 5, 137, 45);
}

Event handlers can be attached to objects as follows:

    mic[0].onclick = function(e)
    {
        /* ... click-handling code here ... */
    }

Oddly, DOM 0 handlers work while DOM 1 handlers (addEventListener) do not... or at least I haven't managed to make them work yet.

The idea is that when the user clicks on one of the objects in the palette, it clones itself and the clone can then be dragged onto the stage, and moved around or rotated. Creating the clone and implementing dragging is not too difficult. Raphael has a translate() function, but I've been unable to make that work with a mousemove event, so I directly manipulated the x, y coordinates of the object instead. So far so good.

Rotation is quite easy:

    mic.rotate(90)

...rotates the object 90 degrees (clockwise).

But once it's rotated, moving the object is a problem. It appears that the entire coordinate system (or reference) for the object rotates with it. So if the user clicks on the object and drags upwards, the object moves to the right instead!

If one were only dealing with right angles, this would be less of a problem. But I wish to have students rotate the objects to indicate their orientation on the stage. So any degree of rotation is necessary and consequently, the object moves off unpredictably (well, it's entirely predictable but unsettling!).

Trigonometry to the rescue.

I decided to work out the mouse movement between consecutive mousemove events and then work out the corresponding changes (delta values) in x and y coordinates in the object's coordinate system. As I don't know MathML and I'm not sure of how many browsers support it, here's the Javascript for that:

    var _clone = function(prototype)
    {
        var theItem = paper.image(
            prototype.attr("href"),
            prototype.attr("x"),
            prototype.attr("y"),
            prototype.attr("width"),
            prototype.attr("height")
        );
        theItem.attr({"angle": 0, "gx": 0, "gy": 0});
    
        current = theItem;
        selected = theItem;
    
        theItem[0].onmousedown = function(e)
        {
            theItem.attr({"gx": e.pageX, "gy": e.pageY});
            current = theItem;
            selected = theItem;
        }
    
        theItem[0].onmousemove = function(e)
        {
            if(current == theItem) {
                //get mouse delta x,y
                var delta = {"x": e.pageX - theItem.attr("gx"), "y":e.pageY - theItem.attr("gy")};
                theItem.attr({"gx": e.pageX, "gy": e.pageY});
                //get distance and angle of mouse movement
                var r = Math.sqrt(Math.pow(delta.x,2) + Math.pow(delta.y, 2));
                var a = theItem.attr("angle")*Math.PI/180 - Math.asin(delta.y/r);
                theItem.attr("x", Math.cos(a)*r + theItem.attr("x"));
                theItem.attr("y", Math.sin(a)*r - theItem.attr("y"));
            }
        }
    
        theItem[0].onmouseup = function(e)
        {
            current = null;
        }
    
        return theItem;
    }

I'm pretty sure I have the trigonometry right (though it's been 20 years since I did anything like this). But the object still jumps around unpredictably.

The only way I see around this is to return the object to 0 degrees for the move and rotate it to whatever degree it was rotated to originally. In my next post, we'll see whether or not this was successful.

 1

About

A vanity publishing venture of David Rodger, sound production teacher and wannabe PHP developer

User