/**
* @module Stippling
* @description This is the code documentation for our Stippling class.
*
* This documentation contains the following methods :
* - Grid
* - GridVisualization
* - Stippling
*
* @author Noah Watzal, Aymeric Hollaus
*/
class Stippling {
/**
* @method constructor
* @description Creates the class instances for Stippling.
* @param grid The Grid value .
* @param radius_extent The value for the radius of the grid.
* @param threshold The threshold value.
* @param delta_threshold
* @param stipples Number of Stipples
*/
constructor(grid, radius_extent, threshold, delta_threshold, stipples) {
this.grid = grid;
this.radius_extent = radius_extent;
this.threshold = 0.4;
this.delta_threshold = 0.01;
this.stipples = this.random_stipples(500);
}
/**
* @method updateThreshold
* @description This method is just for upating the threshold in the class.
* @param threshold The Grid value.
*/
updateThreshhold(threshold) {
this.threshold = threshold;
}
/**
* @method stipple_size
* @description Returns the size of the stipples.
* @returns {Number} min and max radius.
*/
stipple_size(min_radius, max_radius) {
return this;
}
/**
* @method stipple_del_thresh
* @description Returns vlaues of upper and lower threshold.
* @returns {Number} upper and lower threshold.
*/
stipple_del_thresh(radius) {
const area = Math.PI * (radius ** 2);
const upperThreshold = (1.0 + this.threshold / 2.0) * area;
const lowerThreshold = (1.0 - this.threshold / 2.0) * area;
return [upperThreshold, lowerThreshold];
}
/**
* @method random_stipples
* @description Generates the random stipples inside the given width and height of the grid.
* @param num {Number} length of the stipples array.
* @returns the stipples.
*/
random_stipples(num) {
const stipples = new Array(num);
const xran = d3.randomUniform(0, this.grid.width);
const yran = d3.randomUniform(0, this.grid.height);
for (let i = 0; i < num; ++i) {
stipples[i] = [xran(), yran()];
stipples[i].radius = this.radius_extent[0];
}
return stipples;
}
/**
* @method iterate
* @description iteration for deleting, splitting and relaxing of the stipples
* @returns deleted, relaxed and splitted stipples.
*/
iterate() {
const delaunay = d3.Delaunay.from(this.stipples);
const voronoi = delaunay.voronoi([0, 0, this.grid.width, this.grid.height]);
this.calculateDensityAndMoments(delaunay);
const grid_extent = d3.extent(this.grid.values);
const density_to_radius = d3
.scaleLinear()
.domain(grid_extent)
.range(this.radius_extent);
const { deleted, relaxed, splitted } = this.handleDeletionAndSplitting(
density_to_radius,
grid_extent,
delaunay,
voronoi
);
return { deleted, relaxed, splitted };
}
/**
* @method calculateDensityAndMoments
* @param delaunay
* @description calculates density and moments of the diagram.
*/
calculateDensityAndMoments(delaunay) {
for (let i = 0; i < this.stipples.length; i++) {
const st = this.stipples[i];
st.density = 0;
st.momentX = 0;
st.momentY = 0;
st.momentXY = 0;
st.momentXX = 0;
st.momentYY = 0;
}
let found = 0;
for (let y = 0; y < this.grid.height; y++) {
const line = y * this.grid.width;
for (let x = 0; x < this.grid.width; x++) {
found = delaunay.find(x, y, found);
const st = this.stipples[found];
const val = this.grid.values[x + line];
st.density += val;
const xval = x * val;
const yval = y * val;
st.momentX += xval;
st.momentY += yval;
st.momentXY += x * yval;
st.momentXX += x * xval;
st.momentYY += y * yval;
}
}
}
/**
* @method handleDeletionAndSplitting
* @param density_to_radius
* @param grid_extent
* @param delaunay
* @param voronoi
* @description This is the main method of the class which is handling the deletion and splitting of the stipples.
*/
handleDeletionAndSplitting(density_to_radius, grid_extent, delaunay, voronoi) {
const deleted = [];
const splitted = [];
const relaxed = [];
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.density;
const area = Math.abs(d3.polygonArea(polygon)) || 1;
const avg_density = density / area;
const radius = density_to_radius(avg_density);
const [split_threshold, delete_threshold] = this.split_del_thresh(radius);
if (density < delete_threshold * grid_extent[1]) {
deleted.push(i);
} else if (density > split_threshold * grid_extent[1]) {
const centroid = d3.polygonCentroid(polygon);
const [cx, cy] = centroid;
const dist = Math.sqrt(area / Math.PI) / 2.0;
const x = st.momentXX / density - cx * cx;
const y = 2 * (st.momentXY / density - cx * cy);
const z = st.momentYY / density - cy * cy;
var orientation = Math.atan2(y, x - z) / 2.0;
var deltaX = dist * Math.cos(orientation);
var deltaY = dist * Math.sin(orientation);
st[0] = cx + deltaX;
st[1] = cy + deltaY;
st.radius = radius;
centroid[0] -= deltaX;
centroid[1] -= deltaY;
centroid.radius = radius;
splitted.push(st);
splitted.push(centroid);
} else {
st[0] = st.momentX / density;
st[1] = st.momentY / density;
st.radius = radius;
relaxed.push(st);
}
}
this.threshold = this.threshold + this.delta_threshold;
this.stipples.length = relaxed.length + splitted.length;
for (let i = 0; i < relaxed.length; i++) this.stipples[i] = relaxed[i];
for (let i = 0; i < splitted.length; i++)
this.stipples[i + relaxed.length] = splitted[i];
return { deleted, relaxed, splitted };
}
}
export default Stippling;