Stippling.js

/**
 * @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;