Skip to content

Instantly share code, notes, and snippets.

@eweitnauer
Created October 24, 2013 20:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eweitnauer/7144489 to your computer and use it in GitHub Desktop.
Save eweitnauer/7144489 to your computer and use it in GitHub Desktop.
Drag All in Rectangle

Drag multiple circles at once using two fingers. The circles inside the rectangle spanned by your fingers when you start dragging are selected.

<!doctype html>
<meta charset="utf-8">
<title>Drag all in Rectangle</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="mtouch-events.js"></script>
<style>
.overlay { fill: none; pointer-events: all; }
.selection { fill: #eee; stroke: #ddd; }
.selected { stroke-width: 4px; }
</style>
<body>
<script>
var width = 960
,height = 500
,radius = 30
,margin = 10 // if circle is this close to the rectangle, it is still selected
,finger_radius = 40
,color = d3.scale.category10();
var objs = d3.range(20).map(function () {
return [Math.random()*(width-2*radius)+radius
,Math.random()*(height-2*radius)+radius]
});
var mtouch = mtouch_events()
.on("release", dragend)
.on("touch", touch)
.on("mdrag", drag);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append('g');
var g_links = svg.append('g');
var circles = svg.selectAll("circle.node")
.data(objs)
.enter()
.append("circle")
.attr('class', 'node')
.attr('r', radius)
.style({ fill: function (d,i) { return d3.hcl(color(i)).brighter(2) }
,stroke: function (d,i) { return d3.hsl(color(i)) }})
.attr("transform", function(d) { return "translate(" + d + ")"; });
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.datum({})
.call(mtouch);
function update_fingers(fingers) {
var ls = g_links.selectAll('.selection');
if (fingers) {
var rects = [], N = fingers.length;
if (N>1) {
var x0 = Math.min(fingers[0].pos[0], fingers[1].pos[0])
,x1 = Math.max(fingers[0].pos[0], fingers[1].pos[0])
,y0 = Math.min(fingers[0].pos[1], fingers[1].pos[1])
,y1 = Math.max(fingers[0].pos[1], fingers[1].pos[1]);
rects.push({x: x0, y: y0, width: x1-x0, height: y1-y0});
};
ls = ls.data(rects);
ls.enter()
.append('rect')
.classed('selection', true)
.style("stroke-width", margin);;
ls.exit().remove();
}
ls.attr("x", function(d) { return d.x-margin/2 })
.attr("y", function(d) { return d.y-margin/2 })
.attr("width", function(d) { return d.width+margin })
.attr("height", function(d) { return d.height+margin });
}
function select_targets(d, fingers) {
if (d.targets) d.targets.classed('selected', false);
if (fingers.length > 0) {
var f1 = fingers[0].pos
,f2 = fingers[1] ? fingers[1].pos : f1
,p = [(f1[0]+f2[0])/2, (f1[1]+f2[1])/2];
d.targets = get_elements_in_rect(f1, f2);
d.p = p;
d.targets.classed('selected', true);
} else delete d.targets;
}
function do_later(cb) {
d3.transition().duration(0).each('end', cb);
}
function touch(d) {
var fingers = d3.event.fingers;
select_targets(d, fingers);
do_later(update_fingers.bind(this, fingers));
}
function drag(d) {
var fingers = d3.event.fingers;
var f1 = fingers[0].pos
,f2 = fingers[1] ? fingers[1].pos : f1
,p = [(f1[0]+f2[0])/2, (f1[1]+f2[1])/2]
,dx = p[0]-d.p[0], dy = p[1]-d.p[1];
d.p = p;
svg.selectAll('.selection')
.each(function (d) { d.x += dx; d.y += dy });
d.targets.each(function(d_) { d_[0] += dx; d_[1] += dy })
do_later(function() {
update_fingers(fingers);
d.targets.attr("transform", function(d_) {
return "translate(" + d_ + ")" }
);
});
}
function dragend(d) {
var fingers = d3.event.fingers;
select_targets(d, fingers);
do_later(update_fingers.bind(this, fingers));
}
function get_elements_in_rect(a, b) {
var x0 = Math.min(a[0], b[0]), x1 = Math.max(a[0], b[0])
,y0 = Math.min(a[1], b[1]), y1 = Math.max(a[1], b[1]);
var hits = circles
.filter(function (d) {
return d[0]>x0-margin && d[0]<x1+margin && d[1]>y0-margin && d[1]<y1+margin;
})
return hits;
}
</script>
/// Based on the d3.behavior.drag and d3.behavior.zoom.
mtouch_events = function() {
var event = d3_eventDispatch(mtouch, "tap", "dbltap", "hold", "drag", "mdrag", 'touch', 'release')
,fingers = [] // array of augmented touches = fingers
,id2finger = {} // maps ids to fingers
,last_taps = [] // [{timeStamp: xxx, pos: [x,y]}, ...], used to detect dbltaps
,mouse_id = 'mouse'
,tap_max_time = 250
,tap_max_dist2 = 10*10
,hold_time = 500
,hold_max_dist2 = 10*10
,dbltap_max_delay = 400
,dbltap_max_dist = 20;
function mtouch() {
this.on("touchstart.mtouch", touchstarted)
.on("mousedown.mtouch", mousedown)
.on("touchmove.mtouch", touchmoved)
.on("touchend.mtouch", touchended)
.on("touchcancel.mtouch", touchended);
}
mtouch.call = function(f) {
f.apply(mtouch, arguments); return this;
}
/// On mousedown, start listening for mousemove and mouseup events on the
/// whole window. Also call the touchstarted function. If it was not the left
/// mousebutton that was pressed, do nothing.
function mousedown() {
if (!detectLeftButton(d3.event)) return;
var w = d3.select(window);
var thiz = this, argumentz = arguments;
w.on("mousemove.mtouch", function() { touchmoved.apply(thiz, argumentz) });
w.on("mouseup.mtouch", function() {
w.on("mousemove.mtouch", null);
w.on("mouseup.mtouch", null);
touchended.apply(thiz, argumentz);
});
touchstarted.apply(this, arguments);
}
function touchstarted() {
d3.event.preventDefault();
var target = this
,event_ = event.of(target, arguments)
,touches = get_changed_touches();
for (var i=0,N=touches.length; i<N; i++) {
var finger = new Finger(touches[i].identifier, event_, target);
fingers.push(finger);
id2finger[touches[i].identifier] = finger;
event_({type: 'touch', finger: finger, fingers: fingers});
}
}
function touchmoved() {
d3.event.preventDefault();
var target = this
,event_ = event.of(target, arguments)
,touches = get_changed_touches();
for (var i=0,N=fingers.length; i<N; i++) fingers[i].changed = false;
var df = [];
for (var i=0,N=touches.length; i<N; i++) {
var finger = id2finger[touches[i].identifier];
if (!finger) continue;
finger.move(event_);
df.push(finger);
}
event_({type: 'mdrag', dragged_fingers: df, fingers: fingers});
}
function touchended() {
d3.event.preventDefault();
var target = this
,event_ = event.of(target, arguments)
,touches = get_changed_touches();
for (var i=0,N=touches.length; i<N; i++) {
var finger = id2finger[touches[i].identifier];
if (!finger) continue;
finger.end(event_);
delete id2finger[touches[i].identifier];
fingers = d3.values(id2finger);
event_({type: 'release', finger: finger, fingers: fingers});
}
}
function Finger(id, event, target) {
this.id = id;
this.target = target;
this.event = event;
this.parent = target.parentNode;
this.timeStamp0 = d3.event.timeStamp;
this.timeStamp = this.timeStamp0;
this.hold_timer = setTimeout(this.held.bind(this), hold_time);
this.pos = get_position(this.parent, this.id);
this.pos0 = [this.pos[0], this.pos[1]];
this.dist_x = 0; // dx between current and starting point
this.dist_y = 0;
this.dx = 0; // dx in the last dragging step
this.dy = 0;
this.dt = 0; // dt in the last dragging step
this.changed = true; // used by gesture to check whether it needs to update
this.gesture = null; // is set when finger gets bound to a gesture
}
Finger.prototype.cancel_hold = function() {
if (this.hold_timer) clearTimeout(this.hold_timer);
this.hold_timer = null;
}
Finger.prototype.held = function() {
this.event({type: 'hold', id: this.id, fingers: fingers});
this.hold_timer = null;
}
Finger.prototype.move = function(event) {
this.changed = true;
this.event = event;
var p = get_position(this.parent, this.id)
,t = d3.event.timeStamp;
this.dx = p[0] - this.pos[0];
this.dy = p[1] - this.pos[1];
this.dist_x = p[0] - this.pos0[0];
this.dist_y = p[1] - this.pos0[1];
this.pos = p;
this.dt = t-this.timeStamp;
this.timeStamp = t;
if (this.dist_x*this.dist_x+this.dist_y*this.dist_y > hold_max_dist2) {
this.cancel_hold();
}
if (this.gesture) return;
event({type: 'drag', finger: this, x: this.pos[0], y: this.pos[1]
,dx: this.dx, dy: this.dy, fingers: fingers});
}
Finger.prototype.end = function(event) {
var dt = d3.event.timeStamp - this.timeStamp0;
if (dt <= tap_max_time && (this.dist_x*this.dist_x+this.dist_y*this.dist_y) <= tap_max_dist2) {
if (match_tap(d3.event.timeStamp, this.pos[0], this.pos[1])) {
event({type: 'dbltap', finger: this, fingers: fingers});
} else {
event({type: 'tap', finger: this, fingers: fingers});
}
}
this.cancel_hold();
}
function get_changed_touches() {
return d3.event.changedTouches || [{identifier: mouse_id}];
}
function detectLeftButton(event) {
if ('buttons' in event) return event.buttons === 1;
else if ('which' in event) return event.which === 1;
else return event.button === 1;
}
/// Returns true if any tap in the last_taps list is spatially and temporally
/// close enough to the passed time and postion to count as a dbltap. If not,
/// the passed data is added as new tap. All taps that are too old are removed.
function match_tap(timeStamp, x, y) {
var idx = -1, pos = [x,y];
last_taps = last_taps.filter(function (tap, i) {
if (timeStamp - tap.timeStamp <= dbltap_max_delay
&& get_distance(tap.pos, pos) <= dbltap_max_dist) idx = i;
return tap.timeStamp-timeStamp <= dbltap_max_delay && idx !== i;
});
if (idx === -1) last_taps.push({timeStamp: timeStamp, pos: pos});
return idx !== -1;
}
function get_position(container, id) {
if (id === mouse_id) return d3.mouse(container);
else return d3.touches(container).filter(function(p) { return p.identifier === id; })[0];
}
function get_distance(p1, p2) {
return Math.sqrt((p1[0]-p2[0])*(p1[0]-p2[0]) + (p1[1]-p2[1])*(p1[1]-p2[1]));
}
return d3.rebind(mtouch, event, "on");
};
/// Replication of the internal d3_eventDispatch method.
function d3_eventDispatch(target) {
var dispatch = d3.dispatch.apply(this, Array.apply(null, arguments).slice(1));
dispatch.of = function(thiz, argumentz) {
return function(e1) {
try {
var e0 =
e1.sourceEvent = d3.event;
e1.target = target;
d3.event = e1;
dispatch[e1.type].apply(thiz, argumentz);
} finally {
d3.event = e0;
}
};
};
return dispatch;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment