Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Transform complex svg shapes into a circle abstraction

SVGs are ugly please review my:

JSFIDDLE LINK

HTML:

<svg version="1.1" class="overlap-svg" id="alaska"></svg>
<svg version="1.1" class="overlap-svg" id="grid"></svg>

CSS:

.overlap-svg {
    position: absolute;
    left:0;
    top: 0;
}

Question:

If we overlap these 2 svgs, What would the JS function be to highlight only the svg circles that have parts of alaska(red)in them in them?

review description below for more info


  1. Let's say you have a complex shape like the outline of alaska.

enter image description here

  1. Lets say you have another svg of a grid of circles:

enter image description here


How do I transform this:

enter image description here

Into something like this:

enter image description here

the circle should be filled red if any portion of alaska(red) is inside the area of the circle.

Again please review my JSFiddle link above.

like image 436
Armeen Harwood Avatar asked Oct 15 '15 21:10

Armeen Harwood


2 Answers

fiddle

Example output, with resized canvas element overlayed. You can take the svg and load it into a canvas element. Take the element, and because it is a canvas element you can get an array of its pixels.

Your circle abstraction could be built by building the grid out of the pixels of an appropriately resized canvas.

First a helper: Grid Manager.

function GridManager(configIn) {
  var gm_ = {};

  gm_.config = {
    'gridWidth': 10,
    'gridHeight': 10,
    'gridCellWidth': 10,
    'gridCellHeight': 10,
    'gridHeight': 100,
    'dataSrc': []
  };

  // Load new config over defaults
  for (var property in configIn) {
    gm_.config[property] = configIn[property];
  }

  /** 
    * Creates an array using the module's config building a 2d data array 
    * from a flat array. Loops over GridManager.config.dataSrc
    * 
    * Render a checkerboard pattern:
    *   GridManager.config.dataSrc = ["#"," "]
    * 
    * Render you can load a image by passing in its full pixel array, 
    * provided image height and width match GridManager.config.gridHeight
    * and GridManager.config.gridWidth. 
    */
  gm_.createGridSrc = function() {
    var height = this.config.gridHeight;
    var width = this.config.gridWidth;
    var output = [];

    for (var i = 0; i < height; i++) {
      output[i] = [];

      for (var ii = 0; ii < width; ii++) {
        if (this.config.dataSrc !== undefined) {
          var dataSrc = this.config.dataSrc;
          output[i][ii] = dataSrc[i*width + ii % dataSrc.length];
        }
      }
    }
    return output;
  };

  /** 
    * Creates a SVG with a grid of circles based on
    * GridManager.config.dataSrc.
    * 
    * This is where you can customize GridManager output.
    */
  gm_.createSvgGrid = function() {
    var cellWidth = this.config.gridCellWidth;
    var cellHeight = this.config.gridCellHeight;
    var svgWidth = 1000;
    var svgHeight = 1000;
    var radius = 3
    var cellOffset = radius / 2;

    //create svg
    var xmlns = 'http://www.w3.org/2000/svg';
    var svgElem = document.createElementNS (xmlns, 'svg');
    svgElem.setAttributeNS (null, 'viewBox', '0 0 ' + svgWidth + ' ' + svgHeight);
    svgElem.setAttributeNS (null, 'width', svgWidth);
    svgElem.setAttributeNS (null, 'height', svgHeight);
    svgElem.style.display = 'block';

    //create wrapper path
    var g = document.createElementNS (xmlns, 'g');
    svgElem.appendChild (g);

    //create grid
    var data = this.createGridSrc();
    var count = 0;
    for (var i = data.length - 1; i >= 0; i--) {
      for (var ii = data[i].length - 1; ii >= 0; ii--) {
        
        // This svgHeight and svgWidth subtraction here flips the image over
        // perhaps this should be accomplished elsewhere.
        var y = svgHeight - (cellHeight * i) - cellOffset;
        var x = svgWidth - (cellWidth * ii) - cellOffset;

        var cell = document.createElementNS (xmlns, 'circle');
        var template = data[i][ii];
        
        // Machine has averaged the amount of fill per pixel
        // from 0 - 255, so you can filter just the red pixels like this
        // over a certain strength.
        if (template[0] > 10 ) {
          cell.setAttributeNS (null, 'fill', '#ff0000');
          // Consider stashing refs to these in this.groups['red'] or something
          // similar
        } else {
          cell.setAttributeNS (null, 'fill', 'none');
        }

        cell.setAttributeNS (null, 'stroke', '#000000');
        cell.setAttributeNS (null, 'stroke-miterlimit', '#10');
        cell.setAttributeNS (null, 'cx', x);
        cell.setAttributeNS (null, 'cy', y);
        cell.setAttributeNS (null, 'r', radius);

        g.appendChild (cell);
      }
    }
    return svgElem;
  }
  return gm_;
}

And then in main.js

var wrapper = document.getElementById('wrapper');

var mySVG = document.getElementById('alaska').outerHTML;

mySVG = mySVG.slice(0, 4) + ' height="100" ' + mySVG.slice(4);

// Create a Data URI based on the #alaska element.
var mySrc = 'data:image/svg+xml;base64,' + window.btoa(mySVG);

// Create a new image to do our resizing and capture our pixel data from.
var source = new Image();
source.onload = function() {

  var svgRasterStage = document.createElement('canvas');
  svgRasterStage.width = 1000;
  svgRasterStage.height = 1000;

  svgRasterStage.classList.add('hidden');

  // You may not need this at all, I didn't test it.
  wrapper.appendChild(svgRasterStage);

  // Get drawing context for the Canvas
  var svgRasterStageContext = svgRasterStage.getContext('2d');

  // Draw the SVG to the stage.
  svgRasterStageContext.drawImage(source, 0, 0);
  
  // We can now get array of rgba pixels all concatinated together:
  //    [ r, g, b, a, r, g, b, a,  (...)  r, g, b, a, r, g, b, a]
  var rgbaConcat = svgRasterStageContext.getImageData(0, 0, 100, 100).data;

  // Which sucks, so here's a way to convert them to pixels that we can 
  // use with GridManager.createSvgGrid.
  var pixels = [];
  var count = 0;
  
  // NOTE: this is a for with a weird step: i=i-4. i-4 is an infinte loop.
  // anything else just jumbles the pixels.
  for (var i = rgbaConcat.length - 1; i >= 0; i=i-4) {
    var r = rgbaConcat[i - 0];
    var g = rgbaConcat[i - 1];
    var b = rgbaConcat[i - 2];
    var a = rgbaConcat[i - 3];
    pixels.push([r, g, b, a]);
  }

  // We create our GridManager (finally).
  var gm = new GridManager({
    'gridWidth': 100,
    'gridHeight': 100,
    'dataSrc': pixels
  });
  
  // And let her rip!
  wrapper.appendChild(gm.createSvgGrid());
}
like image 168
Graham P Heath Avatar answered Nov 05 '22 00:11

Graham P Heath


I tried to quick-solve this issue, and did some research, but still it is not finished / complete (you can finish it in your fina implementation).

You need to have a function that check If a point is inside a path. I found 2 libraries in JS: Raphael and SnapSVG.

I forked and edited your JSFiddle, and fast-tried to solve it. My first attempt was with SnapSVG's function but it returned me a lesser-than-expected result than Raphael's function.

Open the fiddle and check: https://jsfiddle.net/edmundo096/7sjLb956/4/. Beware that the scale of 2 will slow your browser, although I used it to see a correct result but takes time to see something (mobile browsers may hang up).

var alaska = $('#alaska');
var grid = $('#grid');
var path =  alaska.find('path').first().attr('d');

grid.children().each(function(){
    var circle = $(this);
    var scale = 2;

    // SnapSVG version: var isInside = Snap.path.isPointInside(path, 
    var isInside = Raphael.isPointInsidePath(path, 
                        circle.attr('cx') * scale, 
                        circle.attr('cy') * scale);
    if (isInside) {
        circle.attr('fill', 'blue');
    }
});

(I used jQuery, and 2 external resources: Raphael and SnapSvg from Cloudflare CDN)

As you can see on the next image, it generates a kind of dot map, but still you need to correct the mapping, placement, scale, etc. of the Path.

Raphael first quick-try result:

A quick result of the Raphael function

SnapSVG first quick-try result: A quick result of the SnapSvg function

You can cache your result; save the resulted map in a JSON map object, and then load it separately to save the calculation Time from this complex Paths.

Hope it can help you.

like image 41
edmundo096 Avatar answered Nov 04 '22 23:11

edmundo096