How to calculate viewport aligned polygon bounding box? - javascript

I have a problem with bounding box points calculation. I'm using three.js to render polygons, it's basically 2D with orthographic camera. Unfortunately, simple bounding box calculation - iterate over points and get extreme values doesn't work correctly after camera is rotated. It stays aligned to axes. I'd like bounding box to be aligned to a viewport (just like in the picture below). It can be rotated by any angle, has to be always aligned to a viewport.
I added an example below - how to calculate points of the bounding box on the right?
Image description: left - trivial bounding box without rotation, middle - axis aligned bounding box, right - desired result - viewport aligned bounding box
Fiddle producing middle case: https://jsfiddle.net/tqrc2ue6/5/
var camera, scene, renderer, geometry, material, line, axesHelper, boundingBoxGeometry, boundingBoxLine;
const polygonPoints = [{
x: 10,
y: 10,
z: 0
},
{
x: 15,
y: 15,
z: 0
},
{
x: 20,
y: 10,
z: 0
},
{
x: 25,
y: 20,
z: 0
},
{
x: 15,
y: 20,
z: 0
},
{
x: 10,
y: 10,
z: 0
},
]
function getBoundingBoxGeometry(geometry) {
geometry.computeBoundingBox();
const boundingBox = geometry.boundingBox;
const boundingBoxPoints = [{
x: boundingBox.min.x,
y: boundingBox.min.y,
z: 0
},
{
x: boundingBox.max.x,
y: boundingBox.min.y,
z: 0
},
{
x: boundingBox.max.x,
y: boundingBox.max.y,
z: 0
},
{
x: boundingBox.min.x,
y: boundingBox.max.y,
z: 0
},
{
x: boundingBox.min.x,
y: boundingBox.min.y,
z: 0
},
];
return new THREE.BufferGeometry().setFromPoints(boundingBoxPoints);
}
init();
animate();
function init() {
scene = new THREE.Scene();
axesHelper = new THREE.AxesHelper(10);
scene.add(axesHelper);
//camera = new THREE.OrthographicCamera(-25, 25, -25, 25, -1, 1);
//camera.position.set(15, 15)
//camera.rotation.z = -Math.PI / 4
var frustumSize = 50
var aspect = window.innerWidth / window.innerHeight;
camera = new THREE.OrthographicCamera(frustumSize * aspect / -2, frustumSize * aspect / 2, frustumSize / 2, frustumSize / -2, -1, 1);
//camera.rotation.z = 2 * Math.PI /3
camera.rotation.z = 3 * Math.PI / 4
camera.position.set(15, 15)
scene.add(camera);
geometry = new THREE.BufferGeometry().setFromPoints(polygonPoints);
boundingBoxGeometry = getBoundingBoxGeometry(geometry);
material = new THREE.LineBasicMaterial({
color: 0xffffff
});
line = new THREE.Line(geometry, material);
scene.add(line);
boundingBoxLine = new THREE.Line(boundingBoxGeometry, material)
scene.add(boundingBoxLine);
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
}
function animate() {
requestAnimationFrame(animate);
render();
}
function render() {
renderer.render(scene, camera);
}

The left figure is obtained by computing the bounding box of the original coordinates.
The right figure is obtained by computing the bounding box of the rotated coordinates.
The central figure is obtained by computing the bounding box in the original coordinates and applying the rotation to the corners. It is no more axis aligned wrt the original coordinates.

Yves explained the concept. You need to convert points from one coordinate system to another to solve this. But since it's an orthographic view, you can also use camera projection for conversions.
In this way, we project all the points to the screen coordinate system, we calculate the position of the box in this coordinate, and then we unproject the points of the box to the world coordinate system.
I updated your sample to demonstrate this. Just keep in mind that width and height of this rectangle are valid only if view direction be parallel to one of the X, Y or Z axes.

Related

How can I rotate any shape or point on an HTML5 canvas around an arbitrary point?

I have seen a few cases of questions asking how to rotate a shape around a given point and decided to ask this self-answer question myself. So - in relation to HTML5 canvas, or in fact any two-dimensional surface, how can I rotate a shape around an arbitrary x,y point ?
It turns out the answer is pretty simple but involves a bit of math that may put some folks off. I'm using the Konvajs HTML5 canvas library but the code is easily transportable to your own lib. Also, this example is described as rotating a shape, but it's really rotating a point - the origin of the shape - so you could use it for any point-rotation-around-a-point case.
The rotateAroundPoint() function does the work - the rest of the code in the snippet is there to make it a working example.
Lifting this function out we can see that the inputs are the shape - although this could be any object with x, y and rotation properties, the rotation angle in degrees, and the rotation point - again an object with x & y values.
When we rotate around the point we are carrying out the equivalent of a rotation-in-place, followed by a translation (or move). These must be done in this sequence. Also, because of how 2d-drawing works, we have to work out the new position for the move and this depends on the drawing origin of the shape.
The calculation of the new x & y positions requires the use of sine & cosine functions which require radians - not degrees. So we multiply degrees by PI / 180 to get that.
// Rotate a shape around any point.
// shape is a Konva shape
// angleDegrees is the angle to rotate by, in degrees
// point is an object {x: posX, y: posY}
function rotateAroundPoint(shape, angleDegrees, point) {
let angleRadians = angleDegrees * Math.PI / 180; // sin + cos require radians
const x =
point.x +
(shape.x() - point.x) * Math.cos(angleRadians) -
(shape.y() - point.y) * Math.sin(angleRadians);
const y =
point.y +
(shape.x() - point.x) * Math.sin(angleRadians) +
(shape.y() - point.y) * Math.cos(angleRadians);
shape.rotation(shape.rotation() + angleDegrees); // rotate the shape in place
shape.x(x); // move the rotated shape in relation to the rotation point.
shape.y(y);
}
That's it! Have a play with the snippet - best viewed full-screen. Select a shape to rotate, then click the rotate button a few times to watch it spin around its origin (the natural point of rotation if we just change the rotation angle and nothing else). Then click the reset button, and click the canvas to move the blue target somewhere else on the canvas or shape, and rotate some more to see the effect.
There's also a codepen version here.
// Code to illustrate rotation of a shape around any given point. The important functions here is rotateAroundPoint() which does the rotation and movement math !
let
angle = 0, // display value of angle
startPos = {x: 80, y: 45},
shapes = [], // array of shape ghosts / tails
rotateBy = 20, // per-step angle of rotation
shapeName = $('#shapeName').val(), // what shape are we drawing
shape = null,
ghostLimit = 10,
// Set up a stage
stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
}),
// add a layer to draw on
layer = new Konva.Layer(),
// create the rotation target point cross-hair marker
lineV = new Konva.Line({points: [0, -20, 0, 20], stroke: 'cyan', strokeWidth: 1}),
lineH = new Konva.Line({points: [-20, 0, 20, 0], stroke: 'cyan', strokeWidth: 1}),
circle = new Konva.Circle({x: 0, y: 0, radius: 10, fill: 'transparent', stroke: 'cyan', strokeWidth: 1}),
cross = new Konva.Group({draggable: true, x: startPos.x, y: startPos.y});
// Add the elements to the cross-hair group
cross.add(lineV, lineH, circle);
layer.add(cross);
// Add the layer to the stage
stage.add(layer);
$('#shapeName').on('change', function(){
shapeName = $('#shapeName').val();
shape.destroy();
shape = null;
reset();
})
// Draw whatever shape the user selected
function drawShape(){
// Add a shape to rotate
if (shape !== null){
shape.destroy();
}
switch (shapeName){
case "rectangle":
shape = new Konva.Rect({x: startPos.x, y: startPos.y, width: 120, height: 80, fill: 'magenta', stroke: 'black', strokeWidth: 4});
break;
case "hexagon":
shape = new Konva.RegularPolygon({x: startPos.x, y: startPos.y, sides: 6, radius: 40, fill: 'magenta', stroke: 'black', strokeWidth: 4});
break;
case "ellipse":
shape = new Konva.Ellipse({x: startPos.x, y: startPos.y, radiusX: 40, radiusY: 20, fill: 'magenta', stroke: 'black', strokeWidth: 4});
break;
case "circle":
shape = new Konva.Ellipse({x: startPos.x, y: startPos.y, radiusX: 40, radiusY: 40, fill: 'magenta', stroke: 'black', strokeWidth: 4});
break;
case "star":
shape = new Konva.Star({x: startPos.x, y: startPos.y, numPoints: 5, innerRadius: 20, outerRadius: 40, fill: 'magenta', stroke: 'black', strokeWidth: 4});
break;
};
layer.add(shape);
cross.moveToTop();
}
// Reset the shape position etc.
function reset(){
drawShape(); // draw the current shape
// Set to starting position, etc.
shape.position(startPos)
cross.position(startPos);
angle = 0;
$('#angle').html(angle);
$('#position').html('(' + shape.x() + ', ' + shape.y() + ')');
clearTails(); // clear the tail shapes
stage.draw(); // refresh / draw the stage.
}
// Click the stage to move the rotation point
stage.on('click', function (e) {
cross.position(stage.getPointerPosition());
stage.draw();
});
// Rotate a shape around any point.
// shape is a Konva shape
// angleRadians is the angle to rotate by, in radians
// point is an object {x: posX, y: posY}
function rotateAroundPoint(shape, angleDegrees, point) {
let angleRadians = angleDegrees * Math.PI / 180; // sin + cos require radians
const x =
point.x +
(shape.x() - point.x) * Math.cos(angleRadians) -
(shape.y() - point.y) * Math.sin(angleRadians);
const y =
point.y +
(shape.x() - point.x) * Math.sin(angleRadians) +
(shape.y() - point.y) * Math.cos(angleRadians);
shape.rotation(shape.rotation() + angleDegrees); // rotate the shape in place
shape.x(x); // move the rotated shape in relation to the rotation point.
shape.y(y);
shape.moveToTop(); //
}
$('#rotate').on('click', function(){
let newShape = shape.clone();
shapes.push(newShape);
layer.add(newShape);
// This ghost / tails stuff is just for fun.
if (shapes.length >= ghostLimit){
shapes[0].destroy();
shapes = shapes.slice(1);
}
for (var i = shapes.length - 1; i >= 0; i--){
shapes[i].opacity((i + 1) * (1/(shapes.length + 2)))
};
// This is the important call ! Cross is the rotation point as illustrated by crosshairs.
rotateAroundPoint(shape, rotateBy, {x: cross.x(), y: cross.y()});
cross.moveToTop();
stage.draw();
angle = angle + 10;
$('#angle').html(angle);
$('#position').html('(' + Math.round(shape.x() * 10) / 10 + ', ' + Math.round(shape.y() * 10) / 10 + ')');
})
// Function to clear the ghost / tail shapes
function clearTails(){
for (var i = shapes.length - 1; i >= 0; i--){
shapes[i].destroy();
};
shapes = [];
}
// User cicks the reset button.
$('#reset').on('click', function(){
reset();
})
// Force first draw!
reset();
body {
margin: 10;
padding: 10;
overflow: hidden;
background-color: #f0f0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#^3/konva.min.js"></script>
<p>1. Click the rotate button to see what happens when rotating around shape origin.</p>
<p>2. Reset then click stage to move rotation point and click rotate button again - rinse & repeat</p>
<p>
<button id = 'rotate'>Rotate</button>
<button id = 'reset'>Reset</button>
<select id='shapeName'>
<option value='rectangle'>Rectangle</option>
<option value='hexagon'>Polygon</option>
<option value='ellipse' >Ellipse</option>
<option value='circle' >Circle</option>
<option value='star' selected='selected'>Star</option>
</select>
Angle : <span id='angle'>0</span>
Position : <span id='position'></span>
</p>
<div id="container"></div>

Reverse the effects of a Tween.js animation

I'm trying to animate a three.js block in such a way that it returns to its original position when the animation ends, using tween.js.
Is there a way to achieve this with tween.js only using one tween?
I have got this working as shown below:
var position = {x: -200, y: 150, width: 1, height: 1, depth: 1, rotx: -0.5, roty: 0.7, rotz: 0.9};
var target = {x: 200, y: -100, width: 0.4, height: 3, depth: 8, rotx: 0.3, roty: -0.4, rotz: -0.6};
var position2 = {x: -200, y: 150, width: 1, height: 1, depth: 1, rotx: -0.5, roty: 0.7, rotz: 0.9};
var mesh = new THREE.Mesh(
new THREE.CubeGeometry(190, 45, 30),
new THREE.MeshBasicMaterial({color: 0x444444}),
0
);
mesh.position.set(position.x, position.y, 0);
mesh.rotation.set(position.rotx, position.roty, position.rotz);
scene.add(mesh);
var t1 = new TWEEN.Tween(position).to(target, 2000);
t1.onUpdate(function() {
mesh.position.set(position.x, position.y, 0);
mesh.scale.set(position.width, position.height, position.depth);
mesh.rotation.set(position.rotx, position.roty, position.rotz);
});
t1.easing(TWEEN.Easing.Quadratic.Out);
t1.onComplete(function() {t2.start();});
var t2 = new TWEEN.Tween(target).to(position2, 2000);
t2.onUpdate(function() {
mesh.position.set(target.x, target.y, 0);
mesh.scale.set(target.width, target.height, target.depth);
mesh.rotation.set(target.rotx, target.roty, target.rotz);
});
t2.easing(TWEEN.Easing.Quadratic.In);
t1.start();
And I have the tweens updating in my animation function:
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
mesh.__dirtyPosition = true;
mesh.__dirtyRotation = true;
TWEEN.update();
}
animate();
This is working as I expect it to, but it is clearly very inefficient, and difficult to work around.
Any and all help will be appreciated.
You're overcomplicating things a bit by re-naming the x, y, z properties to width, height, depth or rotx, roty, rotz. This only means you have to manually translate these properties onUpdate when you do scale.x = position.width and rotation.x = position.rotx. I recommend you keep x, y, z, to avoid these repetitive assignments.
// We set our start and target pos using the THREE.js "x, y, z" nomenclature
var startPos = {x: -200, y: 150, z: 0};
var targetPos = {x: 200, y: -100, z: 0};
// Scale also is defined in "x, y, z"
var startScale = {x: 1, y: 1, z: 1};
var targetScale = {x: 0.4, y: 3, z: 8};
// Rotation also has "x, y, z" degrees in Euler angles
var startRot = {x: -0.5, y: 0.7, z: 0.9};
var targetRot = {x: 0.3, y: -0.4, z: -0.6};
// Standard mesh setup
var mesh = new THREE.Mesh(
new THREE.CubeGeometry(190, 45, 30),
new THREE.MeshBasicMaterial({color: 0x444444})
);
mesh.position.copy(startPos);
mesh.rotation.copy(startRot);
scene.add(mesh);
// Create shortcuts for shorter easing names
var QuadOut = TWEEN.Easing.Quadratic.Out;
var QuadIn = TWEEN.Easing.Quadratic.In;
// Create one tween for position
// Notice that you can chain the animation
// back to startPos by doing double ".to().to()""
var t1 = new TWEEN.Tween(mesh.position)
.to(targetPos, 2000, QuadOut)
.to(startPos, 2000, QuadIn);
// Second, we tween the mesh's rotation
var t2 = new TWEEN.Tween(mesh.rotation)
.to(targetRot, 2000, QuadOut)
.to(startRot, 2000, QuadIn);
// Third, we tween the mesh's scale
var t3 = new TWEEN.Tween(mesh.scale)
.to(targetScale, 2000, QuadOut)
.to(startScale, 2000, QuadIn);
t1.start();
t2.start();
t3.start();
And finally, during animate(), you no longer have to change __dirtyPosition or anything, because the tween is updating the mesh's properties directly.
function animate() {
requestAnimationFrame(animate);
TWEEN.update();
renderer.render(scene, camera);
}
animate();

How can I change the Initial position of my geometry in Three.js?

I would like to change the initial position where my geometry appears; Right now it appears in the center of the canvas. I would like it to appear at the left upper corner. Can you help me?
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.z = 1000;
Here is the full code:
Three js example
Change the position of the cube using mesh.position.set(x, y, z)
I used window.innerWidth and window.innerHeight to move your object to the corner of the screen.
Here is an updated fiddle with your box in the upper-left corner of the screen.
If you don't like how the cube flies around when you drag it, you can't use orbitControls any more. To make the cube still rotate normally, use this code (jQuery required):
var isDragging = false;
var previousMousePosition = {
x: 0,
y: 0
};
$(renderer.domElement).on('mousedown', function(e) {
isDragging = true;
})
.on('mousemove', function(e) {
//console.log(e);
var deltaMove = {
x: e.offsetX-previousMousePosition.x,
y: e.offsetY-previousMousePosition.y
};
if(isDragging) {
var deltaRotationQuaternion = new THREE.Quaternion()
.setFromEuler(new THREE.Euler(
toRadians(deltaMove.y * 1),
toRadians(deltaMove.x * 1),
0,
'XYZ'
));
mesh.quaternion.multiplyQuaternions(deltaRotationQuaternion, mesh.quaternion);
}
previousMousePosition = {
x: e.offsetX,
y: e.offsetY
};
});
$(document).on('mouseup', function(e) {
isDragging = false;
});
function toRadians(angle) {
return angle * (Math.PI / 180);
}
function toDegrees(angle) {
return angle * (180 / Math.PI);
}
Source for this code: https://jsfiddle.net/MadLittleMods/n6u6asza/
Example using your code: https://jsfiddle.net/3eau15pv/3/

Canvas transform on cloth simulation + image

I'm using the verlet.js plugin in order create a cloth simulation on canvas with a texture image.
The only thing (and the most important BTW) part that I haven't arrived is that I need skew the drawImage in order to make it fit the correct position.
jsfiddle with the progress
//Drawing the rectangle
ctx.save();
ctx.beginPath();
ctx.moveTo(cloth.particles[i1].pos.x, cloth.particles[i1].pos.y);
ctx.lineTo(cloth.particles[i1+1].pos.x, cloth.particles[i1+1].pos.y);
ctx.lineTo(cloth.particles[i2].pos.x, cloth.particles[i2].pos.y);
ctx.lineTo(cloth.particles[i2-1].pos.x, cloth.particles[i2-1].pos.y);
ctx.lineTo(cloth.particles[i1].pos.x, cloth.particles[i1].pos.y);
ctx.strokeStyle = "#fff";
ctx.stroke();
ctx.restore();
//Wrapping the image
ctx.save();
var off = cloth.particles[i2].pos.x - cloth.particles[i1].pos.x;
//THIS IS WHAT I TRY TO SOLVE TO FIT TO THE RECTANGLES
//ctx.transform(1,0.5,0,1,0,0);
ctx.drawImage(img, cloth.particles[i1].pos.x,cloth.particles[i1].pos.y, off, off, cloth.particles[i1].pos.x,cloth.particles[i1].pos.y, off ,off);
ctx.restore();
}
I have tried to adapt other cloth simulations but without success. Any clue where I could find some info to accomplish that?
Using skew (or rather shear) to fill tiles only works if the cell is a parallelogram, as 2D affine transforms only support this shape.
Here is one approach:
Calculate angle of upper line
Calculate angle of left line
Calculate width and height of cell
In a parallelogram bottom line will equal upper line, and of course right line equals left line.
Then set these angles as skew arguments for the transform coupled with translate to the upper left corner.
Then just repeat for each cell.
Example
var img = new Image;
img.onload = function() {
var ctx = document.querySelector("canvas").getContext("2d"),
tile1 = [
{x: 10, y: 10}, // upper left corner
{x: 210, y: 50}, // upper right
{x: 230, y: 150}, // bottom right
{x: 30, y: 110} // bottom left
],
tile2 = [
{x: 210, y: 50},
{x: 410, y: 5},
{x: 430, y: 105},
{x: 230, y: 150}
];
renderTile(this, tile1);
renderTile(this, tile2);
function renderTile(img, tile) {
var dx, dy, a1, a2, w, h, i = 1;
// reference shape (remove this section):
ctx.setTransform(1,0,0,1,0,0);
ctx.moveTo(tile[0].x, tile[0].y);
while(i < 4) ctx.lineTo(tile[i].x, tile[i++].y);
ctx.closePath();
ctx.strokeStyle = "#0c0";
ctx.lineWidth = 2;
ctx.stroke();
// calc horizontal angle
dx = tile[1].x - tile[0].x; // horizontal diff.
dy = tile[1].y - tile[0].y; // vertical diff.
a1 = Math.atan2(dy, dx); // angle, note dy,dx order here
w = dx|0; // width based on diff for x
// calc vertical angle
dx = tile[3].x - tile[0].x;
dy = tile[3].y - tile[0].y;
a2 = Math.atan2(dx, dy); // note dx,dy order here
h = dy|0;
// draw image to fit parallelogram
ctx.setTransform(1, a1, a2, 1, tile[0].x, tile[0].y);
ctx.drawImage(img, 0, 0, w, h);
}
};
img.src = "http://i.imgur.com/rUeQDjE.png";
<canvas width=500 height=160/>
Note: if your cloth simulation produces other shapes than parallelograms (ie. quadrilaterals), which is very likely since this is a physics simulation, this approach won't work well. In that case you need different techniques which are more compute heavy. For this reason WebGL is a better fit. Just my two cents..

Why can't I draw a complete circle with arc() in Three.js?

I'm trying to draw a complex shape in Three.js using extruded arcs but they just didn't seem to be behaving properly. I don't know if I don't understand the API, but shouldn't this create a complete extruded circle of radius 100 centred at the origin?
var path = new THREE.Path();
path.moveTo(0, 0);
path.arc(0, 0, 100, 0, Math.PI * 2, false);
var shape = path.toShapes(false, false);
var extrudeSettings = {
amount : 20,
steps : 1
};
var geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
var mesh = new THREE.Mesh(geometry, material);
Instead it draws a Pacman shape:
Here's the JSFiddle:
http://jsfiddle.net/c8shqzpn/
You want to create a circle shape so you can extrude it.
Whenever you draw an arc, it connects the start of the arc to the current point, so in your case, you have to use the moveTo() command to set the start point on the perimeter of the circle.
var shape = new THREE.Shape();
shape.moveTo( circleRadius, 0 );
shape.absarc( 0, 0, circleRadius, 0, 2 * Math.PI, false );
three.js r.70
I had similar issues drawing 3/4 of a circle and extruding it and adding the result (a THREE.Mesh) to the screen. The circle seemed to miss a segment. Adding moveTo( x, y ) where x, y are coordinates of the beginning of the arc solved the issue. I used this code:
var extrudeSettings = {
bevelEnabled: false,
steps: 1,
amount: 2
};
var shape = new THREE.Shape();
var circleRadius = 4;
// THIS LINE SOLVES THE ISSUE
shape.moveTo( 0, -circleRadius );
shape.absarc( 0, 0, circleRadius, 0, 1.5 * Math.PI, false );
shape.lineTo( 0, 0 );
shape.closePath();
var geometry = shape.extrude( extrudeSettings );
scene.add( new THREE.Mesh( geometry, new THREE.MeshNormalMaterial() ) );
First my mesh looked like this:
After adding shape.moveTo( 0, -circleRadius ); the mesh looked like this:
Could it be solved if you multiply Math.PI bye 2.1, instead of 2?

Categories