Rotation & Scale in Matrices of SVG Element - javascript

I am helping someone solve this. We have SVG elements in g tag. We are using the transformation on the g tag and converting it into matrix.
Here is the jsbin link.
We are a bit confused by the following:
How to calculate the 'resistor', denoted by dx & dy in line 23 & 24 in the jsbin.
Where we are going wrong with the calculations for rotate & scale using matrices.
Is there a better way of doing this mathematically? I need to mention that the relevant person has been struggling with this for over two weeks so appreciate the help.
The relevant Js code:
var mouseXY; //object to store the mouse event x & y
var angle1; //first angle before rotate
var center; //center of the shape that we want to rotate
//this function is call when mouse down
//on the resize button
//this function bind the mouse event
//to scale the shape
function bindScaleShapeEvent(){
mouseXYPosition(event);
document.addEventListener("mousemove",scaleImage);
document.addEventListener("mouseup",removeScaleImage);
}
//function to scale the shape when
//user mouse move
function scaleImage(event){
var parent = document.getElementById("1"); //parent of shape
var dx = (event.pageX - mouseXY.x);
var dy = (event.pageY - mouseXY.y);
dx /= 80; // 80 is the resistor value
dy /= 80;
var scaleX = dx;
var scaleY = dy;
var matrix = parent.getAttribute("transform");
var matrixArray = matrixToArray(matrix);
//make the new matrix of the shape
scaleString = "matrix(" +
(parseFloat(matrixArray[0]) + scaleX) + " " +
parseFloat(matrixArray[1]) + " " +
parseFloat(matrixArray[2]) + " " +
(parseFloat(matrixArray[3]) + scaleY) + " " +
parseFloat(matrixArray[4]) + " " +
parseFloat(matrixArray[5]) +")";
parent.setAttribute("transform", scaleString);
mouseXYPosition(event);
}
//convert the transformation to array
function matrixToArray(matrix){
matrix = matrix.split('(');
matrix = matrix[1].split(')');
matrix = matrix[0].split(' ');
return matrix;
}
//remove eventListener from the
//shape resize button
function removeScaleImage(){
document.removeEventListener("mousemove",scaleImage);
document.removeEventListener("mouseup",removeScaleImage);
}
//this function is call when mouse down
//on the rotate button
//this function bind the mouse event
//to rotate the shape
function bindRotateShapeEvent(event){
mouseXYPosition(event);
centerXY();//store the center of the shape that we want to rotate
var dx = (mouseXY.x) *3;
var dy = (mouseXY.y) *3;
angle1 = (180 * Math.atan2(dy, dx) / Math.PI); //first angle of the shape
document.addEventListener("mousemove",rotateImage);
document.addEventListener("mouseup",removeRotateImage);
}
//applying rotation on the shape
function rotateImage(event){
var dx = (event.pageX - mouseXY.x);
var dy = (event.pageY - mouseXY.y);
var parent = document.getElementById("1");
var angle2 = (180 * (Math.atan2(dy,dx)) / Math.PI);
var angle = angle2 - angle1;
var radian= 0.06 * angle;
var cos = Math.cos(radian);
var sin = Math.sin(radian);
var matrix ='matrix(' +
(cos) +
' ' + (sin) +
' ' + (-sin) +
' ' + (cos) +
' ' + (-center.cx * cos + center.cy * sin + center.cx) +
' ' + (-center.cx * sin - center.cy * cos + center.cy) +
')';
parent.setAttribute("transform",matrix);
}
function removeRotateImage(){
document.removeEventListener("mousemove",rotateImage);
document.removeEventListener("mouseup",removeRotateImage);
}
//save the center of the shape
function centerXY(){
var shape = document.getElementById("1");
var bbox = shape.getBBox();
center ={
cx: bbox.x + (bbox.width / 2),
cy: bbox.y + (bbox.height / 2)
};
}
//save the move x and y
function mouseXYPosition(event){
mouseXY = {
x: event.pageX,
y: event.pageY
};
}
The Jsbin link to the working demo.

Related

Javascript SVG controlling X Y based on path direction

I have some javascript code that is generating SVG paths to connect HTML elements on a page. This show which box is connected to another box.
The paths are using markers so that I can have an arrow on the end to show its direction.
Overall, this is working well. I have been trying to add a circle to this setup now that is directly in the center of the starting and ending element.
My goal is that the path stops just short of this circle so that the arrow is pointing to it, not on top of it.
Example Screenshot:
Code Sample:
//helper functions, it turned out chrome doesn't support Math.sgn()
function signum(x) {
return (x < 0) ? -1 : 1;
}
function absolute(x) {
return (x < 0) ? -x : x;
}
/**
* Get the offsets of page elements
* #param el
*/
function getOffset(el) {
const rect = el.getBoundingClientRect();
return {
left: rect.left + window.pageXOffset,
top: rect.top + window.pageYOffset,
bottom: rect.bottom - window.pageYOffset,
width: rect.width || el.offsetWidth,
height: rect.height || el.offsetHeight
};
}
/**
* Draw the path on the SVG using proided coords
* #param svg
* #param path
* #param startX
* #param startY
* #param endX
* #param endY
*/
function drawPath(svg, path, startX, startY, endX, endY, circle) {
// Get the path's stroke width (if one wanted to be really precize, one could use half the stroke size)
const style = getComputedStyle(path);
const stroke = parseFloat(style.strokeWidth);
// Check if the svg is big enough to draw the path, if not, set height/width
if (svg.getAttribute("height") < startY) {
svg.setAttribute("height", startY + 20);
}
if (svg.getAttribute("width") < startX + stroke) {
svg.setAttribute("width", startX + stroke + 20);
}
if (svg.getAttribute("width") < endX + stroke) {
svg.setAttribute("width", endX + stroke + 20);
}
/**
M = moveto
L = lineto
H = horizontal lineto
V = vertical lineto
C = curveto
S = smooth curveto
Q = quadratic Bézier curve
T = smooth quadratic Bézier curveto
A = elliptical Arc
Z = closepath
*/
// Straight line from XY Start to XY End
path.setAttribute(
"d",
"M" + startX + " " + startY + " L" + endX + " " + endY
);
// Show the starting and ending circle
if (circle) {
circle.setAttribute("cx", startX);
circle.setAttribute("cy", startY);
circle.setAttribute("cx", endX);
circle.setAttribute("cy", endY);
}
}
/**
* Calculate the coords for where the line will be drawn
* #param svg
* #param path
* #param startElem
* #param endElem
* #param type
*/
function connectElements(svg, path, startElem, endElem, circle) {
// Define our container
const svgContainer = document.getElementById("svgContainer"),
svgTop = getOffset(svgContainer).top,
svgLeft = getOffset(svgContainer).left,
startCoord = startElem,
endCoord = endElem;
let startX, startY, endX, endY;
// Calculate path's start (x,y) coords
// We want the x coordinate to visually result in the element's mid point
startX =
getOffset(startCoord).left +
getOffset(startCoord).width / 2 -
svgLeft;
startY =
getOffset(startCoord).top +
getOffset(startCoord).height / 2 -
svgTop;
// Calculate path's start (x,y) coords
// We want the x coordinate to visually result in the element's mid point
endX =
getOffset(endCoord).left +
0.5 * getOffset(endCoord).width -
svgLeft;
endY =
getOffset(endCoord).top +
getOffset(endCoord).height / 2 -
svgTop;
// Call function for drawing the path
drawPath(svg, path, startX, startY, endX, endY, circle);
}
function connectAll() {
// Loop over our destinations
for (let i = 0; i < dest.length; i++) {
// Define
const marker = document.createElementNS(
"http://www.w3.org/2000/svg",
"marker"
);
const path = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
const markerPath = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
const defs = document.createElementNS(
"http://www.w3.org/2000/svg",
"defs"
);
const circle = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle"
);
// Set definitions attribute
defs.setAttribute("id", "defs");
// Create our center circle
circle.setAttribute("id", "circle_" + dest[i].linkID + "_" + dest[i].boxID);
circle.setAttribute("cx", "0");
circle.setAttribute("cy", "0");
circle.setAttribute("r", "15");
circle.setAttribute("fill", "red");
// Append our circle
document.getElementById('svg1').appendChild(circle);
// Set up marker (Arrow)
marker.setAttribute("id", "Triangle");
marker.setAttribute("viewBox", "0 0 10 10");
marker.setAttribute("refX", "0");
marker.setAttribute("refY", "5");
marker.setAttribute("markerUnits", "strokeWidth");
marker.setAttribute("markerWidth", "4");
marker.setAttribute("markerHeight", "3");
marker.setAttribute("orient", "auto");
// Append our marker (Arrow)
marker.appendChild(markerPath);
markerPath.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
markerPath.setAttribute("fill", "#428bca");
// Create our main path
path.setAttribute(
"id",
"path_" + dest[i].linkID + "_" + dest[i].boxID
);
path.setAttribute("class", "path");
path.setAttribute(
"marker-end",
"url(" + window.location + "#Triangle)"
);
// Only create one set of definitions
if (i === 0) {
document.getElementById('svg1').appendChild(defs);
document.getElementById('defs').appendChild(marker);
}
// Append our path to the SVG
document.getElementById('svg1').appendChild(path);
const svg = document.getElementById("svg1"),
p = document.getElementById(
"path_" + dest[i].linkID + "_" + dest[i].boxID
),
startingBox = document.getElementById("box_" + dest[i].boxID),
destinationBox = document.getElementById(
"box_" + dest[i].destinationBoxID
);
// Connect paths
connectElements(
svg,
p,
startingBox,
destinationBox,
circle
);
}
}
// Define our boxes to connect
var dest = [{
"boxID": "16",
"destinationBoxID": "5",
"linkID": "1"
},
{
"boxID": "18",
"destinationBoxID": "1",
"linkID": "9"
},
{
"boxID": "2",
"destinationBoxID": "5",
"linkID": "8"
}
]
// Run
connectAll()
My Attempt:
One of the things I tried was adjusting the final X coordinate for the path like so:
// Straight line from XY Start to XY End
path.setAttribute(
"d",
"M" + startX + " " + startY + " L" + (endX-30) + " " + endY
);
By telling it to end 30 short, I expected the arrow to not quite go all the way to the mid-point.
Well, this works just fine for horizontal lines but when I have a diagonal, it is putting it -30 to the left of the mid-point as its told to do, when really it needs to account for the direction and adjust the Y as well.
The horizontal line is fine with the -30 but the diagonal one is not where I want it to be (understanding its where I TOLD it to be).
Here is an example of where I would expect to see the diagonal line ending for the top left box:
How can I go about adjusting the XY based on the direction of the path or is there more to it than that?
JS Fiddle: http://jsfiddle.net/m4fupk7g/5/
You need to convert your lines to angles and distances, reduce the distances and then convert them back. For example:
let dx = endX - startX;
let dy = endY - startY;
let angle = Math.atan2(dy, dx);
let distance = Math.sqrt(dx * dx + dy * dy) - 20;
let offsetX = Math.cos(angle) * distance + startX;
let offsetY = Math.sin(angle) * distance + startY;

How to draw multiple self-edges in a node-link diagram in D3

Drawing a single self-link on a node in a node-link diagram can be done as described here: D3 Force Layout Self-Linking Node
What would you change if you need to draw multiple links on the same node?
I tried to add a 'rotation' to it based on the number of self-links that exist.
Given the code from the linked example I made the following changes:
function tick() {
link.attr("d", function(d) {
var x1 = d.source.x,
y1 = d.source.y,
x2 = d.target.x,
y2 = d.target.y,
dx = x2 - x1,
dy = y2 - y1,
dr = Math.sqrt(dx * dx + dy * dy),
// Defaults for normal edge.
drx = dr,
dry = dr,
xRotation = 0, // degrees
largeArc = 0, // 1 or 0
sweep = 1; // 1 or 0
// Self edge.
if ( x1 === x2 && y1 === y2 ) {
// Fiddle with this angle to get loop oriented.
var index = getIndexOfDuplicateEdge();
var degree = 360 / numberOfDuplicateEdges();
var degreeForIndex = degree * index;
xRotation = degreeForIndex; // Previously: -45;
// Needs to be 1.
largeArc = 1;
// Change sweep to change orientation of loop.
//sweep = 0; // I also tried to change it based on index % 2
// Make drx and dry different to get an ellipse
// instead of a circle.
drx = 30;
dry = 20;
// For whatever reason the arc collapses to a point if the beginning
// and ending points of the arc are the same, so kludge it.
x2 = x2 + 1;
y2 = y2 + 1;
}
return "M" + x1 + "," + y1 + "A" + drx + "," + dry + " " + xRotation + "," + largeArc + "," + sweep + " " + x2 + "," + y2;
});
This won't draw my ellipses as expected and I cannot find a way to handle this. Based on SVG from Mozilla the large-arc has to be 1. Sweep can be 0 or 1 and will 'mirror' my ellipsis. I can use xRotation between 90-180 with sweep 0/1 which will cover 180 degrees of my circle. However, i do not find a way to draw the ellipsis at the other 180 degree positions.
The number of self-links can vary, and I always want to have the 'best' distribution between ellipsis.
Ideally, it should look like:
The idea is to divide the circle into as many segments as petals your flower has. Then calculate the start- and end points for each petal on the circle and fitting an elipse on those points.
You can use the following code snippet to do achieve this: (the function assumes you have a svg element with the id "svgthing")
function radtodeg(angle) {
return angle * (180/Math.PI);
}
function flower( center_x, center_y, num_self_edges, start_angle, end_angle, radius, length ) {
var angle_sector = end_angle - start_angle;
var num_points = num_self_edges * 2;
var angle_per_point = angle_sector / num_points;
var angle_per_sector = angle_per_point * 2;
var str_builder = [];
for( var angle = start_angle; angle < end_angle; angle += angle_per_sector ) {
var start_sector_angle = angle;
var end_sector_angle = angle + angle_per_point;
var mid_sector_angle = angle + angle_per_point / 2;
var start_x = center_x + (radius * Math.cos(start_sector_angle));
var start_y = center_y + (radius * Math.sin(start_sector_angle));
var end_x = center_x + (radius * Math.cos(end_sector_angle));
var end_y = center_y + (radius * Math.sin(end_sector_angle));
var mid_x = center_x + (radius * Math.cos(mid_sector_angle));
var mid_y = center_y + (radius * Math.sin(mid_sector_angle));
str_builder.push("<path d='");
str_builder.push("M" + start_x + " " + start_y + ",");
str_builder.push("A " + length + " 1 " + radtodeg(mid_sector_angle) + " 0 1 " + end_x + " " + end_y);
str_builder.push("'/>\n");
str_builder.push("<circle cx='" + start_x + "' cy='" + start_y + "' r='5' />\n");
str_builder.push("<circle cx='" + end_x + "' cy='" + end_y + "' r='5'/>\n");
str_builder.push("<circle cx='" + mid_x + "' cy='" + mid_y + "' r='5'/>\n");
}
str_builder.push("<circle cx='" + center_x + "' cy='" + center_y + "' r='" + radius + "' />\n");
$("#svgthing").html(str_builder.join(""));
}
flower(60, 50, 8, 0, 2 * Math.PI, 50, 10);
The example call will generate a flower with 8 petals.

Raphael JS sweeping clock

Using Raphael JS I want to have a continuing sweeping clock.
I have kind of got it implemented but there is a problem.
The transform on the animation constantly requries a increasing integer to rotate. Eventually I'm going to hit a max integer.
If I use 0,90,180,270. When it hits 0 again it goes back the other way.
CodePen ->
http://codepen.io/ianw92/pen/yNLdZz
Javascript:
var svg = Raphael("container",400,400),
triangle = svg.path("M210 200L190 200L200 100Z").attr({fill:"#000"}),
circle = svg.circle(200,200,5).attr({fill:"#f00"});
// Rotation settings
var handAngle = 45,
centerX = 200,
centerY = 200;
y = 0
function a() {
y = (y + 90) % 360;
triangle.animate({transform: "r"+y + "," + centerX + "," + centerY}, 500, b);
}
function b() {
y = (y + 90) % 360;
triangle.animate({transform: "r"+y + "," + centerX + "," + centerY}, 500, a);
}
a()
Key was to use Raphael.animation(...).repeat(Infinity)
http://codepen.io/ianw92/pen/xGbKJq
var anim=Raphael.animation({transform: "r360" + "," + centerX + "," + centerY}, 60000, 'linear').repeat(Infinity);
triangle.animate(anim);

How to convert the arc chart in to bar chart, build with SVG path and Javascript

I am new to SVG and I need to convert the arc chart to bar chart. following is a Javascript code and jsfiddle link of Arc chart.
function setArc(arc, percent) {
var angle = 75;
var radius = 50;
var path = "M200,200";
for(var i = 0; i <= percent; i++) {
angle -=3.6;
angle %= 360;
var radians= (angle/180) * Math.PI;
var x = 100 + Math.cos(radians) * -1 * radius;
var y = 100 + Math.sin(radians) * radius;
if(i==0) {
path += ' M ' + x + ' ' + y;
}
else {
path += ' L ' + x + ' ' + y;
}
}
arc.setAttribute('d', path);
}
JSFIDDLE
Thanks
In general, it should be enough to delete Math.sin and Math.cos. It will get you a straight diagonal line.
Then choose one from the following:
for vertical bar change the calculation of var x to var x = 0
for horizontal bar change the calculation of var y to var y = 0
Here is simplified your function for vertical bar: JSfiddle

A relative rotation in an animation

I have a problem with Raphael.js. I want to rotate the "compassScale" - set in the following code - in a relative manner.
This works for the paths, but all the texts "animate" to the absolute rotation of 30 degree. I want them to rotate to the 30 degrees relative from their actual positions.
var compassScale = paper.set();
var centerX = 200;
var centerY = 200;
var radius = 195;
var compasCircle = paper.circle(centerX, centerY, radius);
for(var i = 0; i < 360; i++) {
var winkelRad = i * (Math.PI/180)
var xStart = centerX + Math.sin(winkelRad) * radius;
var yStart = centerY + Math.cos(winkelRad) * radius;
var diff = 6;
if(i % 10 === 0){
compassScale.push(paper.text(centerX, centerY - radius + 18, i).rotate(i, centerX, centerY, true));
diff = 12;
} else if(i % 5 === 0) {
diff = 8;
}
var xEnd = centerX + Math.sin(winkelRad) * (radius - diff);
var yEnd = centerY + Math.cos(winkelRad) * (radius - diff);
compassScale.push(paper.path("M" + xStart + " " + yStart + " L" + xEnd + " " + yEnd));
}
compassScale.animate({rotation:"30 " + centerX + " " + centerY}, 5000);
Like you said, the problem is that you're animating all elements to 30 degrees, not their current rotation + 30 degrees. It's actually quite simple once you think of it that way. Here is your revised code that works:
var compassScale = paper.set();
var texts = []; // array to hold the text elements
var centerX = 200;
var centerY = 200;
var radius = 195;
var compasCircle = paper.circle(centerX, centerY, radius);
for(var i = 0; i < 360; i++) {
var winkelRad = i * (Math.PI/180)
var xStart = centerX + Math.sin(winkelRad) * radius;
var yStart = centerY + Math.cos(winkelRad) * radius;
var diff = 6;
if(i % 10 === 0){
texts.push(paper.text(centerX, centerY - radius + 18, i).rotate(i, centerX, centerY, true));
diff = 12;
} else if(i % 5 === 0) {
diff = 8;
}
var xEnd = centerX + Math.sin(winkelRad) * (radius - diff);
var yEnd = centerY + Math.cos(winkelRad) * (radius - diff);
compassScale.push(paper.path("M" + xStart + " " + yStart + " L" + xEnd + " " + yEnd));
}
compassScale.animate({rotation:"30 " + centerX + " " + centerY}, 5000);
// loop through the text elements, adjusting their rotation by adding 30 to their individual rotation
for (var i = 0, l = texts.length; i < l; i += 1) {
// node.attr("rotation") returns something like 50 200 200, so we have to split the string and grab the first number with shift
texts[i].animate({rotation: (30 + +texts[i].attr("rotation").split(" ").shift()) + " " + centerX + " " + centerY}, 5000);
}
Just a quick observation:
Looks like "Rotation" isn't part of the Atrr anymore since ver 2, so you can't use it in "animate", but you can replace that with "transform: "r" + (some degree)"..
eg:
element.animate( {transform: "r" + (-90)}, 2000, 'bounce');

Categories

Resources