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