UP | HOME

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

Author: Roland Conybeare

Created: 2024-09-08 Sun 18:01

Validate