#+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 [[http://www.redblobgames.com/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 [[http://api.jqueryui.com/draggable/][snap/grid/range options]] for drag and drop; MooTools [[http://mootools.net/docs/more/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 very little code, and I think this fits the bill. I'm especially drawn to solutions (and games) where I combine several simple 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
Created September 2014
with Emacs Org-mode, from making-of.org and D3.js.
#+end_export