#+TITLE: Making of: draggable handles #+DATE: <2014-09-01> 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 [[https://www.redblobgames.com/articles/curved-paths/][curved roads page]] 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. #+begin_export html #+end_export #+begin_src js :tangle yes :exports none // minimal diagram just for this blog post function make_diagram(parent_selector, model, vars) { var p = d3.select(parent_selector); var svg = p.selectAll("svg").data(["there can be only one"]); svg = svg.enter().append('svg').attr('viewBox', "0 0 600 100").merge(svg); var g = svg.append('g').attr('class', "draggable"); g.append('circle').attr('class', "shadow").attr('r', 10); g.append('circle').attr('class', "circle").attr('r', 10); var updates = []; function on_drag() { model.set([d3.event.x, d3.event.y]); redraw(); } function redraw() { g.attr('transform', "translate(" + model.get() + ")"); updates.forEach(function(f) { f(); }); } g.call(d3.drag() .subject(() => ({x: model.get()[0], y: model.get()[1]})) .on('drag', on_drag) .on('start', function() { d3.select(this).classed('dragging', true); }) .on('end', function() { d3.select(this).classed('dragging', false); })); // NOTE: this is very fragile!! And the .org-src-container doesn't exist yet when this function is called, so we wait until update time vars.forEach(function (v) { var selector = v[0], obj = v[1], field = v[2], format = v[3]; updates.push(function() { var code_sample = d3.select(parent_selector + " + script + .org-src-container"); var cell = code_sample.select(selector); var text = d3.format(format)(obj[field]); cell.text(text); }); }); redraw(); } #+end_src Here's an example of the kinds of things I want to be able to do: #+begin_export html
#+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. Maybe "combinators". 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