d3 draggable object example #4 – parametric + improved selection identification
Table of Contents
1. Introduction
This page extends example #3 (../drag3/index.html), modifying the algorithm for associating mouse coordinates with a point on the function so that we get smooth dragging behavior
- source
- org-mode source for this page is here: index-src.html
2. Demo
The div element #frame
will appear below this line (only when exported to html):
3. Prerequisites
As for examples #1, #2, #3 (../drag1/index.html)
4. Procedure
Start with example #3: we will reuse the files point.js
, fx.js
, fx_view.js
.
4.1. Add function pt.find_perpendicular
Write a function to: given a line L and a reference point pref, find the target point ptgt in L, such that the line through pref and ptgt is perpendicular to L. ptgt is the point in L that's closest to pref
/* given a line L through points pt1,pt2, * find the point p on the line such that the line through p,target_pt * is perpendicular to L * * point = {x,y} * target_pt, pt1, pt2 :: point * return :: point */ ex.find_perpendicular = function(target_pt, pt1, pt2, clip_flag) { /* * * (x2,y2) = pt2 * / * / * / L * p / * * * / \ M * / \ * pt1 = (x1,y1) * \ * * (x0,y0) pt0 * * parameterise the line L through pt1,pt2: * L comprises the points L(t) = pt1 + t*(pt2-pt1) * given a particular point p = L(t0), consider the line M * through L(t0) and (x0,y0) * M comprises the points M(s) = pt0 + s*(L(t0)-pt0) * * we seek t such that the line M(s) through L(t) and (x0,y0) * is perpendicular to L. * * A vector lv parallel to L is (pt2-pt1). * lv = (x2-x1,y2-y1) * A vector mv parallel to the line thru L(t) * L(t) = pt1 + t*(pt2-pt1) = (1-t)*pt1 + t*pt2 * and pt0 is: * mv = L(t)-pt0 * = ((1-t)*x1 + t*x2) - x0, * (1-t)*y1 + t*y2) - y0) * * lv . mvT * = (x2-x1)*[(1-t)*x1 + t*x2 - x0] * + (y2-y1)*[(1-t)*y1 + t*y2 - y0] * * lv. mvT is 0 when lv and mv are _|_: * (x2-x1)*[(1-t)*x1 + t*x2 - x0] = -(y2-y1)*[(1-t)*y1 + t*y2 - y0] * (x2-x1)*[-t*x1 + t*x2 + x1-x0] = -(y2-y1)*[-t*y1 + t*y2 + y1-y0] * t*(x2-x1)*[-x1 + x2] + (x2-x1)*(x1-x0) = t*[-(y2-y1)]*[-y1 + y2] + -(y2-y1)*(y1-y0) * t*(x2-x1)^2 + t*(y2-y1)^2 = -(x2-x1)*(x1-x0) - (y2-y1)*(y1-y0) * * (x2-x1)*(x1-x0) + (y2-y1)*(y1-y0) * t = -1 * --------------------------------- * (x2-x1)^2 + (y2-y1)^2 * * L(t) = pt1 + t*(pt2 - pt1) */ var pt0 = target_pt; var dx2 = pt2.x - pt1.x; var dy2 = pt2.y - pt1.y; var dx1 = pt1.x - pt0.x; var dy1 = pt1.y - pt0.y; var t = -((dx2*dx1) + (dy2*dy1)) / (dx2*dx2 + dy2*dy2); /* if clip_flag is true: * constrain t to [0,1] */ if(clip_flag) { if(t < 0.0) t = 0.0; if(t > 1.0) t = 1.0; } var xt = pt1.x + t * dx2; var yt = pt1.y + t * dy2; return {x: xt, y: yt}; } /*find_perpendicular*/
4.2. Upgrade fx_view.update_select
to use a smoothly-varying function of mouse coordinates
/* update selection circle * for an event at Point pt */ fx_view.fx_update_select = function(p, target_pt_v) { /* find point on {x,f(x)} that's closest to * mouse location (i.e. to d3.event) */ var mid_pt_ix = pt.find_closest_ix(p, target_pt_v); /* establish three neighboring points; * ideally around best_px_ix, but stay within target_pt_v */ if(mid_pt_ix - 1 < 0) ++mid_pt_ix; if(mid_pt_ix + 1 >= target_pt_v.length) --mid_pt_ix; var pt0 = target_pt_v[mid_pt_ix - 1]; var pt1 = target_pt_v[mid_pt_ix]; var pt2 = target_pt_v[mid_pt_ix + 1]; /* find best points on line segments [pt0,pt1] and [pt1,pt2] respectively */ var perp_lo_pt = pt.find_perpendicular(d3.event, pt0, pt1, true /*clip_flag*/); var perp_hi_pt = pt.find_perpendicular(d3.event, pt1, pt2, true /*clip_flag*/); /* choose nearest of perp_lo_pt and perp_hi_pt */ var perp_pt = pt.find_closest(d3.event, [perp_lo_pt, perp_hi_pt]); fx_view.fx_select_circle .attr("cx", perp_pt.x) .attr("cy", perp_pt.y); } /*fx_update_select*/
4.3. Reuse parametric-drag-example.js
from example #3
1: !function() { 2: var ex = {}; 3: 4: /* Requires: 5: * - point.js 6: * - fx.js 7: * - fx_view.js 8: */ 9: 10: ex.box_pt = {x: 600, y: 400}; 11: 12: /* w :: Window */ 13: ex.start = function(w) 14: { 15: ex.pt2screen = fx.linear_fn(pt.sub_pt(pt.scale_pt(0.5, ex.box_pt), 16: fx.eval_fn(0.0) /*ctr_fx*/), 17: 200.0 /*scale_factor*/); 18: ex.target_pt_v = fx.make_target_pt_v(fx.eval_fn, 19: -1.66, +5.0, 200.0 /*n_pt*/, 20: ex.pt2screen, ex.box_pt); 21: fx_view.init_drag_function(ex.target_pt_v); 22: fx_view.draw("#frame", ex.box_pt, ex.target_pt_v); 23: } 24: 25: this.ex = ex; 26: }();
4.4. Load ~.js_ files in html header
This step is identical to the similar step in example #3
At the top of the .org
file:
#+html_head: <script type="text/javascript" src="/ext/d3/d3.js"></script> #+html_head: <script type="text/javascript" src="point.js"></script> #+html_head: <script type="text/javascript" src="fx.js"></script> #+html_head: <script type="text/javascript" src="fx_view.js"></script> #+html_head: <script type="text/javascript" src="parametric-drag-example.js"></script>
4.5. Insert html fragment to invoke our interactive javascript code
This also follows the same model we used in example #3.
#+begin_html <div id="frame"></div> <script type="text/javascript"> window.onload = function() { ex.start(this); } </script> #+end_html