/**
* Stippling class that handles the creation, iteration and drawing of stipples
*/
class Stippling{
/**
* Constructor
* @param {Number} canvas_height - The height of the canvas where the stippling data is sampled
* @param {Number} canvas_width - The height of the canvas where the stippling data is sampled
* @param {Number} amount_of_init_stipples - The initial amount of stipples
* @param {Number} delta_threshold - The amount the error threshold will be increased in each iteration
* @param {Boolean} is_black_white - Type of input data, either rgb image or greyscale
* @param {Number[]} img_values - The data values on which the stippling should be performed
*/
constructor(canvas_height, canvas_width, amount_of_init_stipples, delta_threshold, is_black_white, img_values){
this.canvas_height = canvas_height;
this.canvas_width = canvas_width;
this.amount_of_init_stipples = amount_of_init_stipples;
this.is_black_white = is_black_white;
this.img_values = img_values;
this.delta_threshold = delta_threshold;
let stipples = new Array(amount_of_init_stipples);
let xran = d3.randomUniform(0, _canvas_width);
let yran = d3.randomUniform(0, _canvas_height);
//initialize random stipples
for (var i = 0; i < amount_of_init_stipples; ++i){
stipples[i] = [xran(), yran()];
stipples[i].density = 0;
stipples[i].moment10 = 0;
stipples[i].moment01 = 0;
stipples[i].moment11 = 0;
stipples[i].moment20 = 0;
stipples[i].moment02 = 0;
}
this.stipples = stipples;
let sum = 0;
for (let y = 0; y < this.canvas_height; y++) {
const line = y * this.canvas_width * 4;
for (let x = 0; x < this.canvas_width; x++) {
const red = img_values[(x * 4) + line];
const green = img_values[(x * 4) + line + 1];
const blue = img_values[(x * 4) + line + 2];
let val = this.rgbToInt(red, green, blue);
sum += val;
}
}
this.overall_sum = sum;
}
/**
* Iterate performs one iteration of the stippling algorithm
* Here a single stipple is deleted, split or moved
* @returns {Number[]} - An array that contains the number of split and deleted stipples
*/
iterate(){
if (this.stipples.length === 0){
alert("No stipples left!");
return;
}
var target_density = this.overall_sum / this.amount_of_init_stipples;
var split_threshold = target_density + (target_density/8 + this.delta_threshold);
var delete_threshold = target_density - (target_density/8 + this.delta_threshold);
this.stipples.forEach(function (stipple) {
stipple.density = 0;
stipple.moment10 = 0;
stipple.moment01 = 0;
stipple.moment11 = 0;
stipple.moment20 = 0;
stipple.moment02 = 0;
});
//compute Voronoi diagram
const delaunay = d3.Delaunay.from(this.stipples),
voronoi = delaunay.voronoi([0, 0, this.canvas_width,this.canvas_height]);
//density for each cell
let found = 0;
let sum = 0;
for (let y = 0; y < this.canvas_height; y++) {
const line = y * this.canvas_width * 4;
for (let x = 0; x < this.canvas_width; x++) {
found = delaunay.find(x, y, found);
const st = this.stipples[found];
const red = this.img_values[(x * 4) + line];
const green = this.img_values[(x * 4) + line + 1];
const blue = this.img_values[(x * 4) + line + 2];
let val = this.rgbToInt(red, green, blue);
sum += val;
st.density += val; // Moment00
const xval = x * val;
const yval = y * val;
st.moment10 += xval;
st.moment01 += yval;
st.moment11 += x * yval;
st.moment20 += x * xval;
st.moment02 += y * yval;
}
}
let deleted = [];
let splitted = [];
let relaxed = [];
//iterate all stipples
for (let i = 0; i < this.stipples.length; i++) {
const polygon = voronoi.cellPolygon(i);
let stipple = this.stipples[i];
let density = stipple.density;
if (density < delete_threshold) {
deleted.push(stipple);
} else if (density > split_threshold) {
const voronoi_centroid = d3.polygonCentroid(polygon);
const cx = voronoi_centroid[0];
const cy = voronoi_centroid[1];
const area = Math.abs(d3.polygonArea(polygon)) || 1;
const dist = Math.sqrt(area / Math.PI) / 2.0;
const x = stipple.moment20 / density - cx * cx;
const y = 2 * (stipple.moment11 / density - cx * cy);
const z = stipple.moment02 / density - cy * cy;
var orientation = Math.atan2(y, x - z) / 2.0;
var deltaX = dist * Math.cos(orientation);
var deltaY = dist * Math.sin(orientation);
// re-use arrays to reduce GC pressure
stipple[0] = cx + deltaX;
stipple[1] = cy + deltaY;
voronoi_centroid[0] -= deltaX;
voronoi_centroid[1] -= deltaY;
voronoi_centroid.density = 0;
splitted.push(stipple);
splitted.push(voronoi_centroid);
} else {
// Relax
stipple[0] = stipple.moment10 / density;
stipple[1] = stipple.moment01 / density;
relaxed.push(stipple);
}
}
let temp = new Array(relaxed.length + splitted.length);
for (let i = 0; i < relaxed.length; i++) temp[i] = relaxed[i];
for (let i = 0; i < splitted.length; i++) temp[i + relaxed.length] = splitted[i];
this.stipples = temp;
this.delta_threshold += target_density * 0.01;
console.log("Deleted:", deleted.length);
console.log("Splitted:", splitted.length);
console.log("Stipples:", this.stipples.length);
return [deleted.length, splitted.length];
}
/**
* Convert an rgb value to an integer. If the image is greyscale only the red channel is used
* @param {Number} red - Red value (0-255)
* @param {Number} green - Green value (0-255)
* @param {Number} blue - Blue value (0-255)
* @returns {Number} - An integer value between 0 and 16 581 375
*/
rgbToInt(red, green, blue){
//console.log(this.is_black_white);
if (this.is_black_white){
if ((red || green || blue) === undefined){
return 0;
}else {
return red;
}
}else {
if ((red || green || blue) === undefined){
return 0;
}else {
return (red * 256 * 256) + (green * 256) + blue;
}
}
}
getStipples(){
return this.stipples;
}
/**
* Draw the stipples to the target svg
*/
drawStipples() {
let svg = d3.select("svg");
let circles = d3.selectAll(".stipple");
circles.remove();
let average = this.overall_sum / this.amount_of_init_stipples;
//Draw stipples
this.stipples.forEach(function (stipple) {
svg.append("circle")
.attr("class", "stipple")
.attr("cx", function (d) {
return stipple[0]})
.attr("cy", function (d) {
return stipple[1]})
.attr("r", function (d) {
return 2 + (stipple.density / average)
});
});
}
}