Last active
July 31, 2018 04:03
-
-
Save redblobgames/c0da29c0539c8e7885664e774ffeae57 to your computer and use it in GitHub Desktop.
Outside the box
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: apache-2.0 | |
scrolling: yes | |
height: 900 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// From http://www.redblobgames.com/x/1637-arrow-outside-the-box/ and http://simblob.blogspot.com/2016/10/outside-box.html | |
// Copyright 2016 Red Blob Games <[email protected]> | |
// License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html> | |
/* NOTE | |
This code is a quick & dirty prototype of the idea, for a blog post. | |
As of Oct 2016 I haven't tried to figure out the best way to make | |
this reusable and modular. | |
*/ | |
var shuffle = [22, 28, 26, 5, 7, 10, 16, 27, 9, 13, 18, 19, 17, 11, 23, 21, 15, 6, 20, 12, 25, 3, 29, 4, 14, 1, 24, 2, 0, 8]; | |
var numCircles = shuffle.length; | |
/** Average of two numbers */ | |
function avg(a, b) { return (a + b) / 2; } | |
/** Move 'end' to be at least minDistance away from 'start', in the same direction */ | |
function projectMinDistance(start, end, minDistance) { | |
var dist = Math.abs(end - start); | |
var sign = end - start >= 0? 1 : -1; | |
return start + Math.max(dist, minDistance) * sign; | |
} | |
function makeLotsOfCircles(svg) { | |
for (var i = 0; i < numCircles; i++) { | |
var row = Math.floor(shuffle[i] / 10), col = shuffle[i] % 10 + (row % 2) * 0.5; | |
var g = svg.append('g') | |
.attr('transform', 'translate(' + [60 + col * 36, 18 + row * 32] + ')'); | |
var color = d3.hsl(i/30*360, 0.3, 0.7); | |
g.append('circle') | |
.attr('id', "c-" + i) | |
.attr('fill', color) | |
.attr('stroke', "black") | |
.attr('stroke-width', 1.5) | |
.attr('stroke-opacity', 0.2) | |
.attr('r', 15); | |
g.append('text') | |
.attr('dy', 5) | |
.text(i); | |
} | |
} | |
/** returns the midpoint of one of the four edges */ | |
function getSideOfRect(rect, side) { | |
switch (side) { | |
case 'left': return {x: rect.left, y: avg(rect.top, rect.bottom)}; | |
case 'right': return {x: rect.right, y: avg(rect.top, rect.bottom)}; | |
case 'top': return {x: avg(rect.left, rect.right), y: rect.top}; | |
case 'bottom': return {x: avg(rect.left, rect.right), y: rect.bottom}; | |
} | |
throw "Invalid side " + side; | |
} | |
function removeArrow(id, svg) { | |
svg.select('#'+id) | |
.interrupt() | |
.transition() | |
.attr('opacity', 0.3) | |
.delay(500) | |
.duration(500) | |
.attr('opacity', 0.0) | |
.remove(); | |
} | |
function drawArrow(id, svg, source, sourceSide, target, targetSide) { | |
var parentRect = svg.node().getBoundingClientRect(); | |
var sourceRect = source.node().getBoundingClientRect(); | |
var targetRect = target.node().getBoundingClientRect(); | |
/* By default browsers other than IE will clip to the svg bounding | |
* box. For years I would set overflow: hidden just for IE. But I | |
* wondered, what happened if I did the opposite in other | |
* browsers? To my surprise, it worked! */ | |
var sourcePoint = getSideOfRect(sourceRect, sourceSide); | |
var targetPoint = getSideOfRect(targetRect, targetSide); | |
var minYDistance = Math.max(100, 0.15*Math.abs(sourcePoint.x, targetPoint.x)); | |
var arrow = svg.selectAll('#'+id).data([1]); | |
arrow | |
.enter() | |
.append('path') | |
.attr('id', id) | |
.attr('class', 'arrow') | |
.attr('opacity', 0.0) | |
.merge(arrow) | |
.attr('transform', "translate(" + [-parentRect.left, -parentRect.top] + ")") | |
.attr('d', ['M', | |
sourcePoint.x, sourcePoint.y, | |
'C', | |
// NOTE: this geometry assumes arrows are | |
// primarily vertical; I've sketched out a better | |
// set of control points but am using this quick & | |
// dirty approach for this demo | |
sourcePoint.x, projectMinDistance(sourcePoint.y, avg(sourcePoint.y, targetPoint.y), minYDistance), | |
targetPoint.x, projectMinDistance(targetPoint.y, avg(targetPoint.y, sourcePoint.y), minYDistance), | |
targetPoint.x, targetPoint.y | |
].join(" ")) | |
.interrupt() | |
.transition() | |
.duration(100) | |
.attr('opacity', 0.7); | |
} | |
function makeLotsOfArrows() { | |
var p = d3.select('#more-arrows'); | |
for (var i = 1; i < numCircles; i += 2) { | |
(function (k) { | |
var id = 'arrow-'+i; | |
var anchor = p.append('cite') | |
.style('margin-right', '1ex') | |
.text(i) | |
.on('mouseover touchstart', function() { | |
drawArrow(id, d3.select('#diagram'), | |
anchor, 'top', | |
d3.select('#c-'+k), 'bottom'); | |
d3.event.preventDefault(); | |
}) | |
.on('mouseout touchend', function() { | |
removeArrow(id, d3.select('#diagram')); | |
d3.event.preventDefault(); | |
}); | |
})(i); | |
} | |
} | |
makeLotsOfCircles(d3.select('#diagram')); | |
makeLotsOfArrows(); | |
function addArrowFromAnchor(citeSelector, anchorSelector, anchorSide, targets) { | |
function makeId(a, b) { return (a+'-'+b).replace(/[^-\w]/g, ''); } | |
d3.select(citeSelector) | |
.on('mouseover touchstart', function() { | |
targets.forEach(function(target) { | |
var targetSelector = target[0], targetSide = target[1]; | |
var id = makeId(targetSelector, citeSelector); | |
drawArrow(id, d3.select('#diagram'), | |
d3.select(anchorSelector), anchorSide, | |
d3.select(targetSelector), targetSide); | |
d3.select(targetSelector).classed('highlighted', true); | |
}); | |
}) | |
.on('mouseout touchend', function() { | |
targets.forEach(function(target) { | |
var targetSelector = target[0]; | |
var id = makeId(targetSelector, citeSelector); | |
removeArrow(id, d3.select('#diagram')); | |
d3.select(targetSelector).classed('highlighted', false); | |
}); | |
}); | |
} | |
addArrowFromAnchor('#interact-arrow', | |
'#anchor-1', 'bottom', | |
[['#c-24', 'top']]); | |
addArrowFromAnchor('#interact-show-svg', | |
'#anchor-2', 'top', | |
[['#diagram', 'bottom']]); | |
addArrowFromAnchor('#interact-show-anchor', | |
'#anchor-3', 'top', | |
[['#anchor-1', 'bottom'], ['#anchor-2', 'right']]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<title>Outside the box</title> | |
<script src="//d3js.org/d3.v4.min.js"></script> | |
<style> | |
@import url("//bl.ocks.org/style.css"); | |
body { margin-left: auto; margin-right: auto; max-width: 35em; } | |
cite { border-bottom: 2px dotted blue; cursor: pointer; } | |
#more-arrows, figcaption { text-align: center; } | |
#diagram { border: 1px solid hsl(0,10%,80%); position: relative; z-index: 1; overflow: visible; } | |
svg.highlighted, span.highlighted { outline: 2px dotted hsl(0,50%,50%); } | |
.arrow { fill: none; stroke: hsl(0,50%,50%); stroke-width: 6.5; marker-end: url(#arrowhead); filter: url(#outline); } | |
text { text-anchor: middle; } | |
</style> | |
<html> | |
<p> | |
(this is the draft page; I posted the final version here: <a href="http://simblob.blogspot.com/2016/10/outside-box.html">http://simblob.blogspot.com/2016/10/outside-box.html</a>) | |
</p> | |
<p> | |
I write interactive explanations; I'm always looking for | |
simpler ways to explain things. | |
</p> | |
<p> | |
Think back to every technical paper you've read. The text connects | |
to diagrams by using text such as "see figure 4". Maybe it's more | |
specific and says "in figure 4 see circle 24". Maybe it's a | |
hypertext link. However if I look at how I take notes on paper, I | |
don't do that! I just <cite id="interact-arrow">draw an arrow</cite><span id="anchor-1"></span> to | |
the thing I want to point to. | |
</p> | |
<figure> | |
<svg id="diagram" width="450" height="100"> | |
<defs> | |
<marker id="arrowhead" viewBox="0 0 10 10" refX="7" refY="5" markerUnits="strokeWidth" markerWidth="4" markerHeight="3" orient="auto"> | |
<path d="M 0 0 L 10 5 L 0 10 z" fill="hsl(0,50%,50%)"/> | |
</marker> | |
<filter id="outline" x="-100%" y="-100%" width="300%" height="300%"> | |
<!-- filter size is overkill but I don't know how to make it 3 pixels wider than the natural size --> | |
<feMorphology result="outline" in="SourceGraphic" operator="dilate" radius="1"/> | |
<feColorMatrix type="matrix" in="outline" result="black-outline" | |
values="0 0 0 0 0 | |
0 0 0 0 0 | |
0 0 0 0 0 | |
0 0 0 1 0"/> | |
<feBlend in="SourceGraphic" in2="black-outline" mode="normal"/> | |
</filter> | |
</defs> | |
</svg> | |
<figcaption>Figure 4. Sea of circles.</figcaption> | |
</figure> | |
<p id="more-arrows"></p> | |
<p> | |
From an early age we have <em>invisible boxes</em> around text and | |
diagrams, keeping them apart. It doesn't have to be this way. | |
Pointing is the simplest way to direct attention to something. Why | |
don't we do it more? I don't know, but these are the kinds of | |
things I'm exploring on the web. | |
</p> | |
<p> | |
How did I implement this? The first guess would be that I'm using | |
a giant SVG overlay that covers the area between the pointer and | |
the target. I'm not! <span id="anchor-2"></span><cite id="interact-show-svg">That's | |
the only SVG element</cite> on the page. What's the trick? By default, the <code>overflow</code> for an SVG element is set to <code>hidden</code>; in some versions of IE it was <code>visible</code> and | |
I had to set it back to hidden. That keeps the content inside its | |
bounding box. But the IE situation made me wonder — if I set it to <code>visible</code>, | |
what happens? It turns out you can draw outside the box! This | |
seems to work across the browsers I've tried. | |
</p> | |
<p> | |
The second thing I need to do is construct an arrow path. The | |
source and target of the arrow may be in different SVG elements, | |
or they might not be SVG at all. For this page I use | |
<cite id="interact-show-anchor">an invisible span</cite><span id="anchor-3"></span> | |
in the text as the anchor. I use <code>getBoundingClientRect()</code> to | |
get the coordinates on the page. Then I pick the midpoint of one | |
side of the rectangle as the arrow source or target. I need to | |
translate all the coordinates into the coordinate space of the SVG | |
I'm drawing into. | |
</p> | |
<p> | |
For this demo I hard-coded the size, curvature, and directions of | |
the arrows. I don't adjust them when the page is resized. For a | |
reusable library, I think it'd be cleaner to have one svg per | |
arrow anchor. | |
</p> | |
<script src="arrows.js"></script> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment