I'm attempting to implement mouse cursor centered zoom in/out on an image. For simplicity, assume I only need to zoom in/out in the horizontal axis. I am using Qt with QPainter to draw my scene and QWidget:: mouseWheelEvent override to calculate a zoom factor and a zoom position.
To put it into words before showing the code, the zoom factor, and zoom position define the transformation needed to transform from the original image into the zoomed image (to put it another way, I don't combine matrices or something similar, but calculate the absolute transformation in the paint event), as can be finally seen here:
void MyWidget::paintEvent(QPaintEvent* e)
{
QPainter painter{this};
painter.translate(m_zoomCenterX, 0.);
painter.scale(m_zoomFactor, 1.);
painter.translate(-m_zoomCenterX, 0.);
// content margins are all 0 btw
painter.drawImage(contentsRect(), m_image);
}
In the mouse wheel event, I try to calculate the scaled difference between the zoom center and the new position which I add to the zoom center. Zoom factor is computed simply by multiplying with a constant according to the angle delta. The final if-else branching does some boundaries checking (which is something I'd like to do so the image stays stretched in the view and doesn't move somewhere I don't want to):
void MyWidget::wheelEvent(QWheelEvent* e)
{
const QRect rect = contentsRect();
// m_zoomCenterX is first initialized to 0 in the MyWidget constructor, m_zoomFactor to 1.
const qreal diff = (e->pos().x() - m_zoomCenterX) / m_zoomFactor;
qDebug() << diff;
m_zoomCenterX += diff;
static constexpr const qreal ZOOM_IN_FACTOR = 1.1,
ZOOM_OUT_FACTOR = .9;
m_zoomFactor *= e->angleDelta().y() > 0. ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR;
if (m_zoomFactor < 1.)
{
m_zoomFactor = 1.;
m_zoomCenterX = 0.;
}
else if (m_zoomFactor > 5.)
{
m_zoomFactor = 5.;
}
if (m_zoomCenterX < 0)
{
m_zoomCenter = 0;
}
else if (m_zoomCenterX >= rect.width())
{
m_zoomCenterX = rect.width() - 1;
}
update();
}
This doesn't seem to work once I try to zoom in/out after let's say zooming in (see picture:
, sorry if it's not much), the zoom center position is likely not correctly computed but I'm not sure why. The qDebug line gives me non-zero differences after consecutive scrollings when the cursor is on a new position, but after the first scrolling it should be zero...
I guess it must be something trivial but I'm quite stuck at the moment.
Well I got it working. I use inverse matrix to compute the new zoom center, but once zoomed in, it will not match the mouse position, so the entire image must be translated additionally by the inverted_position - mouse_position. Here's the final code for any future wanderers. m_extraTranslation is obviously initialised 0 in the constructor.
QMatrix MyWidget::computeScaleMatrix() const noexcept
{
QMatrix scaleMatrix{1., 0., 0., 1., 0., 0.};
scaleMatrix.translate(-m_extraTranslation, 0.);
scaleMatrix.translate(m_zoomCenter.x(), 0.);
scaleMatrix.scale(m_zoomFactor, 1.);
scaleMatrix.translate(-m_zoomCenter.x(), 0.);
return scaleMatrix;
}
void MyWidget::wheelEvent(QWheelEvent* e)
{
const QMatrix invScaleMatrix = computeScaleMatrix().inverted();
const QPointF invPos = invScaleMatrix.map(e->pos());
m_zoomCenter.setX(invPos.x());
static constexpr const qreal ZOOM_IN_FACTOR = 1.1,
ZOOM_OUT_FACTOR = .9;
m_zoomFactor *= e->angleDelta().y() > 0. ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR;
m_extraTranslation = invPos.x() - e->pos().x();
// here I'm making sure the image corners do not wander off inside the widget, this is necessary to do for zooming out
const QMatrix scaleMatrix = computeScaleMatrix();
const qreal startX = scaleMatrix.map(QPointF{0., 0.}).x(),
endX = scaleMatrix.map(QPointF(contentsRect().width() - 1, 0.)).x();
if (startX > 0.)
{
m_extraTranslation += startX;
}
else if (endX < contentsRect().width() - 1)
{
m_extraTranslation -= contentsRect().width() - endX - 1;
}
if (m_zoomFactor < 1.)
{
m_zoomFactor = 1.;
m_zoomCenter = {0, 0};
m_extraTranslation = 0;
}
else if (m_zoomFactor > 5.)
{
m_zoomFactor = 5.;
}
if (m_zoomCenter.x() < 0)
{
m_zoomCenter.setX(0);
}
else if (m_zoomCenter.x() >= contentsRect().width())
{
m_zoomCenter.setX(contentsRect().width() - 1);
}
update();
e->accept();
}
void MyWidget::paintEvent(QPaintEvent* e)
{
QPainter painter{this};
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
painter.setMatrix(computeScaleMatrix()));
painter.drawImage(contentsRect(), m_image);
}
Extending this for the Y axis should be trivial since it would follow the same logic.
Related
Once again, camera zooming on a 2D-Plane. I searched a lot and know that there are similar questions, but I am obviously way too stupid to apply what I was able to find.
Basically I multiply the distance of all elements to the origin by mouseDelta, which is a double between 0.5 and 1. works fine for all elements, but since the anchor of the camera (camX, camY) are the upper left corner of the camera, the objects in the focus of the cam change their position in relation to the focus. I want to scroll "towards" the focus. Here is what I got, but it behaves really weird:
camX and camY, as mentioned, are the coordinates for the upper left of the cam.
mouseDelta is the zoom-level thats stored globally and is changed by each wheel-event.
screenX is the width of the screen/window (fullscreen anyways)
screenY is the height of the screen/window
if (newEvent.type == sf::Event::MouseWheelMoved) //zoom
{
mouseDelta += ((double)newEvent.mouseWheel.delta)/20;
if (mouseDelta > 1) { mouseDelta = 1; }
else if (mouseDelta < 0.5) { mouseDelta = 0.5; }
//resize graphics
for (int i = 0; i < core->universe->world->nodes.size(); i++) {
core->universe->world->nodes.at(i).pic->setSize(mouseDelta);
}
for (int i = 0; i < core->universe->world->links.size(); i++) {
core->universe->world->links.at(i).pic->setSize(mouseDelta);
}
camX = (camX + screenX/2) - (camX + screenX/2)*mouseDelta;
camY = (camY + screenY/2) - (camY + screenY/2)*mouseDelta;
}
My work Environment : Qt 5.8 MSVC2015 64bit, QT GraphicsView, Windows 7 64 bit
When GraphicsView vertical scroll bar is goes away, zoom out should stop.
So I tried with below code, but it failed to work :
void GraphicsView::scale(qreal scaleFactor)
{
QRectF r(0, 0, 1, 1); // A reference
int pos_x = this->horizontalScrollBar()->value();
int pos_y = this->verticalScrollBar()->value();
qreal factor = transform().scale(scaleFactor, scaleFactor).mapRect(r).width(); // absolute zoom factor
if ( factor > 7) { // Check zoom out limit
return;
}
//Failed, this code failed If zoom out again.**
if(pos_x <= 0 && pos_y <= 0 )
{
return;
}
Any suggestion How I can do to fix the above code ?
No reply for my question. Here is work around solution from me,
Check from wheelEvent are we doing zoom in or zoom out. I scale check vertical & horizontal scroll bar.
here _steps is private data member of my class GraphicsView. GraphicsView derived from QGraphicsView.
void GraphicsView::wheelEvent(QWheelEvent * event)
{
// Typical Calculations (Ref Qt Doc)
const int degrees = event->delta() / 8;
_steps = degrees / 15; // _steps = 1 for Zoom in, _steps = -1 for Zoom out.
}
void GraphicsView::scale(qreal scaleFactor)
{
QRectF r(0, 0, 1, 1); // A reference
int pos_x = this->horizontalScrollBar()->value();
int pos_y = this->verticalScrollBar()->value();
qreal factor = transform().scale(scaleFactor, scaleFactor).mapRect(r).width(); // absolute zoom factor
if ( factor > 7) { // Calculate your zoom in limit from factor
return;
}
//When there is no scroll bar, am I still I am zooming, stop it using _steps
if(pos_x <= 0 && pos_y <= 0 && _steps == -1)
{
return;
}
QGraphicsView::scale(scaleFactor, scaleFactor);
}
I know there is better solution then this, But I found this only :(
I am using Qt 5.6, I want to draw a number of text labels around a circle and rotate the text labels to orientate the text according to its position around the circle, so 12 o'clock would have a 0 degree rotation, 3 o'clock would be rotated 90 degrees, 6 o'clock would be rotated 180 degrees etc.
I am aligning the text around its centre position:
void drawText(QPainter* pobjPainter, qreal fltX, qreal fltY
,int intFlags, const QString* pobjText) {
const qreal fltSize = 32767.0;
QPointF ptfCorner(fltX, fltY - fltSize);
if ( (intFlags & Qt::AlignHCenter) == Qt::AlignHCenter ) {
ptfCorner.rx() -= fltSize / 2.0;
} else if ( (intFlags & Qt::AlignRight) == Qt::AlignRight ) {
ptfCorner.rx() -= fltSize;
}
if ( (intFlags & Qt::AlignVCenter) == Qt::AlignVCenter ) {
ptfCorner.ry() += fltSize / 2.0;
} else if ( (intFlags & Qt::AlignTop) == Qt::AlignTop ) {
ptfCorner.ry() += fltSize;
}
QRectF rctPos(ptfCorner, QSizeF(fltSize, fltSize));
pobjPainter->drawText(rctPos, intFlags, *pobjText);
}
I would like to apply a rotation to the text.
I would like to reproduce something similar to what is shown:
http://www.informit.com/articles/article.aspx?p=1405545&seqNum=2
It seems that the rotate function rotates the entire painter canvas, so the coordinates must take into account the rotation which is really giving me a hard time. I want to position the text around the ellipse and then rotate it, how do I know what the coordinates should be?
Sticking with the clock example you could try something like...
virtual void paintEvent (QPaintEvent *event) override
{
QPainter painter(this);
double radius = std::min(width(), height()) / 3;
for (int i = 0; i < 12; ++i) {
int numeral = i + 1;
double radians = numeral * 2.0 * 3.141592654 / 12;
/*
* Calculate the position of the text centre as it would be required
* in the absence of a transform.
*/
QPoint pos = rect().center() + QPoint(radius * std::sin(radians), -radius * std::cos(radians));
/*
* Set up the transform.
*/
QTransform t;
t.translate(pos.x(), pos.y());
t.rotateRadians(radians);
painter.setTransform(t);
/*
* Specify a huge bounding rectangle centred at the origin. The
* transform should take care of position and orientation.
*/
painter.drawText(QRect(-(INT_MAX / 2), -(INT_MAX / 2), INT_MAX, INT_MAX), Qt::AlignCenter, QString("%1").arg(numeral));
}
}
So I've got a class Label that inherits from osg::Geode which I draw in the world space in OpenSceneGraph. After displaying each frame, I then want to read the screen space coordinates of
each Label, so I can find out how much they overlap in the screen space. To this end, I created a class ScreenSpace which should calculate this (the interesting function is calc_screen_coords.)
I wrote a small subroutine that dumps each frame with some extra information, including the ScreenSpace box which represents what the program thinks the screen space coordinates are:
Now in the above picture, there seems to be no problem; but if I rotate it to the other side (with my mouse), then it looks quite different:
And that is what I don't understand.
Is my world to screen space calculation wrong?
Or am I getting the wrong BoundingBox from the Drawable?
Or maybe it has something to do with the setAutoRotateToScreen(true) directive that I give the osgText::Text object?
Is there a better way to do this? Should I try to use a Billboard instead? How would I do that though? (I tried and it totally didn't work for me — I must be missing something...)
Here is the source code for calculating the screen space coordinates of a Label:
struct Pixel {
// elided methods...
int x;
int y;
}
// Forward declarations:
pair<Pixel, Pixel> calc_screen_coords(const osg::BoundingBox& box, const osg::Camera* cam);
void rearange(Pixel& left, Pixel& right);
class ScreenSpace {
public:
ScreenSpace(const Label* label, const osg::Camera* cam)
{
BoundingBox box = label->getDrawable(0)->computeBound();
tie(bottom_left_, upper_right_) = calc_screen_coords(box, cam);
rearrange(bottom_left_, upper_right_);
}
// elided methods...
private:
Pixel bottom_left_;
Pixel upper_right_;
}
pair<Pixel, Pixel> calc_screen_coords(const osg::BoundingBox& box, const osg::Camera* cam)
{
Vec4d vec (box.xMin(), box.yMin(), box.zMin(), 1.0);
Vec4d veq (box.xMax(), box.yMax(), box.zMax(), 1.0);
Matrixd transmat
= cam->getViewMatrix()
* cam->getProjectionMatrix()
* cam->getViewport()->computeWindowMatrix();
vec = vec * transmat;
vec = vec / vec.w();
veq = veq * transmat;
veq = veq / veq.w();
return make_pair(
Pixel(static_cast<int>(vec.x()), static_cast<int>(vec.y())),
Pixel(static_cast<int>(veq.x()), static_cast<int>(veq.y()))
);
}
inline void swap(int& v, int& w)
{
int temp = v;
v = w;
w = temp;
}
inline void rearrange(Pixel& left, Pixel& right)
{
if (left.x > right.x) {
swap(left.x, right.x);
}
if (left.y > right.y) {
swap(left.y, right.y);
}
}
And here is the construction of Label (I tried to abridge it a little):
// Forward declaration:
Geometry* createLeader(straph::Point pos, double height, Color color);
class Label : public osg::Geode {
public:
Label(font, fontSize, text, color, position, height, margin, bgcolor, leaderColor)
{
osgText::Text* txt = new osgText::Text;
txt->setFont(font);
txt->setColor(color.vec4());
txt->setCharacterSize(fontSize);
txt->setText(text);
// Set display properties and height
txt->setAlignment(osgText::TextBase::CENTER_BOTTOM);
txt->setAutoRotateToScreen(true);
txt->setPosition(toVec3(position, height));
// Create bounding box and leader
typedef osgText::TextBase::DrawModeMask DMM;
unsigned drawMode = DMM::TEXT | DMM::BOUNDINGBOX;
drawMode |= DMM::FILLEDBOUNDINGBOX;
txt->setBoundingBoxColor(bgcolor.vec4());
txt->setBoundingBoxMargin(margin);
txt->setDrawMode(drawMode);
this->addDrawable(txt);
Geometry* leader = createLeader(position, height, leaderColor);
this->addDrawable(leader);
}
// elided methods and data members...
}
Geometry* createLeader(straph::Point pos, double height, Color color)
{
Geometry* leader = new Geometry();
Vec3Array* array = new Vec3Array();
array->push_back(Vec3(pos.x, pos.y, height));
array->push_back(Vec3(pos.x, pos.y, 0.0f));
Vec4Array* colors = new Vec4Array(1);
(*colors)[0] = color.vec4();
leader->setColorArray(colors);
leader->setColorBinding(Geometry::BIND_OVERALL);
leader->setVertexArray(array);
leader->addPrimitiveSet(new DrawArrays(PrimitiveSet::LINES, 0, 2));
LineWidth* lineWidth = new osg::LineWidth();
lineWidth->setWidth(2.0f);
leader->getOrCreateStateSet()->setAttributeAndModes(lineWidth, osg::StateAttribute::ON);
return leader;
}
Any pointers or help?
I found a solution that works for me, but is also unsatisfying, so if you have a better solution, I'm all ears.
Basically, I take different points from the Label that I know will be at certain points,
and I calculate the screen space by combining this. For the left and right sides, I take
the bounds of the regular bounding box, and for the top and bottom, I calculate it with the
center of the bounding box and the position of the label.
ScreenSpace::ScreenSpace(const Label* label, const osg::Camera* cam)
{
const Matrixd transmat
= cam->getViewMatrix()
* cam->getProjectionMatrix()
* cam->getViewport()->computeWindowMatrix();
auto topixel = [&](Vec3 v) -> Pixel {
Vec4 vec(v.x(), v.y(), v.z(), 1.0);
vec = vec * transmat;
vec = vec / vec.w();
return Pixel(static_cast<int>(vec.x()), static_cast<int>(vec.y()));
};
// Get left right coordinates
vector<int> xs; xs.reserve(8);
vector<int> ys; ys.reserve(8);
BoundingBox box = label->getDrawable(0)->computeBound();
for (int i=0; i < 8; i++) {
Pixel p = topixel(box.corner(i));
xs.push_back(p.x);
ys.push_back(p.y);
};
int xmin = *min_element(xs.begin(), xs.end());
int xmax = *max_element(xs.begin(), xs.end());
// Get up-down coordinates
int ymin = topixel(dynamic_cast<const osgText::Text*>(label->getDrawable(0))->getPosition()).y;
int center = topixel(box.center()).y;
int ymax = center + (center - ymin);
bottom_left_ = Pixel(xmin, ymin);
upper_right_ = Pixel(xmax, ymax);
z_ = distance_from_camera(label, cam);
}
I have a CCLayer which holds all my game objects and i have implemented scaling and scrolling. To make sure that you can't scroll out of bounds i am calculating a rect that represents the screen and use it to check if the rect is within bounds. The problem is that my calculations are wrong. The layer is scaled by a scaleFactor like so:
world->setScale(scaleFactor);
and then i calculate the scroll rect:
float scrollWidth = winSize.width * ( 1 / scaleFactor); // winSize is the size of the screen (1280x
float scrollHeight = winSize.height * ( 1 / scaleFactor);
if(scrollRect.l < 0) scrollRect.l = 0;
if(scrollRect.l + scrollWidth > levelWidth) scrollRect.l -= (scrollRect.l + scrollWidth - levelWidth);
scrollRect.r = scrollRect.l + scrollWidth;
world->setPosition(-scrollRect.l, -scrollRect.b);
(the scale factor value is between 1.0 and 0.5)
This sort of works but only when the layer is zoomed out to the max or zoomed in to the minimum but when scaleFactor isn't MAX/MIN it is wrong (there is some left space).
What am i doing wrong? I've also tried changing the anchor points (they are currently set to 0,0) but without any success.
You can do this whatever your scale factor...
here _tileMap is your world
//Code for getting difference b/w two position
CCTouch *fingerOne = (CCTouch*)touchArray->objectAtIndex(0);
CCPoint newTouchLocation = fingerOne->getLocationInView();
newTouchLocation = CCDirector::sharedDirector()->convertToGL(newTouchLocation);
newTouchLocation=_tileMap->convertToNodeSpace(newTouchLocation);
CCPoint oldTouchLocation = fingerOne->getPreviousLocationInView();
oldTouchLocation = CCDirector::sharedDirector()->convertToGL(oldTouchLocation);
oldTouchLocation = _tileMap->convertToNodeSpace(oldTouchLocation);
//get the difference in the finger touches when the player was dragging
CCPoint difference = ccpSub(newTouchLocation, oldTouchLocation);
CCPoint ASD=ccpAdd(_tileMap->getPosition(), ccpMult(difference, _tileMap->getScale()));
CCPoint bottomLeft =ASD;
// Bounding Box....
if (bottomLeft.x >0) {
bottomLeft.x = 0;
}
if (bottomLeft.y>0) {
bottomLeft.y = 0;
}
if (bottomLeft.x < -(mapWidth*_tileMap->getScale() - _screenSize.width)) {
bottomLeft.x = -(mapWidth*_tileMap->getScale()- _screenSize.width);
}
if (bottomLeft.y <-(mapHieght*_tileMap->getScale() - _screenSize.height)) {
bottomLeft.y = - (mapHieght*_tileMap->getScale() - _screenSize.height);
}
_tileMap->setPosition(bottomLeft);
I hope this may help you..