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.