D3's general update pattern with a Voronoi diagram & rapidly updating data. Fork of https://bl.ocks.org/HarryStevens/6c4ee7e57df79fbd3137aad5de289cf6
-
-
Save redblobgames/76308dd1a7e89bd08aeadc4a87675353 to your computer and use it in GitHub Desktop.
Voronoi Jiggle
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: gpl-3.0 |
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
<html> | |
<head> | |
<style> | |
body { | |
margin: 0; | |
} | |
.control { | |
position: absolute; | |
right: 5px; | |
top: 5px; | |
} | |
.control .label { | |
font-family: sans-serif; | |
text-align: center; | |
} | |
.control input { | |
display: inline-block; | |
vertical-align: middle; | |
} | |
.control output { | |
display: inline-block; | |
font-family: monospace; | |
vertical-align: middle; | |
} | |
.voronoi { | |
stroke: black; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="control"> | |
<div class="jiggle-control"> | |
<div class="label">Jiggle</div> | |
<div class="range"> | |
<input type="range" min="0.0" step="0.1" max="9.9" value="0.5"> | |
<output></output> | |
</div> | |
</div> | |
<div class="flow-control"> | |
<div class="label">Flow</div> | |
<div class="range"> | |
<input type="range" min="-20" step="0.1" max="20" value="5"> | |
<output></output> | |
</div> | |
</div> | |
</div> | |
<div class="jiggle"></div> | |
<script src="https://unpkg.com/flubber@0.3.0"></script> | |
<script type="module"> | |
import { Delaunay } from "https://cdn.skypack.dev/d3-delaunay@6"; | |
import { interpolateLab } from "https://cdn.skypack.dev/d3-interpolate@3"; | |
import { polygonArea } from "https://cdn.skypack.dev/d3-polygon@3"; | |
import { randomUniform } from "https://cdn.skypack.dev/d3-random@3"; | |
import { scaleLinear, scaleSequential } from "https://cdn.skypack.dev/d3-scale@4"; | |
import { select } from "https://cdn.skypack.dev/d3-selection@3"; | |
import { interval } from "https://cdn.skypack.dev/d3-timer@3"; | |
import { transition } from "https://cdn.skypack.dev/d3-transition@3"; | |
const { abs, max, min } = Math; | |
const width = window.innerWidth; | |
const height = window.innerHeight; | |
const duration = 12; // milliseconds of transition duration | |
const radius = 5; // circle radius | |
const x = randomUniform(radius, width - radius); | |
const y = randomUniform(radius, height - radius); | |
const data = Array.from({ length: 64 }) | |
.map((_, id) => ({ id, x: x(), y: y() })); | |
const color = scaleSequential() | |
.domain([0, width * height / data.length * 2]) | |
.interpolator(interpolatePalette(["#fc8d62", "#ffd92f", "#66c2a5", "#8da0cb"])); | |
const jiggleInput = document.querySelector(".jiggle-control input"); | |
const jiggleValue = document.querySelector(".jiggle-control output"); | |
const flowInput = document.querySelector(".flow-control input"); | |
const flowValue = document.querySelector(".flow-control output"); | |
const svg = select(".jiggle").append("svg") | |
.attr("width", width) | |
.attr("height", height); | |
function redraw(data){ | |
const voronoi = Array.from( | |
Delaunay | |
.from(data.map(({x, y}) => [x, y])) | |
.voronoi([0, 0, width, height]) | |
.cellPolygons(), | |
(p, i) => Object.assign(p, { data: data[i] }) | |
); | |
// transition | |
const t = transition() | |
.duration(duration); | |
// JOIN | |
const polygon = svg.selectAll(".voronoi") | |
.data(voronoi, d => d.data.id); | |
const circle = svg.selectAll(".dot") | |
.data(data, d => d.id); | |
// UPDATE | |
polygon | |
.transition(t) | |
.attrTween("d", (d, i, e) => { | |
const prev = select(e[i]).attr("d"); | |
const curr = `M${d.join("L")}Z`; | |
return flubber.interpolate(prev, curr); | |
}) | |
.style("fill", d => color(abs(polygonArea(d)))); | |
circle | |
.transition(t) | |
.attr("cx", d => d.x) | |
.attr("cy", d => d.y); | |
// ENTER | |
polygon.enter().append("path") | |
.attr("class", "voronoi") | |
.attr("d", d => `M${d.join("L")}Z`) | |
.style("fill", d => color(abs(polygonArea(d)))); | |
circle.enter().append("circle") | |
.attr("class", "dot") | |
.attr("r", 5) | |
.attr("cx", d => d.x) | |
.attr("cy", d => d.y); | |
} | |
redraw(data); | |
const wrapX = (v) => (v + width) % width | |
interval(() => { | |
const jiggle = jiggleInput.valueAsNumber; | |
const flow = flowInput.valueAsNumber; | |
const random = randomUniform(-jiggle, jiggle); | |
jiggleValue.innerHTML = jiggle.toFixed(1); | |
flowValue.innerHTML = flow.toFixed(1); | |
data.forEach(d => { | |
d.x = wrapX(d.x + random() + flow * (d.y/height - 0.5)) | |
d.y = min(height - radius, max(radius, d.y + random())); | |
return d; | |
}); | |
redraw(data); | |
}, duration * 2); | |
// https://observablehq.com/@harrystevens/roll-your-own-color-palette-interpolator | |
function interpolatePalette(palette){ | |
const domain = [0]; | |
for (let i = 1, l = palette.length - 1; i <= l; i++){ | |
domain[i] = i / l; | |
} | |
const scale = scaleLinear(domain, palette) | |
.interpolate(interpolateLab) | |
.clamp(true); | |
return t => scale(t); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment