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(3) | |
| .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> |