class Stippling {
constructor(densityGrid, resolutionGrid, minRadius, maxRadius) {
this.densityGrid = densityGrid;
this.resolutionGrid = resolutionGrid;
this.width = resolutionGrid.width;
this.height = resolutionGrid.height;
this.densityGridExtent = d3.extent(densityGrid.values);
this.resolutionGridExtent = d3.extent(resolutionGrid.values);
this.radiusExtent = [minRadius, maxRadius];
this.resolutionDensityToRadius = d3
.scaleLinear()
.domain(this.resolutionGridExtent)
.range(this.radiusExtent);
this.inkDensityToRadius = d3
.scaleLinear()
.domain(this.densityGridExtent)
.range(this.radiusExtent);
this.threshold = 0.4;
this.delta_threshold = 0.01;
this.stipples = this.random_stipples(this.width*this.height / 625 | 0);
}
thresholds(radius) {
const area = Math.PI * radius * radius;
return [
(1.0 + this.threshold / 2.0) * area,
(1.0 - this.threshold / 2.0) * area
];
}
random_stipples(num) {
const stipples = new Array(num);
const xran = d3.randomUniform(0, this.width);
const yran = d3.randomUniform(0, this.height);
for (var i = 0; i < num; ++i) {
stipples[i] = [xran(), yran()];
stipples[i].radius = this.radiusExtent[0];
}
return stipples;
}
// performs one iteration of the stippling algorithm
iterate() {
const delaunay = d3.Delaunay.from(this.stipples),
voronoi = delaunay.voronoi([0, 0, this.width, this.height]);
// initialize densities
for(let i = 0; i < this.stipples.length; i++) {
const st = this.stipples[i];
st.radiusDensity = 0;
st.resolutionDensity = 0;
st.moment10 = 0;
st.moment01 = 0;
st.moment11 = 0;
st.moment20 = 0;
st.moment02 = 0;
}
// compute the density and the weighted centroid of each cell
var found = 0;
for(let y = 0, {stipples, width} = this, dValues = this.densityGrid.values, rValues = this.resolutionGrid.values; y < this.height; y++) {
const line = y * width;
for(let x = 0; x < width; x++) {
found = delaunay.find(x, y, found);
const st = stipples[found];
const densityVal = dValues[x + line];
const resolutionVal = rValues[x + line];
st.radiusDensity += densityVal;
st.resolutionDensity += resolutionVal; // Moment00
const xval = x*resolutionVal;
const yval = y*resolutionVal;
st.moment10 += xval
st.moment01 += yval
st.moment11 += x * yval;
st.moment20 += x * xval;
st.moment02 += y * yval;
}
}
const {
// resolutionGridExtent,
// densityGridExtent,
resolutionDensityToRadius,
inkDensityToRadius
} = this;
const resolutionGridMax = this.resolutionGridExtent[1];
let deleted = 0;
let splitted = 0
const newStipples = [];
const {abs, atan2, cos, sin, sqrt, PI } = Math;
for (let i = 0; i < this.stipples.length; i++) {
const polygon = voronoi.cellPolygon(i);
if (!polygon) continue;
const st = this.stipples[i];
const density = st.resolutionDensity;
// determine point size based on average intensity
const area = abs(d3.polygonArea(polygon)) || 1;
const avgDensity = density / area;
const resolutionRadius = resolutionDensityToRadius(avgDensity);
const resolutionArea = PI * resolutionRadius * resolutionRadius * resolutionGridMax;
const splitThreshold = (1.0 + this.threshold / 2.0) * resolutionArea;
const deleteThreshold = (1.0 - this.threshold / 2.0) * resolutionArea;
const renderRadius = inkDensityToRadius(st.radiusDensity / area);
if (density < deleteThreshold) {
deleted++;
} else if (density > splitThreshold) {
// Split
splitted++;
const centroid = d3.polygonCentroid(polygon);
const cx = centroid[0];
const cy = centroid[1];
const dist = sqrt(area / PI) / 2.0;
const x = st.moment20 / density - cx * cx;
const y = 2 * (st.moment11 / density - cx * cy);
const z = st.moment02 / density - cy * cy;
var orientation = atan2(y, x - z) / 2.0;
var deltaX = dist * cos(orientation);
var deltaY = dist * sin(orientation);
st[0] = cx + deltaX;
st[1] = cy + deltaY;
st.radius = renderRadius;
// re-use arrays to reduce GC pressure
centroid[0] -= deltaX;
centroid[1] -= deltaY;
centroid.radius = renderRadius;
newStipples.push(st, centroid);
} else {
// Relax
st[0] = st.moment10 / density;
st[1] = st.moment01 / density;
st.radius = renderRadius;
newStipples.push(st);
}
}
// we increase the threshold with each iteration for faster convergence
this.threshold = this.threshold + this.delta_threshold;
this.stipples = newStipples;
// is done?
return splitted + deleted === 0;
}
}