I am building prototype tool to draw simple diagrams.
I need to draw an arrow between two boxes, the problem is i have to find edges of two boxes so that the arrow line does not intersect with the box.
This is the drawing that visualize my problem:
How to find x1,y1 and x2,y2 ?
-- UPDATE --
After 2 days finding solution, this is example & function that i use:
var box1 = { x:1,y:10,w:30,h:30 };
var box2 = { x:100,y:110,w:30,h:30 };
var edge1 = findBoxEdge(box1,box2,1,0);
var edge2 = findBoxEdge(box1,box2,2,0);
function findBoxEdge(box1,box2,box,distant) {
var c1 = box1.x + box1.w/2;
var d1 = box1.y + box1.h/2;
var c2 = box2.x + box2.w/2;
var d2 = box2.y + box2.h/2;
var w,h,delta_x,delta_y,s,c,e,ox,oy,d;
if (box == 1) {
w = box1.w/2;
h = box1.h/2;
} else {
w = box2.w/2;
h = box2.h/2;
}
if (box == 1) {
delta_x = c2-c1;
delta_y = d2-d1;
} else {
delta_x = c1-c2;
delta_y = d1-d2;
}
w+=5;
h+=5;
//intersection is on the top or bottom
if (w*Math.abs(delta_y) > h * Math.abs(delta_x)) {
if (delta_y > 0) {
s = [h*delta_x/delta_y,h];
c = "top";
}
else {
s = [-1*h*delta_x/delta_y,-1*h];
c = "bottom";
}
}
else {
//intersection is on the left or right
if (delta_x > 0) {
s = [w,w*delta_y/delta_x];
c = "right";
}
else {
s = [-1*w,-1*delta_y/delta_x];
c = "left";
}
}
if (typeof(distant) != "undefined") {
//for 2 paralel distant of 2e
e = distant;
if (delta_y == 0) ox = 0;
else ox = e*Math.sqrt(1+Math.pow(delta_x/delta_y,2))
if (delta_x == 0) oy = 0;
else oy = e*Math.sqrt(1+Math.pow(delta_y/delta_x,2))
if (delta_y != 0 && Math.abs(ox + h * (delta_x/delta_y)) <= w) {
d = [sgn(delta_y)*(ox + h * (delta_x/delta_y)),sgn(delta_y)*h];
}
else if (Math.abs(-1*oy + (w * delta_y/delta_x)) <= h) {
d = [sgn(delta_x)*w,sgn(delta_x)*(-1*oy + w * (delta_y/delta_x))];
}
if (delta_y != 0 && Math.abs(-1*ox+(h * (delta_x/delta_y))) <= w) {
d = [sgn(delta_y)*(-1*ox + h * (delta_x/delta_y)),sgn(delta_y)*h];
}
else if (Math.abs(oy + (w * delta_y/delta_x)) <= h) {
d = [sgn(delta_x)*w,sgn(delta_x)*(oy + w * (delta_y/delta_x))];
}
if (box == 1) {
return [Math.round(c1 +d[0]),Math.round(d1 +d[1]),c];
} else {
return [Math.round(c2 +d[0]),Math.round(d2 +d[1]),c];
}
} else {
if (box == 1) {
return [Math.round(c1 +s[0]),Math.round(d1 +s[1]),c];
} else {
return [Math.round(c2 +s[0]),Math.round(d2 +s[1]),c];
}
}
tl;dr -> Look at the jsbin code-example
It is our goal to draw a line from the edges of two Rectangles A & B that would be drawn through their centers.
Therefore we'll have to determine where the line pierces through the edge of a Rect.
We can assume that our Rect is an object containing x and y as offset from the upper left edge and width and height as dimension offset.
This can be done by the following code. The Method you should look at closely is pointOnEdge.
// starting with Point and Rectangle Types, as they ease calculation
var Point = function(x, y) {
return { x: x, y: y };
};
var Rect = function(x, y, w, h) {
return { x: x, y: y, width: w, height: h };
};
var isLeftOf = function(pt1, pt2) { return pt1.x < pt2.x; };
var isAbove = function(pt1, pt2) { return pt1.y < pt2.y; };
var centerOf = function(rect) {
return Point(
rect.x + rect.width / 2,
rect.y + rect.height / 2
);
};
var gradient = function(pt1, pt2) {
return (pt2.y - pt1.y) / (pt2.x - pt1.x);
};
var aspectRatio = function(rect) { return rect.height / rect.width; };
// now, this is where the fun takes place
var pointOnEdge = function(fromRect, toRect) {
var centerA = centerOf(fromRect),
centerB = centerOf(toRect),
// calculate the gradient from rectA to rectB
gradA2B = gradient(centerA, centerB),
// grab the aspectRatio of rectA
// as we want any dimensions to work with the script
aspectA = aspectRatio(fromRect),
// grab the half values, as they are used for the additional point
h05 = fromRect.width / 2,
w05 = fromRect.height / 2,
// the norm is the normalized gradient honoring the aspect Ratio of rectA
normA2B = Math.abs(gradA2B / aspectA),
// the additional point
add = Point(
// when the rectA is left of rectB we move right, else left
(isLeftOf(centerA, centerB) ? 1 : -1) * h05,
// when the rectA is below
(isAbove(centerA, centerB) ? 1 : -1) * w05
);
// norm values are absolute, thus we can compare whether they are
// greater or less than 1
if (normA2B < 1) {
// when they are less then 1 multiply the y component with the norm
add.y *= normA2B;
} else {
// otherwise divide the x component by the norm
add.x /= normA2B;
}
// this way we will stay on the edge with at least one component of the result
// while the other component is shifted towards the center
return Point(centerA.x + add.x, centerA.y + add.y);
};
I wrote a jsbin, you can use to test with some boxes (lower part, in the ready method):
You might want to take a look at a little Geometry helper I wrote some time ago on top of prototype.js
I really hope, that this helps you with your problem ;)
To draw a line between those boxes, you'd first have to define where you want the line to be.
Apparently you want to draw the lines/arrows from the right edge of Rect A to the left edge of
Rect B, somewhat like this:
Assuming your know the origin (upper left Point as { x, y } of a Rect) and its Size (width and height), you first want to determine the position of the center of the edges:
var rectA, rectB; // I assume you have those data
var rectARightEdgeCenter = {
// x is simply the origin's x plus the width
x: rectA.origin.x + rectA.size.width,
// for y you need to add only half the height to origin.y
y: rectA.origin.y + rectA.size.height / 2.0
}
var rectBLeftEdgeCenter = {
// x will be simply the origin's x
x: rectB.origin.x,
// y is half the height added to the origin's y, just as before
y: rectB.origin.y + rectB.size.height / 2.0
}
The more interesting question would be how to determine, from which edge to which other edge you might want to draw the lines in a more dynamic scenario.
If your boxes just pile up from left to right the given solution will fit,
but you might want to check for minimum distances of the edges, to determine a possible best arrow.
Related
I'm making a small game with one player and blocks that builds up the environment. The problem I'm having is knowing the difference between when the player hits the ground (the top of a block), and hitting a wall (the side of the block).
So far the player can walk on the ground just fine, but when he meets a wall, he immediately jumps to the top of that block.
This is my collision detector:
function collisionDetector(){
if(myPlayer.y + myPlayer.h > c.height){ //Bottom of the canvas
myPlayer.vy = 0;
myPlayer.ay = 0;
myPlayer.y = c.height - myPlayer.h;
myPlayer.onGround = true;
console.log(myPlayer.y + myPlayer.h, c.height);
}
if(myPlayer.x + myPlayer.w >= c.width){ //right side of canvas
myPlayer.x = c.width - myPlayer.w;
myPlayer.vx = 0;
}
if(myPlayer.x <= 0){ //Left side of canvas
myPlayer.x = 0;
myPlayer.vx = 0;
}
function hitTest(a,b){ //hitTest between two objects
if(a.y + a.h > b.y && a.y < b.y + b.h && a.x + a.w > b.x && a.x < b.x + b.w){
return true;
}
}
for(var i = 0; i < blocks.length; i++){ //Loop through blocks
if(hitTest(myPlayer, blocks[i])){ //If it touches a block
myPlayer.y = blocks[i].y - myPlayer.h;
myPlayer.onGround = true; //onGround = ready to jump
}
}
}
I realized that I'm setting the players y pos to be on top of what ever block it hits, but I cannot figure out a solution to this problem. Can anyone help me or at least lead me in the right direction? Thanks!
(Let me know if you need more of the code)
PS: the player is just a head. No body hiding behind the blocks.
So basically, what you need to do is to check collision between many points in the player.
In the snippet you can show many points represented in the player.
Bottom almost-left and almost-right (in blue), check against below blocks. They are not fully left or right, in order to prevent a race condition which will allow the player to climb walls. In that case, if the player is pushing against a wall and jumping, the collider will detect both side collision and bottom collision as true, then the player will quickly move to the top until there are no more blocks.
Left and right points (in black), check against edges of the blocks. This is just a point instead of two like the bottom edge, because we don't need more for this particular case. One more for each side could be easily added to get a better detection.
Top point (in red) checks against the top blocks. This is in the middle in order to allow the player a more easy way to tranverse the map. If this is not needed, you would need to add one more point like in the bottom edge (but never reaching the far edge, because that will generate a race condition).
So in summary, to have a good collision detection based on points (instead of raycasts), you need to detect the player like if it where a rounded shape, in order to prevent strange behaviours.
You can player around with the map layout by altering the layout variable. 0's are empty space, 1's are brown blocks and 2's are green blocks.
The collisionDetector fuction has comments to understand what's going on.
Also I have added a jump feature since I understand you would need that as well.
const c = document.getElementById('canvas');
c.width = window.innerWidth;
c.height = window.innerHeight;
const ctx = c.getContext('2d');
// map layout
const layout =
`000000001
001000001
000000101
100110111
222222222`;
// convert layout to blocks
const blocks = [...layout].reduce((a, c, i) => {
if (i === 0 || c === "\n") a.push([]);
if (c === "\n") return a;
const y = a.length - 1;
const row = a[y];
const x = row.length;
row.push({x: x * 32, y: y * 32, t:c, w:32, h:32});
return a;
}, []).reduce((a, c) => a.concat(c), []);
// player starting position
const myPlayer = {x: 32*1.5, y: 0, h: 32, w: 16, onGround: true};
const gravity = -1;
let pkl = 0, pkr = 0;
let pvely = 0;
function render() {
// player logic
const pvelx = pkr + pkl;
const speed = 2;
myPlayer.x += pvelx * speed;
myPlayer.y -= pvely;
if (pvely > -2) pvely += gravity;
const debugColliders = collisionDetector();
ctx.clearRect(0, 0, c.width, c.height);
// player render
ctx.fillStyle = '#FFD9B3';
ctx.fillRect(myPlayer.x, myPlayer.y, myPlayer.w, myPlayer.h);
renderLayout();
debugColliders();
window.requestAnimationFrame(render);
}
function renderLayout() {
const colors = {'1': '#A3825F', '2': '#7FAC72'}
blocks.forEach(b => {
if (+b.t > 0) {
ctx.fillStyle = colors[b.t];
ctx.fillRect(b.x, b.y, b.w, b.h);
}
});
}
window.addEventListener('keydown', e => {
if (e.key == 'ArrowRight') {
pkr = 1;
e.preventDefault();
} else if (e.key == 'ArrowLeft') {
pkl = -1;
e.preventDefault();
} else if (e.key == 'ArrowUp') {
if (myPlayer.onGround)
pvely = 8;
myPlayer.onGround = false;
e.preventDefault();
}
});
window.addEventListener('keyup', e => {
if (e.key == 'ArrowRight') {
pkr = 0;
} else if (e.key == 'ArrowLeft') {
pkl = 0;
}
});
function collisionDetector(){
const p = myPlayer;
const playerTop = p.y;
const playerLeft = p.x;
const playerRight = playerLeft + p.w;
const playerBottom = playerTop + p.h;
const playerHalfLeft = playerLeft + p.w * .25;
const playerHalfRight = playerLeft + p.w * .75;
const playerHMiddle = playerLeft + p.w * .5;
const playerVMiddle = playerTop + p.h * .5;
if(playerBottom > c.height){ //Bottom of the canvas
p.vy = 0;
p.ay = 0;
p.y = c.height - p.h;
p.onGround = true;
}
if(playerRight >= c.width){ //right side of canvas
p.x = c.width - p.w;
p.vx = 0;
}
if(playerLeft <= 0){ //Left side of canvas
p.x = 0;
p.vx = 0;
}
blocks.forEach(b => { //Loop through blocks
if (b.t === "0") return; // If not collidable, do nothing
const blockTop = b.y;
const blockLeft = b.x;
const blockRight = blockLeft + b.w;
const blockBottom = b.y + b.h;
// Player bottom against block top
if ((playerBottom > blockTop && playerBottom < blockBottom) && // If player bottom is going through block top but is above block bottom.
((playerHalfLeft > blockLeft && playerHalfLeft < blockRight) || // If player left is inside block horizontal bounds
(playerHalfRight > blockLeft && playerHalfRight < blockRight))) { // Or if player right is inside block horizontal bounds
p.y = blockTop - p.h;
p.onGround = true;
}
// Player top against block bottom
if ((playerTop < blockBottom && playerTop > blockTop) && // If player top is going through block bottom but is below block top.
((playerHMiddle > blockLeft && playerHMiddle < blockRight))) { // If player hmiddle is inside block horizontal bounds
p.y = blockBottom;
p.onGround = false;
}
// Player right against block left, or player left against block right
if (playerVMiddle > blockTop && playerVMiddle < blockBottom) { // If player vertical-middle is inside block vertical bounds
if ((playerRight > blockLeft && playerRight < blockRight)) { // If player vmiddle-right goes through block-left
p.x = blockLeft - p.w;
} else if ((playerLeft < blockRight && playerRight > blockLeft)) { // If player vmiddle-left goes through block-right
p.x = blockRight;
}
}
});
return function debug() {
ctx.fillStyle = 'black';
ctx.fillRect(playerLeft, playerVMiddle, 1, 1);
ctx.fillRect(playerRight, playerVMiddle, 1, 1);
ctx.fillStyle = 'red';
ctx.fillRect(playerHMiddle, playerTop, 1, 1);
ctx.fillStyle = 'blue';
ctx.fillRect(playerHalfLeft, playerBottom, 1, 1);
ctx.fillRect(playerHalfRight, playerBottom, 1, 1);
}
}
window.requestAnimationFrame(render);
html, body{ width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; }
canvas { background: #7AC9F9; display: block; }
<canvas id="canvas"></canvas>
Introduce a block[i].type attribute. For instance if block[i].type=='floor' then make player stay on floor. If for another instance block[i].type=='wall' then make it stop moving through the wall. When block[i].type=='brick' or square or block or whatever, them it's a mixture of two.
Another part to be edited is when you check the collisions. What if you have only one-direction collision? What I am saying is maybe use or instead of and in this part if(a.y + a.h > b.y && a.y < b.y + b.h && a.x + a.w > b.x && a.x < b.x + b.w){
Also you could check each collision separately, like
function hitTest(a,b){ //hitTest between two objects
var collisions = {up: false, down: false, left: false, right: false};
collisions.up = (a.y + a.h > b.y ) || collisions.up
collisions.down = (a.y < b.y + b.h ) ||collisions.down
collisions.right = ( a.x + a.w > b.x) || collisions.right
collisions.left = (a.x < b.x + b.w) || collisions.left
return collisions
}
var escapeFrom = {
down: function(player, block){
player.y = block.y + block.h;
player.onGround = true; //onGround = ready to jump
},
up: function(player, block){
// you logic to escape from hitting the ceiling
},
// and for the next 2
left: function(player, block) {},
right: function(player, block){}
}
// Now here you check whether your player hits blocks
for(var i = 0; i < blocks.length; i++){ //Loop through blocks
cls = hitTest(myPlayer, blocks[i]) //If it touches a block
Object.keys(cls).map(function(direction, ind){
if (cls[direction]){
// call escape from function to escape collision
escapeFrom[direction](myPlayer, blocks[i]);
}
})
}
This is highly unoptimized, the whole your code is unoptimized, but at least it can help to move further.
I am working to add filter functionality to my d3 graph. When the user searches for a specific node based on label or id, I want to re-render the graph and show the entire graph again but I want the filtered node to sit in the center of the svg element.
here is what I have the helped it to be centered:
// I get the width and height of the SVG element:
var svgWidth = parseInt(svg.style("width").replace(/px/, ""), 10);
var svgHeight = parseInt(svg.style("height").replace(/px/, ""), 10);
// I get the center of the svg:
var centerX = svgWidth / 2;
var centerY = svgHeight / 2;
_.forEach(nodes, function(e) {
// get the full node (with x and y coordinates) based on the id
var nodeObject = g.node(nodeId);
// I look for matches between the nodeId or label and search word
if (searchInput) {
if (nodeObject.id === parseInt(searchInput, 10) || nodeObject.label.toUpperCase().indexOf(searchInput.toUpperCase()) > -1) {
searchedNodes.push(nodeObject);
console.log(searchedNodes);
}
}
}
// after looping through all the nodes rendered
if (searchedNodes.length > 0) {
//var width = searchedNodes[0].elem.getBBox().width;
//var height = searchedNodes[0].elem.getBBox().height;
ctrl.selectedNode = searchedNodes[0];
var offsetX = centerX - searchedNodes[0].x;
var offsetY = centerY - searchedNodes[0].y;
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")" + "scale(" + 3 + ")");
// this line here is incorrect syntax and breaks the build, essentially stopping the script from running
// the graph renders correctly when this line is here
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")").scale(2).event;
}
This is what the graph looks like with the line above that breaks the script included.
When I removed that line, it doesn't center, almost looking like over-renders the graph. Obviously I will need to remove the line of code above that is incorrect but does anybody no why the graph doesn't render correctly in this case?:
// get the user input and re-render the graph
elem.find(".search").bind("keyup", function (e:any) {
var searchInput;
if (e["keyCode"] === 13) {
searchedNodes = [];
searchInput = scope["searchInput"];
currentFilteredNode = null;
enterKeyPressed = true;
renderGraph(searchInput);
}
if (e["keyCode"] === 8) {
searchedNodes = [];
searchInput = scope["searchInput"];
currentFilteredNode = null;
renderGraph(searchInput);
}
});
// if there is searchInput and at least one matching node sort the nodes
// by id and then select and center the first matching one
if (searchInput && searchedNodes.length > 0) {
searchedNodes.sort(function (node1:any, node2:any) {
return node1.id - node2.id;
});
// make sure the noResultsMessage does not get shown on the screen if there are matching results
scope.$apply(function() {
scope["noResultsMessage"] = false;
});
ctrl.selectedNode = searchedNodes[0];
offsetX = centerX - searchedNodes[0].x;
offsetY = centerY - searchedNodes[0].y;
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")" + "scale(" + 3 + ")");
}
// the only other zoom and this runs just on page load
zoom = d3.behavior.zoom();
zoom.on("zoom", function() {
svgGroup.attr("transform", "translate(" + (<any>d3.event).translate + ")" + "scale(" + (<any>d3.event).scale + ")");
// this scales the graph - it runs on page load and whenever the user enters a search input, which re-renders the whole graph
var scaleGraph = function(useAnimation:any) {
var graphWidth = g.graph().width + 4;
var graphHeight = g.graph().height + 4;
var width = parseInt(svg.style("width").replace(/px/, ""), 10);
var height = parseInt(svg.style("height").replace(/px/, ""), 10);
var zoomScale = originalZoomScale;
// Zoom and scale to fit
if (ctrl.autoResizeGraph === "disabled") {
zoomScale = 1;
} else {
// always scale to canvas if set to fill or if auto (when larger than canvas)
if (ctrl.autoResizeGraph === "fill" || (graphWidth > width || graphHeight > height)) {
zoomScale = Math.min(width / graphWidth, height / graphHeight);
}
}
var translate;
if (direction.toUpperCase() === "TB") {
// Center horizontal + align top (offset 1px)
translate = [(width / 2) - ((graphWidth * zoomScale) / 2) + 2, 1];
} else if (direction.toUpperCase() === "BT") {
// Center horizontal + align top (offset 1px)
translate = [(width / 2) - ((graphWidth * zoomScale) / 4) + 2, 1];
} else if (direction.toUpperCase() === "LR") {
// Center vertical (offset 1px)
translate = [1, (height / 2) - ((graphHeight * zoomScale) / 2)];
} else if (direction.toUpperCase() === "RL") {
// Center vertical (offset 1px)
translate = [1, (height / 2) - ((graphHeight * zoomScale) / 4)];
} else {
// Center horizontal and vertical
translate = [(width / 2) - ((graphWidth * zoomScale) / 2), (height / 2) - ((graphHeight * zoomScale) / 2)];
}
zoom.center([width / 2, height / 2]);
zoom.size([width, height]);
zoom.translate(translate);
zoom.scale(zoomScale);
// If rendering the first time, then don't use animation
zoom.event(useAnimation ? svg.transition().duration(500) : svg);
};
CODE FOR FILTERING THE NODES:
// move to the left of the searchedNodes array when the left arrow is clicked
scope["filterNodesLeft"] = function () {
filterNodesIndex--;
if (filterNodesIndex < 0) {
filterNodesIndex = searchedNodes.length - 1;
}
currentFilteredNode = searchedNodes[filterNodesIndex];
runScaleGraph = true;
number = 1;
renderGraph();
};
// move to the right of the searchNodes array when the right arrow is clicked
scope["filterNodesRight"] = function () {
filterNodesIndex++;
if (filterNodesIndex > searchedNodes.length - 1) {
filterNodesIndex = 0;
}
currentFilteredNode = searchedNodes[filterNodesIndex];
runScaleGraph = true;
number = 1;
renderGraph();
};
// get the current filteredNode in the searchNodes array and center it
// when the graph is re-rendered
if (currentFilteredNode) {
ctrl.selectedNode = currentFilteredNode;
offsetX = centerX - currentFilteredNode.x;
offsetY = centerY - currentFilteredNode.y;
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")");
runScaleGraph = false;
}
You will want to find the x and y coordinates of your target node, and set the transform attribute of your group with class 'output' accordingly. You will also need to know the width and height of 'output' in order to position it such that your target node is in the center.
//when diagram is initially displayed
var output = d3.select('.output');
var bbox = output.getBBox();
var centerX = bbox.width * .5;
var centerY = bbox.height * .5;
//in your block where you find a node matches the filter
if (node.label.toUpperCase().indexOf(searchString.toUpperCase()) > -1) {
var offsetX = centerX - node.x;
var offsetY = centerY - node.y;
output.attr('transform', 'translate(' + offsetX + ',' + offsetY + ')');
}
Depending on the node's registration point, you may also need to take in to account the node's width and height to make sure we are directly centered on the node. For example, if the registration point is the top left of the node, you would want to add half the nodes width and half the nodes height to the offset.
-- Edit --
In the following line:
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")" + "scale(" + 3 + ")");
by including "scale(" + 3 + ")" so you are scaling your entire graph - you are not 'zooming in' on the place you have centered, rather the content itself is bigger and so offsetX and offsetY are not the correct cordinates to center on.
The reason things look better when you add that other line, is that you are removing the scale.
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")");
So, we are back to the default scale, immediately prior to your error being thrown.
If you want to scale, you'll need to multiply offsetX and offsetY by whatever you want to scale by.
If you do not want to scale, just remove
"scale(" + 3 + ")"
Here's how I solved it:
// zoom in on the searched or filtered node
function zoomOnNode (node:any) {
// get the width and height of the svg
var svgWidth = parseInt(svg.style("width").replace(/px/, ""), 10);
var svgHeight = parseInt(svg.style("height").replace(/px/, ""), 10);
// loop through all the rendered nodes (these nodes have x and y coordinates)
for (var i = 0; i < renderedNodes.length; i++) {
// if the first matching node passed into the function
// and the renderedNode's id match get the
// x and y coordinates from that rendered node and use it to calculate the svg transition
if (node.id === renderedNodes[i].id) {
var translate = [svgWidth / 2 - renderedNodes[i].x, svgHeight / 2 - renderedNodes[i].y];
var scale = 1;
svg.transition().duration(750).call(zoom.translate(translate).scale(scale).event);
}
}
}
// listen for the enter key press, get all matching nodes and pass in the first matching node in the array to the zoomOnNode function
elem.find(".search").bind("keyup", function (e:any) {
var searchInput;
if (e["keyCode"] === 13) {
searchedNodes = [];
searchInput = scope["searchInput"];
enterKeyPressed = true;
if (searchInput) {
// recursively get all matching nodes based on search input
getMatchingNodes(ctrl.nodes, searchInput);
scope.$apply(function() {
// show the toggle icons if searchedNodes.length is greater then 1
scope["matchingNodes"] = searchedNodes.length;
scope["noResultsMessage"] = false;
if (searchedNodes.length > 0) {
var firstNode = searchedNodes[0];
ctrl.selectedNode = firstNode;
zoomOnNode(firstNode);
} else if (searchedNodes.length === 0) {
ctrl.selectedNode = null;
// add the noResultsMessage to the screen
scope["noResultsMessage"] = true;
}
});
}
}
}
I have a problem on my project.
I am developing a perspective mockup creating module for designers. Users upload images and i get them for placing in mockups with making some perspective calculations. Then users can download this image. I made all of this on clientside with js.
But there is a problem for images which are drawn on canvas with perspective calculations like this;
Sample img: http://oi62.tinypic.com/2h49dec.jpg
orginal image size: 6500 x 3592 and you can see spread edges on image...
I tried a few technics like ctx.imageSmoothingEnabled true etc.. But result was always same.
What can i do for solve this problem? What do you think about this?
edit
For more detail;
I get an image (Resolution free) from user then crop it for mockup ratio. For example in my sample image, user image was cropped for imac ratio 16:9 then making calculation with four dot of screen. By the way, my mockup image size is 6500 x 3592. so i made scale, transform etc this cropped image and put it in mockup on canvas. And then use blob to download this image to client...
Thanks.
Solved.
I use perspective.js for calculation on canvas. so I made some revisions on this js source.
If you wanna use or check source;
// Copyright 2010 futomi http://www.html5.jp/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// perspective.js v0.0.2
// 2010-08-28
/* -------------------------------------------------------------------
* define objects (name space) for this library.
* ----------------------------------------------------------------- */
if (typeof html5jp == 'undefined') {
html5jp = new Object();
}
(function() {
html5jp.perspective = function(ctxd, image) {
// check the arguments
if (!ctxd || !ctxd.strokeStyle) {
return;
}
if (!image || !image.width || !image.height) {
return;
}
// prepare a <canvas> for the image
var cvso = document.createElement('canvas');
cvso.width = parseInt(image.width) * 2;
cvso.height = parseInt(image.height) * 2;
var ctxo = cvso.getContext('2d');
ctxo.drawImage(image, 0, 0, cvso.width, cvso.height);
// prepare a <canvas> for the transformed image
var cvst = document.createElement('canvas');
cvst.width = ctxd.canvas.width;
cvst.height = ctxd.canvas.height;
var ctxt = cvst.getContext('2d');
ctxt.imageSmoothingEnabled = true;
ctxt.mozImageSmoothingEnabled = true;
ctxt.webkitImageSmoothingEnabled = true;
ctxt.msImageSmoothingEnabled = true;
// parameters
this.p = {
ctxd: ctxd,
cvso: cvso,
ctxo: ctxo,
ctxt: ctxt
}
};
var proto = html5jp.perspective.prototype;
proto.draw = function(points) {
var d0x = points[0][0];
var d0y = points[0][1];
var d1x = points[1][0];
var d1y = points[1][1];
var d2x = points[2][0];
var d2y = points[2][1];
var d3x = points[3][0];
var d3y = points[3][1];
// compute the dimension of each side
var dims = [
Math.sqrt(Math.pow(d0x - d1x, 2) + Math.pow(d0y - d1y, 2)), // top side
Math.sqrt(Math.pow(d1x - d2x, 2) + Math.pow(d1y - d2y, 2)), // right side
Math.sqrt(Math.pow(d2x - d3x, 2) + Math.pow(d2y - d3y, 2)), // bottom side
Math.sqrt(Math.pow(d3x - d0x, 2) + Math.pow(d3y - d0y, 2)) // left side
];
//
var ow = this.p.cvso.width;
var oh = this.p.cvso.height;
// specify the index of which dimension is longest
var base_index = 0;
var max_scale_rate = 0;
var zero_num = 0;
for (var i = 0; i < 4; i++) {
var rate = 0;
if (i % 2) {
rate = dims[i] / ow;
} else {
rate = dims[i] / oh;
}
if (rate > max_scale_rate) {
base_index = i;
max_scale_rate = rate;
}
if (dims[i] == 0) {
zero_num++;
}
}
if (zero_num > 1) {
return;
}
//
var step = 0.10;
var cover_step = step * 250;
//
var ctxo = this.p.ctxo;
var ctxt = this.p.ctxt;
//*** ctxt.clearRect(0, 0, ctxt.canvas.width, ctxt.canvas.height);
if (base_index % 2 == 0) { // top or bottom side
var ctxl = this.create_canvas_context(ow, cover_step);
var cvsl = ctxl.canvas;
for (var y = 0; y < oh; y += step) {
var r = y / oh;
var sx = d0x + (d3x - d0x) * r;
var sy = d0y + (d3y - d0y) * r;
var ex = d1x + (d2x - d1x) * r;
var ey = d1y + (d2y - d1y) * r;
var ag = Math.atan((ey - sy) / (ex - sx));
var sc = Math.sqrt(Math.pow(ex - sx, 2) + Math.pow(ey - sy, 2)) / ow;
ctxl.setTransform(1, 0, 0, 1, 0, -y);
ctxl.drawImage(ctxo.canvas, 0, 0);
//
ctxt.translate(sx, sy);
ctxt.rotate(ag);
ctxt.scale(sc, sc);
ctxt.drawImage(cvsl, 0, 0);
//
ctxt.setTransform(1, 0, 0, 1, 0, 0);
}
} else if (base_index % 2 == 1) { // right or left side
var ctxl = this.create_canvas_context(cover_step, oh);
var cvsl = ctxl.canvas;
for (var x = 0; x < ow; x += step) {
var r = x / ow;
var sx = d0x + (d1x - d0x) * r;
var sy = d0y + (d1y - d0y) * r;
var ex = d3x + (d2x - d3x) * r;
var ey = d3y + (d2y - d3y) * r;
var ag = Math.atan((sx - ex) / (ey - sy));
var sc = Math.sqrt(Math.pow(ex - sx, 2) + Math.pow(ey - sy, 2)) / oh;
ctxl.setTransform(1, 0, 0, 1, -x, 0);
ctxl.drawImage(ctxo.canvas, 0, 0);
//
ctxt.translate(sx, sy);
ctxt.rotate(ag);
ctxt.scale(sc, sc);
ctxt.drawImage(cvsl, 0, 0);
//
ctxt.setTransform(1, 0, 0, 1, 0, 0);
}
}
// set a clipping path and draw the transformed image on the destination canvas.
this.p.ctxd.save();
this.set_clipping_path(this.p.ctxd, [
[d0x, d0y],
[d1x, d1y],
[d2x, d2y],
[d3x, d3y]
]);
this.p.ctxd.drawImage(ctxt.canvas, 0, 0);
this.p.ctxd.restore();
}
proto.create_canvas_context = function(w, h) {
var canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.mozImageSmoothingEnabled = true;
ctx.webkitImageSmoothingEnabled = true;
ctx.msImageSmoothingEnabled = true;
return ctx;
};
proto.set_clipping_path = function(ctx, points) {
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (var i = 1; i < points.length; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
ctx.closePath();
ctx.clip();
};
})();
The problem is (most likely, but no code shows so..) that the image is actually too big.
The canvas typically uses bi-linear interpolation (2x2 samples) rather than bi-cubic (4x4 samples). That means if you scale it down a large percentage in one chunk the algorithm will skip some pixels that otherwise should have been sampled, resulting in a more pixelated look.
The solution do is to resize the image in steps, ie. 50% of itself repeatably until a suitable size is achieved. Then use perspective calculations on it. The exact destination size is something you need to find by trial and error, but a good starting point is to use the largest side of the resulting perspective image.
Here is one way to step-down rescale an image in steps.
I am using FlotChart and Flot Tick Rotor [jquery.flot.tickrotor] plugins.
I tried to tweak the rotor to provide some lines between my x-axis labels by drawing some rectangles.
However, the last bar on my stacked graph is also filled with the color I set to my fills.
Can anyone help me?
Here's the image :
Here's my tweaked code :
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* flot-tickrotor: flot plugin to display angled X-axis tick labels.
*
* Requires flot 0.7 or higher and a browser supporting <canvas>.
*
* To activate, just set xaxis.rotateTicks to an angle in degrees. Labels
* are rotated clockwise, so if you want the labels to angle up and to the
* right (/) you need to provide an angle > 90. The text will be flipped so
* that it is still right-side-up.
* Angles greater than or equal to 180 are ignored.
*/
(function ($) {
var options = { };
function init(plot) {
// Taken from flot-axislabels.
// This is kind of a hack. There are no hooks in Flot between
// the creation and measuring of the ticks (setTicks, measureTickLabels
// in setupGrid() ) and the drawing of the ticks and plot box
// (insertAxisLabels in setupGrid() ).
//
// Therefore, we use a trick where we run the draw routine twice:
// the first time to get the tick measurements, so that we can change
// them, and then have it draw it again.
var ticks = []; // preserve between draw() calls.
var font;
var secondPass = false;
var rotateTicks, rotateTicksRads, radsAboveHoriz;
plot.hooks.draw.push(function (plot, ctx) {
var xaxis; // for convenience
if (!secondPass) {
var opts = plot.getAxes().xaxis.options;
if (opts.rotateTicks === undefined) {
return;
}
rotateTicks = parseInt(opts.rotateTicks, 10);
if (rotateTicks.toString() != opts.rotateTicks || rotateTicks >= 180) { // || rotateTicks == 0
return;
}
rotateTicksRads = rotateTicks * Math.PI/180;
if (rotateTicks > 90) {
radsAboveHoriz = Math.PI - rotateTicksRads;
} else {
radsAboveHoriz = Math.PI/2 - rotateTicksRads;
}
font = opts.rotateTicksFont;
if (!font) {
font = $('.tickLabel').css('font');
}
if (!font) {
font = 'arial';
}
var elem, maxLabelWidth = 0, maxLabelHeight = 0, minX = 0, maxX = 0;
// We have to clear the ticks option so that flot core
// doesn't draw ticks superimposed with ours, but we preserve
// the tick data as xaxis.rotatedTicks so that external code
// can still get to it.
// FIXME: It would obviously be better to just interrupt
// the drawing of the ticks and preserve the 'ticks'
// property. That probably requires another hook.
xaxis = plot.getAxes().xaxis;
ticks = plot.getAxes().xaxis.ticks;
xaxis.rotatedTicks = ticks;
opts.ticks = []; // we'll make our own
var x;
for (var i = 0; i < ticks.length; i++) {
var raber = ticks[i].label.split(" ");
elem = $('<span style="font-size:11pt; font:' + font + '">' + ticks[i].label + '</span>');
plot.getPlaceholder().append(elem);
ticks[i].height = elem.outerHeight(true);
ticks[i].width = elem.outerWidth(true);
elem.remove();
if (ticks[i].height > maxLabelHeight) {
maxLabelHeight = ticks[i].height;
}
if (ticks[i].width > maxLabelWidth) {
maxLabelWidth = ticks[i].width;
}
var tick = ticks[i];
// See second-draw code below for explanation of offsets.
if (rotateTicks > 90) {
// See if any labels are too long and require increased left
// padding.
x = Math.round(plot.getPlotOffset().left + xaxis.p2c(tick.v))
- Math.ceil(Math.cos(radsAboveHoriz) * tick.height)
- Math.ceil(Math.cos(radsAboveHoriz) * tick.width);
if (x < minX) {
minX = x;
}
} else {
// See if any labels are too long and require increased right
// padding.
x = Math.round(plot.getPlotOffset().left + xaxis.p2c(tick.v))
+ Math.ceil(Math.cos(radsAboveHoriz) * tick.height)
+ Math.ceil(Math.cos(radsAboveHoriz) * tick.width);
if (x > maxX) {
maxX = x;
}
}
}
// Calculate maximum label height after rotating.
if (rotateTicks > 90) {
var acuteRads = rotateTicksRads - Math.PI/2;
opts.labelHeight = Math.ceil(Math.sin(acuteRads) * maxLabelWidth)
+ Math.ceil(Math.sin(acuteRads) * maxLabelHeight) + 20;
} else {
var acuteRads = Math.PI/2 - rotateTicksRads;
// Center such that the top of the label is at the center of the tick.
opts.labelHeight = Math.ceil(Math.sin(rotateTicksRads) * maxLabelWidth)
+ Math.ceil(Math.sin(acuteRads) * maxLabelHeight) + 20;
}
if (minX < 0) {
plot.getAxes().yaxis.options.labelWidth = -1 * minX;
}
// Doesn't seem to work if there are no values using the
// second y axis.
//if (maxX > xaxis.box.left + xaxis.box.width) {
// plot.getAxes().y2axis.options.labelWidth = maxX - xaxis.box.left - xaxis.box.width;
//}
// re-draw with new label widths and heights
secondPass = true;
plot.setupGrid();
plot.draw();
} else {
if (ticks.length == 0) {
return;
}
xaxis = plot.getAxes().xaxis;
var box = xaxis.box;
var tick, label, xoffset, yoffset;
var showWeek = false;
for (var i = 0; i < ticks.length; i++) {
tick = ticks[i];
if (!tick.label) {
continue;
}
ctx.save();
ctx.font = font;
if (rotateTicks <= 90) {
// Center such that the top of the label is at the center of the tick.
xoffset = -Math.ceil(Math.cos(radsAboveHoriz) * tick.height) - 10;
yoffset = Math.ceil(Math.sin(radsAboveHoriz) * tick.height) - 10;
ctx.translate(Math.round(plot.getPlotOffset().left + xaxis.p2c(tick.v)) + xoffset,
box.top + box.padding + plot.getOptions().grid.labelMargin + yoffset);
ctx.rotate(rotateTicksRads);
} else {
// We want the text to facing up, so we have to
// rotate counterclockwise, which means the label
// has to *end* at the center of the tick.
xoffset = Math.ceil(Math.cos(radsAboveHoriz) * tick.height)
- Math.ceil(Math.cos(radsAboveHoriz) * tick.width);
yoffset = Math.ceil(Math.sin(radsAboveHoriz) * tick.width)
+ Math.ceil(Math.sin(radsAboveHoriz) * tick.height);
ctx.translate(Math.round(plot.getPlotOffset().left + xaxis.p2c(tick.v) + xoffset),
box.top + box.padding + plot.getOptions().grid.labelMargin + yoffset);
ctx.rotate(-radsAboveHoriz);
}
var ticksMe = tick.label.split(" ");
// draw labels
var absXoffset = Math.abs(xoffset);
var leftPad = 5;
ctx.fillText(ticksMe[0], absXoffset - leftPad, 0);
if(showWeek){
ctx.fillText(ticksMe[1], (xoffset + leftPad) , yoffset * 2);
showWeek = false;
if(i == ticks.length - 1){
var offset = Math.abs(xoffset * 3);
ctx.rect(offset - 2, -10, 2 ,(yoffset * 4));
ctx.fillStyle = "#868686";
ctx.fill();
}
}
else{
showWeek = true;
ctx.rect(absXoffset - (leftPad * 2) + 2, -10, -2,(yoffset * 4));
ctx.fillStyle = "#868686";
ctx.fill();
}
ctx.restore();
}
}
});
}
$.plot.plugins.push({
init: init,
options: options,
name: 'tickRotor',
version: '1.0'
});
})(jQuery);
As I figured out, I just need to put on ctx.beginPath() and my problem got solved. :(
I am having problems measure the height of a font which I have included with CSS using this code:
measureFontHeight3: function(font)
{
var left = 0;
var top = 0;
var height = 50;
var width = 50;
// Draw the text in the specified area
var canvas = ig.$new('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
ctx.font = font;
ctx.textBaseline = 'top';
ctx.fillText('gM', 0,0);
// Get the pixel data from the canvas
var data = ctx.getImageData(left, top, width, height).data,
first = false,
last = false,
r = height,
c = 0;
// Find the last line with a non-white pixel
while(!last && r)
{
r--;
for(c = 0; c < width; c++)
{
if(data[r * width * 4 + c * 4 + 3])
{
last = r;
break;
}
}
}
// Find the first line with a non-white pixel
while(r)
{
r--;
for(c = 0; c < width; c++)
{
if(data[r * width * 4 + c * 4 + 3]) {
first = r;
break;
}
}
// If we've got it then return the height
if(first != r)
{
var result = last - first;
console.log("3: " +result);
return result;
}
}
// We screwed something up... What do you expect from free code?
return 0;
},
When I measure a font which the system already has installed, the function is quite accurate, but when I try to measure a font which I have included in a CSS file, the measurement does not work, i.e. it measure wrongly.
Is it because of the new canvas not being able to "see" the new font or is something else wrong ?
Could it be because you want to measure the font before it's been fully loaded ?
In my example it seems to be working fine : Font example