From 882457a351afe45cd72344263de44f5843a3b1b3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 17 Dec 2015 22:18:58 -0500 Subject: [PATCH 1/2] Add Breathe behavior for interpolated select halos (closes #1814) --- css/map.css | 6 --- index.html | 1 + js/id/behavior/breathe.js | 100 ++++++++++++++++++++++++++++++++++++++ js/id/modes/select.js | 1 + test/index.html | 1 + 5 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 js/id/behavior/breathe.js diff --git a/css/map.css b/css/map.css index 4a537591f..8087008e1 100644 --- a/css/map.css +++ b/css/map.css @@ -43,12 +43,6 @@ path.shadow { pointer-events: stroke; } -.shadow { - -webkit-transition: 200ms; - -moz-transition: 200ms; - transition: 200ms; -} - /* points */ g.point .stroke { diff --git a/index.html b/index.html index 42522a2bf..e51b6ef9b 100644 --- a/index.html +++ b/index.html @@ -178,6 +178,7 @@ + diff --git a/js/id/behavior/breathe.js b/js/id/behavior/breathe.js new file mode 100644 index 000000000..954b8bfa9 --- /dev/null +++ b/js/id/behavior/breathe.js @@ -0,0 +1,100 @@ +iD.behavior.Breathe = function() { + var duration = 1000, + selector = '.selected.shadow, .selected .shadow', + params = {}, + done, selection; + + function reset(selection) { + return selection + .style('fill-opacity', null) + .style('r', null) + .style('stroke-opacity', null) + .style('stroke-width', null); + } + + function reselect(surface) { + return function() { + if (done) return true; + var currSelection = surface.selectAll(selector); + if (_.isEqual(currSelection, selection)) return false; // no change + + selection = currSelection; + if (selection.empty()) return false; + + // reset styles, calculate animation params + selection + .call(reset) + .each(function calcAnimationParams(d) { + if (params.hasOwnProperty(d.id)) return; + + // determine default opacity and width + var s = d3.select(this), + tag = s.node().tagName, + p = {}, + opacity, width; + + if (tag === 'circle') { + opacity = parseFloat(s.style('fill-opacity') || 0.5); + width = parseFloat(s.style('r') || 15.5); + } else { + opacity = parseFloat(s.style('stroke-opacity') || 0.7); + width = parseFloat(s.style('stroke-width') || 10); + } + + // calculate min/max interpolation params based on defaults.. + p.tag = tag; + p.opacity0 = Math.max(opacity - 0.4, 0.1); + p.opacity1 = Math.min(opacity + 0.2, 1.0); + p.width0 = Math.max(width - (tag === 'circle' ? 1 : 2), 4); + p.width1 = width + 2; + params[d.id] = p; + }); + + inhale(); + }; + } + + function inhale() { + if (done || selection.empty()) { + selection.call(reset); + return; + } + selection + .transition() + .style('stroke-opacity', function(d) { return params[d.id].opacity1; }) + .style('stroke-width', function(d) { return params[d.id].width1; }) + .style('fill-opacity', function(d) { return params[d.id].opacity1; }) + .style('r', function(d) { return params[d.id].width1; }) + .duration(duration) + .each('end', exhale); + } + + function exhale() { + if (done || selection.empty()) { + selection.call(reset); + return; + } + selection + .transition() + .style('stroke-opacity', function(d) { return params[d.id].opacity0; }) + .style('stroke-width', function(d) { return params[d.id].width0; }) + .style('fill-opacity', function(d) { return params[d.id].opacity0; }) + .style('r', function(d) { return params[d.id].width0; }) + .duration(duration) + .each('end', inhale); + } + + var breathe = function(surface) { + done = false; + d3.timer(reselect(surface), 200); + }; + + breathe.off = function() { + done = true; + if (selection) { + selection.call(reset); + } + }; + + return breathe; +}; diff --git a/js/id/modes/select.js b/js/id/modes/select.js index db821f137..f016f5b92 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -9,6 +9,7 @@ iD.modes.Select = function(context, selectedIDs) { behaviors = [ iD.behavior.Copy(context), iD.behavior.Paste(context), + iD.behavior.Breathe(context), iD.behavior.Hover(context), iD.behavior.Select(context), iD.behavior.Lasso(context), diff --git a/test/index.html b/test/index.html index d551f6667..1fa6301c8 100644 --- a/test/index.html +++ b/test/index.html @@ -157,6 +157,7 @@ + From c185189512a24024f2b79917c22316584c36f7a4 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 18 Dec 2015 20:39:45 -0500 Subject: [PATCH 2/2] Adjust interpolation params, DRY code, perf improvements --- css/map.css | 11 +-- js/id/behavior/breathe.js | 151 ++++++++++++++++++++------------------ 2 files changed, 84 insertions(+), 78 deletions(-) diff --git a/css/map.css b/css/map.css index 8087008e1..3c786a6fb 100644 --- a/css/map.css +++ b/css/map.css @@ -43,6 +43,7 @@ path.shadow { pointer-events: stroke; } + /* points */ g.point .stroke { @@ -127,11 +128,11 @@ g.vertex.vertex-hover { g.vertex.hover:not(.selected) .shadow, g.midpoint.hover:not(.selected) .shadow { - fill-opacity: 0.3; + fill-opacity: 0.5; } g.vertex.selected .shadow { - fill-opacity: 0.5; + fill-opacity: 0.7; } .mode-draw-area g.midpoint, @@ -166,7 +167,7 @@ path.shadow { } path.shadow.hover:not(.selected) { - stroke-opacity: 0.3; + stroke-opacity: 0.4; } path.shadow.selected { @@ -1560,10 +1561,10 @@ text.gpx { } .fill-wireframe path.shadow.hover:not(.selected) { - stroke-opacity: 0.2; + stroke-opacity: 0.4; } .fill-wireframe path.shadow.selected { - stroke-opacity: 0.4; + stroke-opacity: 0.6; } .fill-wireframe .point, diff --git a/js/id/behavior/breathe.js b/js/id/behavior/breathe.js index 954b8bfa9..379c49adf 100644 --- a/js/id/behavior/breathe.js +++ b/js/id/behavior/breathe.js @@ -1,99 +1,104 @@ iD.behavior.Breathe = function() { - var duration = 1000, + var duration = 800, selector = '.selected.shadow, .selected .shadow', + selected = d3.select(null), + classed = [], params = {}, - done, selection; + done; function reset(selection) { - return selection - .style('fill-opacity', null) - .style('r', null) + selection .style('stroke-opacity', null) - .style('stroke-width', null); + .style('stroke-width', null) + .style('fill-opacity', null) + .style('r', null); } - function reselect(surface) { - return function() { - if (done) return true; - var currSelection = surface.selectAll(selector); - if (_.isEqual(currSelection, selection)) return false; // no change - - selection = currSelection; - if (selection.empty()) return false; - - // reset styles, calculate animation params - selection - .call(reset) - .each(function calcAnimationParams(d) { - if (params.hasOwnProperty(d.id)) return; - - // determine default opacity and width - var s = d3.select(this), - tag = s.node().tagName, - p = {}, - opacity, width; - - if (tag === 'circle') { - opacity = parseFloat(s.style('fill-opacity') || 0.5); - width = parseFloat(s.style('r') || 15.5); - } else { - opacity = parseFloat(s.style('stroke-opacity') || 0.7); - width = parseFloat(s.style('stroke-width') || 10); - } - - // calculate min/max interpolation params based on defaults.. - p.tag = tag; - p.opacity0 = Math.max(opacity - 0.4, 0.1); - p.opacity1 = Math.min(opacity + 0.2, 1.0); - p.width0 = Math.max(width - (tag === 'circle' ? 1 : 2), 4); - p.width1 = width + 2; - params[d.id] = p; - }); - - inhale(); - }; + function setAnimationParams(transition, fromTo) { + transition + .style('stroke-opacity', function(d) { return params[d.id][fromTo].opacity; }) + .style('stroke-width', function(d) { return params[d.id][fromTo].width; }) + .style('fill-opacity', function(d) { return params[d.id][fromTo].opacity; }) + .style('r', function(d) { return params[d.id][fromTo].width; }); } - function inhale() { - if (done || selection.empty()) { - selection.call(reset); + function calcAnimationParams(selection) { + selection + .call(reset) + .each(function(d) { + var s = d3.select(this), + tag = s.node().tagName, + p = {'from': {}, 'to': {}}, + opacity, width; + + // determine base opacity and width + if (tag === 'circle') { + opacity = parseFloat(s.style('fill-opacity') || 0.5); + width = parseFloat(s.style('r') || 15.5); + } else { + opacity = parseFloat(s.style('stroke-opacity') || 0.7); + width = parseFloat(s.style('stroke-width') || 10); + } + + // calculate from/to interpolation params.. + p.tag = tag; + p.from.opacity = opacity * 0.6; + p.to.opacity = opacity * 1.25; + p.from.width = width * 0.9; + p.to.width = width * (tag === 'circle' ? 1.5 : 1.25); + params[d.id] = p; + }); + } + + function run(surface, fromTo) { + var toFrom = (fromTo === 'from' ? 'to': 'from'), + currSelected = surface.selectAll(selector), + currClassed = Array.prototype.slice.call(surface.node().classList), + n = 0; + + if (done || currSelected.empty()) { + selected.call(reset); return; } - selection - .transition() - .style('stroke-opacity', function(d) { return params[d.id].opacity1; }) - .style('stroke-width', function(d) { return params[d.id].width1; }) - .style('fill-opacity', function(d) { return params[d.id].opacity1; }) - .style('r', function(d) { return params[d.id].width1; }) - .duration(duration) - .each('end', exhale); - } - function exhale() { - if (done || selection.empty()) { - selection.call(reset); - return; + if (!_.isEqual(currSelected, selected) || !_.isEqual(currClassed, classed)) { + selected.call(reset); + classed = _.clone(currClassed); + selected = currSelected.call(calcAnimationParams); } - selection + + selected .transition() - .style('stroke-opacity', function(d) { return params[d.id].opacity0; }) - .style('stroke-width', function(d) { return params[d.id].width0; }) - .style('fill-opacity', function(d) { return params[d.id].opacity0; }) - .style('r', function(d) { return params[d.id].width0; }) + .call(setAnimationParams, fromTo) .duration(duration) - .each('end', inhale); + .each(function() { ++n; }) + .each('end', function() { + if (!--n) { // call once + surface.call(run, toFrom); + } + }); } var breathe = function(surface) { done = false; - d3.timer(reselect(surface), 200); + d3.timer(function() { + if (done) return true; + + var currSelected = surface.selectAll(selector); + if (currSelected.empty()) return false; + + surface.call(run, 'from'); + return true; + }, 200); }; - breathe.off = function() { + breathe.off = function(surface) { done = true; - if (selection) { - selection.call(reset); - } + d3.timer.flush(); + selected + .transition() + .call(reset) + .duration(0); }; return breathe;