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