UP | HOME

d3 draggable object example #3 – parametric

Table of Contents

1. Introduction

This page uses the same technique as example #2 (../drag2/index.html), except we demonstrate parametric dragging along a curve

source
org-mode source for this page is here: index-src.html

2. Prerequisites

As for example #1 (../drag1/index.html)

3. Demo

(box appears below if-and-only-if publishing to html)

4. Procedure

4.1. Create javascript file point.js

This file contains convenience subroutines for working with (x,y) coordinates

  • pt.scale_pt
  • pt.add_pt
  • pt.distance_squared
  • pt.find_closest
  1: !function() {
  2:     var pt = {};
  3: 
  4:     /* module for working with 2-dimensional coordinates */
  5: 
  6:     /* scale coordinates p.x, p.y by a factor of k */
  7:     pt.scale_pt = function(k, p)
  8:     {
  9:     return {x: k*p.x, y: k*p.y};
 10:     } /*scale_pt*/
 11: 
 12:     /* returns cordinate-wise sum of points p1 and p2 */
 13:     pt.add_pt = function(p1, p2)
 14:     {
 15:     return {x: p1.x+p2.x, y: p1.y+p2.y};
 16:     } /*add_pt*/
 17: 
 18:     pt.sub_pt = function(p1, p2)
 19:     {
 20:     return {x: p1.x-p2.x, y: p1.y-p2.y};
 21:     } /*sub_pt*/
 22: 
 23:     /* returns true iff the rectangle with corners (0,0), and box_pt
 24:      * contains the point p;  false otherwise
 25:      *
 26:      * box :: Point
 27:      * p :: Point
 28:      */
 29:     pt.box_contains = function(box, p)
 30:     {
 31:     return ((0 <= p.x)
 32:         && (p.x <= box.x)
 33:         && (0 <= p.y)
 34:         && (p.y <= box.y));
 35:     } /*box_contains*/
 36: 
 37:     /* find squared distance between two points
 38:      * p1 and p2.
 39:      * 
 40:      * Point = {x,y}
 41:      * p1, p2 :: Point
 42:      */
 43:     pt.distance_squared = function(p1, p2)
 44:     {
 45:     var dpt = pt.sub_pt(p1, p2);
 46: 
 47:     return dpt.x*dpt.x + dpt.y*dpt.y;
 48:     } /*distance_squared*/
 49: 
 50:     /* given a set of points, 
 51:      * find the point that's closest to a particular target point.
 52:      * O(n) in n=fn_pt_v.length
 53:      * 
 54:      * point = {x,y}
 55:      * fn_pt_v :: array(point)
 56:      * target_fn :: point
 57:      * return :: point | null
 58:      */
 59:     pt.find_closest_ix = function(target_pt, fn_pt_v)
 60:     {
 61:     var best_ix = -1;
 62:     var best_d2 = null;
 63:     var best_pt = null;
 64:     var i = 0;
 65: 
 66:     for(var n=fn_pt_v.length; i<n; ++i) {
 67:         var fn_pt = fn_pt_v[i];
 68:         var d2 = pt.distance_squared(target_pt, fn_pt);
 69:         if(best_ix === -1 || d2 < best_d2) {
 70:         best_ix = i;
 71:         best_d2 = d2;
 72:         best_pt = fn_pt;
 73:         }
 74:     }
 75: 
 76:     return best_ix;
 77:     } /*find_closest_ix*/
 78: 
 79:     /* like find_closest_ix(), but returns the closest
 80:      * point,  instead of its index in fn_pt_v.
 81:      * 
 82:      * Equivalent to fn_pt_v[find_closest_ix(target_pt, fn_pt_v)]
 83:      */
 84:     pt.find_closest = function(target_pt, fn_pt_v)
 85:     {
 86:     var best_ix = pt.find_closest_ix(target_pt, fn_pt_v);
 87:     return fn_pt_v[best_ix];
 88:     } /*find_closest*/
 89: 
 90:     /* given a line segment L defined by endpoints p, p_ref,  and a bounding rectange r
 91:      * defined by points (0,0) and b
 92:      * where p_ref is assumed ot be inside r:
 93:      * - if p is also inside r,  return p
 94:      * - otherwise return the point pl on L that intesects the boundary of r
 95:      */
 96:     pt.clip_line = function(p, p_ref, box)
 97:     {
 98:     /* if clipping occurs,  it's for higher values of t */
 99:     var t_min = 1.0;
100:     var t1 = 1.0;
101:     var t2 = 1.0;
102:     var t3 = 1.0;
103:     var t4 = 1.0;
104: 
105:     if(pt.box_contains(box, p)) {
106:         return p;
107:     } else {
108:         /* parametrize L by t in [0,1]:
109:          *   L(t) = p_ref + t * (p - p_ref)
110:          * note that when we assign to p,  we replace L with a sub-segment of itself
111:          */
112:         if(p.x < 0.0) {
113:         /* p on LHS of r */
114:         t1 = (0.0 - p_ref.x) / (p.x - p_ref.x);
115:         }
116: 
117:         if(p.x > box.x) {
118:         /* p on RHS or r */
119:         t2 = (box.x - p_ref.x) / (p.x - p_ref.x);
120:         }
121: 
122:         if(p.y < 0.0) {
123:         /* p above r */
124:         t3 = (0.0 - p_ref.y) / (p.y - p_ref.y);
125:         }
126: 
127:         if(p.y > box.y) {
128:         /* p below r */
129:         t4 = (box.y - p_ref.y) / (p.y - p_ref.y);
130:         }
131: 
132:         t_min = Math.min(t1,t2,t3,t4);
133: 
134:         /* clip p: p <- p_ref + t * (p - p_ref) */
135:         p = pt.add_pt(p_ref, pt.scale_pt(t_min, pt.sub_pt(p, p_ref)));
136: 
137:         return p;
138:     }
139:     } /*clip_line*/
140: 
141:     this.pt = pt;
142: }();

4.2. Create javascript file fx.js

This contains some utility procedures for working with functions of one variable

 1: !function() {
 2:     /* module for working with a particular one-parameter function -- call it f(x) */
 3: 
 4:     var fx
 5:     = { /*procedure to evaluate f(x)*/
 6:         target_fn: null,
 7:         /* like target_fn(),  but returns a point */
 8:         eval_fn: null
 9:     };
10: 
11:     /* target function f(x)
12:      * x :: number
13:      * returns :: number
14:      */
15:     fx.target_fn = function(x) {
16:     return x * x * (x - 0.6);
17:     } /*target_fn*/
18: 
19:     /* f(x), returns a point
20:      * x :: number
21:      * returns :: Point
22:      */
23:     fx.eval_fn = function(x_arg)
24:     {
25:     return {x: x_arg, y: fx.target_fn(x_arg)};
26:     } /*eval_fn*/
27: 
28:     /* given scale k, {offset_x, offset_y}
29:      * returns function
30:      *   f({x,y}) = {offset_x + scale*x, offset_y + scale*y}
31:      */
32:     fx.linear_fn = function(offset_pt, scale) {
33:     /* p :: Point */
34:     return function(p) {
35:         return pt.add_pt(offset_pt,
36:                  pt.scale_pt(scale, p));
37:     }
38:     } /*linear_fn*/
39: 
40:     /* make an array of points, representing
41:      * fx evaluated at regularly-spaced intervals;
42:      * clip so that all points returned fall within the rectangle
43:      * defined by (0,0) and box_pt
44:      *
45:      * fx :: number -> Point
46:      * lo_x, hi_x :: number
47:      * n_pt :: number
48:      * box_pt :: Point
49:      */
50:     fx.make_target_pt_v = function(fx, lo_x, hi_x, n_pt, pt2screen, box_pt)
51:     {
52:     var target_pt_v = [];
53:     var target_pt_i = 0;
54:     var p = null;
55:     var pm1 = null;
56: 
57:     /* evaluate f(x) at regularly spaced x-coordinates on interval [lo_x, hi_x] 
58:      * keep only points with screen coords that fall inside the rectangle
59:      * with corners {(0,0), box_pt}, plus:
60:      * one point just before and one point just after
61:      */
62:     for(var i=0; i<n_pt; ++i) {
63:         tmp = lo_x + (i * (hi_x - lo_x) / n_pt);
64:         pm1 = p;
65:         p = pt2screen(fx(tmp));
66: 
67:         if(pt.box_contains(box_pt, p)) {
68:         if((pm1 !== null) && !pt.box_contains(box_pt, pm1)) {
69:             /* pm1 outside box,  substitute clipped version */
70:             target_pt_v[target_pt_i++] = pt.clip_line(pm1, p, box_pt);
71:         }
72:         target_pt_v[target_pt_i++] = p;
73:         } else if((pm1 !== null) && pt.box_contains(box_pt, pm1)) {
74:         /* if pm1 was inside the box,  then include p 
75:            after all */
76:         target_pt_v[target_pt_i++] = pt.clip_line(p, pm1, box_pt);
77:         }
78:     }
79: 
80:     return target_pt_v;
81:     } /*make_target_pt_v*/
82: 
83:     this.fx = fx;
84: }();

4.3. Create javascript file fx_view.js

This file contains subroutines for managing svg elements (as d3 selections) that display a function f(x)

  1: !function() {
  2:     /* module for displaying a function f(x) using svg,
  3:      * together with a draggable point that's constrained to lie on the function
  4:      *
  5:      * uses: module point.js
  6:      */
  7: 
  8:     /* view elements - things we see on the screen */
  9:     var fx_view
 10:     = { /* procedure to setup this view */
 11:         draw: null,
 12:         /* svg bounding box */
 13:         box: null,
 14:         /* svg path representing points {x,f(x)} */
 15:         fx_path: null,
 16:         /* function to generate a path from a vector of points */
 17:         svg_line_fn: null,
 18:         /* function to respond to a new selection point */
 19:         fx_update_select_circle: null,
 20:         /* svg element to display a selected point on f(x) */
 21:         fx_select_circle: null,
 22:         /* function to update selection given mouse coords */
 23:         fx_update_select: null,
 24:         /* function to invoke when a drag event occurs */
 25:         drag_function : null
 26:     };
 27: 
 28:     /* function to create an SVG approximation to a parametric function 
 29:      * using a series of straight line segments
 30:      */
 31:     fx_view.svg_line_fn = (d3.svg.line()
 32:                .x(function(d) { return d.x; })
 33:                .y(function(d) { return d.y; })
 34:                .interpolate("linear"));
 35: 
 36:     /* create/update selection circle - intended to mark
 37:      * location of selected point pt
 38:      */
 39:     fx_view.fx_update_select_circle
 40:     = function(pt)
 41:     {
 42:     var dd = [{center: pt, radius: 4}];
 43: 
 44:     fx_view.fx_select_circle = (fx_view.box.selectAll(".fxselect")
 45:                     .data(dd));
 46: 
 47:     /* create draggable circle representing a selected point on f(x) */
 48:     fx_view.fx_select_circle
 49:         .enter()
 50:         .append("svg:circle")
 51:         .attr("class", "fxselect")
 52:         .call(fx_view.drag_function)
 53:         .attr("r", function(d) { return d.radius; })
 54:         .style("fill", "black");
 55: 
 56:     fx_view.fx_select_circle
 57:         .attr("cx", function(d) { return d.center.x; })
 58:         .attr("cy", function(d) { return d.center.y; });
 59:     } /*fx_update_select_circle*/
 60: 
 61:     /* parent_id :: string.  pass this to d3.select() to get selection for parent
 62:      *   at which to attach svg box
 63:      * box_pt :: Point.  size of svg bounding box
 64:      */
 65:     fx_view.draw = function(parent_id, box_pt, target_pt_v) {
 66:     /* create an svg bounding box, to contain interactive drawing area */
 67:     fx_view.box = (d3.select(parent_id)
 68:                .append("svg")
 69:                .attr("class", "box")
 70:                .attr("width", box_pt.x)
 71:                .attr("height", box_pt.y));
 72: 
 73:     /* border, so bounding box is visible */
 74:     fx_view.border = (fx_view.box.append("svg:rect")
 75:               .attr("class", "border")
 76:               .attr("x", 1)
 77:               .attr("y", 1)
 78:               .attr("width", box_pt.x - 2)
 79:               .attr("height", box_pt.y - 2)
 80:               .attr("stroke", "blue")
 81:               .attr("stroke-width", 3)
 82:               .style("fill", "none"));
 83: 
 84:     /* create path representing our target function f(x) */
 85:     fx_view.fx_path = (fx_view.box.append("path")
 86:                .attr("d", fx_view.svg_line_fn(target_pt_v))
 87:                .attr("stroke", "blue")
 88:                .attr("stroke-width", 2)
 89:                .attr("fill", "none")
 90:               );
 91: 
 92:     fx_view.fx_update_select_circle(pt.find_closest(pt.scale_pt(0.5, box_pt),
 93:                             target_pt_v));
 94: 
 95:     } /*draw*/
 96: 
 97:     /* update selection circle
 98:      * for an event at Point pt
 99:      */
100:     fx_view.fx_update_select
101:     = function(p, target_pt_v)
102:     {
103:     /* find point on {x,f(x)} that's closest to 
104:      * mouse location (i.e. to d3.event)
105:      */
106:     var best_fx_pt = pt.find_closest(p, target_pt_v);
107: 
108:     fx_view.fx_update_select_circle(best_fx_pt)
109:     } /*fx_update_select*/
110: 
111:     fx_view.init_drag_function
112:     = (function(target_pt_v) {
113:         fx_view.drag_function
114:         = (d3.behavior.drag()
115:            .on("dragstart",
116:                function() {
117:                fx_view.fx_select_circle
118:                    .style("fill", "red")
119:                    .attr("r", 5);
120:                })
121:            .on("drag",
122:                function() {
123:                fx_view.fx_update_select(d3.event, ex.target_pt_v);
124:                })
125:            .on("dragend",
126:                function() {
127:                fx_view.fx_select_circle
128:                    .style("fill", "black")
129:                    .attr("r", 4);
130:                })
131:           );
132:     });
133: 
134:     this.fx_view = fx_view;
135: }();

4.4. Create javascript file parametric-drag-example.js:

This program:

  • draws the cubic polynomial \(f(x) = x^2(x - 0.6)\)
  • draws a draggable filled circle that's constrained to coordinates \((x,f(x))\)
 1: !function() {
 2:     var ex = {};
 3: 
 4:     /* Requires: 
 5:      * - point.js
 6:      * - fx.js
 7:      * - fx_view.js
 8:      */
 9:     ex.box_pt = {x: 600, y: 400};
10: 
11:     /* w :: Window */
12:     ex.start = function(w)
13:     {
14:     ex.pt2screen = fx.linear_fn(pt.sub_pt(pt.scale_pt(0.5, ex.box_pt),
15:                           fx.eval_fn(0.0) /*ctr_fx*/),
16:                     200.0 /*scale factor*/);
17:     ex.target_pt_v = fx.make_target_pt_v(fx.eval_fn,
18:                          -1.66, +5.0, 200.0 /*n_pt*/,
19:                          ex.pt2screen, ex.box_pt);
20:     fx_view.init_drag_function(ex.target_pt_v);
21:     fx_view.draw("#frame", ex.box_pt, ex.target_pt_v);
22:     }
23: 
24:     this.ex = ex;
25: }();

4.5. Load .js files in html header

Tell org-mode's html generator to load d3 and parametric-drag-example.js when it generates this page's html <head> element. We did the same thing in example #2.

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>

then this embedded javascript runs (see browser dev console)..

#+begin_export html
<div id="frame" style="border: 1px solid blue; max-width: 60em"></div>
<script type="text/javascript">
  window.onload = function() { ex.start(this); }
</script>
#+end_export

Author: Roland Conybeare

Created: 2024-09-08 Sun 18:01

Validate