jQuery - use canvas to draw lines between divs - javascript

I have n <div>s, each with <h1> title and <ul> list of items in.
I would like to float these over a canvas and draw lines from <div id="x"> list item y to <div id="z">. I am using jQuery UI to make the <div>s draggable.
The canvas element is part way down the page (a paragraph of text and some form elements precede it) but I can change this if necessary.
[edit]
I tagged the question with graph, but let me add this link: Graph_(mathematics) :-)

I would make the div's positioning to absolute and then set them where you want. Then get their position with this function:
//Get the absolute position of a DOM object on a page
function findPos(obj) {
var curLeft = curTop = 0;
if (obj.offsetParent) {
do {
curLeft += obj.offsetLeft;
curTop += obj.offsetTop;
} while (obj = obj.offsetParent);
}
return {x:curLeft, y:curTop};
}
When you have their position, add it to half their width/height, and you will have the position of their center on the page. Now find the position of the canvas and substract it from the previously found numbers. If you draw a line between those two points, it should link the two divs. In case it's not clear, here's how I would code it:
var centerX = findPos(document.getElementById('x'));
centerX.x += document.getElementById('x').style.width;
centerX.y += document.getElementById('x').style.height;
var centerZ = findPos(document.getElementById('Z'));
centerZ.x += document.getElementById('z').style.width;
centerZ.y += document.getElementById('z').style.height;
//Now you've got both centers in reference to the page
var canvasPos = findPos(document.getElementById('canvas'));
centerX.x -= canvasPos.x;
centerX.y -= canvasPos.y;
centerZ.x -= canvasPos.x;
centerZ.y -= canvasPos.y;
//Now both points are in reference to the canvas
var ctx = document.getElementById('canvas').getContext('2d');
ctx.beginPath();
ctx.moveTo(centerX.x, centerX.y);
ctx.lineTo(centerZ.x, centerZ.y);
ctx.stroke();
//Now you should have a line between both divs. You should call this code each time the position changes
EDIT
By the way, using the findPos function you can also set the initial position of the divs relatively to the canvas (here at (30; 40)):
var position = {x: 30, y: 40};
var canvasPos = findPos(document.getElementById('canvas'));
var xPos = canvasPos.x + position.x;
var yPos = canvasPos.y + position.y;
document.getElementById('x').style.left = xPos+"px";
document.getElementById('x').style.top = yPos+"px";

Related

Use JavaScript to retroactively treat event as though it were 'pointer-event: none'

I have multiple canvas elements layered on top of each other using absolute positioning. Each canvas has some transparent space within it. When I click on this stack of canvases, I get the pixel value where the click event happened and determine if that pixel is transparent like so:
var context = this.element.getContext('2d');
var rect = this.element.getBoundingClientRect();
var x = Math.round(event.clientX - rect.left);
var y = Math.round(event.clientY - rect.top);
var pixelData = context.getImageData(x, y, 1, 1).data;
if (pixelData[3] === 0) {
console.log('Transparent!')
}
When the pixel is transparent, I'd like to treat this layer as though it had the css property pointer-event: none;, which would cause the click event to pass through to the next layer.
Is there a way to accomplish this?
If you have an array of the canvas elements, you can just click the canvas below the current canvas. Like so:
var canvasElements = new Array();
canvasElements[0] = canvas.element.getContext('2d');
canvasElements[1] = canvas2.element.getContext('2d');
canvasElements[2] = canvas3.element.getContext('2d');
Then when you run your test:
var context = this.element.getContext('2d');
var rect = this.element.getBoundingClientRect();
var x = Math.round(event.clientX - rect.left);
var y = Math.round(event.clientY - rect.top);
for(var i = 0; i < canvasElements.length; i++){
var pixelData = canvasElements[1].getImageData(x, y, 1, 1).data;
if (pixelData[3] === 0) {
console.log('Transparent!');
} else {
break;
//clicked canvas i
}
}

paper.js animate point on graph

I'm trying to build an animated graph with paper.js that can react to different input. So I want to smoothly animate one point vertically to a different point.
I've looked at different examples and the closest ones to mine is this one:
paper.tool.onMouseDown = function(event) {
x = event.event.offsetX;
y = event.event.offsetY;
paper.view.attach('frame', moveSeg);
}
var x;
var y;
function moveSeg(event) {
event.count = 1;
if(event.count <= 100) {
myPath.firstSegment.point._x += (x / 100);
myPath.firstSegment.point._y += (y / 100);
for (var i = 0; i < points - 1; i++) {
var segment = myPath.segments[i];
var nextSegment = segment.next;
var vector = new paper.Point(segment.point.x - nextSegment.point.x,segment.point.y - nextSegment.point.y);
vector.length = length;
nextSegment.point = new paper.Point(segment.point.x - vector.x,segment.point.y - vector.y);
}
myPath.smooth();
}
}
This Code animates one Point to the click position, but I couldn't change it to my needs.
What I need is:
var aim = [120, 100];
var target = aim;
// how many frames does it take to reach a target
var steps = 200;
// Segment I want to move
myPath.segments[3].point.x
And then I dont know how to write the loop that will produce a smooth animation.
example of the graph:
I worked out the answer. The following steps in paperscript:
Generate Path
Set aim for the point
OnFrame Event that does the moving (eased)
for further animations just change the currentAim variable.
var myPath = new Path({
segments: [[0,100],[50,100],[100,100]]});
// styling
myPath.strokeColor = '#c4c4c4'; // red
myPath.strokeWidth = 8;
myPath.strokeJoin = 'round';
myPath.smooth();
// where the middle dot should go
var currentAim = [100,100];
// Speed
var steps = 10;
//Animation
function onFrame(event) {
dX1 = (currentAim[0] - myPath.segments[1].point.x )/steps;
dY1 = (currentAim[1] - myPath.segments[1].point.y )/steps;
myPath.segments[1].point.x += dX1;
myPath.segments[1].point.y += dY1;
}

multiple canvases are not working

I tried to generate multiple canvases on the fly and when I create a new canvas, the previous one disappears. See here for an example:
http://jsfiddle.net/adrianh/5jspv/4/
Here is the javascript code:
var circleCount = 0;
function circleRect(rect)
{
var diameter = Math.sqrt(rect.width*rect.width+rect.height*rect.height);
var cx = (rect.right + rect.left)/2;
var cy = (rect.top + rect.bottom)/2;
var left = Math.floor(cx - diameter/2);
var top = Math.floor(cy - diameter/2);
diameter = Math.floor(diameter);
var html = "<canvas id='circleCanvas"+circleCount+"' "+
"width='"+(diameter+2)+"' "+
"height='"+(diameter+2)+"' "+
"style='"+
"position:absolute;"+
"z-index:0;"+
"left:"+(left-1)+"px;"+
"top:"+(top-1)+"px;"+
//"border:1px solid;"+
"' />";
alert(html);
var container = document.getElementById("circles");
container.innerHTML += html;
var c=document.getElementById("circleCanvas"+circleCount);
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.arc(diameter/2+1,diameter/2+1,diameter/2,0,2*Math.PI);
ctx.stroke();
++circleCount;
}
$(".circled").each(function(i, obj) {
var rect = obj.getBoundingClientRect();
circleRect(rect);
});
Why is only one canvas showing up?
It will be more reliable to manipulate the DOM rather than trying to inline things with innerHTML. This code uses jQuery's DOM manipulation methods:
var circleCount = 0;
function circleRect(rect)
{
var diameter = Math.sqrt(rect.width*rect.width+rect.height*rect.height);
var cx = (rect.right + rect.left)/2;
var cy = (rect.top + rect.bottom)/2;
var left = Math.floor(cx - diameter/2);
var top = Math.floor(cy - diameter/2);
diameter = Math.floor(diameter);
var html = $("<canvas id='circleCanvas"+circleCount+"' "+
"width='"+(diameter+2)+"' "+
"height='"+(diameter+2)+"' "+
"style='"+
"position:absolute;"+
"z-index:0;"+
"left:"+(left-1)+"px;"+
"top:"+(top-1)+"px;"+
"' />");
$("#circles").append(html);
var ctx=html[0].getContext("2d");
ctx.beginPath();
ctx.arc(diameter/2+1,diameter/2+1,diameter/2,0,2*Math.PI);
ctx.stroke();
++circleCount;
}
You could also use the standard createElement and appendChild if you don't really need jQuery.
The innerHTML property has a number of drawbacks, although there is nothing specific I can find about not using += with it, the fact that insertAdjacentHTML exists would seem to indicate that you shouldn't really expect it to work well. (forgot this bit earlier) In this case, as you correctly surmised in your comment, the canvas you've drawn on is replaced by a new one when the assignment to innerHTML happens.

Fixing the canvas offset in Raphael.js when DOM element centered

I'm creating circles in a Raphael.js Canvas with a clicking action. When the canvas is drawn at the origin of the website it works fine. But when I move it to the middle some type offseting happens and only when you click on the right side of the canvas the circles are drawn at the left side of the canvas. Even though I have my listener action to the canvas and not the div that the is append to, here is the code:
<body>
<div id="container"></div>
</body>
Here is the JS:
var canvas = Raphael("container", 500, 500);
var ourCanvas = $('svg').last();
ourCanvas.attr("id", "canvas");
var canvasHandler = $("#canvas");
//We create a div with a class to append our canvas
var containerHandler = $("#container");
var circleClass = $("circle.quincy");
var justDragged = false;
canvasHandler.mouseup(function (e) {
var mouseX = e.pageX;
var mouseY = e.pageY;
makeCircle(mouseX, mouseY);
});
function makeCircle(mouseX, mouseY) {
var radius;
var fill;
var thisCirclesID = String(new Date().getTime());
var circle = canvas.circle(mouseX, mouseY, 50).attr({
fill: "hsb(.8, 1, 1)",
stroke: "none",
opacity: .5,
});
}
Here is a JSFiddle
I wonder if the way that I'm using the event position is correct. Any suggestion is more than welcome.
Thanks
M
I figured it out after realizing that the offset of the new position of svg was causing the mouse event to not recognize its real position. I found this blog post that shows how to use the offsetParent method to calculate the new position of my canvas relative its parent . Here is the code:
$(document).ready(function () {
//We create our canvas and add an ID
var canvas = Raphael("container", 500, 500);
var ourCanvas = $('svg').last();
ourCanvas.attr("id", "canvas");
var canvasHandler = $("#canvas");
//We create a div with a class to append our canvas
var containerHandler = $("#container");
var circleClass = $("circle.quincy");
var justDragged = false;
canvasHandler.mouseup(function (e) {
var mouseX = e.pageX - findPos(this)[0];
var mouseY = e.pageY - findPos(this)[1];
makeCircle(mouseX, mouseY);
findPos(this);
console.log("This is the position of the mouse in X: " + e.pageX);
console.log("This is the position of the mouse in Y: " + e.pageY);
});
function findPos(obj) {
var curleft = curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj == obj.offsetParent);
console.log(curleft);
console.log(curtop);
// return [curleft, curtop];
}
return [curleft, curtop];
}
As you can see the findPos(obj) returns and array of the new position of X and Y. So I substract that to my mouseX and mouseY to get the real position on the svg when clicking.
Update
Doing more reading and asking around. I didn't realize what type of elements the web browser returns. Instead of using e.pageX and e.pageY. offsetX offers the position of the mouse in respect to the parent giving the real coordinates. So the code will look like this:
canvasHandler.mouseup(function (e) {
var mouseX = e.offsetX;
var mouseY = e.offsetY;
makeCircle(mouseX, mouseY);
findPos(this);
});
This makes it easier since the offset takes into consideration the real position of the mouse in respect to the element.

Creating a Clickable Grid in a Web Browser

I want to draw a grid of 10 x 10 squares on a HTML5 canvas with number 1-100 displayed on the squares. Clicking a square should call a JavaScript function with the square's number passed as a variable to the function.
First, I encourage you to read this answer to another question involving the HTML5 Canvas. You need to understand that there are no squares. In order to detect a click on a 'square', you would have to keep track of a mapping from each canvas coordinate to the square(s) that it logically contains, handle a single click event on the entire canvas, work out which square(s) you want to change, and then redraw the canvas with the changes you want.
Then—since you seem to have no objection to using a more appropriate technology—I encourage you to do this in either HTML (where each 'square' is something like a <div> that is absolutely-positioned and sized and colored using CSS), or SVG (using <rect> if you need the squares to be able to be rotated, or want to introduce other shapes).
HTML and SVG are both 'retained-mode' graphics mode systems, where drawing a shape 'retains' the concept of that shape. You can move the shape, change its colors, size, etc. and the computer will automatically redraw it for you. Moreover, and more importantly for your use case, you can (with both HTML and SVG):
function changeColor(evt){
var clickedOn = evt.target;
// for HTML
clickedOn.style.backgroundColor = '#f00';
// for SVG
clickedOn.setAttribute('fill','red');
}
mySquare.addEventListener('click',changeColor,false);
Edit: I've created a simple implementation in JavaScript and HTML: http://jsfiddle.net/6qkdP/2/
Here's the core code, in case JSFiddle is down:
function clickableGrid( rows, cols, callback ){
var i=0;
var grid = document.createElement('table');
grid.className = 'grid';
for (var r=0;r<rows;++r){
var tr = grid.appendChild(document.createElement('tr'));
for (var c=0;c<cols;++c){
var cell = tr.appendChild(document.createElement('td'));
cell.innerHTML = ++i;
cell.addEventListener('click',(function(el,r,c,i){
return function(){ callback(el,r,c,i); }
})(cell,r,c,i),false);
}
}
return grid;
}
EDIT: Using HTML elements rather than drawing these things on a canvas or using SVG is another option and quite possibly preferable.
Following up on Phrogz's suggestions, see here for an SVG implementation:
jsfiddle example
document.createSvg = function(tagName) {
var svgNS = "http://www.w3.org/2000/svg";
return this.createElementNS(svgNS, tagName);
};
var numberPerSide = 20;
var size = 10;
var pixelsPerSide = 400;
var grid = function(numberPerSide, size, pixelsPerSide, colors) {
var svg = document.createSvg("svg");
svg.setAttribute("width", pixelsPerSide);
svg.setAttribute("height", pixelsPerSide);
svg.setAttribute("viewBox", [0, 0, numberPerSide * size, numberPerSide * size].join(" "));
for(var i = 0; i < numberPerSide; i++) {
for(var j = 0; j < numberPerSide; j++) {
var color1 = colors[(i+j) % colors.length];
var color2 = colors[(i+j+1) % colors.length];
var g = document.createSvg("g");
g.setAttribute("transform", ["translate(", i*size, ",", j*size, ")"].join(""));
var number = numberPerSide * i + j;
var box = document.createSvg("rect");
box.setAttribute("width", size);
box.setAttribute("height", size);
box.setAttribute("fill", color1);
box.setAttribute("id", "b" + number);
g.appendChild(box);
var text = document.createSvg("text");
text.appendChild(document.createTextNode(i * numberPerSide + j));
text.setAttribute("fill", color2);
text.setAttribute("font-size", 6);
text.setAttribute("x", 0);
text.setAttribute("y", size/2);
text.setAttribute("id", "t" + number);
g.appendChild(text);
svg.appendChild(g);
}
}
svg.addEventListener(
"click",
function(e){
var id = e.target.id;
if(id)
alert(id.substring(1));
},
false);
return svg;
};
var container = document.getElementById("container");
container.appendChild(grid(5, 10, 200, ["red", "white"]));
container.appendChild(grid(3, 10, 200, ["white", "black", "yellow"]));
container.appendChild(grid(7, 10, 200, ["blue", "magenta", "cyan", "cornflowerblue"]));
container.appendChild(grid(2, 8, 200, ["turquoise", "gold"]));
As the accepted answer shows, doing this in HTML/CSS is easiest if this is all your design amounts to, but here's an example using canvas as an alternative for folks whose use case might make more sense in canvas (and to juxtapose against HTML/CSS).
The first step of the problem boils down to figuring out where in the canvas the user's mouse is, and that requires knowing the offset of the canvas element. This is the same as finding the mouse position in an element, so there's really nothing unique to canvas here in this respect. I'm using event.offsetX/Y to do this.
Drawing a grid on canvas amounts to a nested loop for rows and columns. Use a tileSize variable to control the step amount. Basic math lets you figure out which tile (coordinates and/or cell number) your mouse is in based on the width and height and row and column values. Use context.fill... methods to write text and draw squares. I've kept everything 0-indexed for sanity, but you can normalize this as a final step before display (don't mix 1-indexing in your logic, though).
Finally, add event listeners to the canvas element to detect mouse actions which will trigger re-computations of the mouse position and selected tile and re-renders of the canvas. I attached most of the logic to mousemove because it's easier to visualize, but the same code applies to click events if you choose.
Keep in mind that the below approach is not particularly performance-conscious; I only re-render when the cursor moves between cells, but partial re-drawing or moving an overlay to indicate the highlighted element would be faster (if available). There are a lot of micro-optimizations I've ignored. Consider this a proof-of-concept.
const drawGrid = (canvas, ctx, tileSize, highlightNum) => {
for (let y = 0; y < canvas.width / tileSize; y++) {
for (let x = 0; x < canvas.height / tileSize; x++) {
const parity = (x + y) % 2;
const tileNum = x + canvas.width / tileSize * y;
const xx = x * tileSize;
const yy = y * tileSize;
if (tileNum === highlightNum) {
ctx.fillStyle = "#f0f";
}
else {
ctx.fillStyle = parity ? "#555" : "#ddd";
}
ctx.fillRect(xx, yy, tileSize, tileSize);
ctx.fillStyle = parity ? "#fff" : "#000";
ctx.fillText(tileNum, xx, yy);
}
}
};
const size = 10;
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 200;
const ctx = canvas.getContext("2d");
ctx.font = "11px courier";
ctx.textBaseline = "top";
const tileSize = canvas.width / size;
const status = document.createElement("pre");
let lastTile = -1;
drawGrid(canvas, ctx, tileSize);
document.body.style.display = "flex";
document.body.style.alignItems = "flex-start";
document.body.appendChild(canvas);
document.body.appendChild(status);
canvas.addEventListener("mousemove", evt => {
event.target.style.cursor = "pointer";
const tileX = ~~(evt.offsetX / tileSize);
const tileY = ~~(evt.offsetY / tileSize);
const tileNum = tileX + canvas.width / tileSize * tileY;
if (tileNum !== lastTile) {
lastTile = tileNum;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid(canvas, ctx, tileSize, tileNum);
}
status.innerText = ` mouse coords: {${evt.offsetX}, ${evt.offsetX}}
tile coords : {${tileX}, ${tileY}}
tile number : ${tileNum}`;
});
canvas.addEventListener("click", event => {
status.innerText += "\n [clicked]";
});
canvas.addEventListener("mouseout", event => {
drawGrid(canvas, ctx, tileSize);
status.innerText = "";
lastTile = -1;
});

Categories

Resources