14

I'm working on a project using a JavaScript canvas and need to be able to snap the cursor to a certain distance from a polygon. I can already snap to the polygon itself, but I need to have the cursor farther away.

As far as I can tell the best way to go about this is to scale the polygon and snap to that, but when I scale the polygon the distance between the edges of the old polygon and the edges of the new polygon don't always match up.

here is an example of the problem:

enter image description here enter image description here

Edit: The grey represents the original polygon, the red is what I am getting if I scale the polygon normally, and the green is what I'm trying to accomplish

I've already tried translating the polygon to the origin and multiplying by a scale factor, but can't seem to scale each edge by a specific distance.

davey555
  • 720
  • 1
  • 7
  • 15
  • Can you post the code you use for the polygon, translation and scaling? And what do you meant by "don't always match up"? Under what situation does it fail? – K Z Aug 13 '12 at 21:14
  • 1
    If you have a polygon, and you want to create a polygon by creating a new border based upon the set of points some measure `x` units away from any line in your polygon, ignoring corner points, and drawing or erasing lines until your have a shape surrounding your original polygon, you generally *will not* produce a polygon that is a scale version of the original (unless maybe it's a regular polygon). Consider a thin rectangle that's maybe 90 units by 1 unit, and then add 500,000 units to each side... you will end up, for all practical purposes, with a square. – JayC Aug 13 '12 at 21:49

5 Answers5

10

I made a jsFiddle that for a given polygon, calculates an outer polygon that I hope meets your requirement. I have put the math behind it in this pdf document.

Update: code has been made to deal with vertical lines.

function Vector2(x, y)
{
    this.x = x;
    this.y = y;
}

function straight_skeleton(poly, spacing)
{
    // http://stackoverflow.com/a/11970006/796832
    // Accompanying Fiddle: http://jsfiddle.net/vqKvM/35/

    var resulting_path = [];
    var N = poly.length;
    var mi, mi1, li, li1, ri, ri1, si, si1, Xi1, Yi1;
    for(var i = 0; i < N; i++)
    {
        mi = (poly[(i+1) % N].y - poly[i].y)/(poly[(i+1) % N].x - poly[i].x);
        mi1 = (poly[(i+2) % N].y - poly[(i+1) % N].y)/(poly[(i+2) % N].x - poly[(i+1) % N].x);
        li = Math.sqrt((poly[(i+1) % N].x - poly[i].x)*(poly[(i+1) % N].x - poly[i].x)+(poly[(i+1) % N].y - poly[i].y)*(poly[(i+1) % N].y - poly[i].y));
        li1 = Math.sqrt((poly[(i+2) % N].x - poly[(i+1) % N].x)*(poly[(i+2) % N].x - poly[(i+1) % N].x)+(poly[(i+2) % N].y - poly[(i+1) % N].y)*(poly[(i+2) % N].y - poly[(i+1) % N].y));
        ri = poly[i].x+spacing*(poly[(i+1) % N].y - poly[i].y)/li;
        ri1 = poly[(i+1) % N].x+spacing*(poly[(i+2) % N].y - poly[(i+1) % N].y)/li1;
        si = poly[i].y-spacing*(poly[(i+1) % N].x - poly[i].x)/li;
        si1 = poly[(i+1) % N].y-spacing*(poly[(i+2) % N].x - poly[(i+1) % N].x)/li1;
        Xi1 = (mi1*ri1-mi*ri+si-si1)/(mi1-mi);
        Yi1 = (mi*mi1*(ri1-ri)+mi1*si-mi*si1)/(mi1-mi);
        // Correction for vertical lines
        if(poly[(i+1) % N].x - poly[i % N].x==0)
        {
            Xi1 = poly[(i+1) % N].x + spacing*(poly[(i+1) % N].y - poly[i % N].y)/Math.abs(poly[(i+1) % N].y - poly[i % N].y);
            Yi1 = mi1*Xi1 - mi1*ri1 + si1;
        }
        if(poly[(i+2) % N].x - poly[(i+1) % N].x==0 )
        {
            Xi1 = poly[(i+2) % N].x + spacing*(poly[(i+2) % N].y - poly[(i+1) % N].y)/Math.abs(poly[(i+2) % N].y - poly[(i+1) % N].y);
            Yi1 = mi*Xi1 - mi*ri + si;
        }

        //console.log("mi:", mi, "mi1:", mi1, "li:", li, "li1:", li1);
        //console.log("ri:", ri, "ri1:", ri1, "si:", si, "si1:", si1, "Xi1:", Xi1, "Yi1:", Yi1);

        resulting_path.push({
            x: Xi1,
            y: Yi1
        });
    }

    return resulting_path;
}


var canvas = document.getElementById("Canvas");
var ctx = canvas.getContext("2d");

var poly = [
    new Vector2(150, 170),
    new Vector2(400, 120),
    new Vector2(200, 270),
    new Vector2(350, 400),
    new Vector2(210, 470)
];

draw(poly);
draw(straight_skeleton(poly, 10));

function draw(p) {
    ctx.beginPath();
    ctx.moveTo(p[0].x, p[0].y);
    for(var i = 1; i < p.length; i++)
    {
        ctx.lineTo(p[i].x, p[i].y);
    }
    ctx.strokeStyle = "#000000";
    ctx.closePath();
    ctx.stroke();
}

A polygon is put into an array of point objects.

The function draw(p) draws the polygon p on the canvas.

The given polygon is in array poly, the outer in the array poly.

spacing is the distance between the polygons (as along the arrows in your green diagram)


Following Angus Johnson's comment, I have produced some more fiddles to show the issues he raises. This problem is much more difficult problem than I first thought.

MLM
  • 3,660
  • 3
  • 41
  • 66
jing3142
  • 1,821
  • 1
  • 11
  • 16
  • 1
    I've had a very quick look at your code and ISTM that it'll only properly offset the simplest polygons. For example, I can't see any code to manage very acute convex angles where vertices can move exponential distances relative to the offset distance. Also, with acute concave angles with relatively short sides, there needs to be code to remove the self-intersections that result from simple offsetting. – Angus Johnson Aug 15 '12 at 13:43
  • @Angus Johnson I have just tried it with the conditions you specified and I see what you mean. Thanks for pointing this out. I'll give it some more thought. Perhaps davey555 would comment on whether these conditions could arise in his application. – jing3142 Aug 15 '12 at 16:51
7

I made a javascript port of Clipper and with it you can do the scaling in the way you want.

This is an example of inflating polygon:

enter image description here

Check the LIVE DEMO of Javascript Clipper.

and get the clipper.js file from https://sourceforge.net/projects/jsclipper/ .

Full code example of how to offset polygons and and draw them on html5 canvas.

The opposite (deflating) is also possible, if needed:

Offsetting polygons

Timo Kähkönen
  • 11,962
  • 9
  • 71
  • 112
3

ISTM that what you're after is a polygon offsetting algorithm or library.
See An algorithm for inflating/deflating (offsetting, buffering) polygons

Community
  • 1
  • 1
Angus Johnson
  • 4,565
  • 2
  • 26
  • 28
  • Any ideas for doing polygon offsetting in Javascript? – Timo Kähkönen Nov 06 '12 at 01:52
  • 1
    The java library JTS (Java Topology Suite) does offsetting (though it's called buffering there). See https://sourceforge.net/projects/jts-topo-suite/ – Angus Johnson Nov 07 '12 at 12:56
  • It's Java, not Javascript and porting process is needed there also. How it compares to Clipper? – Timo Kähkönen Nov 07 '12 at 13:02
  • Sorry, I'm not aware of any polygon clipping/offsetting libraries written in javascript. I'm guessing you'll need to create/compile an external module that can be called from your javascript code. I have no idea how JTS compares with Clipper (though Clipper performs very favorably with GEOS which is a C++ port of JTS). There is a comparison of a number of clipping libraries here: http://rogue-modron.blogspot.com.au/2011/04/polygon-clipping-wrapper-benchmark.html – Angus Johnson Nov 07 '12 at 13:09
  • Clipper is a good performer in the above mentioned test. Seems that only boost.geometry is faster in some cases. Am I right, that Clipper licence allows porting to Javascript? – Timo Kähkönen Nov 07 '12 at 13:31
  • Yes, you're welcome to port it to Javascript (provided you include appropriate acknowledgements and copyright info) but it'd be a big job. – Angus Johnson Nov 07 '12 at 18:09
  • The best would be if an automatic way would be possible, because then it would be easy to keep up with the updates. I find Pascal->Javascript converter in http://p2js.gelicon.biz/en/, which seems to make intelligent conversion (see sample conversions on the page), but I have no idea what to put in .dpr and .dfm files. Tried `p2js.exe clipper.pas`, but gave Fatal error "Can't find unit system used by clipper". – Timo Kähkönen Nov 07 '12 at 18:39
  • Where could I find test polygons that are on AGG example in http://www.angusj.com/delphi/clipper.php? I mean Great Britain and Spiral and maybe others also. I have made a Javascript port and would want to test it preferably with the same data as in your examples. – Timo Kähkönen Nov 15 '12 at 04:39
  • The test polygons for AGG are in the AGG source: http://www.antigrain.com/agg-2.5.zip . Look in the 'examples' folder for make_gb_poly.cpp & make_arrows.cpp. The source code to construct the spiral is in gpc_test.cpp. – Angus Johnson Nov 15 '12 at 05:50
  • Thanks. I found them. But now I have a question. In Delphi main demo, if I select Clip operation None, and try to offset by pressing +-, nothing happens. This arises a question: is it possible to use Clipper as a plain Ofsetter, to offset a polygon that is not a result of some clip operation? If I have three polygons: subject poly GB, clip poly ARROW and empty polygon SSS. Is it possible to only offset ARROW and do nothing to GB? This produces odd result in my JS port. I'm using command `SSS = cpr.OffsetPolygons(ARROW, 5.0, JoinType.jtRound);` – Timo Kähkönen Nov 15 '12 at 21:10
  • Hi again Timo. Yes it is possible to use the clipper library simply for offsetting. Please ask any further questions about Clipper in SourceForge ( http://sourceforge.net/p/polyclipping/discussion/ ) as this isn't the place. – Angus Johnson Nov 15 '12 at 22:30
2

One way is to find the distance between every edge of the polygon and the cursor point, and keep the smallest.

To compute the distance between a point and a line segment, project the point onto the supporting line; if the projection falls between the endpoints, the solution is the point-to-line distance; otherwise, the solution is the distance to the closest endpoint.

This is easily computed using vector calculus.

1

I can offer a solution using JSTS library (JavaScript port of JTS). The library has methods for inflating/deflating (offsetting) of polygons.

You can set the cap and join styles if you want to get the inflated polygon with different type of edges. The only thing you have to do is to convert you polygon coordinates to a JSTS coordinate which is really simple:

function vectorCoordinates2JTS (polygon) { var coordinates = []; for (var i = 0; i < polygon.length; i++) { coordinates.push(new jsts.geom.Coordinate(polygon[i].x, polygon[i].y)); } return coordinates; }

Once you have converted the coordinates you can inflate your polygon:

function inflatePolygon(poly, spacing) {
  var geoInput = vectorCoordinates2JTS(poly);
  geoInput.push(geoInput[0]);

  var geometryFactory = new jsts.geom.GeometryFactory();

  var shell = geometryFactory.createPolygon(geoInput);
  var polygon = shell.buffer(spacing);
  //try with different cap style
  //var polygon = shell.buffer(spacing, jsts.operation.buffer.BufferParameters.CAP_FLAT);

  var inflatedCoordinates = [];
  var oCoordinates;
  oCoordinates = polygon.shell.points.coordinates;

  for (i = 0; i < oCoordinates.length; i++) {
    var oItem;
    oItem = oCoordinates[i];
    inflatedCoordinates.push(new Vector2(Math.ceil(oItem.x), Math.ceil(oItem.y)));
  }
  return inflatedCoordinates;
}

With code I've posted on jsFiddle you will get something like this:

enter image description here

Additional info: I usually use this type of inflating/deflating (a little changed) for setting boundaries with radius on polygons that are drawn on a map (with Leaflet or Google maps). You just convert lat,lng pairs to JSTS coordinates and everything else is the same. Example:

enter image description here

Marko Letic
  • 2,460
  • 2
  • 28
  • 34