Skip to content

Instantly share code, notes, and snippets.

@HerbCaudill
Last active December 2, 2019 09:54
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 HerbCaudill/808551ba2c04b08e66e493cd7804d012 to your computer and use it in GitHub Desktop.
Save HerbCaudill/808551ba2c04b08e66e493cd7804d012 to your computer and use it in GitHub Desktop.
DevResults Logo
license: mit

This is an experiment in rendering the DevResults logo mathematically using d3.js.

The original logo was constructed in Illustrator:

       
Create a 6x6 grid of dots. Overlay a circle with the same bounding box. Apply envelope distort using the circle. Add lines and color in the column chart.

In code, the tricky part is the envelope distort. We need to do two things:

  1. Render the dots as shapes with geometry that can be distorted. (A <circle> element will always be a circle - you can only manipulate the center and the radius.)
  2. Project the original square shape of the grid onto a circle.

1. Faking a circle

If you break apart a circle in Illustrator, you'll see that it's actually composed of four Bézier curves:

As the internet will quickly tell you, you can't actually make a circle out of Bézier curves, but you can come close. The question is, how long should those control handles be on each point? Eyeballing it, it looks like about half the length of the radius. Turns out it's a little longer: It's 0.551915024494 times the radius. We'll call that magic number K, and drawing a fake circle becomes a simple matter of SVG:

      M ${-r,  0 }
      C ${-r, -K } 
        ${-K, -r } 
        ${ 0, -r } 
      C ${ K, -r }
        ${ r, -K }
        ${ r,  0 }
      C ${ r,  K }
        ${ K,  r }
        ${ 0,  r }
      C ${-K,  r }
        ${-r,  K }
        ${-r,  0 }
      z  

2. Circling a square

The next step is to find a projection such that any point on a unit square can be cast as a point on a unit circle. The math is not hard, but why bother when someone else has already done all the work:

The magic formula in this case turns out to be

The rest is straightforward.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="../js/d3.v3.min.js"></script>
<script src="module.js"></script>
</head>
<body>
<style type="text/css">
body { padding: 2em; }
.dot { fill:#D6CDC1; transition:1s ease all;}
.dot.on { fill: #D8820F;}
.bar { stroke: #D8820F; fill: transparent; }
</style>
<svg width="400" height="400" />
<script>
logo = $dev.logo()
.pattern([0,5,4,3,2,0]);
d3.select("svg").call(logo);
logo.pattern([0,2,3,4,5,0]);
d3.select("svg").call(logo);
</script>
</body>
</html>
console.clear();
var $dev = {
logo: function module() {
let offset, R, grid;
// warp unit square to unit circle
// http://mathproofs.blogspot.com.es/2005/07/mapping-square-to-circle.html
// (we use this to to mimic envelope warp effect)
function warp(p) {
return {
x: p.x * Math.sqrt(1 - Math.pow(p.y/R, 2) / 2),
y: p.y * Math.sqrt(1 - Math.pow(p.x/R, 2) / 2)
};
};
// dot rendering function
// renders one dot centered on {d.x, d.y} and warps it from unit square to unit circle
function dot(d) {
let render = ((x,y) => {
let p1 = {
x: d.x + x,
y: d.y + y
};
let p2 = warp(p1);
return `${p2.x}, ${p2.y}`
})
// approximation of a circle composed of 4 bezier curves
// http://spencermortensen.com/articles/bezier-circle/
// (we draw these instead of plain <circle> elements so we can distort their geometry)
let K = 0.551915024494 * r; // magic coefficient from Mortensen article
return `
M ${render(-r, 0)}
C ${render(-r, -K)}
${render(-K, -r)}
${render( 0, -r)}
C ${render( K, -r)}
${render( r, -K)}
${render( r, 0)}
C ${render( r, K)}
${render( K, r)}
${render( 0, r)}
C ${render(-K, r)}
${render(-r, K)}
${render(-r, 0)}
z `
}
function bar(d, i) {
if (!d) return;
let render = ((x,y) => {
let p1 = {x, y};
let p2 = warp(p1);
return `${p2.x}, ${p2.y}`
})
let midpoint = (p1, p2) => {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2
}
};
let p1 = {
x: i * sp - offset,
y: R*2 - sp - offset + r
}
let p2 = {
x: i * sp - offset,
y: R*2 - sp*d - offset + r
}
let m = midpoint(p1, p2);
let c1 = midpoint(p1, m);
let c2 = midpoint (m, p2);
return `
M ${render(p1.x, p1.y)}
C ${render(c1.x, c1.y )}
${render(c1.x, c1.y)}
${render(m.x, m.y)}
C ${render(c2.x, c2.y )}
${render(c2.x, c2.y)}
${render(p2.x, p2.y)}
`
}
function exports(_selection) {
_selection.each(function(_data) {
R = sp * (N - 1) / 2 + r; // radius of the logo's bounding circle
offset = sp * (N - 1) / 2; // H & V distance to shift everything from Q1 to center
// Create array of coordinates of center points
// (square NxN grid of points, with center of grid at 0,0)
let range = [...Array(N).keys()]; // [0..N]
let grid =
range.map((y) => {
return range.map((x) => {
return {
key: `${x}_${y}`,
x: x * sp - offset,
y: y * sp - offset
}
})
});
// apply pattern
for (var col = 0; col < N; col++) {
for (var row = 0; row < N; row++) {
grid[N - row - 1][col].on = (row < pattern[col]);
}
}
var svg = d3.select(this)
.attr({
width: size,
height: size,
viewBox: `-${R} -${R} ${R*2} ${R*2}` // expand logo to fill SVG exactly
})
var rows = svg.selectAll("g.row")
.data(grid)
rows.enter()
.append("g")
.classed("row", true);
rows.exit().remove();
var dots = rows.selectAll("path.dot")
.data((d) => d, (d) => d.key)
dots.enter()
.append("path")
.classed("dot", true)
dots
.classed({
on: ((d) => d.on),
off: ((d) => !d.on)
})
.transition(1000)
.attr("d", dot)
dots.exit().remove()
var bars = svg.selectAll("path.bar")
.data(pattern)
bars.enter()
.append("path")
.classed("bar", true)
bars
.style("stroke-width", r/3)
.transition(1000)
.attr("d", bar)
bars.exit().remove()
});
}
// expose properties
let size = 400; // size of svg in pixels
exports.size = function(_) {
if (!arguments.length) return size;
size = _;
return exports;
}
let pattern = [0,2,3,4,5,0]
exports.pattern = function(_) {
if (!arguments.length) return pattern;
pattern = _;
return exports;
}
let N = 6; // number of dots on each side
exports.N = function(_) {
if (!arguments.length) return N;
N = _;
return exports;
}
let sp = 3; // spacing from one dot to another
exports.sp = function(_) {
if (!arguments.length) return sp;
sp = _;
return exports;
}
let r = 1; // dot radius
exports.r = function(_) {
if (!arguments.length) return r;
r = _;
return exports;
}
return exports;
}
};
// 20191202
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment