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);
}
Related
I made this red line in JavaScript that goes to closest target (balloon 1 to 3) to the player but I need to make it so that it moves like a laser starting from player position into the target position. I thought about multiple ways of implementing this with no luck.
function Tick() {
// Erase the sprite from its current location.
eraseSprite();
for (var i = 0; i < lasers.length; i++) {
lasers[i].x += lasers[i].direction.x * laserSpeed;
lasers[i].y += lasers[i].direction.y * laserSpeed;
//Hit detection here
}
function detectCharClosest() {
var ballon1char = {
x: balloon1X,
y: balloon1Y
};
var ballon2char = {
x: balloon2X,
y: balloon2Y
};
var ballon3char = {
x: balloon3X,
y: balloon3Y,
};
ballon1char.distanceFromPlayer = Math.sqrt((CharX - balloon1X) ** 2 + (CharY - balloon1Y) ** 2);
ballon2char.distanceFromPlayer = Math.sqrt((CharX - balloon2X) ** 2 + (CharY - balloon2Y) ** 2);
ballon3char.distanceFromPlayer = Math.sqrt((CharX - balloon3X) ** 2 + (CharY - balloon3Y) ** 2);
var minDistance = Math.min(
ballon1char.distanceFromPlayer,
ballon2char.distanceFromPlayer,
ballon3char.distanceFromPlayer);
console.log(ballon1char);
console.log(ballon2char);
console.log(ballon3char);
for (let i = 0; i < 3; i++) {
if (minDistance == ballon1char.distanceFromPlayer)
return ballon1char
if (minDistance == ballon2char.distanceFromPlayer)
return ballon2char
if (minDistance == ballon3char.distanceFromPlayer)
return ballon3char
}
}
function loadComplete() {
console.log("Load is complete.");
canvas = document.getElementById("theCanvas");
ctx = canvas.getContext("2d");
myInterval = self.setInterval(function () { Tick() }, INTERVAL);
myInterval = self.setInterval(function () { laserTicker(detectCharClosest()) }, 2000);
function laserTicker(balloon) {
//gets the closest ballon to go to
laserDo(balloon);
}
function laserDo(balloon) {
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = "#F44336"; // "red";
ctx.moveTo(CharX + 16, CharY + 16);
ctx.lineTo(balloon.x, balloon.y);
// lasers.push({x: })
ctx.stroke();
}
I didn't put all of my code here so If something doesn't make sense please tell me. I'm still new to JavaScript and learning it. One way I thought I could make this work was by taking the distance between the player and the target and dividing it by the speed on the x and y axis then changing having it start from the player position and keeps on adding up on both axis until it reaches the target. That didn't work out though. If you have any suggestions then please tell me.
Thanks
So I am fooling around with pixel manipulation in canvas. Right now I have code that allows you to draw to canvas. Then, when you have something drawn, there is a button you can press to manipulate the pixels, translating them either one tile to the right or one tile to the left, alternating every other row. The code looks something like this:
First, pushing the button will start a function that creates two empty arrays where the pixel data is going to go. Then it goes through the pixels, row by row, making each row it's own array. All the row arrays are added into one array of all the pixels data.
$('#shift').click(function() {
var pixels = [];
var rowArray = [];
// get a list of all pixels in a row and add them to pixels array
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
var src = ctx.getImageData(x, y, 1, 1)
var copy = ctx.createImageData(src.width, src.height);
copy.data.set(src.data);
pixels.push(copy);
};
rowArray.push(pixels);
pixels = [];
};
Continuing in the function, next it clears the canvas and shifts the arrays every other either going one to the right or one to the left.
// clear canvas and points list
clearCanvas(ctx);
// take copied pixel lists, shift them
for (i = 0; i < rowArray.length; i ++) {
if (i % 2 == 0) {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, 1));
} else {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, rowArray[i].length - 1));
};
};
Last part of the function now takes the shifted lists of pixel data and distributes them back onto the canvas.
// take the new shifted pixel lists and distribute
// them back onto the canvas
var listCounter = 0;
var listCounter2 = 0;
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
ctx.putImageData(rowArray[listCounter][listCounter2], x, y);
listCounter2 ++;
}
listCounter2 = 0;
listCounter ++;
}
});
As of right now, it works fine. No data is lost and pixels are shifted correctly. What I am wondering if possible, is there a way to do this that is more efficient? Right now, doing this pixel by pixel takes a long time so I have to go by 20x20 px tiles or higher to have realistic load times. This is my first attempt at pixel manipulation so there is probably quite a few things I'm unaware of. It could be my laptop is not powerful enough. Also, I've noticed that sometimes running this function multiple times in a row will significantly reduce load times. Any help or suggestions are much appreciated!
Full function :
$('#shift').click(function() {
var pixels = [];
var rowArray = [];
// get a list of all pixels in a row and add them to pixels array
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
var src = ctx.getImageData(x, y, 1, 1)
var copy = ctx.createImageData(src.width, src.height);
copy.data.set(src.data);
pixels.push(copy);
};
rowArray.push(pixel);
pixels = [];
};
// clear canvas and points list
clearCanvas(ctx);
// take copied pixel lists, shift them
for (i = 0; i < pixelsListList.length; i ++) {
if (i % 2 == 0) {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, 1));
} else {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, rowArray[i].length - 1));
};
};
// take the new shifted pixel lists and distribute
// them back onto the canvas
var listCounter = 0;
var listCounter2 = 0;
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
ctx.putImageData(rowArray[listCounter][listCounter2], x, y);
listCounter2 ++;
}
listCounter2 = 0;
listCounter ++;
}
});
Performance pixel manipulation.
The given answer is so bad that I have to post a better solution.
And with that a bit of advice when it comes to performance critical code. Functional programming has no place in code that requires the best performance possible.
The most basic pixel manipulation.
The example does the same as the other answer. It uses a callback to select the processing and provides a set of functions to create, filter, and set the pixel data.
Because images can be very large 2Megp plus the filter is timed to check performance. The number of pixels, time taken in µs (1/1,000,000th second), pixels per µs and pixels per second. For realtime processing of a HD 1920*1080 you need a rate of ~125,000,000 pixels per second (60fps).
NOTE babel has been turned off to ensure code is run as is. Sorry IE11 users time to upgrade don`t you think?
canvas.addEventListener('click', ()=>{
var time = performance.now();
ctx.putImageData(processPixels(randomPixels,invertPixels), 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
info.textContent = "Time to process " + pixelCount.toLocaleString() + " pixels : " + (time | 0).toLocaleString() + "µs, "+ (rate|0) + "pix per µs "+pps+" pixel per second";
});
const ctx = canvas.getContext("2d");
const pixelCount = innerWidth * innerHeight;
canvas.width = innerWidth;
canvas.height = innerHeight;
const randomPixels = putPixels(ctx,createImageData(canvas.width, canvas.height, randomRGB));
function createImageData(width, height, filter){
return processPixels(ctx.createImageData(width, height), filter);;
}
function processPixels(pixelData, filter = doNothing){
return filter(pixelData);
}
function putPixels(context,pixelData,x = 0,y = 0){
context.putImageData(pixelData,x,y);
return pixelData;
}
// Filters must return pixeldata
function doNothing(pd){ return pd }
function randomRGB(pixelData) {
var i = 0;
var dat32 = new Uint32Array(pixelData.data.buffer);
while (i < dat32.length) { dat32[i++] = 0xff000000 + Math.random() * 0xFFFFFF }
return pixelData;
}
function invertPixels(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
i ++; // skip alpha
}
return pixelData;
}
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
background : rgba(255,255,255,0.75);
}
.m {
top : 100px;
z-index : 10;
}
#info {
z-index : 10;
}
<div class="abs" id="info"></div>
<div class="abs m">Click to invert</div>
<canvas class="abs" id="canvas"></canvas>
Why functional programming is bad for pixel processing.
To compare below is a timed version of George Campbell Answer that uses functional programming paradigms. The rate will depend on the device and browser but is 2 orders of magnitude slower.
Also if you click, repeating the invert function many times you will notice the GC lags that make functional programming such a bad choice for performance code.
The standard method (first snippet) does not suffer from GC lag because it barely uses any memory apart from the original pixel buffer.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
//maybe put inside resize event listener
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const pixelCount = innerWidth * innerHeight;
//create some test pixels (random colours) - only once for entire width/height, not for each pixel
let randomPixels = createImageData(width, height, randomRGB);
//create image data and apply callback for each pixel, set this in the ImageData
function createImageData(width, height, cb){
let createdPixels = ctx.createImageData(width, height);
if(cb){
let pixelData = editImageData(createdPixels, cb);
createdPixels.data.set(pixelData);
}
return createdPixels;
}
//edit each pixel in ImageData using callback
//pixels ImageData, cb Function (for each pixel, returns r,g,b,a Boolean)
function editImageData(pixels, cb = (p)=>p){
return Array.from(pixels.data).map((pixel, i) => {
//red or green or blue or alpha
let newValue = cb({r: i%4 === 0, g:i%4 === 1, b:i%4 === 2, a:i%4 === 3, value: pixel});
if(typeof newValue === 'undefined' || newValue === null){
throw new Error("undefined/null pixel value "+typeof newValue+" "+newValue);
}
return newValue;
});
}
//callback to apply to each pixel (randomize)
function randomRGB({a}){
if(a){
return 255; //full opacity
}
return Math.floor(Math.random()*256);
};
//another callback to apply, this time invert
function invertRGB({a, value}){
if(a){
return 255; //full opacity
}
return 255-value;
};
ctx.putImageData(randomPixels, 0, 0);
//click to change invert image data (or any custom pixel manipulation)
canvas.addEventListener('click', ()=>{
var time = performance.now();
randomPixels.data.set(editImageData(randomPixels, invertRGB));
ctx.putImageData(randomPixels, 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
if(rate < 1){
rate = "less than 1";
}
info.textContent = "Time to process " + pixelCount.toLocaleString() + " pixels : " + (time|0).toLocaleString() + "µs, "+ rate + "pix per µs "+pps+" pixel per second";
});
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
background : rgba(255,255,255,0.75);
}
.m {
top : 100px;
z-index : 10;
}
#info {
z-index : 10;
}
<div class="abs" id="info"></div>
<div class="abs m">George Campbell Answer. Click to invert</div>
<canvas class="abs" id="canvas"></canvas>
Some more pixel processing
The next sample demonstrates some basic pixel manipulation.
Random. Totaly random pixels
Invert. Inverts the pixel colors
B/W. Converts to simple black and white (not perceptual B/W)
Noise. Adds strong noise to pixels. Will reduce total brightness.
2 Bit. Pixel channel data is reduced to 2 bits per RGB.
Blur. Most basic blur function requires a copy of the pixel data to work and is thus expensive in terms of memory and processing overheads. But as NONE of the canvas/SVG filters do the correct logarithmic filter this is the only way to get a good quality blur for the 2D canvas. Unfortunately it is rather slow.
Channel Shift. Moves channels blue to red, red to green, green to blue
Shuffle pixels. Randomly shuffles pixels with one of its neighbours.
For larger images. To prevent filters from blocking the page you would move the imageData to a worker and process the pixels there.
document.body.addEventListener('click', (e)=>{
if(e.target.type !== "button" || e.target.dataset.filter === "test"){
testPattern();
pixels = getImageData(ctx);
info.textContent = "Untimed content render."
return;
}
var time = performance.now();
ctx.putImageData(processPixels(pixels,pixelFilters[e.target.dataset.filter]), 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
info.textContent = "Filter "+e.target.value+ " " +(e.target.dataset.note ? e.target.dataset.note : "") + pixelCount.toLocaleString() + "px : " + (time | 0).toLocaleString() + "µs, "+ (rate|0) + "px per µs "+pps+" pps";
});
const ctx = canvas.getContext("2d");
const pixelCount = innerWidth * innerHeight;
canvas.width = innerWidth;
canvas.height = innerHeight;
var min = Math.min(innerWidth,innerHeight) * 0.45;
function testPattern(){
var grad = ctx.createLinearGradient(0,0,0,canvas.height);
grad.addColorStop(0,"#000");
grad.addColorStop(0.5,"#FFF");
grad.addColorStop(1,"#000");
ctx.fillStyle = grad;
ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height);
"000,AAA,FFF,F00,00F,A00,00A,FF0,0FF,AA0,0AA,0F0,F0F,0A0,A0A".split(",").forEach((col,i) => {
circle("#"+col, min * (1-i/16));
});
}
function circle(col,size){
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(canvas.width / 2, canvas.height / 2, size, 0 , Math.PI * 2);
ctx.fill();
}
testPattern();
var pixels = getImageData(ctx);
function getImageData(ctx, x = 0, y = 0,width = ctx.canvas.width, height = ctx.canvas.height){
return ctx.getImageData(x,y,width, height);
}
function createImageData(width, height, filter){
return processPixels(ctx.createImageData(width, height), filter);;
}
function processPixels(pixelData, filter = doNothing){
return filter(pixelData);
}
function putPixels(context,pixelData,x = 0,y = 0){
context.putImageData(pixelData,x,y);
return pixelData;
}
// Filters must return pixeldata
function doNothing(pd){ return pd }
function randomRGB(pixelData) {
var i = 0;
var dat32 = new Uint32Array(pixelData.data.buffer);
while (i < dat32.length) { dat32[i++] = 0xff000000 + Math.random() * 0xFFFFFF }
return pixelData;
}
function randomNoise(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = Math.random() * dat[i++];
dat[i] = Math.random() * dat[i++];
dat[i] = Math.random() * dat[i++];
i ++; // skip alpha
}
return pixelData;
}
function twoBit(pixelData) {
var i = 0;
var dat = pixelData.data;
var scale = 255 / 196;
while (i < dat.length) {
dat[i] = (dat[i++] & 196) * scale;
dat[i] = (dat[i++] & 196) * scale;
dat[i] = (dat[i++] & 196) * scale;
i ++; // skip alpha
}
return pixelData;
}
function invertPixels(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
i ++; // skip alpha
}
return pixelData;
}
function simpleBW(pixelData) {
var bw,i = 0;
var dat = pixelData.data;
while (i < dat.length) {
bw = (dat[i] + dat[i+1] + dat[i+2]) / 3;
dat[i++] = bw;
dat[i++] = bw;
dat[i++] = bw;
i ++; // skip alpha
}
return pixelData;
}
function simpleBlur(pixelData) {
var i = 0;
var dat = pixelData.data;
var buf = new Uint8Array(dat.length);
buf.set(dat);
var w = pixelData.width * 4;
i += w;
while (i < dat.length - w) {
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
i ++; // skip alpha
}
return pixelData;
}
function channelShift(pixelData) {
var r,g,i = 0;
var dat = pixelData.data;
while (i < dat.length) {
r = dat[i];
g = dat[i+1];
dat[i] = dat[i+2];
dat[i+1] = r;
dat[i+2] = g;
i += 4;
}
return pixelData;
}
function pixelShuffle(pixelData) {
var r,g,b,n,rr,gg,bb,i = 0;
var dat = pixelData.data;
var next = [-pixelData.width*4,pixelData.width*4,-4,4];
var len = dat.length;
while (i < dat.length) {
n = (i + next[Math.random() * 4 | 0]) % len;
r = dat[i];
g = dat[i+1];
b = dat[i+2];
dat[i] = dat[n];
dat[i+1] = dat[n + 1];
dat[i+2] = dat[n + 2];
dat[n] = r;
dat[n+1] = g;
dat[n+2] = b;
i += 4;
}
return pixelData;
}
const pixelFilters = {
randomRGB,
invertPixels,
simpleBW,
randomNoise,
twoBit,
simpleBlur,
channelShift,
pixelShuffle,
}
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
}
.m {
top : 30px;
z-index : 20;
}
#info {
z-index : 10;
background : rgba(255,255,255,0.75);
}
<canvas class="abs" id="canvas"></canvas>
<div class="abs" id="buttons">
<input type ="button" data-filter = "randomRGB" value ="Random"/>
<input type ="button" data-filter = "invertPixels" value ="Invert"/>
<input type ="button" data-filter = "simpleBW" value ="B/W"/>
<input type ="button" data-filter = "randomNoise" value ="Noise"/>
<input type ="button" data-filter = "twoBit" value ="2 Bit" title = "pixel channel data is reduced to 2 bits per RGB"/>
<input type ="button" data-note="High quality blur using logarithmic channel values. " data-filter = "simpleBlur" value ="Blur" title = "Blur requires a copy of pixel data"/>
<input type ="button" data-filter = "channelShift" value ="Ch Shift" title = "Moves channels blue to red, red to green, green to blue"/>
<input type ="button" data-filter = "pixelShuffle" value ="Shuffle" title = "randomly shuffles pixels with one of its neighbours"/>
<input type ="button" data-filter = "test" value ="Test Pattern"/>
</div>
<div class="abs m" id="info"></div>
It makes more sense to use something like ctx.getImageData or .createImageData only once per image, not for each pixel.
You can loop the ImageData.data "array-like" Uint8ClampedArray. Each 4 items in the array represent a single pixel, these being red, green, blue, and alpha parts of the pixel. Each can be an integer between 0 and 255, where [0,0,0,0,255,255,255,255,...] means the first pixel is transparent (and black?), and the second pixel is white and full opacity.
here is something I just made, not benchmarked but likely more efficient.
It creates image data, and you can edit image data by passing in a function to the edit image data function, the callback function is called for each pixel in an image data and returns an object containing value (between 0 and 255), and booleans for r, g, b.
For example for invert you can return 255-value.
this example starts with random pixels, clicking them will apply the invertRGB function to it.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
//maybe put inside resize event listener
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
//create some test pixels (random colours) - only once for entire width/height, not for each pixel
let randomPixels = createImageData(width, height, randomRGB);
//create image data and apply callback for each pixel, set this in the ImageData
function createImageData(width, height, cb){
let createdPixels = ctx.createImageData(width, height);
if(cb){
let pixelData = editImageData(createdPixels, cb);
createdPixels.data.set(pixelData);
}
return createdPixels;
}
//edit each pixel in ImageData using callback
//pixels ImageData, cb Function (for each pixel, returns r,g,b,a Boolean)
function editImageData(pixels, cb = (p)=>p){
let i = 0;
let len = pixels.data.length;
let outputPixels = [];
for(i=0;i<len;i++){
let pixel = pixels.data[i];
outputPixels.push( cb(i%4, pixel) );
}
return outputPixels;
}
//callback to apply to each pixel (randomize)
function randomRGB(colour){
if( colour === 3){
return 255; //full opacity
}
return Math.floor(Math.random()*256);
};
//another callback to apply, this time invert
function invertRGB(colour, value){
if(colour === 3){
return 255; //full opacity
}
return 255-value;
};
ctx.putImageData(randomPixels, 0, 0);
//click to change invert image data (or any custom pixel manipulation)
canvas.addEventListener('click', ()=>{
let t0 = performance.now();
randomPixels.data.set(editImageData(randomPixels, invertRGB));
ctx.putImageData(randomPixels, 0, 0);
let t1 = performance.now();
console.log(t1-t0+"ms");
});
#canvas {
position: absolute;
top: 0;
left: 0;
}
<canvas id="canvas"></canvas>
code gist: https://gist.github.com/GCDeveloper/c02ffff1d067d6f1b1b13341a72efe79
check out https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas which should help, including loading an actual image as ImageData for usage.
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 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.