Skip to content

Instantly share code, notes, and snippets.

@lsbardel
Last active July 31, 2021 08:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lsbardel/548765ba13023ecd2fae46a99350bde1 to your computer and use it in GitHub Desktop.
Save lsbardel/548765ba13023ecd2fae46a99350bde1 to your computer and use it in GitHub Desktop.
Triangular mesh on Canvas
license: bsd-3-clause

A colored mesh on svg and canvas thanks to d3-canvas-transition module. Adapted from Color Mesh II block.

It uses the d3 symbol API to draw triangles both on canvas and svg.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Triangular mesh on Canvas</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://giottojs.org/d3-canvas-transition/0.2.6/d3-canvas-transition.js"></script>
</head>
<body>
<div id="paper">
<label>
<input id='svg' name="type" type="radio" checked>
<span>svg</span>
</label>
<label>
<input id='canvas' name="type" type="radio">
<span>canvas</span>
</label>
</div>
<div id="example" style="max-width: 960px"></div>
<script>
(function () {
d3.select('#svg').on('click', function () {
draw('svg');
});
d3.select('#canvas').on('click', function () {
draw('canvas');
});
if (d3.resolution() > 1) {
d3.select('#paper').append('label').html(
"<input id='canvas-low' name='type' type='radio'><span>canvas low resolution</span>"
);
d3.select('#canvas-low').on('click', function () {
draw('canvas', 1);
});
}
var example = d3.select("#example"),
width = d3.getSize(example.style('width')),
height = Math.min(500, width),
radius = 30,
margin = 2*radius;
var sampler = poissonDiscSampler(width + 2 * margin, height + 2 * margin, radius),
samples = [],
sample;
while (sample = sampler()) samples.push([sample[0] - margin, sample[1] - margin]);
draw('svg');
function draw(type, r) {
example.select('.paper').remove();
var paper = example
.append(type)
.classed('paper', true)
.attr('width', width).attr('height', height).canvasResolution(r).canvas(true);
var voronoi = d3.voronoi()
.extent([[-margin, -margin], [width + margin, height + margin]]),
grid = voronoi.triangles(samples).map(mesh),
marks = d3.symbol().type(function (d) {return d;}).size(function (d) {return d.area;});
paper
.selectAll("path")
.data(grid)
.enter()
.append("path")
.attr("transform", function (d) {
return "translate(" + d.centeroid[0] + "," + d.centeroid[1] + ")";
})
.attr("d", marks)
.style("stroke", function (d) {
return color(d.centeroid);
})
.style("fill", "#333")
.transition()
.delay(function(d, i) { return i * 1; })
.style("fill", function (d) {
return color(d.centeroid);
});
function color (d) {
var dx = d[0] - width / 2,
dy = d[1] - height / 2;
return d3.lab(100 - (dx * dx + dy * dy) / 5000, dx / 10, dy / 10);
}
}
function mesh (d) {
var area = d3.polygonArea(d),
centeroid = d3.polygonCentroid(d);
return {
area: area,
centeroid: centeroid,
draw: function (context, size) {
var m = Math.sqrt(size/area);
context.moveTo(p(m, d[0], 0), p(m, d[0], 1));
context.lineTo(p(m, d[1], 0), p(m, d[1], 1));
context.lineTo(p(m, d[2], 0), p(m, d[2], 1));
context.closePath();
}
};
function p(m, v, j) {
return m*(v[j] - centeroid[j]);
}
};
// Based on https://www.jasondavies.com/poisson-disc/
function poissonDiscSampler(width, height, radius) {
var k = 30, // maximum number of samples before rejection
radius2 = radius * radius,
R = 3 * radius2,
cellSize = radius * Math.SQRT1_2,
gridWidth = Math.ceil(width / cellSize),
gridHeight = Math.ceil(height / cellSize),
grid = new Array(gridWidth * gridHeight),
queue = [],
queueSize = 0,
sampleSize = 0;
return function () {
if (!sampleSize) return sample(Math.random() * width, Math.random() * height);
// Pick a random existing sample and remove it from the queue.
while (queueSize) {
var i = Math.random() * queueSize | 0,
s = queue[i];
// Make a new candidate between [radius, 2 * radius] from the existing sample.
for (var j = 0; j < k; ++j) {
var a = 2 * Math.PI * Math.random(),
r = Math.sqrt(Math.random() * R + radius2),
x = s[0] + r * Math.cos(a),
y = s[1] + r * Math.sin(a);
// Reject candidates that are outside the allowed extent,
// or closer than 2 * radius to any existing sample.
if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) return sample(x, y);
}
queue[i] = queue[--queueSize];
queue.length = queueSize;
}
};
function far(x, y) {
var i = x / cellSize | 0,
j = y / cellSize | 0,
i0 = Math.max(i - 2, 0),
j0 = Math.max(j - 2, 0),
i1 = Math.min(i + 3, gridWidth),
j1 = Math.min(j + 3, gridHeight);
for (j = j0; j < j1; ++j) {
var o = j * gridWidth;
for (i = i0; i < i1; ++i) {
if (s = grid[o + i]) {
var s,
dx = s[0] - x,
dy = s[1] - y;
if (dx * dx + dy * dy < radius2) return false;
}
}
}
return true;
}
function sample(x, y) {
var s = [x, y];
queue.push(s);
grid[gridWidth * (y / cellSize | 0) + (x / cellSize | 0)] = s;
++sampleSize;
++queueSize;
return s;
}
}
}());
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment