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

Created: 2026-06-22 Mon 01:53

Validate