Source: stippling.js

/**
 * 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)
                });
        });
    }

}