Drawing curved SVG arrow lines from div to div - javascript

I want to draw two curved arrow lines using SVG to connect two elements to indicate they go back and forth, like this:
I've read a bit about SVG but I'm not totally sure how to create a line that's vertical.
Second, if SVG takes coordinates, do I have to find the coordinate position of the elements before creating the SVG drawing? Does it have to be re-drawn if the window size is adjusted?

Make an svg element that (invisibly) underlies the entire document. This will hold both arrows. Insert two svg path elements (the arrows) whose start and end coordinates are calculated based on the positions of the div's to be connected, and whose curve is created in whatever way you want based on those start and end coordinates.
For the example below, click on "Run code snippet". Then click and drag either of the div's to see how the arrows are dynamically created, i.e. they move with the divs. jQuery and jQueryUI are used in the code snippet simply to allow the easy draggability of the divs and have nothing to do with the creation and use of the arrows.
This example has two arrows starting and ending at the middle of the divs' sides. The details of the curve are, of course, up to you. The arrow lines are constructed using the d attribute of the svg path. In this example, "M" is the "moveTo" coordinates where the path will start and the "C" points are the first and second control points and final coordinate for a cubic bezier curve. You'll have to look those up to understand what they are, but they are a general way of creating smooth curves in an svg element. The arrowheads are added using an svg <marker> element which you can read about here.
A more complex document would need more care to determine the start and end coordinates of the svg path elements, i.e. the arrows, but this example at least gives you a place to begin.
Answers to your specific questions:
If SVG takes coordinates, do I have to find the coordinate position of the elements before creating the SVG drawing? Yes, as I've done in my code.
Does it have to be re-drawn if the window size is adjusted? Probably yes, depending on what happens to the divs themselves when the window is resized.
var divA = document.querySelector("#a");
var divB = document.querySelector("#b");
var arrowLeft = document.querySelector("#arrowLeft");
var arrowRight = document.querySelector("#arrowRight");
var drawConnector = function() {
var posnALeft = {
x: divA.offsetLeft - 8,
y: divA.offsetTop + divA.offsetHeight / 2
};
var posnARight = {
x: divA.offsetLeft + divA.offsetWidth + 8,
y: divA.offsetTop + divA.offsetHeight / 2
};
var posnBLeft = {
x: divB.offsetLeft - 8,
y: divB.offsetTop + divB.offsetHeight / 2
};
var posnBRight = {
x: divB.offsetLeft + divB.offsetWidth + 8,
y: divB.offsetTop + divB.offsetHeight / 2
};
var dStrLeft =
"M" +
(posnALeft.x ) + "," + (posnALeft.y) + " " +
"C" +
(posnALeft.x - 100) + "," + (posnALeft.y) + " " +
(posnBLeft.x - 100) + "," + (posnBLeft.y) + " " +
(posnBLeft.x ) + "," + (posnBLeft.y);
arrowLeft.setAttribute("d", dStrLeft);
var dStrRight =
"M" +
(posnBRight.x ) + "," + (posnBRight.y) + " " +
"C" +
(posnBRight.x + 100) + "," + (posnBRight.y) + " " +
(posnARight.x + 100) + "," + (posnARight.y) + " " +
(posnARight.x ) + "," + (posnARight.y);
arrowRight.setAttribute("d", dStrRight);
};
$("#a, #b").draggable({
drag: function(event, ui) {
drawConnector();
}
});
setTimeout(drawConnector, 250);
/* The setTimeout delay here is only required to prevent
* the initial appearance of the arrows from being
* incorrect due to the animated expansion of the
* Stack Overflow code snippet results after clicking
* "Run Code Snippet." If this was a simpler website,
* a simple command, i.e. `drawConnector();` would suffice.
*/
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
#instructions {
position: fixed;
left: 50%;
}
#a, #b {
color: white;
text-align: center;
padding: 10px;
position: fixed;
width: 100px;
height: 20px;
left: 100px;
}
#a {
background-color: blue;
top: 20px;
}
#b {
background-color: red;
top: 150px;
}
<p id="instructions">Click and drag either div to see automatic arrow adjustments.</p>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.min.js"></script>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
<defs>
<marker id="arrowhead" viewBox="0 0 10 10" refX="3" refY="5"
markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
</defs>
<g fill="none" stroke="black" stroke-width="2" marker-end="url(#arrowhead)">
<path id="arrowLeft"/>
<path id="arrowRight"/>
</g>
</svg>
<div id="a">Div 1</div>
<div id="b">Div 2</div>

I found Andrew Willems's answer very useful. I've modified it to make a library, draw_arrow.js , which exports a function draw_arrow( sel1, locs1, sel2, locs2, arr ). This draws an arrow from the element identified by CSS selector sel1 to that identified by sel2. locs1 and locs2 indicate where the arrow should start or end on the element. arr identifies an SVG path to hold the arrow.
You can download this, and see two demos, from the links at the end of http://www.chromophilia.uk/blog/dress-reform-architecture-and-modernism/ . I needed the arrows to depict the relationships between various topics related to Modernism, as part of an animation. That's what drove me to find and adapt Andrew's code. NOTE: that link isn't currently working, because of some problem with the WordPress database, which I'll have to fix. The arrows library and a library for successively displaying HTML elements, plus demos, can be got via the links in my comment to Henry Mont below.
Here's a suggested improvement. I originally wrote this up as a new, additional, answer, but several commenters have execrated that, so I'll have to put it here and hope it gets noticed. I'm pursuing this because modularity is important. A routine such as draw_arrow should require its user to do as little as possible to the code around it. But at the moment, it needs the user to create one <path> element inside the <svg> for each arrow to be drawn, and to invent IDs for the paths. I suggest it would be better for draw_arrow to do this, by updating the DOM tree. Comments in favour or against?

We finally have it! Take a look at this:
https://www.npmjs.com/package/arrows-svg
there is also a React version:
https://www.npmjs.com/package/react-arrows
So if you have two divs with let's say ids named: from and to according to divs from your example, then you do:
import arrowCreate, { DIRECTION } from 'arrows'
const arrow = arrowCreate({
className: 'arrow',
from: {
direction: DIRECTION.LEFT,
node: document.getElementById('from'),
translation: [-0.5, -1],
},
to: {
direction: DIRECTION.LEFT,
node: document.getElementById('to'),
translation: [0.9, 1],
},
})
/*
- arrow.node is HTMLElement
- arrow.timer is idInterval from setInterval()
REMEMBER about clearInterval(node.timer) after unmount
*/
document.body.appendChild(arrow.node);
and of course some css:
.arrow {
pointer-events: none;
}
.arrow__path {
stroke: #000;
fill: transparent;
stroke-dasharray: 4 2;
}
.arrow__head line {
stroke: #000;
stroke-width: 1px;
}
Tested and it works!

Related

How to draw lots of lines in html? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 6 years ago.
Improve this question
I have lots of images in my page and I am looking for a way to draw a line that will connect one image to the other ( it doesn't have to be an arrow, just a normal line. ).For example, let us consider ($) as an image:
$
$
Now how can I connect those 2 images ($) with a line?
Thanks!
Since you seem to be asking about basic JavaScript, HTML, and CSS, here's a simple method using only those. It's nice to understand the math and theory behind doing these kinds of graphical calculations instead of entirely relying on libraries.
Use a HTML div as a line by calculating the distance and angle between two images.
// Get the position of the first image
var imgOnePosition = document.getElementById("one").getBoundingClientRect();
// Get the position of the second image
var imgTwoPosition = document.getElementById("two").getBoundingClientRect();
// Calculate the angle between the two images' positions.
// Math.atan2() returns a value in radians so convert it to degrees as well
var angle = Math.atan2(imgOnePosition.top - imgTwoPosition.top, imgOnePosition.left - imgTwoPosition.left) * (180 / Math.PI);
// Calculate the distance, hopefully you remember this from basic algebra :)
var distance = Math.sqrt(Math.pow(imgOnePosition.top - imgTwoPosition.top, 2) + Math.pow(imgOnePosition.left - imgTwoPosition.left, 2));
// Create a new DIV to represent our line
var line = document.createElement("div");
// Now we style it
line.style.position = "absolute"; // so that we can change left and top
line.style.width = distance + "px";
line.style.height = "2px";
line.style.left = "50%"; // Center the element in its parent
line.style.top = "50%"; // Center the element in its parent
line.style.background = "#000";
line.style.transformOrigin = "0% 50%"; // Rotate around one edge instead of the middle
line.style.transform = "rotate(" + (angle) + "deg)";
// Add the line to the SECOND image's parent element.
// It's the 2nd image instead of 1st because of the order we did the math in calculating the angle
document.getElementById("two").appendChild(line);
body, img {
margin: 0;
padding: 0;
display: block;
}
#container {
position: relative;
background: #ddd;
width: 320px;
height: 240px;
}
.img-container {
position: absolute;
}
<div id="container">
<div id="one" class="img-container" style="left: 50px; top: 100px;" >
<img src="http://imgur.com/8B1rYNY.png" />
</div>
<div id="two" class="img-container" style="left: 150px; top: 190px;" >
<img src="http://imgur.com/8w6LAV6.png" />
</div>
</div>
If you want the line to appear behind the images instead of in front, you could modify their z-index values so they're ordered properly.
Edit: The above works if the images are the exact same size. If they are different sizes, calculate the center point of the images and use that instead of just the top left corner of the getBoundingClientRect().
// Get the position of the first image
var imgOneRect = document.getElementById("one").getBoundingClientRect();
var imgOnePosition = {
left: (imgOneRect.left + imgOneRect.right) / 2,
top: (imgOneRect.top + imgOneRect.bottom) / 2
}
// Get the position of the second image
var imgTwoRect = document.getElementById("two").getBoundingClientRect();
var imgTwoPosition = {
left: (imgTwoRect.left + imgTwoRect.right) / 2,
top: (imgTwoRect.top + imgTwoRect.bottom) / 2
}
div tag: with a background-color, width, height, transform: rotate(50deg) and well positioning properties
SVG tag
PNG image
Canvas

Getting SVG container size in snapSVG?

What is the correct way to get the size of the 'paper' object with SnapSVG, as soon as it has been created?
My HTML looks something as follows:
<div id="myContainer" style="width: 900px; height: 100px" />
And then the Javascript code:
function initViewer(elementId) {
var element, elementRef, snap;
elementRef = '#' + elementId;
element = $(elementRef);
element.append('<svg style="width: ' + element.width() + 'px; height: ' + element.height() + 'px; border: solid 1px black;"/>');
snap = Snap(elementRef + ' svg');
console.log(snap.getBBox());
}
What I observe here is the bounding box has '0' for all attributes, so I can't rely on the bounding box values here. Are there any ways of doing this, without have to go to a parent element?
What I am essentially wanting form all this is the width and the height of the SVG, so I can draw the shapes of the appropriate size for the view.
JS Fiddle, illustrating the issue: https://jsfiddle.net/ajmas/kdnx2eyf/1/
getBBox() on a canvas returns the bounding box that contains all elements on that canvas. Since there are no elements in your SVG, it returns all 0s.
But the SVG element is just like any other DOM element - you could get its width and height in a number of different ways. You could retrieve the object by ID or whatever and use .offsetWidth or .offsetHeight.
You could also traverse the object itself. I have no idea if this works on all browsers but if you really want to use the snap object, you could do this:
snap=Snap(elementRef + ' svg');
snap.node.clientHeight
snap.node.clientWidth
But you also just set the height and width of it using the div it is contained in. Why can't you just use element.width() and element.height()?
I find that getBBox() doesn't work on a paper (a Snap "drawing surface"), only on elements in a paper. But node.clientWidth works for me for Snap.svg papers. Demo below.
var paper = Snap("#mySVG");
paper.rect(0, 0, 200, 100).attr({fill : "#cde"});
//var tMessage0 = paper.getBBox().width; // throws an error
var tMessage1 = paper.text(4, 24, "paper width = " + paper.node.clientWidth);
var tMessage2 = paper.text(4, 48, "text width = " + tMessage1.getBBox().width);
<script src="https://cdn.jsdelivr.net/snap.svg/0.1.0/snap.svg-min.js"></script>
<body>
<svg id="mySVG" width="200" height="100">
</svg>
</body>

Click through element but detect hover

I have an element that sits in the middle of my page as a sensor. That element is larger than the area underneath it, which contains links. I need to be able to both register when the mouse is over / moves over the sensor, as well as click the links below.
I've looked on SO, but I can't find a solution that works for my issue given that I need this circular sensor to float in the middle of the page.
#css
.sensor {
pointer-events: none; # does not register jquery's mouseenter, but allows the links to be clicked
}
#javascript
$('.sensor').mouseenter(doStuff) #only works if pointer events are enabled
Here's a fiddle of a basic mockup:
http://jsfiddle.net/3rym41ra/
Thanks in advance.
I placed a circular sensor on the page which changes the background when hovered.
Since the sensor now is a parent of the links, all events will bubble up. You can click on the elements while still using certain areas of the sensor as you like
$('body').mousemove(function(e) {
// We want it circular on page so we take 50% left and 50% top as the middle
// Cirle has radius = 100px
var middle = {
x: $(window).width() / 2,
y: $(window).height() / 2
}; // Our middle-point
var normalize = {
x: middle.x - e.pageX,
y: middle.y - e.pageY
}; // mouse-position relative to the middle
var radius = 100; // radius
// some Math
if (normalize.x * normalize.x + normalize.y * normalize.y < radius * radius) {
// inside circle
$('body').css('background', 'red');
} else {
// outside
$('body').css('background', 'white');
}
})
body,
html {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
One
Two
Three
Four

Why is div's jQuery position() 0,0 after onload but moments later is the real value?

Dang, I figured it out... See below. The div layer I was querying was hidden via CSS then revealed in JS after my position query. Since I never saw it hidden I didn't realize it was, and that's why jQuery returned 0,0.
I feel like an idiot.
The code below was just meant to be illustrative and included all the code I thought was necessary, but it left out a critical CSS definition and a critical JS call:
I've got a div layer with some sub-div layers for menu items.
<div id="menuItems">
<div id="menuItem0">Menu Item 0</div>
<div id="menuItem1">Menu Item 1</div>
</div>
The position and dimensions are defined in an external CSS.
#menuItem0 { top: 0px; left: 100px; height: 40px; width: 200px; background-color: green;}
#menuItem1 { top: 0px; left: 350px; height: 40px; width: 200px; background-color: red;}
And on load the script is supposed to show the positions and dimensions of the menu items:
$(window).load(function () {
prepareMenuItems();
});
function prepareMenuItems() {
var numberOfMenuItems = $("#menuItems").children().length;
for (var i=0; i<numberOfMenuItems; i++) {
console.log("left: "+$("#menuItem" + i).position().left +
" top:" + $("#menuItem" + i).position().top +
" w:" + $("#menuItem" + i).width() +
" h:"+ $("#menuItem" + i).height());
}
}
In my live demo what I see in Google Chrome (latest) is:
left: 0 top: 0 w: 200 h: 40
left: 0 top: 0 w: 200 h: 40
The left and top are ZERO when they should not be.
If I force a break once the page has loaded and had a moment the Chrome debugger will evaluate a watch on $("#menuItem0").position().left properly. But why doesn't it on load? It spits out the right width and height, so the CSS has clearly been loaded. And it knows the answers later since a forced break gives the right answer so this isn't me looking at the wrong thing's position.
Help! It's driving me mad.
(Sorry about the earlier typos I was trying to be illustrative rather than literal and I didn't realize people would test the given code. Magritte did so via Fiddler and the code works as expected, so it must have something to do with external references to the CSS or JS or something...).
I figured it out... I was an idiot... I didn't realize one of the CSS rules was making a parent div hidden at the start and code milliseconds later would make it visible. So at the time jQuery was querying it for position the layer was hidden and jQuery's behavior is (I didn't realize) to return 0 when a div is hidden. Milliseconds later the layer is shown and now on break the jQuery worked as expected. The secret was, of course, to not have that parent div layer hidden at the start.
Thank you to you guys who responded. You guys were awesome to look at it, fix my stupid typos, and force me to look further into it to make a working demo to better prove my point (which led me to the solution).
Seems to work for me, you have a few typos: http://jsfiddle.net/MQfqA/1/
function prepareMenuItems() {
var numberOfMenuItems = $("#menuItems").children().length;
for (var i=0; i<numberOfMenuItems; i++) {
console.log("left: "+$("#menuItem" + i).position().left +
" top:" + $("#menuItem" + i).position().top +
" w:" + $("#menuItem" + i).width() +
" h:"+ $("#menuItem" + i).height());
}
}
your syntax was wrong. This now works
$(document).ready(function () {
prepareMenuItems();
});
function prepareMenuItems() {
var numberOfMenuItems = $("#menuItems").children().length;
for (var i=0; i<numberOfMenuItems; i++) {
console.log("left: "+$("#menuItem" + i).position().left +
" top:"+$("#menuItem" + i).position().top+
" w:"+$("#menuItem" + i).width() +
" h:"+ $("#menuItem" + i).height());
}
}
My impression or did you forget to close a quotation on line 11?

animating a rectangle with text in Raphael javascript library

I am quite new to javascript and to Raphael. I am trying to move a button-like rectangle with text inside. Here is my code :
window.onload = function() {
var paper = new Raphael(document.getElementById('canvas_container'), "100%", "100%");
var box1 = paper.rect(100, 100, 120, 50, 10).attr({fill: 'darkorange', stroke: '#3b4449', 'stroke-width': 2, cursor: 'pointer'});
var box2 = paper.rect(400,100,120,50,10).attr({fill: 'lightblue', stroke: '#3b4449', 'stroke-width': 2});
var text2 = paper.text(box2.attrs.x + box2.attrs.width/2,box2.attrs.y + box2.attrs.height/2,"[x: " + box2.attrs.x + " y: " + box2.attrs.y + "]").attr({"font-family": "arial", "font-size": 16});
var button2 = paper.set();
button2.push(box2);
button2.push(text2);
box1.click(function(){
// this did not work
// button2.animate({x: 100, y: 50 }, 1000, 'bounce', function() { // callback function
// text2.attr('text',"[x: " + box2.attrs.x + " y: " + box2.attrs.y + "]");
// });
button2.animate({transform: "t100,100"}, 1000, 'bounce', function() { // callback function
text2.attr('text',"[x: " + box2.attrs.x + " y: " + box2.attrs.y + "]");
});
});
}
The button2.animate({x: 100, y: 50 }, 1000, 'bounce'); line did not worked properly, the text was not in the right position at the end. By using the transform: I can not use coordinates, I would have to compute them. Also I am not able to get the right coordinates of the blue box at the end when using the transform method.
I was not able to find any answer yet, hope someone can help me.
Thank you
Since you didn't explain how exactly you want to move your button, I'm assuming you want to move the box2 above box1.
There are some misunderstandings and errors in your code, allow me explain one by one.
Why the first way cause text move to wrong position at end ?
Because a set is NOT a group of element which knows its relative position inside the group. A set is merely a collection of elements which is designed for us to operate them in a more convenient way.
So, the code below will move all element in the set to (100, 50)
set.animate({x: 100, y: 50 }, 1000);
and that's why the text is there.
I couldn't find the document, but you can find some explanation here .
Why x, y in attributes seems to be wrong when using transform ?
No, the attribute is correct.
When you transform an element, the result of the transformation will not reflect back to the attributes. You can think like this, when transform(), you are actually attach "transformation" to the elements. Therefore :
paper.circle(100, 100, 5).transform("t100");
You can describe the circle as :
a circle at (100, 100) which will be moved 100px horizontally.
but not - a circle at (200, 100) which will be moved 100px horizontally.
So, here is the code that dose what you want, note that I'm using getBBox() to get coordinate of the button2 set.
box1.click(function(){
var moveX = box1.attr("x") - button2.getBBox().x;
var moveY = (box1.attr("y") - 50) - button2.getBBox().y;
button2.animate({transform: "t" + moveX + "," + moveY}, 1000, 'ease-in-out', function () {
text2.attr('text', "[x: " + button2.getBBox().x + "y: " + button2.getBBox().x + "]");
});
});
Welcome to SO, and suggest you to write a SSCCE next time.
UPDATE
I do not fully understand why the transformation does not reflect back
to the attributes. If I move the circle at the position (100,100)
100px horizontally it will results in a circle at position (200,100).
This is what the bounding box gives me. So why I am not able to get
the coordinates from the circle after the transformation and have to
use the bounding-box method ?
Transform DOSE NOT change the original attribute in the element, because it is something you attach to a element, not function that change a element directly. If you want to know attributes AFTER the transformation applied, you have to use getBBox(), or take a look about matrix.
This is how Raphael.js works. Either you use bounding box function, or extend the Raphael.js by yourself like this
I have changed my previous answer about how I describe transformation a little bit, hope it can help you to understand better this time.
Your code works great but it has the drawback, that you have to
compute the transformation values instead of simply setting the
position. Is there any other way to move a rectangle with text inside
to a position of your choice ?
You can always write helper functions to do these ugly jobs for you anyway, I don't see there's anything wrong with it.

Categories