Rotating after scaling results in border offset


#1

I have a border that gets drawn using the translation, rotation and scale transformations of an image. The border and the image match up perfectly when I rotate without any scale. However, once I scale the image and begin rotating an offset begins to grow between the image and the border and gets more severe as the angle increases. After a full rotation, the image and border match up again. Can’t think of what I’m doing wrong here.

 ofPushMatrix();
ofSetColor(255,255,255);
ofTranslate(pic->imageCenter.x, pic->imageCenter.y);
ofScale((pic->width/pic->image.getWidth()), (pic->height/pic->image.getHeight()), 0);
ofRotateZRad(pic->angle);
pic->image.draw(-1*pic->image.getWidth()/2, -1*pic->image.getHeight()/2);
ofPopMatrix();

ofPushMatrix();
ofTranslate(pic->imageCenter.x, pic->imageCenter.y);
ofRotateZRad(pic->angle);
ofSetColor(0,0,255);
ofNoFill();
ofSetLineWidth(3);
ofDrawRectangle(-1*pic->width/2, -1*pic->height/2, pic->width, pic->height);
ofPopMatrix();


#2

Hi br0mid,
First, to draw just the border the simplest is:

ofPushMatrix();
// Here some matrix transformations, ofTranslate, ofScale, ofRotate.
// Then draw the image
pic->image.draw(-1*pic->image.getWidth()/2, -1*pic->image.getHeight()/2);
// And draw the border
ofNoFill();
ofDrawRectangle(
    -1*pic->image.getWidth()/2, -1*pic->image.getHeight()/2,
    pic->image.getWidth(), pic->image.getHeight()
);
ofPopMatrix();

But if I understand what you are trying to achieved this is not a good solution: if you want to draw circles at the image corners their radius will also scale, and this is probably not what you want. And they will be deformed when the scale factor is different for X and Y.
Your code is almost correct, but you have to invert the order between scale and rotate:

ofTranslate(pic->imageCenter.x, pic->imageCenter.y);
ofRotateZRad(pic->angle);
ofScale((pic->width/pic->image.getWidth()), (pic->height/pic->image.getHeight()), 0);

With this change, the rest of your code works, and the rectangle for the border fit the image.
Have you noticed that your image is skewed when rotated with differents scaleX and scaleY? This is because of this incorrect order in the matrix transformations.

As a complement to this answer, my recipe for this kind of situation is different. I prefer to compute the display matrix myself, meaning:

	glm::mat4 mat ;
	mat = glm::translate( mat, glm::vec3( x, y, 0.f ) ) ;
	mat = glm::rotate( mat, angle, glm::vec3( 0.f, 0.f, 1.f ) ) ;
	mat = glm::scale( mat, glm::vec3( scaleX, scaleY, 1.f ) ) ;
	mat = glm::translate( mat, glm::vec3( -anchorX, -anchorY, 0.f ) ) ;

	ofPushMatrix() ;
	ofMultMatrix( mat ) ;
	// draw something. for example:
	img.draw( 0.f, 0.f );
	ofPopMatrix() ;

instead of

	ofPushMatrix() ;
	ofTranslate( x, y );
	ofRotateRad( angle );
	ofScale( scale, scale );
	ofTranslate( - anchorX, - anchorY );
	// draw something. for example:
	img.draw( 0.f, 0.f );
	ofPopMatrix() ;

The result is just the same. What’s the point then ? The point is that it is possible to use this matrix to transform points. This allow, for example, to compute where the bottom right corner of the image is:

glm::vec4 bottomRight( imgWidth, imgHeight, 0.f, 1.f );
glm::vec2 displayedBottomRight( mat * bottomRight );

And this is interesting because it allow to draw it:

ofDrawCircle( displayedBottomRight, 5.f ) ;

But also to detect if the mouse is over this corner by computing the distance to it:

glm::vec2 mouse( ofGetMouseX(), ofGetMouseY() ) ;
float distance = glm::distance( mouse, displayedBottomRight );
if( distance < 10.f ) // do something

And sometimes it is handy to have the mouse coordinates expressed within the image coordinates system, for exemple to be able to resize the image by dragging the corners :wink: :

glm::vec2 pos( glm::inverse( mat ) * glm::vec4( mouse, 0.f, 1.f ) ) ;
// pos will be close to ( imgWidth, imgHeight ) when the mouse is over
// the bottom right corner. This pos allow us to compute the scaleX and the scaleY
// required if we want to drag this corner to the mouse position.

Here’s a full working exemple:

// ofApp.h
class ofApp : public ofBaseApp
{
	public:
		void setup() ;
		void draw() ;
		ofImage img ;
		float time ;
} ;

// ofApp.cpp
void ofApp::setup()
{
	img.load( "img.jpg" ) ;
}
void ofApp::draw()
{
	// First, the objects to draw
	// Image size
	float imgWidth = img.getWidth() ;
	float imgHeight = img.getHeight() ;
	// The 4 corners of the image
	// We need vec4 for later, this is why I put a
	// 1.f for the last coordinate (homogeneous coordinates)
	glm::vec4 topLeft( 0.f, 0.f, 0.f, 1.f ) ;
	glm::vec4 topRight( imgWidth, 0.f, 0.f, 1.f ) ;
	glm::vec4 bottomLeft( 0.f, imgHeight, 0.f, 1.f ) ;
	glm::vec4 bottomRight( imgWidth, imgHeight, 0.f, 1.f ) ;

	float ofw = ofGetWidth() ;
	float ofh = ofGetHeight() ;
	if( ! ofGetMousePressed() ) time = ofGetElapsedTimef() * 0.1f ;

	// Now some random display properties
	float anchorX = imgWidth * 0.3f ;
	float anchorY = imgHeight * 0.4f ;
	float x = ( 0.4f + 0.2f * ofNoise( time, 0.f ) ) * ofw ;
	float y = ( 0.4f + 0.2f * ofNoise( time, 1.f ) ) * ofh ;
	float scaleX = ( 0.6f + 0.8f * ofNoise( time, 2.f ) ) ;
	float scaleY = ( 0.6f + 0.8f * ofNoise( time, 3.f ) ) ;
	float angle = ofNoise( time, 4.f ) * glm::two_pi< float >() ;

	// Instead of ...
	// ofTranslate( x, y );
	// ofRotateRad( angle );
	// ofScale( scale, scale );
	// ofTranslate( - anchorX, - anchorY );
	// ... I compute a matrix that do exactly the same:
	glm::mat4 mat ;
	mat = glm::translate( mat, glm::vec3( x, y, 0.f ) ) ;
	mat = glm::rotate( mat, angle, glm::vec3( 0.f, 0.f, 1.f ) ) ;
	mat = glm::scale( mat, glm::vec3( scaleX, scaleY, 1.f ) ) ;
	mat = glm::translate( mat, glm::vec3( -anchorX, -anchorY, 0.f ) ) ;

	// Apply the matrix
	ofPushMatrix() ;
	ofMultMatrix( mat ) ;

	// Draw the image
	ofSetColor( 255 ) ;
	img.draw( 0.f, 0.f ) ;

	// Draw border of the img. Just draw a rectangle with the same dimensions
	ofNoFill() ;
	ofSetColor( 0, 0, 255 ) ;
	ofDrawRectangle( 0.f, 0.f, imgWidth, imgHeight ) ;

	ofPopMatrix() ;

	// Anchor point
	ofFill() ;
	ofSetColor( 255, 0, 0 ) ;
	ofDrawCircle( x, y, 5.f ) ;

	// Compute the display position of each corner
	glm::vec2 tl( mat * topLeft ) ;
	glm::vec2 tr( mat * topRight ) ;
	glm::vec2 bl( mat * bottomLeft ) ;
	glm::vec2 br( mat * bottomRight ) ;

	// Display them
	ofSetColor( 0, 0, 255 ) ;
	ofDrawCircle( tl, 5.f ) ;
	ofDrawCircle( tr, 5.f ) ;
	ofDrawCircle( bl, 5.f ) ;
	ofDrawCircle( br, 5.f ) ;

	// Compute the distance between the mouse and the tl corner
	glm::vec2 mouse( ofGetMouseX(), ofGetMouseY() ) ;
	if( glm::distance( mouse, tl ) < 30.f ) ofSetColor( 0, 255, 0 ) ;
	else ofSetColor( 100 ) ;
	ofDrawLine( mouse, tl ) ;

	// Compute the mouse position in the image coordinate system
	glm::vec2 pos( glm::inverse( mat ) * glm::vec4( mouse, 0.f, 1.f ) ) ;
	ofDrawBitmapStringHighlight( "Press mouse button to stop motion", 10.f, 20.f ) ;
	ofDrawBitmapStringHighlight(
		"Mouse position in image coordinates system : "
		+ ofToString( pos.x, 0 ) + ":" + ofToString( pos.y, 0 ),
		10.f, 40.f
	) ;
}


src.zip (2,0 Ko)


Translate mouseCoordinates