Getting CSS transform rotateX angle from matrix3d - javascript

I'm working on a tool that lets you browse through content in a "virtual magazine". In order to realize the turn-over animation, I need to be able to get the current rotation angle during every frame of the animation, in order to flip front- and backside when it's at 90 degrees.
I got this to work, using requestAnimationFrame and the matrix calculation from this article:
https://css-tricks.com/get-value-of-css-rotation-through-javascript/
This only works, when turning over pages left and right though (with rotateY). If I want to turn over vertically (calender style) I need to be able to calculate the angle for rotateX.
Can anyone math savvy help me out here?
Cheers and thanks in advance!
Edit: Here is the function that works for transformY. The element to be checked gets animated by adding transform: rotateY(-180deg) with a transform origin of left:
function checkTransitionProgress() {
// SRC: https://css-tricks.com/get-value-of-css-rotation-through-javascript/
el = loremPages[index];
var st = window.getComputedStyle(el, null);
var tr = st.getPropertyValue("-webkit-transform") ||
st.getPropertyValue("-moz-transform") ||
st.getPropertyValue("-ms-transform") ||
st.getPropertyValue("-o-transform") ||
st.getPropertyValue("transform") ||
false;
var values = tr.split('(')[1].split(')')[0].split(',');
var a = values[0];
var b = values[1];
var c = values[2];
var d = values[3];
var scale = Math.sqrt(a * a + b * b);
var sin = b / scale;
var angle = Math.round(Math.atan2(b, a) * (180 / Math.PI));
if (direction == 'forwards') {
if (angle < 90) {
window.requestAnimationFrame(checkTransitionProgress);
} else {
target.style.zIndex = index;
target.frontSide.classList.remove('lorem__side--in-front');
target.backSide.classList.add('lorem__side--in-front');
}
} else {
if (angle > 90) {
window.requestAnimationFrame(checkTransitionProgress);
} else {
target.style.zIndex = targetInvertedIndex;
target.frontSide.classList.add('lorem__side--in-front');
target.backSide.classList.remove('lorem__side--in-front');
}
}
}
checkTransitionProgress();

Figured it out myself. You have to use a = values[5] and b = values[4] for rotateX.

Related

Three.js: rotate camera with both touch and device orientation

I am making a 3D project with threejs which allows control of the camera with mouse for computer devices, and also allows control with touch events and deviceorientation event for smartphones.
As an example, this site works the same way as what I want to do.
As I am using OrbitControls to move camera on the PC version, I bound the touchstart/move/end events to mousedown/move/up and it works perfectly.
The problem is when I try to add the device orientation event's values. Here is what I tried to add in OrbitControls.js :
THREE.OrbitControls = function (object, domElement) {
const scope = this;
let lastBeta = 0;
let lastGamma = 0;
this.deviceOrientation = {};
function onDeviceOrientationChangeEvent(event) {
scope.deviceOrientation = event;
// Z
var alpha = scope.deviceOrientation.alpha
? THREE.Math.degToRad(scope.deviceOrientation.alpha)
: 0;
// X'
var beta = scope.deviceOrientation.beta
? THREE.Math.degToRad(scope.deviceOrientation.beta)
: 0;
// Y''
var gamma = scope.deviceOrientation.gamma
? THREE.Math.degToRad(scope.deviceOrientation.gamma)
: 0;
// O
var orient = scope.screenOrientation
? THREE.Math.degToRad(scope.screenOrientation)
: 0;
rotateLeft(lastGamma - gamma);
rotateUp(lastBeta - beta);
lastBeta = beta; //is working
lastGamma = gamma; //doesn't work properly
}
window.addEventListener('deviceorientation', onDeviceOrientationChangeEvent, false);
};
As beta's values are within a [-180,180] degree range the vertical rotation encounters no problem, whereas gamma's range is [-90,90] and values are also changing suddenly when orientating device' screen up and down (even if, I think, it should return horizontal rotation).
And even when converting gamma's range to make it takes values from -180 to 180, the sudden shifts make everything goes wrong.
I guess that I have to use quaternions as in deviceOrientationControls.js, but I really don't know how it works and every attempt I've made so far was a fail. Can someone help me please?
PS: Here is a link to the description on the deviceorientation event to have a better comprehension of what really are alpha beta and gamma.
EDIT
I added a snippet bellow to show the beta and gamma variations.
let deltaBeta = 0;
let deltaGamma = 0;
if (window.DeviceOrientationEvent) {
window.addEventListener('deviceorientation', function (e) {
const beta = (e.beta != null) ? Math.round(e.beta) : 0;
const gamma = (e.gamma != null) ? Math.round(e.gamma) : 0;
deltaBeta = Math.abs(beta - deltaBeta);
deltaGamma = Math.abs(gamma - deltaGamma);
$("#beta").html("Beta: " + beta);
$("#gamma").html("Gamma: " + gamma);
if (Math.abs(deltaBeta) > Math.abs(Number($("#deltaBeta").html()))) {
$("#deltaBeta").html(deltaBeta);
if (Number($("#deltaBeta").html()) >= 30) {
$("#deltaBeta").removeAttr("class", "blue").addClass("red");
}
}
if (Math.abs(deltaGamma) > Math.abs(Number($("#deltaGamma").html()))) {
$("#deltaGamma").html(deltaGamma);
if (Number($("#deltaGamma").html()) >= 30) {
$("#deltaGamma").removeAttr("class", "blue").addClass("red");
}
}
}, true);
} else {
$("#gamma").html("deviceorientation not supported");
}
.red {
color: red;
font-weight: bold;
}
.blue {
color: blue;
font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<div>
<span id="beta"></span>
<span> [-180; 180]</span>
</div>
<div>
<span>DeltaMax</span>
<span id="deltaBeta" class="blue">0</span>
</div>
<div>
<span id="gamma"></span>
<span> [-90; 90]</span>
</div>
<div>
<span>DeltaMax</span>
<span id="deltaGamma" class="blue">0</span>
</div>
</body>
I found a solution using a function to convert quaternions to radians, so I wanted to share it if someone wants to do a click/touch+device orientation control using OrbitControls.
I take the initial orientation (x1,y1,z1) and calculate the new one (x2,y2,z3) and the difference between them is the variation of the rotation done by the camera. I add these line to the initial update function
this.update = function () {
// Z
const alpha = scope.deviceOrientation.alpha
? THREE.Math.degToRad(scope.deviceOrientation.alpha)
: 0;
// X'
const beta = scope.deviceOrientation.beta
? THREE.Math.degToRad(scope.deviceOrientation.beta)
: 0;
// Y''
const gamma = scope.deviceOrientation.gamma
? THREE.Math.degToRad(scope.deviceOrientation.gamma)
: 0;
// O
const orient = scope.screenOrientation
? THREE.Math.degToRad(scope.screenOrientation)
: 0;
const currentQ = new THREE.Quaternion().copy(scope.object.quaternion);
setObjectQuaternion(currentQ, alpha, beta, gamma, orient);
const currentAngle = Quat2Angle(currentQ.x, currentQ.y, currentQ.z, currentQ.w);
// currentAngle.z = left - right
this.rotateLeft((lastGamma - currentAngle.z) / 2);
lastGamma = currentAngle.z;
// currentAngle.y = up - down
this.rotateUp(lastBeta - currentAngle.y);
lastBeta = currentAngle.y;
}
Listeners
function onDeviceOrientationChangeEvent(event) {
scope.deviceOrientation = event;
}
window.addEventListener('deviceorientation', onDeviceOrientationChangeEvent, false);
function onScreenOrientationChangeEvent(event) {
scope.screenOrientation = window.orientation || 0;
}
window.addEventListener('orientationchange', onScreenOrientationChangeEvent, false);
Functions
var setObjectQuaternion = function () {
const zee = new THREE.Vector3(0, 0, 1);
const euler = new THREE.Euler();
const q0 = new THREE.Quaternion();
const q1 = new THREE.Quaternion(-Math.sqrt(0.5), 0, 0, Math.sqrt(0.5));
return function (quaternion, alpha, beta, gamma, orient) {
// 'ZXY' for the device, but 'YXZ' for us
euler.set(beta, alpha, -gamma, 'YXZ');
// Orient the device
quaternion.setFromEuler(euler);
// camera looks out the back of the device, not the top
quaternion.multiply(q1);
// adjust for screen orientation
quaternion.multiply(q0.setFromAxisAngle(zee, -orient));
}
} ();
function Quat2Angle(x, y, z, w) {
let pitch, roll, yaw;
const test = x * y + z * w;
// singularity at north pole
if (test > 0.499) {
yaw = Math.atan2(x, w) * 2;
pitch = Math.PI / 2;
roll = 0;
return new THREE.Vector3(pitch, roll, yaw);
}
// singularity at south pole
if (test < -0.499) {
yaw = -2 * Math.atan2(x, w);
pitch = -Math.PI / 2;
roll = 0;
return new THREE.Vector3(pitch, roll, yaw);
}
const sqx = x * x;
const sqy = y * y;
const sqz = z * z;
yaw = Math.atan2((2 * y * w) - (2 * x * z), 1 - (2 * sqy) - (2 * sqz));
pitch = Math.asin(2 * test);
roll = Math.atan2((2 * x * w) - (2 * y * z), 1 - (2 * sqx) - (2 * sqz));
return new THREE.Vector3(pitch, roll, yaw);
}

Worst quality perspective image on canvas

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.

Why do my laser rays flicker?

I'm trying my hands at some simple game programming in Javascript and have come to realize I need to change the way I handle sprites. The only question is, "how"?
I have a hero that moves around with the arrow keys and fires laser rays with WASD. This is how I define rays:
function Ray (x, y, width, height, direction, index) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.direction = direction;
this.index = index;
this.speed = 512;
this.disabled = false;
}
The index just indicates where in an array of rays (heh) it is being stored. I currently have a hard-coded limit of 5 simultaneous rays, although the other restrictions (screen size, ray size, speed, hero size etc) shouldn't allow for more than 4:
var rays = [];
var numberOfRays = 0;
var rayLimit = 5;
var shotClock = 300;
And so, in the update() function that gets called by the game loop, I have listeners for the WASD keys. They look like this:
// D
if (68 in keysDown && numberOfRays <= rayLimit && Date.now() - lastShootTime > shotClock) {
lastShootTime = Date.now();
var newRayIndex = findFreeRay();
rays[newRayIndex] = new Ray(hero.x + hero.width + 12, hero.y + hero.height / 2, rayImage.width, rayImage.height, 'right', newRayIndex);
numberOfRays++;
}
(findFreeRay() just returns the lowest unused or disabled (off the screen) index in rays[])
Earlier in the update() method (I have also tried putting it later) I have the logic for updating ray movement:
rays.forEach(function(ray) {
if (ray != null && !ray.disabled) {
switch(ray.direction) {
case 'right':
ray.x += ray.speed * modifier;
break;
case 'left':
ray.x -= ray.speed * modifier;
break;
case 'up':
ray.y -= ray.speed * modifier;
break;
case 'down':
ray.y += ray.speed * modifier;
break;
}
}
});
Finally, there is the image for the ray (actually, one for horizontal rays and another one for vertical rays). Currently, I am using one Image object of each globally, that the existing rays share. But I have also tried, without much luck, to create individual image objects for every ray.
// Ray images
var rayReady = false;
var rayImage = new Image();
rayImage.onload = function() {
rayReady = true;
};
rayImage.src = "images/ray.png";
var rayVertReady = false;
var rayVertImage = new Image();
rayVertImage.onload = function() {
rayVertReady = true;
};
rayVertImage.src = "images/ray_vert.png";
And here is how they get drawn:
if (rayReady && rayVertReady && numberOfRays > 0) {
rays.forEach(function(ray) {
if (ray.x > canvas.width
|| ray.x + ray.width < 0
|| ray.y > canvas.height
|| ray.y + ray.height < 0) {
numberOfRays--;
ray.disabled = true;
}
else if (ray.direction == 'right' || ray.direction == 'left'){
ctx.drawImage(rayImage, ray.x, ray.y);
}
else {
ctx.drawImage(rayVertImage, ray.x, ray.y);
}
});
}
The problem
After shooting only a few rays, new ones start to either flicker and disappear, or stay invisible altogether. They actually exist as gameplay objects though, as they can hit targets.
What likely causes this flickering?
(credit to Matt Hackett for the base of this code)
fiddle: http://jsfiddle.net/Vr3MW/

Zooming in fabricjs, object positions

I have a problem ( i believe it is similar to the one calcOffset is fixing) with positions of objects.
I zoom in-out with mouse scroll wheel. After zooming, shapes are displayed at new positions but cannot be handled at these position. Only at their pre-zoom positions.
jsfiddle example
Here is my code for zooming:
function displaywheel(e){
var SCALE_FACTOR = 1.1;
var evt=window.event || e
var delta=evt.detail? evt.detail*(-120) : evt.wheelDelta
var objects = canvas.getObjects();
var dd = 1;
if (delta == 120) dd=SCALE_FACTOR;
if (delta == -120) dd=1/SCALE_FACTOR;
globscale = globscale * dd;
for (var i in objects) {
objects[i].setCoords;
objects[i].scaleX = globscale;
objects[i].scaleY = globscale;
objects[i].left = objects[i].left * dd;
objects[i].top = objects[i].top * dd;
objects[i].setCoords;
}
canvas.renderAll();
canvas.calcOffset();
}
setCoords is a function, you need to call it like this:
objects[i].setCoords();
objects[i].setCoords; does absolutely nothing.
http://jsfiddle.net/w5NjC/1/

Math to find edge between two boxes

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.

Categories

Resources