Cells adapt their shape to their environment. When pressed by surrounding cells, they get stressed and become more opaque.
Drag a cell to play.
Made by Philippe Rivière with blockbuilder.org.
license: gpl-3.0 |
Cells adapt their shape to their environment. When pressed by surrounding cells, they get stressed and become more opaque.
Drag a cell to play.
Made by Philippe Rivière with blockbuilder.org.
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<style> | |
body { | |
margin: 0; | |
position: fixed; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
background: #333; | |
} | |
.active path { | |
stroke-width: 3px; | |
} | |
</style> | |
</head> | |
<body> | |
<script> | |
var svg = d3.select("body").append("svg") | |
.attr("width", 960) | |
.attr("height", 500) | |
.append('g') | |
.attr('transform', 'translate(480,250)'); | |
var color = d3.scaleOrdinal(d3.schemeCategory10); | |
color = d3.scaleOrdinal(['#999', '#966', '#696']); | |
color = function(i) { return d3.cubehelix((i%100)*3.60, 1.2, 0.6); } | |
// my data is a set of initial positions and a radius | |
var data = d3.range(500) | |
.map(function (i) { | |
var a = 2 * Math.PI * Math.random(), | |
d = Math.sqrt(Math.random()); | |
return { | |
id: i, | |
r: 4 * (1 + 5 * Math.random() * Math.random()), | |
x: 300 * Math.cos(a) * d, | |
y: 200 * Math.sin(a) * d, | |
color: color(i), | |
}; | |
}); | |
// add n feelers | |
data = data.map(function (d) { | |
var n = Math.floor(4 + d.r / 3), | |
start = 2 * Math.PI * Math.random(); | |
d.children = d3.range(n) | |
.map(function (i) { | |
var angle = i * Math.PI * 2 / n + start, | |
t = { | |
length: 1, | |
angle: angle, | |
sin: Math.sin(angle), | |
cos: Math.cos(angle), | |
parent: d, | |
}; | |
return t; | |
}); | |
return d; | |
}); | |
a = 0; | |
var simulation = d3.forceSimulation(data) | |
.alphaTarget(0.02) | |
.force("surface", function (alpha) { | |
cells.each(function (d) { | |
d.surface = Math.sqrt( | |
d.children.map(function (t) { | |
return t.length * t.length; | |
}) | |
.reduce(function (a, b) { | |
return a + b; | |
}, 0) / d.children.length); | |
}) | |
}) | |
.force("collidecell", function (alpha) { | |
cells.each(function (d) { | |
var e = d3.extent(d.children.map(function (t) { | |
return t.length; | |
})), | |
p1 = d.r + 3; | |
d.polygon = d.children.map(function (t) { | |
return [d.x + p1 * t.length * t.sin, d.y - p1 * t.length * t.cos]; | |
}); | |
}); | |
var quadtree = d3.quadtree(data, | |
function (d) { | |
return d.x; | |
}, | |
function (d) { | |
return d.y; | |
}); | |
var collisions = 0; | |
cells.each(function (d, i) { | |
// simulation.findMany(…) | |
quadtree.visit(function (node, x0, y0, x1, y1) { | |
if (x1 < d.x - 2 * d.r || x0 > d.x + 2 * d.r || y1 < d.y - 2 * d.r || y0 > d.y + 2 * d.r) { | |
return true; | |
} | |
var p = node.data; | |
if (!p) return; | |
if (p.id == d.id) return; | |
var dx = p.x - d.x, | |
dy = p.y - d.y, | |
dist2 = dx * dx + dy * dy; | |
if (dist2 > 4 * (d.r + p.r) * (d.r + p.r)) return; | |
var stress = 0; | |
d.children.forEach(function (t) { | |
var txy = [d.x + d.r * t.length * t.sin, d.y - d.r * t.length * t.cos]; | |
if (d3.polygonContains(p.polygon, txy)) { | |
collisions++; | |
stress++; | |
t.length /= 1.05; | |
var tens = d.surface / p.surface, | |
f = 0.1 * (stress > 2 ? 6 : 1); | |
d.vx += f * Math.atan((d.x - p.x) * tens); | |
d.vy += f * Math.atan((d.y - p.y) * tens); | |
p.vx -= f * Math.atan((d.x - p.x) / tens); | |
p.vy -= f * Math.atan((d.y - p.y) / tens); | |
} | |
}); | |
}); | |
}); | |
//console.log('collisions', collisions); | |
}) | |
.force("tension", function (alpha) { | |
cells.each(function (d) { | |
var l = d.children.length; | |
d.children.forEach(function (t, i) { | |
var m = d.children[(l - 1) % l].length + d.children[(l + 1) % l].length; | |
var f = 1 / 10; // spiky-ness | |
t.length = (1 - f) * t.length + f * m / 2; | |
}); | |
}) | |
}) | |
.force("expand", function (alpha) { | |
cells.each(function (d) { | |
var u = 1 / Math.sqrt(d.surface); | |
d.children.forEach(function (t) { | |
t.length *= u; | |
t.length = Math.min(t.length, 2); | |
}) | |
}) | |
}) | |
.force("internal", function (alpha) { | |
var f = 1 / 10; | |
cells.each(function (d) { | |
d.vx += f * d3.sum(d.children, function (t) { | |
return t.length * t.sin; | |
}); | |
d.vy += f * d3.sum(d.children, function (t) { | |
return -t.length * t.cos; | |
}); | |
}) | |
}) | |
.force("x", d3.forceX(function(d) { return d.x; }).strength(0.01)) | |
.force("y", d3.forceY(function(d) { return d.y; }).strength(0.01)) | |
.force("move", function(){ | |
cells.each(function (d) { | |
d.vx += Math.random()-0.5; | |
d.vy += Math.random()-0.5; | |
}); | |
}) | |
.on("tick", ticked); | |
// cells | |
var cells = svg.selectAll('g.cell') | |
.data(data) | |
.enter() | |
.append('g') | |
.classed('cell', true) | |
.attr('transform', function (d) { | |
return 'translate(' + [d.x, d.y] + ')' | |
}); | |
if (false) cells.append('circle') | |
.attr('r', 1) | |
.attr('fill', function (d) { | |
return d.color; | |
}); | |
// segments | |
var paths = cells | |
.append('path') | |
.attr('stroke', function (d, i) { | |
return color(i); | |
}) | |
.attr('fill', function (d, i) { | |
return color(i); | |
}) | |
.attr('fill-opacity', 0.3); | |
function ticked() { | |
cells | |
.attr('transform', function (d) { | |
return 'translate(' + [d.x, d.y] + ')' | |
}); | |
paths | |
.attr('d', function (d) { | |
var arc = 2 * Math.PI / d.children.length, | |
data = d.children | |
.map(function (t, i) { | |
return [i * arc, d.r * t.length]; | |
}); | |
return line(data) + 'Z'; | |
}) | |
.transition() | |
.attr('fill-opacity', function (d) { | |
return 0.05 - 8*Math.log(d.surface); | |
}); | |
} | |
var line = d3.radialArea() | |
.curve(d3.curveCatmullRomClosed) | |
.innerRadius(function (d) { | |
return 0.4 * line.outerRadius()(d); | |
}); | |
var line = d3.radialLine() | |
.curve(d3.curveCatmullRomClosed); | |
var ease = function (t) { | |
return t > 1 ? 1 : t < 0 ? 0 : d3.easeExpIn(t); | |
} | |
cells | |
.call(d3.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended)); | |
function dragstarted(d) { | |
d3.select(this).raise().classed("active", true); | |
} | |
function dragged(d) { | |
d3.select(this) | |
.attr('transform', function (d) { | |
return 'translate(' + [d.x = d3.event.x, d.y = d3.event.y] + ')'; | |
}); | |
} | |
function dragended(d) { | |
d3.select(this).classed("active", false); | |
simulation.alpha(1).restart(); | |
} | |
</script> | |
</body> | |
</html> |