Skip to content

Instantly share code, notes, and snippets.

@eweitnauer
Created October 24, 2013 20:27
Show Gist options
  • Save eweitnauer/7144421 to your computer and use it in GitHub Desktop.
Save eweitnauer/7144421 to your computer and use it in GitHub Desktop.
Hold and Tap

Hold any circle and tap another one to connect / disconnect both.

<!DOCTYPE html>
<meta charset="utf-8">
<title>Hold and Tap</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="mtouch-events.js"></script>
<style>
.link { stroke: silver; stroke-width: 3px }
.selected { stroke-width: 4px }
</style>
<body>
<script>
var width = 960
,height = 500
,radius = 40
,color = d3.scale.category10();
var objs = d3.range(12).map(function (x, i) {
return {id: i
,x: width/5*(i%4+1)
,y: height/4*(Math.floor(i/4)+1)}
});
var links = [];
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g_links = svg.append('g');
var mtouch = mtouch_events()
.on("tap", tapped)
.on("touch", touched)
.on("release", released);
var circles = svg.selectAll("circle")
var circles = svg.selectAll("circle.node")
.data(objs)
.enter()
.append("circle")
.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.x+ ',' + d.y + ")" })
.call(mtouch);
function update_links() {
console.log('updating', links);
var ls = g_links.selectAll('.link')
.data(links);
ls.enter()
.append('line')
.classed('link', true);
ls.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
ls.exit()
.remove();
}
function tapped(d) {
console.log('tap');
var fingers = d3.event.fingers;
if (fingers.length != 2) return;
var f1 = d3.event.finger
,f2 = fingers[0] === f1 ? fingers[1] : fingers[0]
,d1 = d3.select(f1.target).datum()
,d2 = d3.select(f2.target).datum();
var idx = -1;
links.some(function (l, i) {
if ((l.source.id == d1.id && l.target.id == d2.id) ||
(l.source.id == d2.id && l.target.id == d1.id)) {
idx = i;
return true;
}
return false;
});
console.log(idx);
if (idx == -1) links.push({source: d1
,target: d2});
else links.splice(idx, 1);
update_links();
}
function touched() {
d3.select(this).classed('selected', true);
}
function released() {
d3.select(this).classed('selected', false);
}
</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