I have a 2D canvas used in a ImGUI context, that requires Zoom and Pan functionality.
Currently the Pan functionality works fine. However the zoom functionality does scale but the result is not centered around the mouse cursor location.
I have the following three methods that are supposed to work together:
void Canv::HandleInput(const ImVec2& canvasPosition, const ImVec2& canvasSize) {
canvasPosition_ = canvasPosition;
canvasSize_= canvasSize;
ImVec2 mousePosition = ImGui::GetMousePos();
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
const auto mouse_delta = ImGui::GetIO().MouseDelta;
pan -= ImVec2(mouse_delta.x, -mouse_delta.y) / (zoom * units_per_pixel);
}
// Problematic section <<<============|
if ((ImGui::GetIO().MouseWheel != 0)
&& ImGui::IsMouseHoveringRect(canvasPosition, canvasPosition + canvas_size)) {
const float zoom_delta = ImGui::GetIO().MouseWheel * 0.1f;
const float old_zoom = zoom;
const ImVec2 diff = (mousePosition - canvasPosition);
zoom *= std::pow(2.0f, zoom_delta);
zoom = std::clamp(zoom, 0.2f, 10.0f);
pan += ((diff / old_zoom) - (diff / zoom)) / units_per_pixel;
}
}
ImVec2 Canv::worldToScreen(const ImVec2& worldPosition) {
const float scale = (zoom * units_per_pixel);
return ImVec2(
(worldPosition.x - pan.x) * scale + canvasPosition_.x,
(canvasPosition_.y + canvasSize_.y) - (worldPosition.y - pan.y) * scale);
}
ImVec2 Canv::screenToWorld(const ImVec2& screenPosition) {
const float scale = (zoom * units_per_pixel);
return ImVec2(
(screenPosition.x - canvasPosition_.x) / scale + pan.x,
(canvasPosition_.y + canvasSize_.y - screenPosition.y) / scale + pan.y);
}
The various members are as follows:
float zoom = 0.0f;
float units_per_pixel = 10.0f;
ImVec2 pan;
ImVec2 canvasPosition_;
ImVec2 canvasSize_;
Usage of the Canvas is like the following:
void app::update() {
if (ImGui::Begin("Canv")) {
const ImVec2 canvasPosition = ImGui::GetWindowPos ();
const ImVec2 canvas_size = ImGui::GetWindowSize();
canvas.HandleInput(canvasPosition, canvas_size);
}
ImGui::End();
..... other drawing things
}
As can be seen from the capture, the x-coordinate is fine when zooming in or zooming out. However the y-coordinate seems to be shifting up when zooming in and down when zooming out.
Based on the screen/world transform functions, you need to reverse out the pan (screen offset) correctly, that seems to be in world units. Which is not the case when doing mousePosition - canvasPosition
.
You have to remember that you want to preserve the mouse "world" position post zoom. So what you need to do is figure out the difference between the pre and post zoom mouse positions. Then correct for the difference by adjusting the pan.
This should get the result you're after:
if (......) {
const ImVec2 mpos1 = screenToWorld(mousePosition);
const float zoom_delta = ImGui::GetIO().MouseWheel * 0.1f;
zoom *= std::pow(2.0f, zoom_delta);
zoom = std::clamp(zoom, 0.2f, 10.0f);
const ImVec2 mpos2 = screenToWorld(mousePosition);
pan += (mpos1 - mpos2);
}
if the above is too compute intensive (it shouldn't be though) you can pull out the equations for the y coordinate from the screenToWorld and do simplifications and then you only need to capture the prev zoom or what you call old zoom - the x equations are fine, as they're not inverted and proportionally tied to the size like the y coordinated are.