Maelstrom: Added an official zoom mode

From db690ea7ff26e15279781ac2851d6eeac57ec36b Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 2 Apr 2026 22:25:04 -0700
Subject: [PATCH] Added an official zoom mode

---
 Data/Images/zoom-in.png  | Bin 0 -> 781 bytes
 Data/Images/zoom-out.png | Bin 0 -> 422 bytes
 Data/UI/game.xml         |  36 +++++++--
 game/Maelstrom_Globals.h |   1 +
 game/controls.cpp        |   2 +
 game/game.cpp            | 159 +++++++++++++++++----------------------
 game/game.h              |   8 +-
 game/init.cpp            |   4 +-
 8 files changed, 108 insertions(+), 102 deletions(-)
 create mode 100644 Data/Images/zoom-in.png
 create mode 100644 Data/Images/zoom-out.png

diff --git a/Data/Images/zoom-in.png b/Data/Images/zoom-in.png
new file mode 100644
index 0000000000000000000000000000000000000000..d38fbf3bff92715887b9dcbdf045caa5f81d17b1
GIT binary patch
literal 781
zcmV+o1M>WdP)<h;3K|Lk000e1NJLTq001EX001Bm1^@s6%qY!?0008iNkl<ZXx{CZ
z%S%*o6vyu!#~B|vjg6^6k&&Wg4MI>;0}=G1sD+E7hhd8#i2eX+*(&G(1>qv1h#*3G
zV2dIu+7voW;v%wHNF!-!`6{(}__!^;A6vAlV4QL0p3R4I@9*B<`JH=y=bYa)3|nlm
z#g+^d%d#xXGCDHF9!Gi{kbhiS{YUBkpy<7nD-qy?kHvFQ@!3{!$prUTWo<;5rfHg{
zF(a*@TrA%%dXr-1DTa?Nv?HA9iH`!9zfX+KibYRx*ksn{2+4TAm>9<a2gT8=Yz=>o
zIO6xb@8D}P$(eN7l_p1cdqme0@#iklSIpMv&XF9E*eT9jWNY+sy^yWp!yzLX>j(^8
zgF2toYG9lNbTO`h>qYPr$&qY1o8YbM#liUotBL13(ci-ra5)@;!?2e5J5uHd8Rm{)
z$2zg@t~k-fPK#+=$x-}N&4M3^CExH%kaF21&XFTd@LXpl(>@S9Wnz-|#p-y^bE?m6
zF;cDhV;|&@uZ@3Q-v7-JPM@?38^z#eaW#Fw55Ck0kx-gPPPj=xu=gEAKs^g|T@gcb
zV#RqDxJ4YeMcLiM{rg1FQXI65`$Wd>kR0))Cq-MMkdCkI0ETQ6rwQ4-I~ata@fN=L
zNzsjCtb2S}o~!7^5zS)X3no0w`!+C76}bgF^2016$__e>9hZ6Dm+Dsex|sJCy#>-T
zyBg4Y;djPA<gBq)Y;6?7VX<aNG(9{w%=jJXAvg)&5UG9Wql@g!QCchBI4izlA^z@k
z*_Ed9IYqTePLV|Say`J8ts?tJv(6DLkQ|Y0C3g%^RXB5uMgAz43+9N0K7Dey^0blc
zigC<J+Js~O+XpPt7Tcma7}s#qXY?ak|A%V+#|#Zvj4~J?GT0&mhZq;gksmYec5=fs
zW(<mDafbKj$S*thCUL(@Y;G5Q)kdxfjTe1{<E3GXEw<QVOXkZjuifG|vQ-pv00000
LNkvXXu0mjf)J1rZ

literal 0
HcmV?d00001

diff --git a/Data/Images/zoom-out.png b/Data/Images/zoom-out.png
new file mode 100644
index 0000000000000000000000000000000000000000..507cd2f1cffa9c1b3695380ad00700d2bdf3b1ab
GIT binary patch
literal 422
zcmV;X0a^ZuP)<h;3K|Lk000e1NJLTq001EX001Bm1^@s6%qY!?0004PNkl<ZXx{DD
z&nts*90%~{+4gKGKV~gIO01|6wK&X=a#5Td-24Gb`3sh_gHp=bfwGdM`~kR87IMN4
zYD+T0fz>oWz7FsAWlkm&dA~2OK6`q;-_Q56XRF7u6h%=KMNt$*QIy{kV~jDz3eh0#
zr`<o?M;rCi-mk7_7q_u5i7MCu!>jP|2)c5xvI%Be;p-+p--Q(!7@CCp^)Q+#tgQu5
z<-mM9%!Rqm7IbD{lKbV(Y}>YN`(~^}gYX1U&Go>6C~VHdhakL*K))N-EWwLo4y+SC
zufV!lcpWROoj!QE49iA%eFJ8yIiOlzzlFmI4C3nav*+^^_W+gin_eGfBZE9p19ZpP
z5F35s!QLjIkApF5u#xwtw>i)cwKdN5y0}jZd|@BX0gsV_CsXWWp7#ez{o7oG?nBlw
znu6X4>+|o~&l%!+37VyQd{Wb_@AWUPD2k#e%0GK~^G-w8jwYfgit-2Y4$C~y(P1;q
Q(EtDd07*qoM6N<$f>zVVk^lez

literal 0
HcmV?d00001

diff --git a/Data/UI/game.xml b/Data/UI/game.xml
index 88aacf10..5f574aa5 100644
--- a/Data/UI/game.xml
+++ b/Data/UI/game.xml
@@ -122,6 +122,20 @@
 						</Image>
 					</Elements>
 				</Button>
+				<Button name="zoom" action="CONTROL_ZOOM" image="circle">
+					<Size w="30" h="30"/>
+					<Anchor anchorFrom="TOP" anchorTo="BOTTOM" anchor="pause" y="8"/>
+					<Elements>
+						<Image name="zoom_in" image="zoom-in">
+							<Size w="24" h="24"/>
+							<Anchor anchorFrom="CENTER" anchorTo="CENTER"/>
+						</Image>
+						<Image name="zoom_out" image="zoom-out">
+							<Size w="24" h="24"/>
+							<Anchor anchorFrom="CENTER" anchorTo="CENTER"/>
+						</Image>
+					</Elements>
+				</Button>
 
 				<Thumbstick image="split-circle">
 					<Size w="85" h="85"/>
@@ -199,6 +213,20 @@
 						</Image>
 					</Elements>
 				</Button>
+				<Button name="zoom" action="CONTROL_ZOOM" image="circle">
+					<Size w="45" h="45"/>
+					<Anchor anchorFrom="TOP" anchorTo="BOTTOM" anchor="pause" y="12"/>
+					<Elements>
+						<Image name="zoom_in" image="zoom-in">
+							<Size w="36" h="36"/>
+							<Anchor anchorFrom="CENTER" anchorTo="CENTER"/>
+						</Image>
+						<Image name="zoom_out" image="zoom-out">
+							<Size w="36" h="36"/>
+							<Anchor anchorFrom="CENTER" anchorTo="CENTER"/>
+						</Image>
+					</Elements>
+				</Button>
 
 				<Thumbstick image="split-circle">
 					<Size w="128" h="128"/>
@@ -253,13 +281,5 @@
 			</Elements>
 
 		</Area>
-
-		<Button name="test_button" action="CONTROL_TEST" image="circle">
-			<Size w="45" h="45"/>
-			<Anchor anchorFrom="TOP" anchorTo="TOP" y="8"/>
-		</Button>
-		<Label name="test_label" template="SmallYellow">
-			<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="test_button" x="4"/>
-		</Label>
 	</Elements>
 </Panel>
diff --git a/game/Maelstrom_Globals.h b/game/Maelstrom_Globals.h
index 22a49026..f3ecbc9a 100644
--- a/game/Maelstrom_Globals.h
+++ b/game/Maelstrom_Globals.h
@@ -142,6 +142,7 @@ extern Replay   gReplay;
 extern Controls	controls;
 extern PrefsVariable<int> gSoundLevel;
 extern PrefsVariable<int> gGammaCorrect;
+extern PrefsVariable<int> gZoomGame;
 // int scores.cpp :
 extern Scores	hScores[NUM_SCORES];
 
diff --git a/game/controls.cpp b/game/controls.cpp
index 84ccb5c4..c7ec3d77 100644
--- a/game/controls.cpp
+++ b/game/controls.cpp
@@ -58,12 +58,14 @@ Controls::Bind(Prefs *prefs)
 Controls controls;
 PrefsVariable<int> gSoundLevel("SoundLevel", 4);
 PrefsVariable<int> gGammaCorrect("GammaCorrect", 3);
+PrefsVariable<int> gZoomGame("ZoomGame", 0);
 
 
 void LoadControls(void)
 {
 	gSoundLevel.Bind(prefs);
 	gGammaCorrect.Bind(prefs);
+	gZoomGame.Bind(prefs);
 	controls.Bind(prefs);
 }
 
diff --git a/game/game.cpp b/game/game.cpp
index 07ed182a..01502168 100644
--- a/game/game.cpp
+++ b/game/game.cpp
@@ -185,8 +185,8 @@ GamePanelDelegate::OnLoad()
 	m_frags = m_panel->GetElement<UIElement>("frags");
 
 	m_paused = m_panel->GetElement<UIElement>("paused");
-
-	m_zoom = false;
+	m_zoomIn = m_panel->GetElement<UIElement>("zoom_in");
+	m_zoomOut = m_panel->GetElement<UIElement>("zoom_out");
 
 	return true;
 }
@@ -490,9 +490,7 @@ GamePanelDelegate::OnDraw(DRAWLEVEL drawLevel)
 		return;
 	}
 
-	if (m_zoom) {
-		StartZoomedDrawing();
-	}
+	StartZoomedDrawing();
 
 	/* -- Draw the star field */
 	for ( i=0; i<MAX_STARS; ++i ) {
@@ -514,9 +512,7 @@ GamePanelDelegate::OnDraw(DRAWLEVEL drawLevel)
 
 	DrawBorder();
 
-	if (m_zoom) {
-		StopZoomedDrawing();
-	}
+	StopZoomedDrawing();
 }
 
 bool
@@ -527,15 +523,12 @@ GamePanelDelegate::HandleEvent(const SDL_Event &event)
 			m_touchControls->Show();
 		}
 	} else if (event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED ||
-	           event.type == SDL_EVENT_WINDOW_SAFE_AREA_CHANGED ||
-	           event.type == SDL_EVENT_REMOTE_PLAYERS_CHANGED) {
+	           event.type == SDL_EVENT_WINDOW_SAFE_AREA_CHANGED) {
 		UpdateZoom();
 	}
 	return false;
 }
 
-static int gZoomMode = 0;
-
 bool
 GamePanelDelegate::OnAction(UIBaseElement *sender, const char *action)
 {
@@ -568,9 +561,8 @@ GamePanelDelegate::OnAction(UIBaseElement *sender, const char *action)
 			control = PAUSE_KEY;
 		} else if (SDL_strcasecmp(action, "ABORT") == 0) {
 			control = ABORT_KEY;
-		} else if (SDL_strcasecmp(action, "TEST") == 0) {
-			gZoomMode = (gZoomMode + 1) % 3;
-			UpdateZoom();
+		} else if (SDL_strcasecmp(action, "ZOOM") == 0) {
+			ToggleZoomGame();
 			return true;
 		} else {
 			error("Unknown control action '%s'", action);
@@ -610,73 +602,60 @@ GamePanelDelegate::UpdateZoom()
 	SDL_GetRenderSafeArea(renderer, &rect);
 	SDL_SetRenderLogicalPresentation(renderer, saved_w, saved_h, saved_mode);
 
-	// We can zoom if we're on a phone in landscape mode and not local multiplayer
-	bool zoom = false;
-
-	if (IsPhone() && rect.w > rect.h) {
-		int i;
-
-		int local_players = 0;
-		OBJ_LOOP(i, MAX_PLAYERS) {
-			if (!gPlayers[i]->IsValid()) {
-				continue;
-			}
+	if (IsPhone() || IsTablet() || gZoomGame) {
+		StartZoomUI(rect);
+	} else {
+		StopZoomUI();
+	}
 
-			if (IS_LOCAL_CONTROL(gPlayers[i]->GetControlType())) {
-				++local_players;
-			}
+	if (gZoomGame) {
+		if (!m_texture) {
+			m_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_UNKNOWN, SDL_TEXTUREACCESS_TARGET, GAME_WIDTH, GAME_HEIGHT);
 		}
-		if (local_players == 1) {
-			zoom = true;
+		if (m_zoomIn) {
+			m_zoomIn->Hide();
+		}
+		if (m_zoomOut) {
+			m_zoomOut->Show();
 		}
-	}
-	if (gZoomMode == 0 || (gZoomMode == -1 && !zoom)) {
-		m_panel->GetElement<UIElement>("test_label")->SetText("(0) Zoom disabled");
-	} else if (gZoomMode == 1 || (gZoomMode == -1 && zoom)) {
-		zoom = true;
-		m_panel->GetElement<UIElement>("test_label")->SetText("(1) Zoomed and centered on ship");
-	} else if (gZoomMode == 2) {
-		zoom = true;
-		m_panel->GetElement<UIElement>("test_label")->SetText("(2) Zoomed and ship moves freely");
-	}
-
-	if (zoom) {
-		StartZoom(rect);
 	} else {
-		StopZoom();
+		if (m_texture) {
+			SDL_DestroyTexture(m_texture);
+			m_texture = nullptr;
+		}
+		if (m_zoomIn) {
+			m_zoomIn->Show();
+		}
+		if (m_zoomOut) {
+			m_zoomOut->Hide();
+		}
 	}
 }
 
 void
-GamePanelDelegate::StartZoom(const SDL_Rect &rect)
+GamePanelDelegate::StartZoomUI(const SDL_Rect &rect)
 {
-	SDL_Renderer *renderer = screen->GetRenderer();
 	float scale = (float)GAME_WIDTH / rect.w;
 	int x = (int)SDL_round(rect.x * scale);
 	int y = (int)SDL_round(rect.y * scale);
 	int height = (int)SDL_round(rect.h * scale);
 	ui->SetPosition(x, y);
 	ui->SetSize(GAME_WIDTH, height);
-
-	if (!m_texture) {
-		m_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_UNKNOWN, SDL_TEXTUREACCESS_TARGET, GAME_WIDTH, GAME_HEIGHT);
-	}
-
-	m_zoom = true;
 }
 
 void
-GamePanelDelegate::StopZoom()
+GamePanelDelegate::StopZoomUI()
 {
 	ui->SetPosition(0, 0);
 	ui->SetSize(GAME_WIDTH, GAME_HEIGHT);
+}
 
-	if (m_texture) {
-		SDL_DestroyTexture(m_texture);
-		m_texture = nullptr;
-	}
+void
+GamePanelDelegate::ToggleZoomGame()
+{
+	gZoomGame = !gZoomGame;
 
-	m_zoom = false;
+	UpdateZoom();
 }
 
 void
@@ -684,17 +663,20 @@ GamePanelDelegate::StartZoomedDrawing()
 {
 	SDL_Renderer *renderer = screen->GetRenderer();
 
+	if (!gZoomGame) {
+		screen->SetLogicalSize(GAME_WIDTH, GAME_HEIGHT);
+		return;
+	}
+
 	screen->GetClip(&m_savedClip);
 
-	if (gZoomMode != 0) {
-		// Don't clip
-		SDL_Rect clip;
-		clip.y = 0;
-		clip.x = 0;
-		clip.w = GAME_WIDTH;
-		clip.h = GAME_HEIGHT;
-		screen->ClipBlit(&clip);
-	}
+	// Don't clip
+	SDL_Rect clip;
+	clip.y = 0;
+	clip.x = 0;
+	clip.w = GAME_WIDTH;
+	clip.h = GAME_HEIGHT;
+	screen->ClipBlit(&clip);
 
 	SDL_SetRenderTarget(renderer, m_texture);
 	screen->Clear();
@@ -705,32 +687,26 @@ GamePanelDelegate::StopZoomedDrawing()
 {
 	SDL_Renderer *renderer = screen->GetRenderer();
 
+	if (!gZoomGame) {
+		screen->SetLogicalSize(ui->X() + ui->Width() + ui->X(), ui->Y() + ui->Height() + ui->Y());
+		return;
+	}
+
 	SDL_SetRenderTarget(renderer, nullptr);
 	SDL_SetRenderLogicalPresentation(renderer, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
 
 	int w = 0, h = 0;
 	SDL_GetRenderOutputSize(renderer, &w, &h);
 
+	int cameraX, cameraY;
+	gPlayers[0]->GetCameraPos(&cameraX, &cameraY);
+	GetRenderCoordinates(cameraX, cameraY);
+
 	SDL_Rect src;
-	if (gZoomMode == 0) {
-		src.x = 0;
-		src.y = 0;
-		src.w = GAME_WIDTH;
-		src.h = GAME_HEIGHT;
-	} else if (gZoomMode == 1 || gZoomMode == -1) {
-		int cameraX, cameraY;
-		gPlayers[0]->GetCameraPos(&cameraX, &cameraY);
-		GetRenderCoordinates(cameraX, cameraY);
-		cameraX += (SPRITES_WIDTH / 2);
-		cameraY += (SPRITES_WIDTH / 2);
-
-		src.w = GAME_WIDTH;
-		src.h = GAME_HEIGHT;
-		src.x = cameraX - src.w / 2;
-		src.y = cameraY - src.h / 2;
-	} else if (gZoomMode == 2) {
-		src = m_savedClip;
-	}
+	src.w = m_savedClip.w;
+	src.h = m_savedClip.h;
+	src.x = cameraX - src.w / 2;
+	src.y = cameraY - src.h / 2;
 
 	float minu = (float)src.x / m_texture->w;
 	float minv = (float)src.y / m_texture->h;
@@ -802,7 +778,7 @@ GamePanelDelegate::StopZoomedDrawing()
 void
 GamePanelDelegate::DrawBorder()
 {
-	if (m_zoom && gZoomMode != 0) {
+	if (gZoomGame) {
 		return;
 	}
 
@@ -1521,7 +1497,12 @@ GamePanelDelegate::GameOver()
 
 	ui->ShowPanel(PANEL_GAMEOVER);
 
-	StopZoom();
+	StopZoomUI();
+
+	if (m_texture) {
+		SDL_DestroyTexture(m_texture);
+		m_texture = nullptr;
+	}
 }
 
 /* ----------------------------------------------------------------- */
diff --git a/game/game.h b/game/game.h
index fd7e8029..852652d3 100644
--- a/game/game.h
+++ b/game/game.h
@@ -42,8 +42,9 @@ class GamePanelDelegate : public UIPanelDelegate
 
 protected:
 	void UpdateZoom();
-	void StartZoom(const SDL_Rect &rect);
-	void StopZoom();
+	void StartZoomUI(const SDL_Rect &rect);
+	void StopZoomUI();
+	void ToggleZoomGame();
 	void StartZoomedDrawing();
 	void StopZoomedDrawing();
 	void DrawBorder();
@@ -88,6 +89,8 @@ class GamePanelDelegate : public UIPanelDelegate
 	UIElement *m_frags;
 
 	UIElement *m_paused;
+	UIElement *m_zoomIn;
+	UIElement *m_zoomOut;
 
 	enum {
 		STATE_PLAYING,
@@ -105,7 +108,6 @@ class GamePanelDelegate : public UIPanelDelegate
 		STATE_START_NEXT_WAVE,
 	} m_state;
 
-	bool m_zoom;
 	SDL_Texture *m_texture = nullptr;
 	SDL_Rect m_savedClip;
 };
diff --git a/game/init.cpp b/game/init.cpp
index 3c122f60..cf2923fb 100644
--- a/game/init.cpp
+++ b/game/init.cpp
@@ -220,8 +220,8 @@ static void DrawLoadBar()
 
 void SetStar(int which)
 {
-	int x = FastRandom(GAME_WIDTH);
-	int y = FastRandom(GAME_HEIGHT);
+	int x = FastRandom(GAME_WIDTH - 2*SPRITES_WIDTH) + SPRITES_WIDTH;
+	int y = FastRandom(GAME_HEIGHT - 2*SPRITES_WIDTH) + SPRITES_WIDTH;
 
 	gTheStars[which]->xCoord = x;
 	gTheStars[which]->yCoord = y;