3

I need the ability to start the raphael "drag" event on a circle object that is created when left mouse button is clicked.

Details:
I'm working on a piece of code that manipulates a closed path. I need these features:
1. moving existing points around by mouse drag
2. remove points via right-click
3. add points by left-click on the path, resulting in the path splitting at that location
4. if point is created by left-click, allow user to drag it to another location before releasing the mouse and dropping it

Here's my function that creates a new circle on a given raphael "path" object:

// create a new circle wrapped around the given point reference
function make_circle(point, path) {
    var c = paper.circle(point[1], point[2], 6)
                 .attr({fill: "#DDD", stroke: "black"});

    // record the point and path reference in the circle to allow updates
    c.data("point", point).data("path", path);

    // set event handlers
    c.drag(dragmove,dragstart);
    c.update = update_coordinates_circle;
    c.mousemove(handle_mousemove_circle);
    c.mouseup(handle_mouseup_circle);
    c.mousedown(handle_mousedown_circle);

    return c;
}

Then I can do this:

var pt1 = ["M", 0, 0],
    pt2 = ["L", 10, 0],
    pt3 = ["L", 10, 10],
    pt4 = ["L", 0, 10],
    point_set = [pt1, pt2, pt3, pt4, ["Z"]],
    path = paper.path(point_set),
    c1 = make_circle(pt1, path),
    c2 = make_circle(pt2, path),
    c3 = make_circle(pt3, path),
    c4 = make_circle(pt4, path);  

When creating new point via clicking on path, I do this:

make_circle(new_point, this).data("just_created", true);  

... and the mousemove handler of this circle checks:

if (this.data("just_created")) { ... // follow mouse  

My full code here: http://jsfiddle.net/T7XS3/

The problem is that because the circle radius is small, moving the mouse to quickly before releasing a new point breaks the mousemove handler since it's attached to the circle.

When moving an existing circle via its "drag" event, everything works fine. No matter how quickly the mouse moves the circle stays with it.

So again, is it possible to start a raphael "drag" event on an object that is created when the left mouse button is pressed, but has not yet been released?


Solution (http://jsfiddle.net/A27NZ/3/):

var width = 300,
    height = 300,
    paper_offset = 50,
    maxX = width,
    maxY = height,
    paper = Raphael(paper_offset, paper_offset, 300, 300),
    dragging;

make_path([100, 100], [200, 100], [200, 200], [100, 200], "green");

// reset 'dragging' to avoid initial drag
dragging = null;


// some math to determine if a point is between two other points, within some threshold
// based on: http://stackoverflow.com/questions/328107/how-can-you-determine-a-point-is-between-two-other-points-on-a-line-segment
function isBetween(a, b, c) {
    var x1 = a[1],
        x2 = b[1],
        x3 = c[1],
        y1 = a[2],
        y2 = b[2],
        y3 = c[2],
        THRESHOLD = 1000;

    var dotproduct = (x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1);
    if (dotproduct < 0) return false; // early return if possible

    var squaredlengthba = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
    if (dotproduct > squaredlengthba) return false; // early return if possible

    var crossproduct = (y3 - y1) * (x2 - x1) - (x3 - x1) * (y2 - y1);
    if (Math.abs(crossproduct) <= THRESHOLD) return true;
    else return false;
}


function global_mousemove(e) {
    if (dragging) {
        handle_mousemove_circle.call(dragging, e);
    }
}


function global_mouseup(e) {
    dragging = null;
}


if (document.addEventListener) {
    document.addEventListener("mousemove", global_mousemove, false);
    document.addEventListener("mouseup", global_mouseup, false);
} else {
    document.attachEvent('onmousemove', global_mousemove);
    document.attachEvent('onmouseup', global_mouseup);
}


// move circle to given coordinates
function update_circle_xy(new_x, new_y) {
    var point = this.data("point"),
        path = this.data("path");

    // don't follow the mouse outside the canvas, we don't want to lose the point
    if (new_x <= 0) {
        new_x = 0;
    } else if (new_x >= maxX) {
        new_x = maxX;
    }

    if (new_y < 0) {
        new_y = 0;
    } else if (new_y > maxY) {
        new_y = maxY;
    }

    // update circle coords
    this.attr({
        cx: new_x,
        cy: new_y
    });

    // update the referenced point
    point[1] = new_x;
    point[2] = new_y;

    // redraw the path
    path.attr({
        path: path.data("point_set")
    });
}


// move circle based on mouse event
function handle_mousemove_circle(e) {
    var new_x = e.clientX - paper_offset,
        new_y = e.clientY - paper_offset;

    update_circle_xy.call(this, new_x, new_y);
}


// handle mouse down on circle
// e.which 1 = left click
// e.which 3 = right click
function handle_mousedown_circle(e) {
    // remove the target point on right-click
    if (e.which === 3) {
        var path = this.data("path"),
            point_set = path.data("point_set"),
            point = this.data("point"),
            index = point_set.indexOf(point);

        // don't do anything if we only have 2 points left
        // (checking if < 4 because last element is not a point ("Z"))
        if (point_set.length < 4) return false;

        // remove the target point
        point_set.splice(index, 1);

        // if removed point was head of set, make the following point the new head
        if (index === 0) point_set[0][0] = "M";

        // redraw the path
        path.attr({
            path: point_set
        });

        // finally, remove the circle
        this.remove();
    } else if (e.which === 1) {
        dragging = this;
    }
}


// handle mouse click on path
function handle_mousedown_path(e) {

    // split on left-click
    if (e.which === 1) {
        var X = e.clientX - paper_offset,
            Y = e.clientY - paper_offset,
            new_point = ["L", X, Y],
            point_set = this.data("point_set"),
            index;

        // "open" the path by removing the end ("Z")
        point_set.pop();

        for (var i = 0; i < point_set.length; i += 1) {
            // cur point
            var pt1 = point_set[i], // cur point
                pt2; // next point

            // circular wrap for next point
            if (i === point_set.length - 1) {
                pt2 = point_set[0];
            } else {
                pt2 = point_set[i + 1];
            }

            // check if these are the two points we want to split between
            if (isBetween(pt1, pt2, new_point)) {
                index = i + 1;
                break;
            }
        }

        // we should have found a place to insert the point, put it there
        if (index) {
            point_set.splice(index, 0, new_point);
        } else {
            return; // we didn't find a place to put the new point
        }

        // "close" the path with a ("Z")
        point_set.push("Z");

        // redraw the path
        this.attr({
            path: point_set
        });

        // create new circle to represent the new point
        c = make_circle(new_point, this);
    }
}


// create a new circle wrapped around the given point reference
function make_circle(point, path) {
    var c = paper.circle(point[1], point[2], 6).attr({
        fill: "#DDD",
        stroke: "black"
    });

    // record the point and path reference in the circle to allow updates
    c.data("point", point).data("path", path);

    // set event handlers
    c.mousedown(handle_mousedown_circle);

    // start dragging the new circle
    dragging = c;
    return c;
}


// create a new colored path from four point coordinate pairs
function make_path(p1, p2, p3, p4, color) {

    // starting points
    var pt1 = ["M", p1[0], p1[1]],
        pt2 = ["L", p2[0], p2[1]],
        pt3 = ["L", p3[0], p3[1]],
        pt4 = ["L", p4[0], p4[1]],
        point_set = [pt1, pt2, pt3, pt4, ["Z"]],
        path = paper.path(point_set).attr({
            stroke: color,
                "stroke-width": 5,
                "stroke-linecap": "round"
        });

    // keep a reference to the set of points
    path.data("point_set", point_set);

    // add listener to the path to allow path-splitting
    path.mousedown(handle_mousedown_path);

    // create the circles that represent the points
    make_circle(pt1, path);
    make_circle(pt2, path);
    make_circle(pt3, path);
    make_circle(pt4, path);
}
mikhail
  • 5,019
  • 2
  • 34
  • 47

2 Answers2

1

I would have thought you could use some variation of the technique in this answer "Raphaël Object: Simulate click" to pass the event to the circle, but that doesn't work.

My other idea was is based on how the Raphael source works, when you make an element draggable a handler is added for the mousedown event. It should be possible to directly call that handler function in the right context and pass it the mousedown event you already have (inside handle_mousedown_path). But it's very hacky and I couldn't get it to work, in case someone else can here's what I was trying to do:

c = make_circle(new_point, this).data("just_created", true);
c.events[0].f.call(c, e); // This is very specific to this scenario

The only other way I can think to do it is to add the mousemove/mouseup handlers to the whole document. So create a global variable dragging and when you create a circle set dragging to be the element, in the global mouseup handler you can clear the variable:

function global_mouseup(e) {      
    dragging = null;
}

You already have a handler for the move, so use that, we just need to ensure this is correct for the function (see this answer: Javascript: how to set "this" variable easily?)

function global_mousemove(e) {     
    if (dragging) { 
        handle_mousemove_circle.call(dragging, e);    
    }
}

Now, bind them to the document (and remove the individual handlers on the circle itself):

if (document.addEventListener) {
    document.addEventListener("mousemove", global_mousemove, false);
    document.addEventListener("mouseup", global_mouseup, false);
} else {
    document.attachEvent('onmousemove', global_mousemove);
    document.attachEvent('onmouseup', global_mouseup);
}  

(See this answer: Add event handler to HTML element using javascript for why we're using that syntax, although if you're using a framework you can use it's command).

And there you go, working fiddle.

It's not quite perfect as the Raphael drag handler is smarter and knows to keep the object inside the canvas, you'll need to change your handle_mousemove_circle function to fix that.

Community
  • 1
  • 1
SpaceDog
  • 3,249
  • 1
  • 17
  • 25
  • 1
    +1 This is along the lines of what I was thinking. Thank you for a working example, I updated handle_mousemove_circle() to work a little better and be more compatible with IE and FF http://jsfiddle.net/A27NZ/2/ – mikhail Dec 20 '13 at 21:38
  • Great stuff, and thanks for sharing the final working code, hopefully someone else finds that useful in the future. – SpaceDog Dec 21 '13 at 04:02
  • Updated again: http://jsfiddle.net/A27NZ/3/ . Refactored and improved to include some ideas from _[Raphael's demo](http://raphaeljs.com/picker.html)_ that seems to do border detection quite well. I think I'm finally satisfied with it (for now...) – mikhail Dec 21 '13 at 19:31
0

The solution provided by SpaceDog is working quite good for me. There is however one minor problem: If you add two new circles in a row by clicking on a path and than dragging the new circle somewhere, every second "drag" is erroneous, since the mousedown event on the path triggers the standard dragging function for html elements (see this fiddle by mikhail)

To prevent this behaviour, simply add another EventListener to your document, that checks on every mousedown, if the "custom" dragging function is used. If so, the default behaviour is prevented:

if (document.addEventListener) {
    document.addEventListener("mousemove", global_mousemove, false);
    document.addEventListener("mouseup", global_mouseup, false);
    document.addEventListener("mousedown", global_mousedown, false);
} else {
    document.attachEvent('onmousemove', global_mousemove);
    document.attachEvent('onmouseup', global_mouseup);
    document.attachEvent('onmousedown', global_mousedown);
}

function global_mousedown(e) {  
    if(dragging){
        e.preventDefault();     
    }
}