Related
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>
I’m trying to do a function to detect intersections between two circles. If yes it scores true, otherwise it scores false, but I think I got lost so it does not display what I want. If anyone can help me please . Thank you
Surely I have incorrectly coded in javascript if there is a person who knows the answer I am all ears
function AreCirclesIntersecting(c0,c1) {
x0 = c0['center']['x'];
y0 = c0['center']['y'];
r0 = c0['center']['r'];
x1 = c1['center']['x'];
y1 = c1['center']['y'];
r1 = c1['center']['r'];
var a, dx, dy, d, h, rx, ry;
var x2, y2;
/* dx and dy are the vertical and horizontal distances between
* the circle centers.
*/
dx = x1 - x0;
dy = y1 - y0;
/* Determine the straight-line distance between the centers. */
d = Math.sqrt((dy*dy) + (dx*dx));
/* Check for solvability. */
if (d > (r0 + r1)) {
/* no solution. circles do not intersect. */
return false;
}
if (d < Math.abs(r0 - r1)) {
/* no solution. one circle is contained in the other */
return false;
}
/* 'point 2' is the point where the line through the circle
* intersection points crosses the line between the circle
* centers.
*/
/* Determine the distance from point 0 to point 2. */
a = ((r0*r0) - (r1*r1) + (d*d)) / (2.0 * d) ;
/* Determine the coordinates of point 2. */
x2 = x0 + (dx * a/d);
y2 = y0 + (dy * a/d);
/* Determine the distance from point 2 to either of the
* intersection points.
*/
h = Math.sqrt((r0*r0) - (a*a));
/* Now determine the offsets of the intersection points from
* point 2.
*/
rx = -dy * (h/d);
ry = dx * (h/d);
/* Determine the absolute intersection points. */
var xi = x2 + rx;
var xi_prime = x2 - rx;
var yi = y2 + ry;
var yi_prime = y2 - ry;
return [xi, xi_prime, yi, yi_prime];
}
const circles = [
{center: {x: 10.0, y: 10.0}, radius: 5.0},
{center: {x: 20.0, y: 20.0}, radius: 15.0},
{center: {x: 20.0, y: 10.0}, radius: 5.0},
{center: {x: 20.0, y: 25.0}, radius: 7.5},
];
const q7_result1 = AreCirclesIntersecting(circles[0], circles[1]);
console.log(q7_result1); // Expected output: true
const q7_result2 = AreCirclesIntersecting(circles[0], circles[2]);
console.log(q7_result2); // Expected output: true
const q7_result3 = AreCirclesIntersecting(circles[1], circles[3]);
console.log(q7_result3); // Expected output: false
const q7_result4 = AreCirclesIntersecting(circles[2], circles[3]);
console.log(q7_result4); // Expected output: false
I can't speak to the math, but there's an issue in how you're retrieving your r values. in circles they're radius directly on the object, but you're trying to retrieve them with ['center']['r']. I modified your code and it's outputing results now:
function AreCirclesIntersecting(c0,c1) {
x0 = c0['center']['x'];
y0 = c0['center']['y'];
r0 = c0['radius']; // MODIFICATION HERE
x1 = c1['center']['x'];
y1 = c1['center']['y'];
r1 = c1['radius']; // MODIFICATION HERE
var a, dx, dy, d, h, rx, ry;
var x2, y2;
/* dx and dy are the vertical and horizontal distances between
* the circle centers.
*/
dx = x1 - x0;
dy = y1 - y0;
/* Determine the straight-line distance between the centers. */
d = Math.sqrt((dy*dy) + (dx*dx));
/* Check for solvability. */
if (d > (r0 + r1)) {
/* no solution. circles do not intersect. */
return false;
}
if (d < Math.abs(r0 - r1)) {
/* no solution. one circle is contained in the other */
return false;
}
/* 'point 2' is the point where the line through the circle
* intersection points crosses the line between the circle
* centers.
*/
/* Determine the distance from point 0 to point 2. */
a = ((r0*r0) - (r1*r1) + (d*d)) / (2.0 * d) ;
/* Determine the coordinates of point 2. */
x2 = x0 + (dx * a/d);
y2 = y0 + (dy * a/d);
/* Determine the distance from point 2 to either of the
* intersection points.
*/
h = Math.sqrt((r0*r0) - (a*a));
/* Now determine the offsets of the intersection points from
* point 2.
*/
rx = -dy * (h/d);
ry = dx * (h/d);
/* Determine the absolute intersection points. */
var xi = x2 + rx;
var xi_prime = x2 - rx;
var yi = y2 + ry;
var yi_prime = y2 - ry;
return [xi, xi_prime, yi, yi_prime];
}
const circles = [
{center: {x: 10.0, y: 10.0}, radius: 5.0},
{center: {x: 20.0, y: 20.0}, radius: 15.0},
{center: {x: 20.0, y: 10.0}, radius: 5.0},
{center: {x: 20.0, y: 25.0}, radius: 7.5},
];
const q7_result1 = AreCirclesIntersecting(circles[0], circles[1]);
console.log(q7_result1); // Expected output: true
const q7_result2 = AreCirclesIntersecting(circles[0], circles[2]);
console.log(q7_result2); // Expected output: true
const q7_result3 = AreCirclesIntersecting(circles[1], circles[3]);
console.log(q7_result3); // Expected output: false
const q7_result4 = AreCirclesIntersecting(circles[2], circles[3]);
console.log(q7_result4); // Expected output: false
// draw the circles for debugging
(() => {
const canvas = document.createElement('canvas');
canvas.width = canvas.height = '100';
document.body.append(canvas);
const context = canvas.getContext('2d');
for (const {center: {x,y},radius :r} of circles) {
context.beginPath();
context.arc(x,y,r,0, 2 * Math.PI);
context.stroke();
}
})()
Javascript natively offers a hypothenus function,
useful here to calculate the distance between 2 points on a 2 D system
const circles =
[ { center: { x: 10.0, y: 10.0 } , radius: 5.0 }
, { center: { x: 20.0, y: 20.0 } , radius: 15.0 }
, { center: { x: 20.0, y: 10.0 } , radius: 5.0 }
, { center: { x: 20.0, y: 25.0 } , radius: 7.5 }
]
function AreCirclesIntersecting(c0,c1)
{
let delta_x = Math.abs(c0.center.x - c1.center.x)
, delta_y = Math.abs(c0.center.y - c1.center.y)
, dist = Math.hypot(delta_x, delta_y)
, out = (dist > (c0.radius + c1.radius) )
, c0_in_c1 = !out && ( c1.radius > (c0.radius + dist ))
, c1_in_c0 = !out && ( c0.radius > (c1.radius + dist ))
;
return !(out || c0_in_c1 || c1_in_c0)
}
function testing (name, expected, indx_a, indx_b )
{
let test = AreCirclesIntersecting(circles[indx_a], circles[indx_b])
, divTest = document.createElement('div')
, c0 = circles[indx_a]
, c1 = circles[indx_b]
;
divTest.className = 'test'
divTest.innerHTML = `
<p>
<strong>${name}</strong> <br>
${JSON.stringify(circles[indx_a]) } green <br>
${JSON.stringify(circles[indx_b]) } red <br><br>
expected: ${expected}<br>result:<strong>${test}</strong><br>sucess: ${(expected===test)? '✅':'❌'}
</p>
<svg viewBox="0 0 50 40" xmlns="http://www.w3.org/2000/svg" width="200" heigth="160">
<circle cx="${c0.center.x}" cy="${c0.center.y}" r="${c0.radius}" fill="none" stroke="green"/>
<circle cx="${c1.center.x}" cy="${c1.center.y}" r="${c1.radius}" fill="none" stroke="red"/>
</svg>`
document.body.appendChild(divTest)
}
testing('q7_result1',true, 0,1)
testing('q7_result2',true, 0,2)
testing('q7_result3',false,1,3)
testing('q7_result4',false,2,3)
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
}
div.test {
display : inline-block;
margin : .5em;
border : 1px solid grey;
padding : 1em;
width : 400px;
}
div.test p { margin: .3em; }
You could do like this (check the interactive demo):
document.addEventListener('keydown', (function () {
function getCircleGeometry(circle) {
const {
top: circleTop,
left: circleLeft,
right: circleRight,
bottom: circleBottom,
} = circle.getBoundingClientRect();
return {
circleRadius: (circleBottom - circleTop) / 2,
circleCenterX: (circleLeft + circleRight) / 2,
circleCenterY: (circleTop + circleBottom) / 2
}
}
function collision(circle1, circle2) {
const {
circleRadius: circle1Radius,
circleCenterX: circle1CenterX,
circleCenterY: circle1CenterY
} = getCircleGeometry(circle1);
const {
circleRadius: circle2Radius,
circleCenterX: circle2CenterX,
circleCenterY: circle2CenterY
} = getCircleGeometry(circle2);
const deltaX = circle1CenterX - circle2CenterX;
const deltaY = circle1CenterY - circle2CenterY;
// Is the distance between the centers less than the sum of the radii
return deltaX ** 2 + deltaY ** 2 < (circle1Radius + circle2Radius) ** 2;
}
const circle1 = document.getElementById('circle1');
const circle2 = document.getElementById('circle2');
let circleTop = 26;
let circleLeft = 8;
return function (e) {
e.preventDefault();
switch(e.keyCode) {
case 37: // left
circleLeft -= 1;
break;
case 38: // up
circleTop -= 1;
break;
case 39: // right
circleLeft += 1;
break;
case 40: // down
circleTop += 1;
break;
}
circle1.style.top = `${circleTop}px`;
circle1.style.left = `${circleLeft}px`;
circle1.style['background-color'] = circle2.style['background-color'] = collision(circle1, circle2) ? 'red' : 'blue';
}
})());
.circle {
position: absolute;
border-radius: 50%;
height: 20px;
width: 20px;
}
<div>Use the keyboard arrows to move the circle</div>
<div class="circle" id="circle1" style="background-color: red;"></div>
<div class="circle" id="circle2" style="background-color: red;"></div>
This is a link only answer because there is a great answer to this question on the Stack Overflow Mathematics site.
In a comment to the answer on Mathematics, contributor Jared Updike posted a link to a gist he wrote for a JavaScript function intersectTwoCircles to provide the intersection points of two circles. Gist comments contain ports of the code into several other languages.
While the parameter formats of the intersectTwoCircles and the posted AreCirclesIntersecting functions differ slightly, they appear to be returning the same information.
According to a staff answer about the licensing of gists on Github:
If you set your pages and repositories to be viewed publicly, you grant each User of GitHub a nonexclusive, worldwide license to use, display, and perform Your Content through the GitHub Service and to reproduce Your Content solely on GitHub as permitted through GitHub’s functionality (for example, through forking).
which, along with the authors comment on Mathematica, seems to indicate while you are free to use it as you see fit, I am not licensed to display a copy of the code here.
As a matter of etiquette (and required under many open source software licenses) please provide attribution of the source of intersectionTwoCircles if you use it.
I have two objects a parent (red) and a child (blue). The parent object is fixed and can't be moved, only the child object is movable and the child is always bigger than the parent. In whatever way the child object is moved it should always be contained inside the child, which means we should never see the red rectangle.
Demo: https://codesandbox.io/s/force-contain-of-object-inside-another-object-fabric-js-7nt7q
I know there are solutions to contain an object within the canvas or other object boundaries (ex. Move object within canvas boundary limit) which mainly force the top/right/bottom/left values to not exceed the parent values, but here we have the case of two rotated objects by the same degree.
I have a real-life scenario when a user uploads a photo to a frame container. The photo is normally always bigger than the frame container. The user can move the photo inside the frame, but he should not be allowed to create any empty spaces, the photo should always be contained inside the photo frame.
I would go with a pure canvas (no fabricjs), do it from scratch that way you understand well the problem you are facing, then if you need it that same logic should be easily portable to any library.
You have some rules:
The parent object is fixed and can't be moved,
The child object is movable.
The child is always bigger than the parent.
The child object is always constrained by the parent.
So my idea is to get all four corners, that way on the move we can use those coordinates to determine if it can be moved to the new location or not, the code should be easy to follow, but ask if you have any concerns.
I'm using the ray-casting algorithm:
https://github.com/substack/point-in-polygon/blob/master/index.js
With that, all we need to do is check that the corners of the child are not inside the parent and that the parent is inside the child, that is all.
I'm no expert with FabricJS so my best might not be much...
but below is my attempt to get your code going.
<canvas id="canvas" width="500" height="350"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<script>
var canvas = new fabric.Canvas("canvas");
canvas.stateful = true;
function getCoords(rect) {
var x = rect.left;
var y = rect.top;
var angle = (rect.angle * Math.PI) / 180;
var coords = [{ x, y }];
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.height * Math.cos(angle);
y += rect.height * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
return coords;
}
function inside(p, vs) {
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i].x, yi = vs[i].y;
var xj = vs[j].x, yj = vs[j].y;
var intersect =
yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
var parent = new fabric.Rect({
width: 150, height: 100, left: 200, top: 50, angle: 25, selectable: false, fill: "red"
});
var pCoords = getCoords(parent);
var child = new fabric.Rect({
width: 250, height: 175, left: 180, top: 10, angle: 25, hasControls: false, fill: "rgba(0,0,255,0.9)"
});
canvas.add(parent);
canvas.add(child);
canvas.on("object:moving", function (e) {
var cCoords = getCoords(e.target);
var inBounds = true;
cCoords.forEach(c => { if (inside(c, pCoords)) inBounds = false; });
pCoords.forEach(c => { if (!inside(c, cCoords)) inBounds = false; });
if (inBounds) {
e.target.setCoords();
e.target.saveState();
e.target.set("fill", "rgba(0,0,255,0.9)");
} else {
e.target.set("fill", "black");
e.target.animate({
left: e.target._stateProperties.left,
top: e.target._stateProperties.top
},{
duration: 500,
onChange: canvas.renderAll.bind(canvas),
easing: fabric.util.ease["easeInBounce"],
onComplete: function() {
e.target.set("fill", "rgba(0,0,255,0.9)");
}
});
}
});
</script>
That code is on sandbox as well:
https://codesandbox.io/s/force-contain-of-object-inside-another-object-fabric-js-dnvb5
It certainly is nice not to worry about coding all the click/hold/drag fabric makes that real easy...
I was experimenting with FabricJS and there a nice property of the canvas
(canvas.stateful = true;)
that allows us to keep track of where we've been, and if we go out of bounds we can revert that movement, also playing with animate that gives the user visual feedback that the movement is not allowed.
Here is another version without animation:
<canvas id="canvas" width="500" height="350"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<script>
var canvas = new fabric.Canvas("canvas");
canvas.stateful = true;
function getCoords(rect) {
var x = rect.left;
var y = rect.top;
var angle = (rect.angle * Math.PI) / 180;
var coords = [{ x, y }];
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.height * Math.cos(angle);
y += rect.height * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
return coords;
}
function inside(p, vs) {
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i].x, yi = vs[i].y;
var xj = vs[j].x, yj = vs[j].y;
var intersect =
yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
var parent = new fabric.Rect({
width: 150, height: 100, left: 200, top: 50, angle: 25, selectable: false, fill: "red"
});
var pCoords = getCoords(parent);
var child = new fabric.Rect({
width: 250, height: 175, left: 180, top: 10, angle: 25, hasControls: false, fill: "rgba(0,0,255,0.9)"
});
canvas.add(parent);
canvas.add(child);
canvas.on("object:moving", function (e) {
var cCoords = getCoords(e.target);
var inBounds = true;
cCoords.forEach(c => { if (inside(c, pCoords)) inBounds = false; });
pCoords.forEach(c => { if (!inside(c, cCoords)) inBounds = false; });
if (inBounds) {
e.target.setCoords();
e.target.saveState();
} else {
e.target.left = e.target._stateProperties.left;
e.target.top = e.target._stateProperties.top;
}
});
</script>
This algorithm also opens the door for other shapes as well, here is a hexagon version:
https://raw.githack.com/heldersepu/hs-scripts/master/HTML/canvas_contained2.html
you can just create the new Class, with your object inside, and do all actions only with parent of the children, something like this:
fabric.RectWithRect = fabric.util.createClass(fabric.Rect, {
type: 'rectWithRect',
textOffsetLeft: 0,
textOffsetTop: 0,
_prevObjectStacking: null,
_prevAngle: 0,
minWidth: 50,
minHeight: 50,
_currentScaleFactorX: 1,
_currentScaleFactorY: 1,
_lastLeft: 0,
_lastTop: 0,
recalcTextPosition: function () {
//this.insideRect.setCoords();
const sin = Math.sin(fabric.util.degreesToRadians(this.angle))
const cos = Math.cos(fabric.util.degreesToRadians(this.angle))
const newTop = sin * this.insideRectOffsetLeft + cos * this.insideRectOffsetTop
const newLeft = cos * this.insideRectOffsetLeft - sin * this.insideRectOffsetTop
const rectLeftTop = this.getPointByOrigin('left', 'top')
this.insideRect.set('left', rectLeftTop.x + newLeft)
this.insideRect.set('top', rectLeftTop.y + newTop)
this.insideRect.set('width', this.width - 40)
this.insideRect.set('height', this.height - 40)
this.insideRect.set('scaleX', this.scaleX)
this.insideRect.set('scaleY', this.scaleY)
},
initialize: function (textOptions, rectOptions) {
this.callSuper('initialize', rectOptions)
this.insideRect = new fabric.Rect({
...textOptions,
dirty: false,
objectCaching: false,
selectable: false,
evented: false,
fragmentType: 'rectWidthRect'
});
canvas.bringToFront(this.insideRect);
this.insideRect.width = this.width - 40;
this.insideRect.height = this.height - 40;
this.insideRect.left = this.left + 20;
this.insideRect.top = this.top + 20;
this.insideRectOffsetLeft = this.insideRect.left - this.left
this.insideRectOffsetTop = this.insideRect.top - this.top
this.on('moving', function(e){
this.recalcTextPosition();
})
this.on('rotating',function(){
this.insideRect.rotate(this.insideRect.angle + this.angle - this._prevAngle)
this.recalcTextPosition()
this._prevAngle = this.angle
})
this.on('scaling', function(fEvent){
this.recalcTextPosition();
});
this.on('added', function(){
this.canvas.add(this.insideRect)
});
this.on('removed', function(){
this.canvas.remove(this.insideRect)
});
this.on('mousedown:before', function(){
this._prevObjectStacking = this.canvas.preserveObjectStacking
this.canvas.preserveObjectStacking = true
});
this.on('deselected', function(){
this.canvas.preserveObjectStacking = this._prevObjectStacking
});
}
});
and then just add your element to your canvas as usual:
var rectWithRect = new fabric.RectWithRect(
{
fill: "red",
}, // children rect options
{
left:100,
top:100,
width: 300,
height: 100,
dirty: false,
objectCaching: false,
strokeWidth: 0,
fill: 'blue'
} // parent rect options
);
canvas.add(rectWithRect);
by the way, you can use method like this to create nested elements, text with background and other.
Codesandbox DEMO
I'm trying to display a nested SVG (path) and I'd like to manipulate the "inner" SVG so that's it's centred and scaled based on an "inner bounding box" - let's say a square 500px^2.
Here's an example but I can't get my head around the viewport and scaling to do what I need it to do.
<!DOCTYPE html>
<html>
<head>
<style>
#svg {
background:silver;
}
path {
stroke: red;
stroke-width: 0.0001;
fill: none;
}
</style>
<script src="/scripts/snippet-javascript-console.min.js?v=1"></script>
</head>
<body>
<script src="https://maps.googleapis.com/maps/api/js?v=3&libraries=geometry"></script>
<svg id="svg" height="800" width="624" viewport="0 0 800 624" preserveAspectRatio="xMinYMin meet"></svg>
<script type="text/javascript">
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 = ["ki{eFvqfiVqAWQIGEEKAYJgBVqDJ{BHa#jAkNJw#Pw#V{APs#^aABQAOEQGKoJ_FuJkFqAo#{A}#sH{DiAs#Q]?WVy#`#oBt#_CB]KYMMkB{AQEI#WT{BlE{#zAQPI#ICsCqA_BcAeCmAaFmCqIoEcLeG}KcG}A}#cDaBiDsByAkAuBqBi#y#_#o#o#kB}BgIoA_EUkAMcACa#BeBBq#LaAJe#b#uA`#_AdBcD`#iAPq#RgALqAB{#EqAyAoOCy#AmCBmANqBLqAZkB\\iCPiBJwCCsASiCq#iD]eA]y#[i#w#mAa#i#k#g#kAw#i#Ya#Q]EWFMLa#~BYpAFNpA`Aj#n#X`#V`AHh#JfB#xAMvAGZGHIDIAWOEQNcC#sACYK[MSOMe#QKKKYOs#UYQISCQ?Q#WNo#r#OHGAGCKOQ_BU}#MQGG]Io##c#FYNg#d#s#d#ODQAMOMaASs#_#a#SESAQDqBn#a#RO?KK?UBU\\kA#Y?WMo#Iy#GWQ_#WSSGg#AkABQB_Ap#_A^o#b#Q#o#IS#OHi#n#OFS?OI}#iAQMQGQC}#DOIIUK{#IUOMyBo#kASOKIQCa#L[|AgATWN[He#?QKw#FOPCh#Fx#l#TDLELKl#aAHIJEX#r#ZTDV#LENQVg#RkA#c#MeA?WFOPMf#Ej#Fj##LGHKDM?_#_#iC?a#HKRIl#NT?FCHMFW?YEYGWQa#GYBiAIq#Gq#L_BHSHK|#WJETSLQZs#z#_A~#uA^U`#G\\CRB\\Tl#p#Th#JZ^bB`#lAHLXVLDP?LGFSKiDBo#d#wBVi#R]VYVE\\#`#Lh#Fh#CzAk#RSDQA]GYe#eAGWSiBAWBWBIJORK`#KPOPSTg#h#}Ad#o#F[E_#EGMKUGmAEYGMIMYKs#?a#J}##_BD_#HQJMx#e#LKHKHWAo#UoAAWFmAH}#?w#C[YwAAc#HSNM|Ao#rA}#zAq#`#a#j#eAxAuBXQj#MXSR[b#gAFg#?YISOGaAHi#Xw#v#_#d#WRSFqARUHQJc#d#m#`A[VSFUBcAEU#WFULUPa#v#Y~#UrBc#dBI~#?l#P~ABt#N`HEjA]zAEp##p#TrBCl#CTQb#k#dAg#jAU^KJYLK#k#A[Js#d#a#b#]RgBl#[FMAw#[]G]?m#D_#F]P[Vu#t#[TMF_#Do#E_##q#P]PWZUZw#vAkAlAGJOj#IlAMd#OR{#p#a#d#sBpD]v#a#`Aa#n#]TODgBVk#Pe#^cBfBc#Rs#La#RSPm#|#wCpDS^Wp#QZML{#l#qBbCYd#k#lAIVCZBZNTr#`#RRHZANIZQPKDW#e#CaASU?I#YTKRQx##\\VmALYRQLCL?v#P|#D\\GJEFKDM#OCa#COOYIGm#YMUCM#]JYr#uAx#kAt#}#jAeAPWbAkBj#s#bAiAz#oAj#m#VQlAc#VQ~#aA`Au#p#Q`AIv#MZORUV_#p#iB|AoCh#q#dAaANUNWH[N{AJ[^m#t#_Av#wA\\a#`#W`#In#Al#B^E`#Wl#u#\\[VQ\\K`#Eb#?R#dAZP#d#CRExAs#\\Yt#{#LG\\MjAATINOXo#d#kAl#_AHYBOCe#QiBCm#Fq#\\wADo#AyGEeBWuB#YHu#Tu#Lk#VcCTo#d#aA\\WJE`#G~#FP?VI\\U~#sANO`#SfAMj#U\\WjAsAXS`#UNENALBHFFL?^Ml#Uj#]b#q#RUJSPkChEc#XcAb#sA|#]PaA\\OJKNER?TDTNj#Jn#?p#OfC#ZR`B#VCV_#n#{#l#WbACv#OlABnAPl#LNNHbBBNBLFFJ#^GLg#x#i#|AMP[X}#XOJKPET?l#LhAFXp#fBDRCd#S\\_#Ps#PQ#}A]S?QDe#V]b#MR[fAKt#ErAF~CANILYDKGIKe#{#Yy#e#sB[gA[c#e#YUCU?WBUHUNQPq#`AiArAMV[^e#Zc#JQJKNMz#?r#Bb#PfAAfA#VVbADn#E`#KHSEe#SMAKDKFM\\^dDCh#m#LoAQ_##MFOZLfBEl#QbASd#KLQBOAaAc#QAQ#QHc#v#ONMJOBOCg#c#]O[EMBKFGL?RHv#ARERGNe#h#{#h#WVGNDt#JLNFPFz#LdBf#f#PJNHPF`ADPJJJDl#I`#B^Tp#bALJNDNALIf#i#PGPCt#DNE`#Uv#[dAw#RITGRCtAARBPJLPJRZxB?VEX_#vAAR?RDNHJJBh#UnBm#h#IRDRJNNJPNbBFRJLLBLCzAmAd#Uf#Gf#?P#PFJNHPFTH`BDTHNJJJ#LG`#m#^YPER#RDPHNNJRLn#HRLN^VNPHTFX#\\UlDFb#FHh#NP#HKPsB?}ASkCQ{#[y#q#}#cA{#KOCQDa#t#{CFGJCf#Nl#ZtA~#r#p#`#h#rAxBd#rA\\fARdAPjANrB?f#AtBCd#QfBkAjJOlBChA?rBFrBNlBdAfKFzAC~#Iz#Mz#Sv#s#jBmAxBi#hAWt#Sv#Qx#O`BA`#?dAPfBVpAd#`BfBlFf#fBdA~Cr#pAz#fApBhBjAt#H?IL?FBFJLx#^lHvDvh#~XnElCbAd#pGhDbAb#nAr#`Ad#`GhDnBbAxCbBrWhNJJDPARGP_#t#Qh#]pAUtAoA`Ny#jJApBBNFLJFJBv#Hb#HBF?\\"];
init();
</script>
</body>
</html>
The example is using an encoded google polyline and this path is likely to change - I'd like to force any path to fit within the confines of a 500px square - centred.
Any pointers on how I can do this? I know SVG is a bit tricky with absolute positioning.
This is vanilla SVG but I would consider using some js libraries if they can handle positioning better (any suggestions?)
EDIT: Adding a mockup of what I'm trying to do
From what I understand, you want to have a 624*800px SVG, and in its middle an area of 500*500px in which to draw your path. To do that you can place a nested SVG of that size right where you want it. As you know the size of the outer SVG, you can easily compute the position by hand. In the markup, leave off the viewBox.
For the outer SVG you has some errors in xour code: the attribute name is viewBox, and for the values, width coems before height:
viewBox="0 0 624 800"
Later, when adding the path to that inner <svg> element (no <g> required), set its viewBox using the values from the path properties. That's all.
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(svg, props) {
path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
path.setAttribute('d', props.path);
svg.setAttribute('viewBox', [props.x, props.y, props.width, props.height].join(' '));
svg.appendChild(path);
}
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.querySelector('#svg svg'), svgProps)
}
//array with encoded paths, will be decoded later
var paths = ["ki{eFvqfiVqAWQIGEEKAYJgBVqDJ{BHa#jAkNJw#Pw#V{APs#^aABQAOEQGKoJ_FuJkFqAo#{A}#sH{DiAs#Q]?WVy#`#oBt#_CB]KYMMkB{AQEI#WT{BlE{#zAQPI#ICsCqA_BcAeCmAaFmCqIoEcLeG}KcG}A}#cDaBiDsByAkAuBqBi#y#_#o#o#kB}BgIoA_EUkAMcACa#BeBBq#LaAJe#b#uA`#_AdBcD`#iAPq#RgALqAB{#EqAyAoOCy#AmCBmANqBLqAZkB\\iCPiBJwCCsASiCq#iD]eA]y#[i#w#mAa#i#k#g#kAw#i#Ya#Q]EWFMLa#~BYpAFNpA`Aj#n#X`#V`AHh#JfB#xAMvAGZGHIDIAWOEQNcC#sACYK[MSOMe#QKKKYOs#UYQISCQ?Q#WNo#r#OHGAGCKOQ_BU}#MQGG]Io##c#FYNg#d#s#d#ODQAMOMaASs#_#a#SESAQDqBn#a#RO?KK?UBU\\kA#Y?WMo#Iy#GWQ_#WSSGg#AkABQB_Ap#_A^o#b#Q#o#IS#OHi#n#OFS?OI}#iAQMQGQC}#DOIIUK{#IUOMyBo#kASOKIQCa#L[|AgATWN[He#?QKw#FOPCh#Fx#l#TDLELKl#aAHIJEX#r#ZTDV#LENQVg#RkA#c#MeA?WFOPMf#Ej#Fj##LGHKDM?_#_#iC?a#HKRIl#NT?FCHMFW?YEYGWQa#GYBiAIq#Gq#L_BHSHK|#WJETSLQZs#z#_A~#uA^U`#G\\CRB\\Tl#p#Th#JZ^bB`#lAHLXVLDP?LGFSKiDBo#d#wBVi#R]VYVE\\#`#Lh#Fh#CzAk#RSDQA]GYe#eAGWSiBAWBWBIJORK`#KPOPSTg#h#}Ad#o#F[E_#EGMKUGmAEYGMIMYKs#?a#J}##_BD_#HQJMx#e#LKHKHWAo#UoAAWFmAH}#?w#C[YwAAc#HSNM|Ao#rA}#zAq#`#a#j#eAxAuBXQj#MXSR[b#gAFg#?YISOGaAHi#Xw#v#_#d#WRSFqARUHQJc#d#m#`A[VSFUBcAEU#WFULUPa#v#Y~#UrBc#dBI~#?l#P~ABt#N`HEjA]zAEp##p#TrBCl#CTQb#k#dAg#jAU^KJYLK#k#A[Js#d#a#b#]RgBl#[FMAw#[]G]?m#D_#F]P[Vu#t#[TMF_#Do#E_##q#P]PWZUZw#vAkAlAGJOj#IlAMd#OR{#p#a#d#sBpD]v#a#`Aa#n#]TODgBVk#Pe#^cBfBc#Rs#La#RSPm#|#wCpDS^Wp#QZML{#l#qBbCYd#k#lAIVCZBZNTr#`#RRHZANIZQPKDW#e#CaASU?I#YTKRQx##\\VmALYRQLCL?v#P|#D\\GJEFKDM#OCa#COOYIGm#YMUCM#]JYr#uAx#kAt#}#jAeAPWbAkBj#s#bAiAz#oAj#m#VQlAc#VQ~#aA`Au#p#Q`AIv#MZORUV_#p#iB|AoCh#q#dAaANUNWH[N{AJ[^m#t#_Av#wA\\a#`#W`#In#Al#B^E`#Wl#u#\\[VQ\\K`#Eb#?R#dAZP#d#CRExAs#\\Yt#{#LG\\MjAATINOXo#d#kAl#_AHYBOCe#QiBCm#Fq#\\wADo#AyGEeBWuB#YHu#Tu#Lk#VcCTo#d#aA\\WJE`#G~#FP?VI\\U~#sANO`#SfAMj#U\\WjAsAXS`#UNENALBHFFL?^Ml#Uj#]b#q#RUJSPkChEc#XcAb#sA|#]PaA\\OJKNER?TDTNj#Jn#?p#OfC#ZR`B#VCV_#n#{#l#WbACv#OlABnAPl#LNNHbBBNBLFFJ#^GLg#x#i#|AMP[X}#XOJKPET?l#LhAFXp#fBDRCd#S\\_#Ps#PQ#}A]S?QDe#V]b#MR[fAKt#ErAF~CANILYDKGIKe#{#Yy#e#sB[gA[c#e#YUCU?WBUHUNQPq#`AiArAMV[^e#Zc#JQJKNMz#?r#Bb#PfAAfA#VVbADn#E`#KHSEe#SMAKDKFM\\^dDCh#m#LoAQ_##MFOZLfBEl#QbASd#KLQBOAaAc#QAQ#QHc#v#ONMJOBOCg#c#]O[EMBKFGL?RHv#ARERGNe#h#{#h#WVGNDt#JLNFPFz#LdBf#f#PJNHPF`ADPJJJDl#I`#B^Tp#bALJNDNALIf#i#PGPCt#DNE`#Uv#[dAw#RITGRCtAARBPJLPJRZxB?VEX_#vAAR?RDNHJJBh#UnBm#h#IRDRJNNJPNbBFRJLLBLCzAmAd#Uf#Gf#?P#PFJNHPFTH`BDTHNJJJ#LG`#m#^YPER#RDPHNNJRLn#HRLN^VNPHTFX#\\UlDFb#FHh#NP#HKPsB?}ASkCQ{#[y#q#}#cA{#KOCQDa#t#{CFGJCf#Nl#ZtA~#r#p#`#h#rAxBd#rA\\fARdAPjANrB?f#AtBCd#QfBkAjJOlBChA?rBFrBNlBdAfKFzAC~#Iz#Mz#Sv#s#jBmAxBi#hAWt#Sv#Qx#O`BA`#?dAPfBVpAd#`BfBlFf#fBdA~Cr#pAz#fApBhBjAt#H?IL?FBFJLx#^lHvDvh#~XnElCbAd#pGhDbAb#nAr#`Ad#`GhDnBbAxCbBrWhNJJDPARGP_#t#Qh#]pAUtAoA`Ny#jJApBBNFLJFJBv#Hb#HBF?\\"];
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="800" width="624" viewBox="0 0 624 800" preserveAspectRatio="xMinYMin meet">
<svg x="62" y="150" width="500" height="500"></svg>
</svg>
I'm currently working on a SVG "pie" which would allow users to have an overview of all categories of the website in a coloured wheel.
For now i'm able to draw the pie with eventlistener on differents shapes.
var svg = document.getElementById('categSVG');
var startAngle = -89.999;
var nbCateg = 6;
var svgMiddleX = svg.width / 2;
var svgMiddleY = svg.height / 2;
var segmentWidth = 100;
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle) {
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
];
return d.join(" ");
}
function pathGen(nSegment) {
var newPath;
for (var i = 1; i <= nSegment; i++) {
//Adding path
newPath = document.createElementNS('http://www.w3.org/2000/svg', "path");
newPath.setAttribute("id", "arc" + i);
newPath.setAttribute("stroke", '#' + (Math.random() * 0xFFFFFF << 0).toString(16));
newPath.setAttribute("d", describeArc(150, 150, 100, startAngle, 270));
newPath.setAttribute("stroke-width", segmentWidth);
svg.appendChild(newPath);
startAngle += 360 / nbCateg;
}
}
function init() {
pathGen(nbCateg);
var elem;
for (var i = 1; i <= nbCateg; i++) {
document.getElementById("arc" + i).addEventListener("click", function() {
console.log('I would redirect to ' + this.id); //will change
});
}
}
init();
svg {
height: 1000px;
width: 1000px;
}
path {
fill: none;
}
<svg id="categSVG"></svg>
But i can't find a way to display text (html ID in this case) in the middle of the pie part.
So that's my question, how can I calculate X and Y coords to place a text in the middle of each shape ?
To note that number of parts can vary.
Here's my fiddle
(I'm using random colors for tests)
Thank you.
After many tries, I finally found a solution :
Firstly, I included svg.js :
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.4/svg.js"></script>
After that, I added this after "svg.appendChild(newPath)" :
bbox = SVG.adopt(newPath).bbox();
x=bbox.cx;
y=bbox.cy;
text.setAttributeNS(null,"x",""+x);
text.setAttributeNS(null,"y",""+y);
text.setAttributeNS(null,"font-size","30");
svg.appendChild(text);
This solution uses a BBOX to calculate center x and y of a path entity, after that you can easily param your text (don't forget to append a Node to set the text).
Hope it will help someone.