SVG path with stroke fills the svg element - javascript

I have a set of coordinates that can be used to create a polygon in google maps. I used the mercantor-projection formula described in this answer to convert it into a set of points (x,y) that can be used in an svg . The conversion was successful and it was rendered properly.
The snippet below renders well without the setting the stroke property in the element. If you un-comment line 29 where it sets the stroke property to red, it will fill up the entire svg element instead of outlining the element with red which highlights the main problem of this post.
const COORDINATES = [[-86.917595,32.664169],[-86.918817,32.644278],[-86.898304,32.567687],[-86.90675,32.537298],[-86.890829,32.513742],[-86.878989,32.487323],[-86.885203,32.482438],[-86.870212,32.468974],[-86.860056,32.450861],[-86.86709,32.439188],[-86.849898,32.438325],[-86.824394,32.424725],[-86.845448,32.415416],[-86.843758,32.400416],[-86.827244,32.378816],[-86.815399,32.370821],[-86.807378,32.354356],[-86.814912,32.340803],[-86.820921,32.33324],[-86.816107,32.30997],[-86.798268,32.308632],[-86.773163,32.340728],[-86.780447,32.3686],[-86.778365,32.394601],[-86.749981,32.389105],[-86.727181,32.404497],[-86.717897,32.402814],[-86.719028,32.372059],[-86.711337,32.360767],[-86.683537,32.353395],[-86.655597,32.376147],[-86.653419,32.397247],[-86.618,32.405717],[-86.613453,32.398584],[-86.614841,32.374266],[-86.595335,32.361345],[-86.581873,32.375019],[-86.570551,32.375006],[-86.542537,32.363517],[-86.532531,32.338775],[-86.496774,32.344437],[-86.491902,32.364327],[-86.463564,32.377288],[-86.461277,32.403473],[-86.456273,32.405837],[-86.444721,32.399841],[-86.411172,32.409937],[-86.412229,32.528746],[-86.412446,32.579324],[-86.413116,32.707386],[-86.524434,32.707058],[-86.714219,32.705694],[-86.71339,32.661732],[-86.771532,32.660717],[-86.816574,32.660117],[-86.917595,32.664169]];
const getPoint = ([x, y]) => {
return {
x: (x + 180) * (256 / 360),
y: (256 / 2) - (256 * Math.log(Math.tan((Math.PI / 4) + ((y * Math.PI / 180) / 2))) / (2 * Math.PI))
};
};
const svg = document.querySelector('svg');
const points = [];
let minX = 256;
let minY = 256;
let maxX = 0;
let maxY = 0;
for(const coordinate of COORDINATES) {
const point = getPoint(coordinate);
minX = Math.min(minX, point.x);
minY = Math.min(minY, point.y);
maxX = Math.max(maxX, point.x);
maxY = Math.max(maxY, point.y);
points.push(`${point.x},${point.y}`);
}
const g = document
.createElementNS("http://www.w3.org/2000/svg", 'g');
const path = document
.createElementNS("http://www.w3.org/2000/svg", 'path');
svg.setAttribute(
'viewBox',
[minX, minY, maxX - minX, maxY - minY].join(' ')
);
path.setAttribute('d', 'M' + points.join(' ') + 'z');
path.setAttribute('fill', 'blue');
//path.setAttribute('stroke', 'red');
g.appendChild(path);
svg.appendChild(g);
<svg height="400" width="400" preserveAspectRatio="xMinYMin meet"></svg>
Why does setting the stroke property fills the SVG element with the specified stroke color?
It would be great if anyone can point out how to solve the problem of the snippet above.

The problem is related to the scaling of your SVG. Setting the stroke width to a small value like 0.0025 fixes it.
const COORDINATES = [[-86.917595,32.664169],[-86.918817,32.644278],[-86.898304,32.567687],[-86.90675,32.537298],[-86.890829,32.513742],[-86.878989,32.487323],[-86.885203,32.482438],[-86.870212,32.468974],[-86.860056,32.450861],[-86.86709,32.439188],[-86.849898,32.438325],[-86.824394,32.424725],[-86.845448,32.415416],[-86.843758,32.400416],[-86.827244,32.378816],[-86.815399,32.370821],[-86.807378,32.354356],[-86.814912,32.340803],[-86.820921,32.33324],[-86.816107,32.30997],[-86.798268,32.308632],[-86.773163,32.340728],[-86.780447,32.3686],[-86.778365,32.394601],[-86.749981,32.389105],[-86.727181,32.404497],[-86.717897,32.402814],[-86.719028,32.372059],[-86.711337,32.360767],[-86.683537,32.353395],[-86.655597,32.376147],[-86.653419,32.397247],[-86.618,32.405717],[-86.613453,32.398584],[-86.614841,32.374266],[-86.595335,32.361345],[-86.581873,32.375019],[-86.570551,32.375006],[-86.542537,32.363517],[-86.532531,32.338775],[-86.496774,32.344437],[-86.491902,32.364327],[-86.463564,32.377288],[-86.461277,32.403473],[-86.456273,32.405837],[-86.444721,32.399841],[-86.411172,32.409937],[-86.412229,32.528746],[-86.412446,32.579324],[-86.413116,32.707386],[-86.524434,32.707058],[-86.714219,32.705694],[-86.71339,32.661732],[-86.771532,32.660717],[-86.816574,32.660117],[-86.917595,32.664169]];
const getPoint = ([x, y]) => {
return {
x: (x + 180) * (256 / 360),
y: (256 / 2) - (256 * Math.log(Math.tan((Math.PI / 4) + ((y * Math.PI / 180) / 2))) / (2 * Math.PI))
};
};
const svg = document.querySelector('svg');
const points = [];
let minX = 256;
let minY = 256;
let maxX = 0;
let maxY = 0;
for(const coordinate of COORDINATES) {
const point = getPoint(coordinate);
minX = Math.min(minX, point.x);
minY = Math.min(minY, point.y);
maxX = Math.max(maxX, point.x);
maxY = Math.max(maxY, point.y);
points.push(`${point.x},${point.y}`);
}
const g = document
.createElementNS("http://www.w3.org/2000/svg", 'g');
const path = document
.createElementNS("http://www.w3.org/2000/svg", 'path');
svg.setAttribute(
'viewBox',
[minX, minY, maxX - minX, maxY - minY].join(' ')
);
path.setAttribute('d', 'M' + points.join(' ') + 'z');
path.setAttribute('fill', 'blue');
path.setAttribute('stroke', 'red');
path.setAttribute('stroke-width', '.0025');
g.appendChild(path);
svg.appendChild(g);
<svg height="400" width="400" preserveAspectRatio="xMinYMin meet"></svg>

Related

Is is possible to bend a row of divs around a point?

This is the result i want to make with divs
How can i achieve this result?
Edit: my goal was not to use divs only that i didn't want to use canvas. But i haven't thought about SVG's so thank you!
Here's a quick and dirty alternative example using SVG arcs
const cx = 100; // Circle centre
const cy = 100;
const width = 40; // Width of line
const radius = 100; // Radius of circle
const TwoPi = Math.PI * 2;
// Compute circumference
const circ = TwoPi * radius;
const height = circ / 12; // Length of each segment
const parent = document.getElementById("curve");
for (let i = 0; i < circ; i += height) {
let seg = document.createElementNS("http://www.w3.org/2000/svg", "path");
let rs = (i / circ) * TwoPi;
let re = ((i + height) / circ) * TwoPi;
let ss = Math.sin(rs);
let cs = Math.cos(rs);
let se = Math.sin(re);
let ce = Math.cos(re);
// Build wedge path element
seg.setAttribute("d",
`M${(cs * radius) + cx},${(ss * radius) + cy}` +
`A${radius},${radius} ${((re - rs) / Math.PI) * 180},0,1 ${(ce * radius) + cx},${(se * radius) + cy}` +
`L${(ce * (radius - width)) + cx},${(se * (radius - width)) + cy}` +
`A${radius - width},${radius - width} ${((re - rs) / Math.PI) * -180},0,0 ${(cs * (radius - width)) + cx},${(ss * (radius - width)) + cy}z`
);
seg.setAttribute("class", "pathSeg");
parent.appendChild(seg);
}
.pathSeg { stroke: black; stroke-width: 3px; fill: white }
.pathSeg:hover { fill: red }
<svg width="200" height="200" viewBox="0 0 200 200">
<g id="curve"></g>
</svg>
HTML elements aren't really suited to this kind of layout since they're inherently rectangular whereas your segments have curved boundaries.
CSS transforms will only allow you to apply affine transformations which affect the whole shape equally and cannot create these kinds of curves.
SVG or Canvas would be much better fits for this kind of drawing depending on what you're planning on doing with it.
If you really need to go the HTML element route, then your best bet would be to layout the divs and apply clipping masks to them to accomplish the curved sections. Here's a basic example of plotting divs along a circle path:
const cx = 100; // Circle centre
const cy = 100;
const width = 40; // Width of line
const height = 30; // Length of each segment
const radius = 100; // Radius of circle
const TwoPi = Math.PI * 2;
// Compute circumference
const circ = TwoPi * radius;
const parent = document.documentElement;
for (let i = 0; i < circ; i += height) {
let div = document.createElement("div");
div.className = "pathSeg";
div.style.width = `${width}px`;
div.style.height = `${height}px`;
div.style.transform = `translate(${cx}px, ${cy}px) rotate(${(i / circ) * 360}deg) translate(${radius}px, 0)`;
parent.appendChild(div);
}
.pathSeg {
position: absolute;
border: 1px solid black;
}

How to draw an irregular shaped polygon using the given angles

I am making a drawing application. I have created a class Polygon. Its constructor will receive three arguments and these will be its properties:
points(Number): Number of points the polygon will have.
rotation(Number): The angle the whole polygon will be rotated.
angles(Array Of number): The angles between two lines of the polygon.
I have been trying for the whole day, but I couldn't figure out the correct solution.
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');
let isMouseDown = false;
let tool = 'polygon';
let savedImageData;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
const mouse = {x:null,y:null}
let mousedown = {x:null,y:null}
const toDegree = val => val * 180 / Math.PI
class Polygon {
constructor(points, rotation, angles){
this.points = points;
this.rotation = rotation;
//if angles are given then convert them to radian
if(angles){
this.angles = angles.map(x => x * Math.PI/ 180);
}
//if angles array is not given
else{
/*get the angle for a regular polygon for given points.
3-points => 60
4-points => 90
5-points => 108
*/
let angle = (this.points - 2) * Math.PI/ this.points;
//fill the angles array with the same angle
this.angles = Array(points).fill(angle)
}
let sum = 0;
this.angles = this.angles.map(x => {
sum += x;
return sum;
})
}
draw(startx, starty, endx, endy){
c.beginPath();
let rx = (endx - startx) / 2;
let ry = (endy - starty) / 2;
let r = Math.max(rx, ry)
c.font = '35px cursive'
let cx = startx + r;
let cy = starty + r;
c.fillRect(cx - 2, cy - 2, 4, 4); //marking the center
c.moveTo(cx + r, cy);
c.strokeText(0, cx + r, cy);
for(let i = 1; i < this.points; i++){
//console.log(this.angles[i])
let dx = cx + r * Math.cos(this.angles[i] + this.rotation);
let dy = cy + r * Math.sin(this.angles[i] + this.rotation);
c.strokeStyle = 'red';
c.strokeText(i, dx, dy, 100);
c.strokeStyle ='black';
c.lineTo(dx, dy);
}
c.closePath();
c.stroke();
}
}
//update();
c.beginPath();
c.lineWidth = 1;
document.addEventListener('mousemove', function(e){
//Getting the mouse coords according to canvas
const canvasData = canvas.getBoundingClientRect();
mouse.x = (e.x - canvasData.left) * (canvas.width / canvasData.width);
mouse.y = (e.y - canvasData.top) * (canvas.height / canvasData.height);
if(tool === 'polygon' && isMouseDown){
drawImageData();
let pol = new Polygon(5, 0);
pol.draw(mousedown.x, mousedown.y, mouse.x, mouse.y);
}
})
function saveImageData(){
savedImageData = c.getImageData(0, 0, canvas.width, canvas.height);
}
function drawImageData(){
c.putImageData(savedImageData, 0, 0)
}
document.addEventListener('mousedown', () => {
isMouseDown = true;
mousedown = {...mouse};
if(tool === 'polygon'){
saveImageData();
}
});
document.addEventListener('mouseup', () => isMouseDown = false);
<canvas></canvas>
In the above code I am trying to make a pentagon but it doesn't work.
Unit polygon
The following snippet contains a function polygonFromSidesOrAngles that returns the set of points defining a unit polygon as defined by the input arguments. sides, or angles
Both arguments are optional but must have one argument
If only sides given then angles are calculated to make the complete polygon with all side lengths equal
If only angles given then the number of sides is assumed to be the number of angles. Angles are in degrees 0-360
If the arguments can not define a polygon then there are several exceptions throw.
The return is a set of points on a unit circle that define the points of the polygon. The first point is at coordinate {x : 1, y: 0} from the origin.
The returned points are not rotated as that is assumed to be a function of the rendering function.
All points on the polygon are 1 unit distance from the origin (0,0)
Points are in the form of an object containing x and y properties as defined by the function point and polarPoint
Method used
I did not lookup an algorithm, rather I worked it out from the assumption that a line from (1,0) on the unit circle at the desired angle will intercept the circle at the correct distance from (1,0). The intercept point is used to calculate the angle in radians from the origin. That angle is then used to calculate the ratio of the total angles that angle represents.
The function that does this is calcRatioOfAngle(angle, sides) returning the angle as a ratio (0-1) of Math.PI * 2
It is a rather long handed method and likely can be significantly reduced
As it is unclear in your question what should be done with invalid arguments the function will throw a range error if it can not proceed.
Polygon function
Math.PI2 = Math.PI * 2;
Math.TAU = Math.PI2;
Math.deg2Rad = Math.PI / 180;
const point = (x, y) => ({x, y});
const polarPoint = (ang, dist) => ({x: Math.cos(ang) * dist, y: Math.sin(ang) * dist});
function polygonFromSidesOrAngles(sides, angles) {
function calcRatioOfAngle(ang, sides) {
const v1 = point(Math.cos(ang) - 1, Math.sin(ang));
const len2 = v1.x * v1.x + v1.y * v1.y;
const u = -v1.x / len2;
const v2 = point(v1.x * u + 1, v1.y * u);
const d = (1 - (v2.y * v2.y + v2.x * v2.x)) ** 0.5 / (len2 ** 0.5);
return Math.atan2(v2.y + v1.y * d, v2.x + 1 + v1.x * d) / (Math.PI * (sides - 2) / 2);
}
const vetAngles = angles => angles.reduce((sum, ang) => sum += ang, 0) === (angles.length - 2) * 180;
var ratios = [];
if(angles === undefined) {
if (sides < 3) { throw new RangeError("Polygon must have more than 2 side") }
const rat = 1 / sides;
while (sides--) { ratios.push(rat) }
} else {
if (sides === undefined) { sides = angles.length }
else if (sides !== angles.length) { throw new RangeError("Numbers of sides does not match number of angles") }
if (sides < 3) { throw new RangeError("Polygon must have more than 2 side") }
if (!vetAngles(angles)) { throw new RangeError("Set of angles can not create a "+sides+" sided polygon") }
ratios = angles.map(ang => calcRatioOfAngle(ang * Math.deg2Rad, sides));
ratios.unshift(ratios.pop()); // rotate right to get first angle at start
}
var ang = 0;
const points = [];
for (const rat of ratios) {
ang += rat;
points.push(polarPoint(ang * Math.TAU, 1));
}
return points;
}
Render function
Function to render the polygon. It includes the rotation so you don't need to create a separate set of points for each angle you want to render the polygon at.
The radius is the distance from the center point x,y to any of the polygons vertices.
function drawPolygon(ctx, poly, x, y, radius, rotate) {
ctx.setTransform(radius, 0, 0, radius, x, y);
ctx.rotate(rotate);
ctx.beginPath();
for(const p of poly.points) { ctx.lineTo(p.x, p.y) }
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
Example
The following renders a set of test polygons to ensure that the code is working as expected.
Polygons are rotated to start at the top and then rendered clock wise.
The example has had the vetting of input arguments removed.
const ctx = can.getContext("2d");
can.height = can.width = 512;
Math.PI2 = Math.PI * 2;
Math.TAU = Math.PI2;
Math.deg2Rad = Math.PI / 180;
const point = (x, y) => ({x, y});
const polarPoint = (ang, dist) => ({x: Math.cos(ang) * dist, y: Math.sin(ang) * dist});
function polygonFromAngles(sides, angles) {
function calcRatioOfAngle(ang, sides) {
const x = Math.cos(ang) - 1, y = Math.sin(ang);
const len2 = x * x + y * y;
const u = -x / len2;
const x1 = x * u + 1, y1 = y * u;
const d = (1 - (y1 * y1 + x1 * x1)) ** 0.5 / (len2 ** 0.5);
return Math.atan2(y1 + y * d, x1 + 1 + x * d) / (Math.PI * (sides - 2) / 2);
}
var ratios = [];
if (angles === undefined) {
const rat = 1 / sides;
while (sides--) { ratios.push(rat) }
} else {
ratios = angles.map(ang => calcRatioOfAngle(ang * Math.deg2Rad, angles.length));
ratios.unshift(ratios.pop());
}
var ang = 0;
const points = [];
for(const rat of ratios) {
ang += rat;
points.push(polarPoint(ang * Math.TAU, 1));
}
return points;
}
function drawPolygon(poly, x, y, radius, rot) {
const xdx = Math.cos(rot) * radius;
const xdy = Math.sin(rot) * radius;
ctx.setTransform(xdx, xdy, -xdy, xdx, x, y);
ctx.beginPath();
for (const p of poly) { ctx.lineTo(p.x, p.y) }
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
const segs = 4;
const tests = [
[3], [, [45, 90, 45]], [, [90, 10, 80]], [, [60, 50, 70]], [, [40, 90, 50]],
[4], [, [90, 90, 90, 90]], [, [90, 60, 90, 120]],
[5], [, [108, 108, 108, 108, 108]], [, [58, 100, 166, 100, 116]],
[6], [, [120, 120, 120, 120, 120, 120]], [, [140, 100, 180, 100, 100, 100]],
[7], [8],
];
var angOffset = -Math.PI / 2; // rotation of poly
const w = ctx.canvas.width;
const h = ctx.canvas.height;
const wStep = w / segs;
const hStep = h / segs;
const radius = Math.min(w / segs, h / segs) / 2.2;
var x,y, idx = 0;
for (y = 0; y < segs && idx < tests.length; y ++) {
for (x = 0; x < segs && idx < tests.length; x ++) {
drawPolygon(polygonFromAngles(...tests[idx++]), (x + 0.5) * wStep , (y + 0.5) * hStep, radius, angOffset);
}
}
canvas {
border: 1px solid black;
}
<canvas id="can"></canvas>
I do just a few modification.
Constructor take angles on degree
When map angles to radian complement 180 because canvas use angles like counterclockwise. We wan to be clockwise
First point start using the passed rotation
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');
let isMouseDown = false;
let tool = 'polygon';
let savedImageData;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
const mouse = {x:null,y:null}
let mousedown = {x:null,y:null}
const toDegree = val => val * 180 / Math.PI;
const toRadian = val => val * Math.PI / 180;
class Polygon {
constructor(points, rotation, angles){
this.points = points;
this.rotation = toRadian(rotation);
//if angles array is not given
if(!angles){
/*get the angle for a regular polygon for given points.
3-points => 60
4-points => 90
5-points => 108
*/
let angle = (this.points - 2) * 180 / this.points;
//fill the angles array with the same angle
angles = Array(points).fill(angle);
}
this.angles = angles;
let sum = 0;
console.clear();
// To radians
this.angles = this.angles.map(x => {
x = 180 - x;
x = toRadian(x);
return x;
})
}
draw(startx, starty, endx, endy){
c.beginPath();
let rx = (endx - startx) / 2;
let ry = (endy - starty) / 2;
let r = Math.max(rx, ry)
c.font = '35px cursive'
let cx = startx + r;
let cy = starty + r;
c.fillRect(cx - 2, cy - 2, 4, 4); //marking the center
c.moveTo(cx + r, cy);
let sumAngle = 0;
let dx = cx + r * Math.cos(this.rotation);
let dy = cy + r * Math.sin(this.rotation);
c.moveTo(dx, dy);
for(let i = 0; i < this.points; i++){
sumAngle += this.angles[i];
dx = dx + r * Math.cos((sumAngle + this.rotation));
dy = dy + r * Math.sin((sumAngle + this.rotation));
c.strokeStyle = 'red';
c.strokeText(i, dx, dy, 100);
c.strokeStyle ='black';
c.lineTo(dx, dy);
}
c.closePath();
c.stroke();
}
}
//update();
c.beginPath();
c.lineWidth = 1;
document.addEventListener('mousemove', function(e){
//Getting the mouse coords according to canvas
const canvasData = canvas.getBoundingClientRect();
mouse.x = (e.x - canvasData.left) * (canvas.width / canvasData.width);
mouse.y = (e.y - canvasData.top) * (canvas.height / canvasData.height);
if(tool === 'polygon' && isMouseDown){
drawImageData();
let elRotation = document.getElementById("elRotation").value;
let rotation = elRotation.length == 0 ? 0 : parseInt(elRotation);
let elPoints = document.getElementById("elPoints").value;
let points = elPoints.length == 0 ? 3 : parseInt(elPoints);
let elAngles = document.getElementById("elAngles").value;
let angles = elAngles.length == 0 ? null : JSON.parse(elAngles);
let pol = new Polygon(points, rotation, angles);
pol.draw(mousedown.x, mousedown.y, mouse.x, mouse.y);
}
})
function saveImageData(){
savedImageData = c.getImageData(0, 0, canvas.width, canvas.height);
}
function drawImageData(){
c.putImageData(savedImageData, 0, 0)
}
document.addEventListener('mousedown', () => {
isMouseDown = true;
mousedown = {...mouse};
if(tool === 'polygon'){
saveImageData();
}
});
document.addEventListener('mouseup', () => isMouseDown = false);
<!DOCTYPE html>
<html lang="en">
<body>
Points: <input id="elPoints" style="width:30px" type="text" value="3" />
Rotation: <input id="elRotation" style="width:30px" type="text" value="0" />
Angles: <input id="elAngles" style="width:100px" type="text" value="[45, 45, 90]" />
<canvas></canvas>
</body>
</html>

Convert Google Maps polyline to SVG

I am using the JavaScript from a previous question (Convert a Google Maps polygon path to an SVG path) to display an SVG from a list of coordinates. See JsFiddle: http://jsfiddle.net/londonfed/9xhsL39u/51/
However, this does not render properly (it shows a red blob). It should show a straight line as illustrated in this screenshot:
The above image was taken from https://developers.google.com/maps/documentation/utilities/polylineutility?csw=1
You can copy paste the polyline inside the encoded polyline input field to see how the SVG should render:
ofkyHluWg#?EyDQi#}ByFJMcAgCeAyCeAiDu#iC`#_#??a#^IY{#{Cu#aDSg#KQSq#m#kCYgAIc#QJKYc#{#W{#Ga#C_#SkBQcA??GaAMuASwAS_BGo#GWQ]c#i#GMKWGCCAUuA[sBSaBKm#E]OaB
Any ideas much appreciated.
There are a couple of things wrong. Mainly:
Your coordinates are tiny, so a default stroke-width of "1" is way too big. 0.0001 is more the scale you want.
You want an open path, not a closed one, so don't include a "z".
function latLng2point(latLng) {
return {
x: (latLng.lng + 180) * (256 / 360),
y: (256 / 2) - (256 * Math.log(Math.tan((Math.PI / 4) + ((latLng.lat * Math.PI / 180) / 2))) / (2 * Math.PI))
};
}
function poly_gm2svg(gmPaths, fx) {
var point,
gmPath,
svgPath,
svgPaths = [],
minX = 256,
minY = 256,
maxX = 0,
maxY = 0;
for (var pp = 0; pp < gmPaths.length; ++pp) {
gmPath = gmPaths[pp], svgPath = [];
for (var p = 0; p < gmPath.length; ++p) {
point = latLng2point(fx(gmPath[p]));
minX = Math.min(minX, point.x);
minY = Math.min(minY, point.y);
maxX = Math.max(maxX, point.x);
maxY = Math.max(maxY, point.y);
svgPath.push([point.x, point.y].join(','));
}
svgPaths.push(svgPath.join(' '))
}
return {
path: 'M' + svgPaths.join(' M'),
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
};
}
function drawPoly(node, props) {
var svg = node.cloneNode(false),
g = document.createElementNS("http://www.w3.org/2000/svg", 'g'),
path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
node.parentNode.replaceChild(svg, node);
path.setAttribute('d', props.path);
g.appendChild(path);
svg.appendChild(g);
svg.setAttribute('viewBox', [props.x, props.y, props.width, props.height].join(' '));
}
function init() {
for (var i = 0; i < paths.length; ++i) {
paths[i] = google.maps.geometry.encoding.decodePath(paths[i]);
}
svgProps = poly_gm2svg(paths, function (latLng) {
return {
lat: latLng.lat(),
lng: latLng.lng()
}
});
drawPoly(document.getElementById('svg'), svgProps)
}
//array with encoded paths, will be decoded later
var paths = ["ofkyHluWg#?EyDQi#}ByFJMcAgCeAyCeAiDu#iC`#_#??a#^IY{#{Cu#aDSg#KQSq#m#kCYgAIc#QJKYc#{#W{#Ga#C_#SkBQcA??GaAMuASwAS_BGo#GWQ]c#i#GMKWGCCAUuA[sBSaBKm#E]OaB"];
init();
#svg {
background:silver;
}
path {
stroke: red;
stroke-width: 0.0001;
fill: none;
}
<script src="https://maps.googleapis.com/maps/api/js?v=3&libraries=geometry"></script>
<svg id="svg" height="400" width="400" viewport="0 0 400 400" preserveAspectRatio="xMinYMin meet"></svg>

How to get center point of svg polygon?

I have an SVG image and I need to get the center point of all polygons for painting text. I am trying to do this with the below script:
function calculateCenterPoint(areas) {
var maxX = 0,
minX = Infinity,
maxY = 0,
minY = Infinity;
Array.prototype.forEach.call(areas, function (e) {
var i = 0,
coords = e.getAttribute('points').split(',');
while (i < coords.length) {
var x = parseInt(coords[i++], 10),
y = parseInt(coords[i++], 10);
if (x < minX)
minX = x;
if (x > maxX)
maxX = x;
if (y < minY)
minY = y;
if (y > maxY)
maxY = y;
}
});
return {
x: minX + (maxX - minX) / 2,
y: minY + (maxY - minY) / 2
}; }
but it is not working in IE 11, or edge.
Links:
http://istra46760.tmweb.ru/poselki/ushakovskie-dachi.html
https://gyazo.com/ba906bde72e5394c8a0275281108663b
instead of looking at the points, get the bounding rect of the svg
const el = document.querySelector("path");
const bbox = el.getBoundingClientRect();
const center = {
x: bbox.left + bbox.width / 2,
y: bbox.top + bbox.height / 2
};
or you could do
const bbox = el.getBBox();

How to get bezier curve size in HTML5 canvas with cp2 point?

I want to get the rendered size (width/height) of a bézier curve in HTML5 canvas
context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
with this code, for instance
// control points
var cp1x = 200,
cp1y = 150,
cp2x = 260,
cp2y = 10;
var x = 0,
y = 0;
// calculation
var curveWidth = cp1x > x ? cp1x - x : x - cp1x,
curveHeight = cp1y > y ? cp1y - y : y - cp1y;
However, the cp2 point can increase the curve distance (length, size). I.e., suppose cp2 point is the red point in this image and its x coordinate is bigger than cp1's x, which looks to be the end point of the bézier curve:
So, how can I consider the length of cp2 point in curveWidth and in curveHeight to be exact?
To get extent of a quadratic bezier
The points
var x1 = ? // start
var y1 = ?
var x2 = ? // control
var y2 = ?
var x3 = ? // end
var y3 = ?
The extent
extent = {
minx : null,
miny : null,
maxx : null,
maxy : null,
}
The Math.
These equation apply for the x and y axis (thus two equations)
For quadratic bezier
F(u) = a(1-u)^2+2b(1-u)u+cu^2
which is more familiar in the form of a quadratic equation
Ax^2 + Bx + C = 0
so the bezier rearranged
F(u) = (a-2b+c)u^2+2(-a+b)u+a
We need the derivative so that becomes
2(a-2b+c)u-2a+2b
simplify divide common factor 2 to give
(a-2b+c)u + b - a = 0
separate out u
b-a = (a-2b + c)u
(b-a) / (a - 2b + c) = u
Then algorithm optimised for the fact the (b-a) part of (a-2b-c)
function solveB2(a,b,c){
var ba = b-a;
return ba / (ba - (c-b)); // the position on the curve of the maxima
}
var ux = solveB2(x1,x2,x3);
var uy = solveB2(y1,y2,y3);
These two values are positions along the curve so we now just have to find the coordinates of these two points. We need a function that finds a point on a quadratic bezier
function findPoint(u,x1,y1,x2,y2,x3,y3){ // returns array with x, and y at u
var xx1 = x1 + (x2 - x1) * u;
var yy1 = y1 + (y2 - y1) * u;
var xx2 = x2 + (x3 - x2) * u;
var yy2 = y2 + (y3 - y2) * u;
return [
xx1 + (xx2 - xx1) * u,
yy1 + (yy2 - yy1) * u
]
}
First check that they are on the curve and find the point at ux,uy
if(ux >= 0 && ux <= 1){
var px = findPoint(ux,x1,y1,x2,y2,x3,y3);
}
if(uy >= 0 && uy <= 1){
var py = findPoint(uy,x1,y1,x2,y2,x3,y3);
}
Now test against the extent
extent.minx = Math.min(x1,x3,px[0],py[0]);
extent.miny = Math.min(y1,y3,px[1],py[1]);
extent.maxx = Math.max(x1,x3,px[0],py[0]);
extent.maxy = Math.max(y1,y3,px[1],py[1]);
And you are done
extent has the coordinates of the box around the bezier. Top left (min) and bottom right (max)
You can also get the minimum bounding box if you rotate the bezier so that the start and end points fall on the x axis. Then do the above and the resulting rectangle is the minimum sized rectangle that can be placed around the bezier.
Cubics are much the same but just a lot more typing.
And a demo, just to make sure I got it all correct.
var canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 400;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
var x1,y1,x2,y2,x3,y3;
var ux,uy,px,py;
var extent = {
minx : null,
miny : null,
maxx : null,
maxy : null,
}
function solveB2(a,b,c){
var ba = b-a;
return ba / (ba - (c-b)); // the position on the curve of the maxima
}
function findPoint(u,x1,y1,x2,y2,x3,y3){ // returns array with x, and y at u
var xx1 = x1 + (x2 - x1) * u;
var yy1 = y1 + (y2 - y1) * u;
var xx2 = x2 + (x3 - x2) * u;
var yy2 = y2 + (y3 - y2) * u;
return [
xx1 + (xx2 - xx1) * u,
yy1 + (yy2 - yy1) * u
]
}
function update(time){
ctx.clearRect(0,0,800,400);
// create random bezier
x1 = Math.cos(time / 1000) * 300 + 400;
y1 = Math.sin(time / 2100) * 150 + 200;
x2 = Math.cos((time + 3000) / 1200) * 300 + 400;
y2 = Math.sin(time / 2300) * 150 + 200;
x3 = Math.cos(time / 1400) * 300 + 400;
y3 = Math.sin(time / 2500) * 150 + 200;
// solve for bounds
var ux = solveB2(x1,x2,x3);
var uy = solveB2(y1,y2,y3);
if(ux >= 0 && ux <= 1){
px = findPoint(ux,x1,y1,x2,y2,x3,y3);
}else{
px = [x1,y1]; // a bit of a cheat but saves having to put in extra conditions
}
if(uy >= 0 && uy <= 1){
py = findPoint(uy,x1,y1,x2,y2,x3,y3);
}else{
py = [x3,y3]; // a bit of a cheat but saves having to put in extra conditions
}
extent.minx = Math.min(x1,x3,px[0],py[0]);
extent.miny = Math.min(y1,y3,px[1],py[1]);
extent.maxx = Math.max(x1,x3,px[0],py[0]);
extent.maxy = Math.max(y1,y3,px[1],py[1]);
// draw the rectangle
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.strokeRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
ctx.fillStyle = "rgba(255,200,0,0.2)";
ctx.fillRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
// show points
ctx.fillStyle = "blue";
ctx.fillRect(x1-3,y1-3,6,6);
ctx.fillRect(x3-3,y3-3,6,6);
ctx.fillStyle = "black";
ctx.fillRect(px[0]-4,px[1]-4,8,8);
ctx.fillRect(py[0]-4,py[1]-4,8,8);
ctx.lineWidth = 3;
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.quadraticCurveTo(x2,y2,x3,y3);
ctx.stroke();
// control point
ctx.lineWidth = 1;
ctx.strokeStyle = "#0a0";
ctx.strokeRect(x2-3,y2-3,6,6);
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.lineTo(x3,y3);
ctx.stroke();
// do it all again
requestAnimationFrame(update);
}
requestAnimationFrame(update);
UPDATE
While musing over the bezier I realised that I could remove a lot of code if I assumed that the bezier was normalised (the end points start at (0,0) and end at (1,1)) because the zeros can be removed and the ones simplified.
While changing the code I also realized that I had needlessly calculated the x and y for both the x and y extent coordinates. Giving 4 values while I only need 2.
The resulting code is much simpler. I remove the function solveB2 and findPoint as the calculations seam too trivial to bother with functions.
To find the x and y maxima from quadratic bezier defined with x1, y1, x2, y2, x3, y3
// solve quadratic for bounds by normalizing equation
var brx = x3 - x1; // get x range
var bx = x2 - x1; // get x control point offset
var x = bx / brx; // normalise control point which is used to check if maxima is in range
// do the same for the y points
var bry = y3 - y1;
var by = y2 - y1
var y = by / bry;
var px = [x1,y1]; // set defaults in case maximas outside range
if(x < 0 || x > 1){ // check if x maxima is on the curve
px[0] = bx * bx / (2 * bx - brx) + x1; // get the x maxima
}
if(y < 0 || y > 1){ // same as x
px[1] = by * by / (2 * by - bry) + y1;
}
// now only need to check the x and y maxima not the coordinates of the maxima
extent.minx = Math.min(x1,x3,px[0]);
extent.miny = Math.min(y1,y3,px[1]);
extent.maxx = Math.max(x1,x3,px[0]);
extent.maxy = Math.max(y1,y3,px[1]);
And the example code which has far better performance but unlike the previous demo this version does not calculate the actual coordinates of the x and y maximas.
var canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 400;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
var x1,y1,x2,y2,x3,y3;
var ux,uy,px,py;
var extent = {
minx : null,
miny : null,
maxx : null,
maxy : null,
}
function update(time){
ctx.clearRect(0,0,800,400);
// create random bezier
x1 = Math.cos(time / 1000) * 300 + 400;
y1 = Math.sin(time / 2100) * 150 + 200;
x2 = Math.cos((time + 3000) / 1200) * 300 + 400;
y2 = Math.sin(time / 2300) * 150 + 200;
x3 = Math.cos(time / 1400) * 300 + 400;
y3 = Math.sin(time / 2500) * 150 + 200;
// solve quadratic for bounds by normalizing equation
var brx = x3 - x1; // get x range
var bx = x2 - x1; // get x control point offset
var x = bx / brx; // normalise control point which is used to check if maxima is in range
// do the same for the y points
var bry = y3 - y1;
var by = y2 - y1
var y = by / bry;
var px = [x1,y1]; // set defaults in case maximas outside range
if(x < 0 || x > 1){ // check if x maxima is on the curve
px[0] = bx * bx / (2 * bx - brx) + x1; // get the x maxima
}
if(y < 0 || y > 1){ // same as x
px[1] = by * by / (2 * by - bry) + y1;
}
// now only need to check the x and y maxima not the coordinates of the maxima
extent.minx = Math.min(x1,x3,px[0]);
extent.miny = Math.min(y1,y3,px[1]);
extent.maxx = Math.max(x1,x3,px[0]);
extent.maxy = Math.max(y1,y3,px[1]);
// draw the rectangle
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.strokeRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
ctx.fillStyle = "rgba(255,200,0,0.2)";
ctx.fillRect(extent.minx,extent.miny,extent.maxx-extent.minx,extent.maxy-extent.miny);
// show points
ctx.fillStyle = "blue";
ctx.fillRect(x1-3,y1-3,6,6);
ctx.fillRect(x3-3,y3-3,6,6);
ctx.fillStyle = "black";
ctx.fillRect(px[0]-4,px[1]-4,8,8);
ctx.lineWidth = 3;
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.quadraticCurveTo(x2,y2,x3,y3);
ctx.stroke();
// control point
ctx.lineWidth = 1;
ctx.strokeStyle = "#0a0";
ctx.strokeRect(x2-3,y2-3,6,6);
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.lineTo(x3,y3);
ctx.stroke();
// do it all again
requestAnimationFrame(update);
}
requestAnimationFrame(update);

Categories