#+end_export
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:
#+begin_export html
#+end_export
#+begin_src js
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));
#+end_src
#+results:
I used to use [[https://d3js.org/][d3.js]] 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 [[https://github.com/d3/d3-drag][mouse drag api]]. 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: [[https://caniuse.com/pointer][pointer events]]). In 2017 I made [[href:/x/1845-draggable/][my own drag library]] so that I can use it outside of d3. In 2022 [[href:/x/2251-draggable/][I greatly simplified it by using pointerevents]], which got [[https://caniuse.com/pointer][widespread support in 2020]].
Once I pull out the ui-specific code, all I have left is a setter and getter function, which I put into an object:
#+begin_src js :tangle yes
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() + ")");
}
#+end_src
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:
#+begin_src js :tangle yes
function ref(obj, prop) {
return {
get: function() { return obj[prop]; },
set: function(v) { obj[prop] = v; }
};
};
#+end_src
#+begin_export html

#+end_export
#+begin_src js
var obj = {pos: [150, 25]};
var model = ref(obj, 'pos');
#+end_src
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:
#+begin_src js :tangle yes
function cartesian(x, y) {
return {
get: function() { return [x.get(), y.get()]; },
set: function(p) { x.set(p[0]); y.set(p[1]); }
};
};
#+end_src
#+begin_export html

#+end_export
#+begin_src js
var obj = {x: 150, y: 25};
var model = cartesian(ref(obj, 'x'), ref(obj, 'y'));
#+end_src
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:
#+begin_src js :tangle yes
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(_) { }
};
}
#+end_src
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:
#+begin_export html

#+end_export
#+begin_src js
var obj = {x: 25};
var model = cartesian(clamped(ref(obj, 'x'), 0, 200),
constant(50));
#+end_src
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:
#+begin_src js :tangle yes
function add(m, a) {
return {
get: function() { return m.get() + a; },
set: function(v) { m.set(v - a); }
};
}
#+end_src
Let's try it again:
#+begin_export html

#+end_export
#+begin_src js
var obj = {x: 150};
var model = cartesian(add(clamped(ref(obj, 'x'), 0, 200), 100),
constant(50));
#+end_src
The code is getting a little hard to read. Let's turn these functions into methods that return new objects:
#+begin_src js :tangle yes
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); }
});
}
#+end_src
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:
#+begin_src js :tangle yes
Model.prototype.multiply = function(k) {
var m = this;
return new Model({
get: function() { return m.get() * k; },
set: function(v) { m.set(v / k); }
});
}
#+end_src
#+begin_export html

#+end_export
#+begin_src js
var obj = {x: 5};
var model = cartesian(Model.ref(model5, 'x')
.clamped(0, 10)
.multiply(20)
.add(100),
Model.constant(50))
#+end_src
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:
#+begin_src js :tangle yes
Model.prototype.rounded = function() {
var m = this;
return new Model({
get: function() { return m.get(); },
set: function(v) { m.set(Math.round(v)); }
});
}
#+end_src
#+begin_export html

#+end_export
#+begin_src js
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));
#+end_src
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?
#+begin_src js :tangle yes :exports none
function polar(r, a) {
return new Model({
get: function() { return [r.get() * Math.cos(a.get()), r.get() * Math.sin(a.get())]; },
set: function(p) { r.set(Math.sqrt(p[0]*p[0] + p[1]*p[1])); if (p[0] != 0.0 || p[1] != 0.0) a.set(Math.atan2(p[1], p[0])); }
});
}
Model.prototype.offset = function(p) {
var m = this;
return new Model({
get: function() { return [m.get()[0] + p[0], m.get()[1] + p[1]]; },
set: function(r) { m.set([r[0] - p[0], r[1] - p[1]]); }
});
}
#+end_src
#+begin_export html

#+end_export
#+begin_src js
var model = {radius: 1, angle: 0};
#+end_src
I originally built some of this flexibility for a Flash demo that I never did much with, and I reused it for the [[href:/articles/curved-paths/][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.
There are some things that this system /doesn't/ handle. There are several alternatives I could consider:
- An observer/observable system, so if I change the underlying value, it will automatically update. I'd be able to hook up multiple draggable handles to the same underlying values. (In 2018 I wrote [[https://simblob.blogspot.com/2018/03/how-i-implement-my-interactive-diagrams.html][more about this topic]].)
- A separate constraint solver, instead of building object chains, so that I could specify the constraints separately in a declarative way. It's often better to specify things as data instead of code.
- Although the transformations are very generic, in practice most everything I do is a geometric transformation. It might be cleaner to separate geometric transformation from constraints. SVG transforms provide geometric transforms. Constraints are "snap to nearest" instead of rounding and bounding region instead of clamping. JQueryUI has [[https://api.jqueryui.com/draggable/][snap/grid/range options]] for drag and drop; MooTools [[https://mootools.net/more/docs/1.6.0/Drag/Drag][does too]]. Maybe that's a better model.
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 [[href:/making-of/little-things/#drag-point][little things that make drag behavior nicer]].
#+begin_export html
#+end_export
#+begin_export html
Created September 2014
with Emacs Org-mode, from making-of.org and D3.js.
#+end_export