Skip to content

Commit ec8e37b

Browse files
committed
Add CanvasUi - 2D canvas pan/zoom interaction control
1 parent 938d438 commit ec8e37b

File tree

2 files changed

+1258
-0
lines changed

2 files changed

+1258
-0
lines changed

include/cinder/CanvasUi.h

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
/*
2+
Copyright (c) 2024, The Cinder Authors
3+
All rights reserved.
4+
5+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that
6+
the following conditions are met:
7+
8+
* Redistributions of source code must retain the above copyright notice, this list of conditions and
9+
the following disclaimer.
10+
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
11+
the following disclaimer in the documentation and/or other materials provided with the distribution.
12+
13+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
14+
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
15+
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
16+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
17+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
18+
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
19+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
20+
POSSIBILITY OF SUCH DAMAGE.
21+
*/
22+
23+
//! CanvasUi provides smooth pan and zoom interaction for 2D content, similar to image editors,
24+
//! drawing apps, or any application where users need to navigate around 2D content that may be
25+
//! larger than the screen or require detailed inspection.
26+
//!
27+
//! CanvasUi manages transformations between three coordinate spaces:
28+
//! - **Content space**: Your content's coordinate system (arbitrary units)
29+
//! - **Viewport space**: Rectangular region within window where content is displayed
30+
//! - **Window space**: Full application window coordinates (pixels from top-left)
31+
//!
32+
//! Content can be bounded (defined limits) or unbounded (infinite space). Default interactions
33+
//! include mouse drag panning, mouse wheel zooming, and keyboard shortcuts.
34+
35+
#pragma once
36+
37+
#include "cinder/Matrix.h"
38+
#include "cinder/Rect.h"
39+
#include "cinder/Signals.h"
40+
#include "cinder/app/KeyEvent.h"
41+
#include "cinder/app/MouseEvent.h"
42+
#include "cinder/app/Window.h"
43+
#include <optional>
44+
45+
namespace cinder {
46+
47+
//! 2D canvas with pan/zoom interaction controls
48+
//!
49+
//! Provides a complete 2D navigation system with smooth animations, configurable
50+
//! mouse/keyboard controls, and support for both bounded and unbounded content.
51+
class CI_API CanvasUi {
52+
public:
53+
// Hotkey system (uses Cinder's KeyEvent system)
54+
struct HotkeyBinding {
55+
int key; // KeyEvent key code (KEY_0, KEY_PLUS, etc.)
56+
unsigned int modifiers; // KeyEvent modifier flags (CTRL_DOWN, SHIFT_DOWN, etc.)
57+
HotkeyBinding( int k, unsigned int m )
58+
: key( k )
59+
, modifiers( m )
60+
{
61+
}
62+
};
63+
64+
// Mouse interaction system (uses Cinder's MouseEvent system)
65+
struct MouseButtonBinding {
66+
unsigned int button; // MouseEvent button flags (LEFT_DOWN, RIGHT_DOWN, MIDDLE_DOWN)
67+
unsigned int modifiers; // MouseEvent modifier flags (CTRL_DOWN, SHIFT_DOWN, etc.)
68+
MouseButtonBinding( unsigned int b, unsigned int m )
69+
: button( b )
70+
, modifiers( m )
71+
{
72+
}
73+
};
74+
75+
//! Default constructor, unbounded canvas, no signal connections
76+
CanvasUi();
77+
//! Initiates connections to \a window, unbounded canvas
78+
CanvasUi( const app::WindowRef& window, int signalPriority = -1 );
79+
//! Initiates connections to \a window, sets content bounds to \a bounds
80+
CanvasUi( const app::WindowRef& window, const Rectf& bounds, int signalPriority = -1 );
81+
~CanvasUi();
82+
83+
// Non-movable, non-copyable
84+
CanvasUi( CanvasUi&& ) = delete;
85+
CanvasUi& operator=( CanvasUi&& ) = delete;
86+
CanvasUi( const CanvasUi& ) = delete;
87+
CanvasUi& operator=( const CanvasUi& ) = delete;
88+
89+
//! Set content bounds \a bounds for bounded mode
90+
void setContentBounds( const Rectf& bounds );
91+
//! Enable unbounded mode (infinite content space)
92+
void setUnbounded() { mCanvasBounds = std::nullopt; }
93+
//! Get current content bounds (nullopt if unbounded)
94+
const std::optional<Rectf>& getContentBounds() const { return mCanvasBounds; }
95+
//! Check if content is bounded
96+
bool isBounded() const { return mCanvasBounds.has_value(); }
97+
98+
99+
//! Get current model matrix (content to window transform)
100+
const mat4& getModelMatrix() const;
101+
//! Get inverse model matrix (window to content transform)
102+
const mat4& getInverseModelMatrix() const;
103+
//! Get current position offset in viewport coordinates
104+
vec2 getPosition() const { return mPosition; }
105+
//! Get current zoom factor
106+
float getZoom() const { return mZoom; }
107+
108+
//! Convert window coordinates to content coordinates
109+
vec2 toContent( const vec2& posWindow ) const;
110+
//! Convert content coordinates to window coordinates
111+
vec2 toWindow( const vec2& posContent ) const;
112+
113+
//! Get visible content area of current viewport
114+
Rectf getVisibleRect() const;
115+
//! Get visible content area for specific viewport rectangle \a rectViewport
116+
Rectf getVisibleRect( const Rectf& rectViewport ) const;
117+
118+
//! Pan by \a deltaViewport pixels in viewport coordinates (useful for keyboard navigation)
119+
void panBy( const vec2& deltaViewport );
120+
//! Pan to \a posViewport position in viewport coordinates
121+
void panToViewportPos( const vec2& posViewport, bool disableAnimation = false );
122+
//! Pan to center \a posContent in the viewport
123+
void panToContentPos( const vec2& posContent, bool disableAnimation = false );
124+
//! Zoom by \a factor around viewport center
125+
void zoomBy( float factor, bool disableAnimation = false );
126+
//! Zoom by \a factor around \a anchorWindow point (window coordinates)
127+
void zoomBy( float factor, const vec2& anchorWindow, bool disableAnimation = false );
128+
//! Zoom to specific \a zoom level around viewport center
129+
void zoomTo( float zoom, bool disableAnimation = false );
130+
//! Zoom to specific \a zoom level around \a anchorWindow point (window coordinates)
131+
void zoomTo( float zoom, const vec2& anchorWindow, bool disableAnimation = false );
132+
133+
//! Fit all content in viewport (requires bounded content)
134+
void fitAll( bool disableAnimation = false );
135+
//! Fit content width to viewport width (requires bounded content)
136+
void fitWidth( bool disableAnimation = false );
137+
//! Fit content height to viewport height (requires bounded content)
138+
void fitHeight( bool disableAnimation = false );
139+
//! Fit and center rectangle \a contentRect in the viewport
140+
void fitRect( const Rectf& contentRect, bool disableAnimation = false );
141+
//! Zoom in by one step
142+
void zoomIn( bool disableAnimation = false );
143+
//! Zoom out by one step
144+
void zoomOut( bool disableAnimation = false );
145+
146+
//! Set custom viewport \a viewport rectangle in window coordinates
147+
void setCustomViewport( const Rectf& viewport );
148+
//! Disable custom viewport (use full window)
149+
void disableCustomViewport();
150+
//! Check if custom viewport is active
151+
bool hasCustomViewport() const { return mHasCustomViewport; }
152+
//! Get current viewport rectangle
153+
const Rectf& getViewport() const { return mViewport; }
154+
155+
//! Connect to \a window for automatic event handling
156+
void connect( const app::WindowRef& window, int signalPriority = 0 );
157+
//! Disconnect from window events
158+
void disconnect();
159+
//! Check if connected to a window
160+
bool isConnected() const { return mWindow != nullptr; }
161+
162+
//! Enable or disable all interactions
163+
void enable( bool enabled = true ) { mEnabled = enabled; }
164+
//! Disable all interactions
165+
void disable() { mEnabled = false; }
166+
//! Check if interactions are enabled
167+
bool isEnabled() const { return mEnabled; }
168+
//! Enable or disable keyboard shortcuts
169+
void setKeyboardEnabled( bool enabled ) { mKeyboardEnabled = enabled; }
170+
//! Check if keyboard shortcuts are enabled
171+
bool isKeyboardEnabled() const { return mKeyboardEnabled; }
172+
//! Enable or disable mouse wheel zooming
173+
void setMouseWheelEnabled( bool enabled ) { mMouseWheelEnabled = enabled; }
174+
//! Check if mouse wheel zooming is enabled
175+
bool isMouseWheelEnabled() const { return mMouseWheelEnabled; }
176+
177+
//! Set mouse \a buttons for panning
178+
void setPanMouseButtons( const std::vector<MouseButtonBinding>& buttons );
179+
180+
//! Set mouse wheel zoom \a multiplier (relative to zoom factor)
181+
void setMouseWheelMultiplier( float multiplier ) { mWheelMultiplier = multiplier; }
182+
//! Get mouse wheel zoom multiplier
183+
float getMouseWheelMultiplier() const { return mWheelMultiplier; }
184+
//! Set mouse wheel direction inverted
185+
void setMouseWheelInverted( bool inverted ) { mWheelInverted = inverted; }
186+
//! Check if mouse wheel is inverted
187+
bool isMouseWheelInverted() const { return mWheelInverted; }
188+
//! Enable zoom-to-cursor behavior
189+
void setZoomToCursor( bool enabled ) { mZoomToCursor = enabled; }
190+
//! Check if zoom-to-cursor is enabled
191+
bool isZoomToCursor() const { return mZoomToCursor; }
192+
//! Enable or disable animations (when disabled, overrides all other animation settings)
193+
void setAnimationEnabled( bool enabled ) { mAnimationEnabled = enabled; }
194+
//! Check if animations are enabled
195+
bool isAnimationEnabled() const { return mAnimationEnabled; }
196+
//! Enable animations for mouse pan operations (only effective when animations are enabled)
197+
void setAnimateMousePan( bool enabled ) { mAnimateMousePan = enabled; }
198+
//! Check if mouse pan animations are enabled (returns false if animations are disabled)
199+
bool hasAnimateMousePan() const { return mAnimationEnabled && mAnimateMousePan; }
200+
201+
//! Set animation \a duration in seconds
202+
void setAnimationDuration( float duration ) { mAnimationDuration = glm::clamp( duration, 0.1f, 2.0f ); }
203+
//! Get animation duration
204+
float getAnimationDuration() const { return mAnimationDuration; }
205+
//! Set animation \a easing curve power
206+
void setAnimationEasing( float easing ) { mAnimationEasing = glm::clamp( easing, 1.0f, 5.0f ); }
207+
//! Get animation easing curve power
208+
float getAnimationEasing() const { return mAnimationEasing; }
209+
210+
//! Set zoom limits between \a minZoom and \a maxZoom
211+
void setZoomLimits( float minZoom, float maxZoom )
212+
{
213+
mMinZoom = minZoom;
214+
mMaxZoom = maxZoom;
215+
}
216+
//! Get minimum zoom limit
217+
float getMinZoom() const { return mMinZoom; }
218+
//! Get maximum zoom limit
219+
float getMaxZoom() const { return mMaxZoom; }
220+
//! Set zoom \a factor for zoom in/out (0.1 = 10% change per step). Default is 0.2.
221+
void setZoomFactor( float factor ) { mZoomFactor = factor; }
222+
//! Get zoom factor
223+
float getZoomFactor() const { return mZoomFactor; }
224+
225+
//! Set \a hotkeys for zoom in
226+
void setHotkeysZoomIn( const std::vector<HotkeyBinding>& hotkeys );
227+
//! Set \a hotkeys for zoom out
228+
void setHotkeysZoomOut( const std::vector<HotkeyBinding>& hotkeys );
229+
//! Set \a hotkeys for reset zoom
230+
void setHotkeysReset( const std::vector<HotkeyBinding>& hotkeys );
231+
232+
//! Allow panning content outside viewport bounds (auto-disables constrainZoomToContent when enabled). Default is \c false.
233+
void setAllowPanOutside( bool allow );
234+
//! Check if content can be panned outside viewport bounds. Default is \c false
235+
bool getAllowPanOutside() const { return mAllowPanOutside; }
236+
//! Prevent zooming out beyond fitAll() level (auto-disables allowPanOutside when enabled, ignored for unbounded content). Default is \c false.
237+
void setConstrainZoomToContent( bool constrain );
238+
//! Return if zoom is constrained to content. Default is \c false.
239+
bool getConstrainZoomToContent() const { return mConstrainZoomToContent; }
240+
241+
//! Manual event handling (alternative to connect())
242+
void mouseDown( app::MouseEvent& event );
243+
void mouseUp( app::MouseEvent& event );
244+
void mouseDrag( app::MouseEvent& event );
245+
void mouseWheel( app::MouseEvent& event );
246+
void keyDown( app::KeyEvent& event );
247+
void resize( const ivec2& newSize );
248+
249+
private:
250+
// Transform state
251+
vec2 mPosition{ 0, 0 }; // Position offset in viewport-local coordinates (relative to viewport origin)
252+
float mZoom{ 1.0f }; // Current zoom scale factor
253+
std::optional<Rectf> mCanvasBounds; // Optional content bounds for constrained panning
254+
255+
// Viewport state
256+
Rectf mViewport{ 0, 0, 1, 1 }; // Current viewport in window coordinates
257+
bool mHasCustomViewport{ false }; // Whether custom viewport is set, otherwise uses full Window
258+
ivec2 mWindowSize{ 1, 1 }; // Full window size
259+
260+
// Cached transform matrices
261+
mutable mat4 mModelMatrix;
262+
mutable mat4 mInverseModelMatrix;
263+
mutable bool mIsDirty{ true };
264+
265+
// Configuration
266+
bool mEnabled{ true };
267+
bool mKeyboardEnabled{ true };
268+
bool mMouseWheelEnabled{ true };
269+
bool mAllowPanOutside{ false };
270+
bool mConstrainZoomToContent{ false };
271+
272+
// Mouse wheel settings
273+
float mWheelMultiplier{ 1.0f }; // Multiplier relative to zoom factor (1.0 = same as zoom factor)
274+
bool mWheelInverted{ false };
275+
bool mZoomToCursor{ true };
276+
bool mAnimationEnabled{ true }; // Animation enabled by default for buttons/hotkeys
277+
bool mAnimateMousePan{ false }; // Mouse pan animation disabled by default for responsiveness
278+
279+
// Zoom settings
280+
float mMinZoom{ 0.01f };
281+
float mMaxZoom{ 100.0f };
282+
float mZoomFactor{ 0.2f }; // Default 20% zoom change per step
283+
284+
std::vector<HotkeyBinding> mHotkeysZoomIn;
285+
std::vector<HotkeyBinding> mHotkeysZoomOut;
286+
std::vector<HotkeyBinding> mHotkeysReset;
287+
std::vector<MouseButtonBinding> mPanMouseButtons;
288+
289+
// Animation state
290+
bool mAnimating{ false };
291+
float mAnimationDuration{ 0.3f }; // Duration of animation in seconds
292+
float mAnimationEasing{ 3.0f }; // Easing power for cubic ease-out (1.0 = linear, higher = more curved)
293+
float mZoomAnimationStart{ 1.0f }; // Starting zoom level
294+
float mZoomAnimationTarget{ 1.0f }; // Target zoom level
295+
vec2 mPositionAnimationStart{ 0.0f }; // Starting position
296+
vec2 mPositionAnimationTarget{ 0.0f }; // Target position
297+
vec2 mZoomAnimationAnchor{ 0.0f }; // Anchor point for zoom animation
298+
vec2 mZoomAnimationContentPoint{ 0.0f }; // Content point under anchor at start
299+
double mAnimationStartTime{ 0.0 }; // When animation started
300+
bool mAnimatingZoom{ false }; // Whether zoom is being animated
301+
bool mAnimatingPosition{ false }; // Whether position is being animated
302+
303+
// Interaction state
304+
bool mIsDragging{ false };
305+
ivec2 mDragStartPosWindow;
306+
vec2 mDragStartPosition;
307+
int mDragInitiator{ 0 }; // Which button started the drag (LEFT_DOWN, RIGHT_DOWN, or MIDDLE_DOWN)
308+
309+
// Connection state
310+
app::WindowRef mWindow;
311+
std::vector<signals::Connection> mConnections;
312+
313+
314+
// Internal methods
315+
void fitWithCentering( float newZoom, bool disableAnimation = false );
316+
void zoomByStep( float factor ); // Zoom from center with constraints
317+
float getEffectiveMinZoom() const; // Get minimum zoom considering content constraint
318+
void zoomByStep( float factor, const vec2& anchorViewport ); // Zoom from specified anchor in viewport-local coordinates
319+
void setZoomAndPosition( float newZoom, const vec2& newPositionViewport ); // Helper for zoom presets, position in viewport-local coordinates
320+
void updateMatrices() const;
321+
void constrainPosition();
322+
vec2 calculateConstrainedPosition( const vec2& desiredPos ) const;
323+
vec2 calculateConstrainedPosition( const vec2& desiredPos, float zoom ) const;
324+
void updateViewportSize();
325+
void updateAnimation();
326+
void startZoomAnimation( float zoomFactor, const vec2& anchorViewport ); // Anchor in viewport-local coordinates
327+
void startPositionAnimation( const vec2& targetPositionViewport ); // Target position in viewport-local coordinates
328+
void startZoomAndPositionAnimation( float targetZoom, const vec2& targetPositionViewport ); // Target position in viewport-local coordinates
329+
bool matchesHotkey( const app::KeyEvent& event, const std::vector<HotkeyBinding>& hotkeys ) const;
330+
bool matchesPanMouseButton( const app::MouseEvent& event, const std::vector<MouseButtonBinding>& buttons ) const;
331+
};
332+
333+
} // namespace cinder

0 commit comments

Comments
 (0)