Making of: draggable handles

 from Red Blob Games
1 Sep 2014

For some of my projects I want the reader to drag something around on a diagram. I use the position to control some aspect of the diagram. The curved roads page[1] uses this. I want an easy way to constrain the ways you can drag something, and I also want a way to transform the variable being controlled into a position on the screen. I’m going to describe the implementation.

Here’s an example of the kinds of things I want to be able to do:

I want to make things like this easy, with as little code as I can get away with.

Let’s start at the beginning.

The first thing that comes to mind is to have a function that takes the mouse position and sets an internal variable. I can have a separate function that takes the internal variable and sets the circle position. Let’s try this with a simple example:

var position = [100, 50];  // internal variable
function on_drag() {
    // set internal variable based on mouse position
    position = [d3.event.x, d3.event.y];
    redraw();
}
function redraw() {
    // set circle's position based on internal variable
    d3.select("#handle")
        .attr("cx", position[0])
        .attr("cy", position[1]);
}
d3.select("#handle")  // capture mouse drag event
    .call(d3.drag().on('drag', on_drag));

I used to use d3.js[2] for making my diagrams (v3 back in 2014 when I wrote this page, v4 later, then stopped using d3 in 2015), and it has a mouse drag api[3]. The code above is for d3 v4/v5. When you drag the mouse on an element, it’ll call the callback function. You can capture the mousedown, mousemove, and mouseup events (or better yet: pointer events[4]). In 2017 I made my own drag library so that I can use it outside of d3. In 2022 I greatly simplified it by using pointerevents, which got widespread support in 2020[5].

Once I pull out the ui-specific code, all I have left is a setter and getter function, which I put into an object:

var model = {
    position: [100, 50],
    get: function() { return this.position; },
    set: function(p) { this.position = p; }
};

function on_drag() {
    model.set([d3.event.x, d3.event.y]);
    redraw();
}
function redraw() {
    d3.select("#handle")
        .attr('transform', "translate(" + model.get() + ")");
}

This seems like a trivial object. Why bother? Because I want to transform the position into something other than a simple internal variable. For example, I want to directly store the value in an object or array instead of always using the getter. Here’s a setter/getter pair that lets me do that:

function ref(obj, prop) {
    return {
        get: function() { return obj[prop]; },
        set: function(v) { obj[prop] = v; }
    };
};
var obj = {pos: [150, 25]};
var model = ref(obj, 'pos');

Sometimes I want to treat the x and y axes separately. Here’s a setter/getter pair that splits a point into x and y components:

function cartesian(x, y) {
    return {
        get: function() { return [x.get(), y.get()]; },
        set: function(p) { x.set(p[0]); y.set(p[1]); }
    };
};
var obj = {x: 150, y: 25};
var model = cartesian(ref(obj, 'x'), ref(obj, 'y'));

Once the components are split, I can transform them separately. Here’s a way to add bounds to a number, and a way to have a constant value:

function clamped(m, lo, hi) {
    return {
        get: function() { return m.get(); },
        set: function(v) { m.set(Math.min(hi, Math.max(lo, v))); }
    };
}

function constant(v) {
    return {
        get: function() { return v; },
        set: function(_) { }
    };
}

I can combine these to make a horizontal slider. The x position stays within the range [0,200] and the y position is always 50:

var obj = {x: 25};
var model = cartesian(clamped(ref(obj, 'x'), 0, 200), 
                      constant(50));

However, this isn’t really what I want. I want the handle’s position to start at 100 when the slider’s value is 0. To fix this, I can define something that lets me add 100 to the value:

function add(m, a) {
    return {
        get: function() { return m.get() + a; },
        set: function(v) { m.set(v - a); }
    };
}

Let’s try it again:

var obj = {x: 150};
var model = cartesian(add(clamped(ref(obj, 'x'), 0, 200), 100), 
                      constant(50));

The code is getting a little hard to read. Let’s turn these functions into methods that return new objects:

function Model(init) {
    this.get = init.get;
    this.set = init.set;
}

Model.ref = /* static */ function(obj, prop) {
    return new Model({
        get: function() { return obj[prop]; },
        set: function(v) { obj[prop] = v; }
    });
};

Model.constant = /* static */ function(v) {
    return new Model({
        get: function() { return v; },
        set: function(_) { }
    });
};

Model.prototype.clamped = function(lo, hi) {
    var m = this;
    return new Model({
        get: function() { return m.get(); },
        set: function(v) { m.set(Math.min(hi, Math.max(lo, v))); }
    });
}

Model.prototype.add = function(a) {
    var m = this;
    return new Model({
        get: function() { return m.get() + a; },
        set: function(v) { m.set(v - a); }
    });
}

While I’m at it, I really want the value to go from 0 to 10 while the position goes from 100 to 300. I need a multiplier for that:

Model.prototype.multiply = function(k) {
    var m = this;
    return new Model({
        get: function() { return m.get() * k; },
        set: function(v) { m.set(v / k); }
    });
}
var obj = {x: 5};
var model = cartesian(Model.ref(model5, 'x')
                           .clamped(0, 10)
                           .multiply(20)
                           .add(100), 
                      Model.constant(50))

That’s a bit easier to understand. Note that the order matters. If I had used add first and then clamped, it’d be different than using clamped and then add.

Let’s make the x and y values sit on a grid by rounding the values to the nearest integer:

Model.prototype.rounded = function() {
    var m = this;
    return new Model({
        get: function() { return m.get(); },
        set: function(v) { m.set(Math.round(v)); }
    });
}
var obj = {x: 2, y: 1};
var model = cartesian(Model.ref(model6, 'x')
                           .clamped(0, 18)
                           .rounded()
                           .multiply(30)
                           .add(30), 
                      Model.ref(model6, 'y')
                           .clamped(0, 3)
                           .rounded()
                           .multiply(20)
                           .add(15));

How do I think about this code?

The way I think of it is that I’m starting with the underlying value and transforming it into a pixel coordinate. I started with an x value and limited its range to 0…12. I multiply it by 30 and add 25 to it to scale and translate it to pixel coordinates. Alternatively, I could have first scaled and translated, then limited its range to the pixel range.

I hope by now you see that there are lots of tiny modifiers that could be written to give a range of behaviors for these draggable handles. These objects go both directions. In the forwards direction, the getters transform the underlying value into a pixel coordinate, so that I can draw it. In the backwards direction, the setters transform pixel coordinates into the underlying values, so that I can handle mouse click/drag. Chaining simple things together can express many different patterns. Here’s one that makes a grid of polar coordinates. Can you imagine how it was made?

var model = {radius: 1, angle: 0};

I originally built some of this flexibility for a Flash demo that I never did much with, and I reused it for the curved roads page. I had add variable reference, constant, clamp, round, scalar, multiply scalar, add vector, scale vector, project along vector, cartesian decomposition, polar decomposition, and callback functions. It worked great for the Flash road demo, and it worked reasonably well but not perfectly for the curved roads article.

Is there a name for this pattern? I don't know. I can't remember where I learned it. Maybe "combinators".

There are some things that this system doesn't handle. There are several alternatives I could consider:

I'm always looking for things that let me get a lot of functionality for little code, and I think this fits the bill. I'm especially drawn to solutions (and games) where I combine several smaller pieces to produce complex behavior. I like this solution, but I'm also curious about other approaches, so I might try something else for a future project.

Also see little things that make drag behavior nicer.

Email me , or tweet @redblobgames, or comment: