Mousewheel Zoom-To-Cursor

BlitzMax Forums/BlitzMax OpenGL Programming/Mousewheel Zoom-To-Cursor

Hello everybody. I'm having trouble implementing a specific feature and was looking for some advice.

I'd like to zoom the 2D playing field in a manner similar to Supreme-Commander, in that the zoom centers on the mouse cursor's current location. Have tried many different methods with little success.

Note that the current method included in my source code is simply a 'safe mode' that does not try and center upon the cursor.

Use mousewheel to zoom, and click-drag to pan around the playing field. You are welcome to use any code snippets you might find useful. Be warned this is full of globals and other bad programming practices.. the program is simply a quick-n-dirty test, to get the zoom algorithm down and be thrown away thereafter.

Thank you in advance for any assistance provided!



EDIT: Link Removed. Solution code posted below. Have a nice day.

Any ideas? Someone surely has tackled this problem before..

Are you after a solution using native OpenGl code which is what this forum is for. If so, I can't help.
I checked your code though and it doesn't seem to use OGL directly (or use the OGL driver). I couldn't find any zoom or pan functions in place.
Isn't it a case of taking the mousex and mousey and making that the centre of your world, set the origin and then zooming?

I am using Max2D - that is based upon OGL, or so I assumed.

Being horribly written, the zoom and pan functions are built right into the event manager.

This would be much easier if I could access OGL code directly, while still using the nice conveniences provided by Max2D.

Nevertheless, I will implement your suggestion, setting the origin to the mouse's world-coordinates. This seems incredibly logical now that you mention it.

*feeling dumb*

I think this does not directly pertain to OGL, as your asking about zooming to a specific point and not anything specific to OGL (you would get more responses in the general programming section anyways, as more people frequent it then here.)

I will move my post, thanks

P.S. Default driver is DX (on Windows) but not sure what you're developing on.
If you're after native OGL then this might help with the zoom while panning a camera is simply resetting the setorigin.

So, I figured out how to do it. Finally sat down and drew everything out, which helped immensely.


I'll post the code below in case anyone is interested, and so that this is not an entirely wasted thread.





NOTE: Much better version posted below




Neither of the code pieces below actually REQUIRE MaxGUI. I'm simply using the Desktop() TGadget to pick the user's native resolution, then setting the Globals GW and GH to the native Width and Height (resolution) respectively. If you'd like to compile this without having MaxGUI, just take out the "Import Maxgui.Drivers" line, and then set the globals GW and GH to your desired full screen resolution. Is there a better technique for reading the native resolution without resorting to maxgui?

Anyway, back on topic.. the code..


SuperStrict

Framework BRL.GLMax2D
Import BRL.Random
Import maxgui.drivers



Type obj
	Global List:TList
	
	Field X:Float
	Field Y:Float
	Field Size:Float
	Field HalfSize:Float
	Field R:Int
	Field G:Int
	Field B:Int
	

	
	Method New() 
		X = Rand(0 , 800) 
		Y = Rand(0 , 800) 
		Size = Rnd(0 , 1) 
		Size = size^2
		HalfSize = Size / 2.0
		R = Rand(0,255)
		G = Rand(0,255)	
		B = Rand(0 , 255) 
		
		If Not List Then List = New TList
		
		List.AddLast(Self)
		
	End Method
	
	Method Draw(ViewOriginX:Float,ViewOriginY:Float,_VIEWSCALE:Float) 
		SetColor(r,g,b)
		Local drawx:Float = ((X- ViewOriginX) * _viewscale) 
		Local drawy:Float = ((Y- ViewOriginY) * _viewscale)
		DrawRect(drawX-HalfSize,drawY-HalfSize,size+1,size+1)
	End Method
	
End Type







Global GW:Int = ClientWidth(Desktop())
Global GH:Int = ClientHeight(Desktop())
Global GHW:Float = GW/2.0
Global GHH:Float = GH/2.0



Global Dragging:Byte = 0
Global MSX:Int
Global MSY:Int


Global Viewscale:Float = 1.0
Global WorldCoord_ViewOriginX:Float = GHW
Global WorldCoord_ViewOriginY:Float = GHH

Global BigViewLastScale:Float
Global BigViewLastX:Float
Global BigViewLastY:Float
Global BigViewZoomScale:Float = 1.0
Global BigViewActive:Byte = False

Global ZoomDuration:Float=250
Global ZoomFactor:Float = 1.5

Global ZoomMouseX:Int
Global ZoomMouseY:Int
Global ZoomMouseBuffer:Int = 5
Global ZoomTimer:Int
Global ZoomFromX:Float
Global ZoomFromY:Float
Global ZoomFromScale:Float
Global ZoomTargetX:Float=GHW
Global ZoomTargetY:Float=GHH
Global ZoomTargetScale:Float = 1
Global ZoomMAX:Float = 40.0
Global ZoomMin:Float = 0.4
	
	
	
	
	

SetGraphicsDriver(GLMax2DDriver())
Global GraphicsContext:TGraphics = CreateGraphics(GW,GH,32,60, Graphics_BACKBUFFER)
SetGraphics(GraphicsContext)


	
AddHook EmitEventHook, EventHook


For Local i:Int = 0 To 2000
	Local temp:obj = New obj
Next



While Not KeyHit(KEY_ESCAPE)
Cls

SetOrigin(GHW , GHH) 


CalculateViewScale()
SetScale(viewscale , viewscale)

For Local t:obj = EachIn Obj.List
	t.draw(WorldCoord_ViewOriginX, WorldCoord_ViewOriginY, viewscale) 
Next





SetScale(1 , 1) 
SetOrigin(0 , 0) 

SetColor(255,255,255)
DrawText("SCALE:  " + viewscale , 50 , 50) 
DrawText("Draw Origin: [WORLD ]: " + WorldCoord_ViewOriginX + " , " + WorldCoord_ViewOriginY , 50 , 90)
DrawText("Zoom Target: [WORLD ]: " + ZoomTargetX + " , " + ZoomTargetY , 50 , 105) 



Flip 0

Wend







Function SetZoomTarget(SX:Int , SY:Int, Direction:Int)
	
	If (ZoomTargetScale < ZoomMax And Direction > 0) Or (ZoomTargetScale > ZoomMin And Direction < 0)
	
		
		ZoomFromX = WorldCoord_ViewOriginX
		ZoomFromY = WorldCoord_ViewOriginY
		ZoomFromScale = Viewscale
		
			
		If Direction > 0
						
			If ((ZoomMouseX > SX + ZoomMouseBuffer) Or (ZoomMouseX < SX - ZoomMouseBuffer) Or (ZoomMouseY > SY + ZoomMouseBuffer) Or (ZoomMouseY < SY - ZoomMouseBuffer))
				
				ZoomTargetX = ((SX - GHW) / viewscale) + WorldCoord_ViewOriginX
				ZoomTargetY = ((SY - GHH) / viewscale) + WorldCoord_ViewOriginY
				
				ZoomMouseX = SX
				ZoomMouseY = SY
			
			EndIf
		
			ZoomTargetScale:* ZoomFactor
			
		Else
		
			ZoomTargetScale:/ ZoomFactor

		EndIf
		
				
		If ZoomTargetScale > ZoomMax
			ZoomTargetScale = ZoomMax
		Else If ZoomTargetScale < ZoomMin
			ZoomTargetScale = ZoomMin
		EndIf

		Zoomtimer = MilliSecs()
		
		
	EndIf

End Function



Function CalculateViewScale()
	
	Local Amount:Float = ((MilliSecs()-ZoomTimer) / ZoomDuration)
		
	If  Amount < 1.0
		
		Local FlatAmount:Float = Amount ^ 0.5
		Local HalfAmount:Float = Amount/2.0
		WorldCoord_ViewOriginX=  ZoomFromX + (ZoomTargetX - ZoomFromX) * FlatAmount
		WorldCoord_ViewOriginY=  ZoomFromY + (ZoomTargetY - ZoomFromY) * FlatAmount
		ViewScale	=	ZoomFromScale + (ZoomTargetScale - ZoomFromScale) * HalfAmount
	
	Else	If Amount < 2.0
	
		Local HalfAmount:Float = amount/2.0
		ViewScale	=	ZoomFromScale + (ZoomTargetScale - ZoomFromScale) *HalfAmount
		
	EndIf
	
		
	SetBlend(LightBlend) 
	SetAlpha(0.2) 
	SetColor(255 , 55 , 250)
	SetLineWidth(2)	
		Local X1:Float = ( (ZoomTargetX - WorldCoord_ViewOriginX) * viewscale) 
		Local Y1:Float = ( (ZoomTargetY - WorldCoord_ViewOriginY) * viewscale)
		DrawLine(X1 - 15 , Y1 , X1 + 15 , Y1) 
		DrawLine(X1 , Y1 - 15 , X1 , Y1 + 15 ) 
		
	SetLineWidth(1)
	SetBlend(SolidBlend)
		
End Function



Function BigViewMode(State:Byte) 
	If State
		If Viewscale > BigViewZoomScale
			BigViewActive = True
			BigViewLastScale = Viewscale
			BigViewLastX = WorldCoord_ViewOriginX
			BigViewLastY = WorldCoord_ViewOriginY
			Viewscale = BigViewZoomScale
			Zoomtimer = 2.0 * ZoomDuration - MilliSecs()
		EndIf
		
	Else
		If BigViewActive
		
			ZoomFromX = WorldCoord_ViewOriginX
			ZoomFromY = WorldCoord_ViewOriginY
			ZoomFromScale = BigViewZoomScale
			ZoomTargetX = BigViewLastX
			ZoomTargetY = BigViewLastY
			BigViewActive = False
			Viewscale = BigViewLastScale
			Zoomtimer = MilliSecs() 
		EndIf
	
	EndIf
End Function






Function EventHook:Object(ID:Int , Data:Object , Context:Object) 
	Local Event:TEvent = TEvent(data)
	If Event = Null Then Return event
	
	Select event.id
		Case EVENT_MOUSEWHEEL
			
			If Not BigViewActive Then SetZoomTarget(event.x , event.y, event.data) 
			
							
		Case EVENT_APPTERMINATE
			End
			
		Case EVENT_KEYDOWN
			Select Event.Data
				Case KEY_ESCAPE End
				Case KEY_LShift , KEY_RShift , KEY_SPACE
					BigViewMode(True)
									
			End Select
				
		Case EVENT_KEYUP
			Select event.data
				Case KEY_LShift , KEY_RShift , KEY_SPACE
					BigViewMode(False)
										
			End Select
			
	
		Case EVENT_MOUSEDOWN
			Dragging = 1
			MSX = Event.X
			MSY = Event.Y
			
		Case EVENT_MOUSEUP
			Dragging = 0
			
		Case EVENT_MOUSEMOVE
			
			
			If Dragging
				Local dx:Float = (Event.X-MSX)/viewscale
				Local dy:Float = (Event.Y-MSY)/viewscale
				WorldCoord_ViewOriginX:- dx
				WorldCoord_ViewOriginY:- dy
				ZoomTargetX:- dx
				ZoomTargetY:- dy
				
				MSX = Event.X
				MSY = Event.Y
				
			EndIf			

			
End Select


End Function








Again check out the version below, it's much better.

Improved version

Mousewheel to zoom in / out, centered upong location of cursor. Click and drag to pan. Double clicking instantly zooms full-in on the mouse cursor.








SuperStrict

Framework BRL.GLMax2D
Import BaH.Random
Import maxgui.drivers
Import BRL.PNGLoader
Import BRL.BMPLoader


Type obj
	Global List:TList
	
	Field X:Float
	Field Y:Float
	Field Size:Float
	Field HalfSize:Float
	Field R:Int
	Field G:Int
	Field B:Int
	

	
	Method New() 
		X = Rand(0 , GW) 
		Y = Rand(0 , GH) 
		Size = Rnd(0 , 1) 
		Size = size^3
		HalfSize = Size / 2.0
		R = Rand(0,255)
		G = Rand(0,255)	
		B = Rand(0 , 255) 
		
		If Not List Then List = New TList
		
		List.AddLast(Self)
		
	End Method
	
	Method Draw(ZoomOriginX:Float,ZoomOriginY:Float,_VIEWSCALE:Float) 
		SetColor(r,g,b)
		Local drawx:Float = ( (X - ZoomOriginX  ) * _viewscale)
			Local drawy:Float = ((Y- ZoomOriginY ) * _viewscale)
		DrawRect(drawX-HalfSize,drawY-HalfSize,size+1,size+1)
	End Method
	
End Type







Global GW:Int = ClientWidth(Desktop())
Global GH:Int = ClientHeight(Desktop())
Global GHW:Float = GW/2.0
Global GHH:Float = GH/2.0


Global Dragging:Byte = 0
Global MSX:Int
Global MSY:Int
Global MPosX:Int
Global MPosY:Int
Global DoubleClickTime:Int
Global DoubleClickDelay:Int = 300

Global Viewscale:Float = 1.0
Global WorldViewOriginX:Float = GHW
Global WorldViewOriginY:Float = GHH

Global ZoomTargetX:Float = GHW
Global ZoomTargetY:Float = GHH
Global ZoomTargetScale:Float = 1
Global ZoomFactor:Float = 1.25
Global ZoomMAX:Float = 30.0
Global ZoomMin:Float = 0.50


'Global RootDir:String = CurrentDir()

	
	
AddHook EmitEventHook, EventHook
	

SetGraphicsDriver(GLMax2DDriver() ) 


?Debug
	Global GraphicsContext:TGraphics = CreateGraphics(GW , GH , 0 , 60 , Graphics_BACKBUFFER) 
?Not Debug
	Global GraphicsContext:TGraphics = CreateGraphics(GW , GH , 32 , 60 , Graphics_BACKBUFFER)
?

SetGraphics(GraphicsContext) 


HideMouse()


For Local i:Int = 0 To 3000
	Local temp:obj = New obj
Next



Local ms:Int,time:Int,dt:Float

While Not KeyHit(KEY_ESCAPE)
	'ms = MilliSecs() 
	'dt = ms - time
	'time = ms
	
	'Print 1000.0/dt
	
	
	
	Cls
	
	SetScale(1 , 1)
	SetOrigin(GHW,GHH)
	
	

	
	SetColor(255 , 255 , 0) 
	SetLineWidth(5)
		Local tempx:Float = (-WorldViewOriginX * viewscale) 
		Local tempy:Float = (-WorldViewOriginY * viewscale )
		Local tempx2:Float = tempx+(GW*viewscale)
		Local tempy2:Float = tempy+(GH*viewscale)
		
		DrawLine(tempx , tempy , tempx2 , tempy) 
		DrawLine(tempx2, tempy , tempx2 , tempy2) 
		DrawLine(Tempx2 , tempy2 , tempx , tempy2) 
		DrawLine(tempx , tempy2 , tempx , tempy) 
	SetLineWidth(1)
	SetColor(255 , 255 , 255) 
	
	
		SetViewScale() 
	
	
	For Local t:obj = EachIn Obj.List
		t.draw(WorldViewOriginX, WorldViewOriginY, viewscale) 
		
	Next
		
				
	SetScale(1 , 1) 
	SetOrigin(0 , 0)
	
	
	SetBlend(LightBlend) 
	SetAlpha(0.4) 
	SetColor(255 , 155 , 0)
	SetLineWidth(2)	

		DrawLine(MPosX- 15 , MPosY, MPosX+ 15 , MPosY) 
		DrawLine(MPosX, MPosY- 15 , MPosX, MPosY+ 15 ) 
	
			
	SetLineWidth(1)
	SetBlend(SolidBlend) 
	
	
	
	SetColor(255 , 255 , 255) 
	DrawText("SCALE:  " + viewscale , 50 , 50) 
	
	Flip 0

Wend





Function SetViewScale()
	
		
		WorldViewOriginX = ZoomTargetX
		WorldViewOriginY = ZoomTargetY
		ViewScale = ZoomTargetScale
		
		SetScale(Viewscale , Viewscale)
	
End Function






Function ZoomIn(MouseScreenX:Int , MouseScreenY:Int) 
	
	If Not (ZoomTargetScale  >= ZoomMax) 	
	
		Local mx:Float = (MouseScreenX - GHW) /ZoomTargetScale
		Local my:Float = (MouseScreenY - GHH) /ZoomTargetScale
		Local z:Float = 1.0 - (1.0/ZoomFactor)
		
		ZoomTargetX = (mx)*(z) + WorldViewOriginX
		ZoomTargetY = (my)*(z) + WorldViewOriginY
					
		If ZoomTargetX > GW
			ZoomTargetX = GW
		Else If ZoomTargetX < 0
			ZoomTargetX = 0
		EndIf
				
		If ZoomTargetY > GH
			ZoomTargetY = GH
		Else If ZoomTargetY < 0
			ZoomTargetY = 0
		EndIf
			
					
		ZoomTargetScale:* ZoomFactor
		
		If ZoomTargetScale  > ZoomMax Then ZoomTargetScale  = ZoomMax
	
	EndIf
	
	
End Function




Function ZoomOut(MouseScreenX:Int , MouseScreenY:Int)

	If Not (ZoomTargetScale  =< ZoomMin) 
	
		Local mx:Float = (MouseScreenX - GHW) / ZoomTargetScale
		Local my:Float = (MouseScreenY - GHH) / ZoomTargetScale	
		Local z:Float = 1.0 - ZoomFactor
		
		ZoomTargetX  = mx*(z)+ WorldViewOriginX
		ZoomTargetY = my*(z)+ WorldViewOriginY
	
		ZoomTargetScale:/ ZoomFactor
		
		If ZoomTargetScale < ZoomMin Then ZoomTargetScale = ZoomMin
		
	EndIf
End Function





Function DoubleClick(button:Int , MouseScreenX:Int , MouseScreenY:Int)
	ZoomTargetX = WorldViewOriginX + (MouseScreenX - GHW) / ZoomTargetScale 	
	ZoomTargetY = WorldViewOriginY + (MouseScreenY - GHH) / ZoomTargetScale
	ZoomTargetScale = ZoomMax
End Function



Function Drag(MouseScreenX:Int , MouseScreenY:Int) 
	Local dx:Float = (MouseScreenX - MSX) / viewscale
	Local dy:Float = (MouseScreenY - MSY) / viewscale
	
	WorldViewOriginX:- dx
	WorldViewOriginY:- dy
	ZoomTargetX:- dx
	ZoomTargetY:- dy
	
	MSX = MouseScreenX
	MSY = MouseScreenY
	
	If WorldViewOriginX < 0
		WorldViewOriginX = 0
		ZoomTargetX = 0
	Else If WorldViewOriginX > GW
		WorldViewOriginX = GW
		ZoomTargetX = GW
	EndIf
	
	If WorldViewOriginY < 0
		WorldViewOriginY = 0
		ZoomTargetY = 0
	Else If WorldViewOriginY > GH
		WorldViewOriginY = GH
		ZoomTargetY = GH
	EndIf	
			
	
End Function




Function EventHook:Object(ID:Int , Data:Object , Context:Object) 
	Local Event:TEvent = TEvent(data)
	If Event = Null Then Return event
	
	Select event.id
		Case EVENT_MOUSEWHEEL
				
			If Event.Data > 0
				ZoomIn(event.x,Event.Y)
			Else
				ZoomOut(event.x,Event.Y)
			EndIf
		
							
		Case EVENT_APPTERMINATE
			End
			
			
		Case EVENT_KEYDOWN
			Select Event.Data
				Case KEY_ESCAPE End
					
									
			End Select
				
	
			
	
		Case EVENT_MOUSEDOWN
				
				Local ms:Int = MilliSecs()
					If ms - DoubleClickTime =< DoubleClickDelay
						DoubleClick(event.Data , event.X , event.Y) 
						Return Null
					Else DoubleClickTime = ms
				EndIf

			Select Event.data
			
				Case 1
					Dragging = 1
					MSX = Event.X
					MSY = Event.Y
					
			
				Case 3
					Dragging = 1
					MSX = Event.X
					MSY = Event.Y
					
					
			
			End Select
			
			
		Case EVENT_MOUSEUP
			Select Event.data
				Case 1,3
					Dragging = 0
			
			End Select
			
			
		Case EVENT_MOUSEMOVE
			
			MPosx = Event.x
			MPosY = Event.y
			
			If Dragging
				Drag(event.x,event.y)
				
			EndIf			

			
End Select


End Function






There is a small "Jump" at the limits of max / min zoom. It can be fixed simply by removing one line of code from each of the zoom functions making the effect much more fluid.

I've also commented out the zoomTarget* limitation code in ZoomIn() because I cannot see what it is achieving!

Otherwise this is a fantastic bit of code and I hope to find a good use for it at some point.

Cheers,

Function ZoomIn(MouseScreenX:Int , MouseScreenY:Int) 
	If Not (ZoomTargetScale  >= ZoomMax) 	
		Local mx:Float = (MouseScreenX - GHW) /ZoomTargetScale
		Local my:Float = (MouseScreenY - GHH) /ZoomTargetScale
		Local z:Float = 1.0 - (1.0/ZoomFactor)
		ZoomTargetX = (mx)*(z) + WorldViewOriginX
		ZoomTargetY = (my)*(z) + WorldViewOriginY

rem Cannot see what this achieves					
		If ZoomTargetX > GW
			ZoomTargetX = GW
		Else If ZoomTargetX < 0
			ZoomTargetX = 0
		EndIf
		If ZoomTargetY > GH
			ZoomTargetY = GH
		Else If ZoomTargetY < 0
			ZoomTargetY = 0
		EndIf
end rem 			
					
		ZoomTargetScale:* ZoomFactor

'# BUGFIX - Scaremonger
'		If ZoomTargetScale  > ZoomMax Then ZoomTargetScale  = ZoomMax
	
	EndIf
End Function

Function ZoomOut(MouseScreenX:Int , MouseScreenY:Int)
	If Not (ZoomTargetScale <= ZoomMin) 
		Local mx:Float = (MouseScreenX - GHW) / ZoomTargetScale
		Local my:Float = (MouseScreenY - GHH) / ZoomTargetScale	
		Local z:Float = 1.0 - ZoomFactor
		ZoomTargetX  = mx*(z)+ WorldViewOriginX
		ZoomTargetY = my*(z)+ WorldViewOriginY
		ZoomTargetScale:/ ZoomFactor
		
'# BUGFIX - Scaremonger
'		If ZoomTargetScale < ZoomMin Then ZoomTargetScale = ZoomMin

	EndIf
End Function



Ive been looking through the code but cant understand it enough to adapt to my own uses. I need it for a map drawn with tile images,

Glad it's been of use... If you still need help adapting it, send me an email (usnavyfish AT gmail)