From 9a0410ff9ee660e83312c43d3312ba74dd7d5e2b Mon Sep 17 00:00:00 2001
From: Joseph Walton-Rivers <joseph@walton-rivers.uk>
Date: Mon, 2 Jan 2023 12:22:42 +0000
Subject: [PATCH] refine camera controls for hex demo

---
 demo/CMakeLists.txt              |   1 +
 demo/demo/hexboard/board.cpp     |  44 +++++++---
 demo/demo/hexboard/camera.cpp    |  34 ++++++++
 demo/include/hexboard/camera.hpp | 140 +++++++++++++++++++++++++++++++
 demo/include/hexboard/scene.hpp  |   6 ++
 include/fggl/grid/layout.hpp     |  25 +++---
 include/fggl/input/mouse.hpp     |  28 +++++--
 include/fggl/math/fmath.hpp      |   7 ++
 8 files changed, 256 insertions(+), 29 deletions(-)
 create mode 100644 demo/demo/hexboard/camera.cpp
 create mode 100644 demo/include/hexboard/camera.hpp

diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt
index c8acffb..62fc663 100644
--- a/demo/CMakeLists.txt
+++ b/demo/CMakeLists.txt
@@ -11,6 +11,7 @@ add_executable(demo
         demo/robot/programmer.cpp
         demo/models/viewer.cpp
         demo/hexboard/board.cpp
+        demo/hexboard/camera.cpp
 )
 
 target_include_directories(demo
diff --git a/demo/demo/hexboard/board.cpp b/demo/demo/hexboard/board.cpp
index ad9d434..c562474 100644
--- a/demo/demo/hexboard/board.cpp
+++ b/demo/demo/hexboard/board.cpp
@@ -20,12 +20,14 @@
 
 namespace demo::hexboard {
 
-	Scene::Scene(fggl::App &app) : GameBase(app), m_board(nullptr) {
+	Scene::Scene(fggl::App &app) : GameBase(app), m_board(nullptr), m_screen(1920.F, 1080.F) {
 	}
 
 	void Scene::activate() {
 		m_board = std::make_unique<fggl::grid::HexGrid>();
 		m_layout = std::make_unique<fggl::grid::Layout>( fggl::grid::Orientation::make_pointy(), 64.0F );
+		//m_layout->m_origin = { 1920.0F * -0.5F, 1080.0F * -0.5F};
+
 		m_selections = std::make_unique<SelectionModel>();
 
 		fggl::grid::TerrainType grass{
@@ -48,11 +50,13 @@ namespace demo::hexboard {
 			}
 		}
 
+		m_camera = std::make_unique<Camera2D>();
 	}
 
 	void Scene::deactivate() {
 		m_board = nullptr;
 		m_selections = nullptr;
+		m_layout = nullptr;
 	}
 
 	void Scene::update(float delta) {
@@ -66,16 +70,36 @@ namespace demo::hexboard {
 		// check if a button was pressed
 		auto& input = this->input();
 		{
-			const fggl::math::vec2 screenPos(
-				fggl::math::rescale_ndc(input.mouse.axis(fggl::input::MouseAxis::X), 0, 1920),
-				fggl::math::rescale_ndc(input.mouse.axis(fggl::input::MouseAxis::Y), 0, 1080)
-			);
-			m_selections->hover = fggl::grid::round2( m_layout->toGrid(screenPos) );
+			const auto mouseNDC = fggl::input::mouse_axis(input.mouse);
+			const auto screenPos = ndc_to_screen(mouseNDC, m_screen);
+
+			// calculate what the user clicked on
+			auto worldPx = m_camera->unproject(screenPos);
+			m_selections->hover = fggl::grid::round2( m_layout->toGrid(worldPx) );
 
 			if (input.mouse.pressed(fggl::input::MouseButton::LEFT)) {
 				m_selections->selected = m_selections->hover;
+				m_camera->moveBy( mouseNDC * (m_screen * 0.5F) );
+			}
+
+			if ( input.mouse.down(fggl::input::MouseButton::RIGHT) ) {
+				if (input.mouse.pressed( fggl::input::MouseButton::RIGHT )) {
+					m_dragging = screenPos;
+				}
+
+				auto offset = screenPos - m_dragging.value();
+				m_camera->teleportBy(offset * 0.01F);
+			} else if ( input.mouse.released(fggl::input::MouseButton::RIGHT) ) {
+				m_dragging = {};
 			}
+
 		}
+
+		m_camera->update(delta);
+
+		// flip y, because reasons
+		//auto offset = m_camera->getFocusLocation();
+		//m_layout->m_origin = -offset;
 	}
 
 	void Scene::drawGrid(fggl::gfx::Paint& paint) {
@@ -84,22 +108,22 @@ namespace demo::hexboard {
 			auto terrain = m_board->getTerrain(tile);
 			if ( terrain.has_value() ) {
 				const auto& terrainData = terrain.value();
-				m_layout->paintHex(paint, tile, terrainData.colour);
+				m_layout->paintHex(paint, tile, terrainData.colour, m_camera->getFocusLocation());
 			}
 		}
 	}
 
 	void Scene::drawSelections(fggl::gfx::Paint& paint) {
-		if ( m_selections == nullptr ) {
+		if ( m_selections == nullptr || m_dragging.has_value() ) {
 			return;
 		}
 
 		if ( m_selections->selected.has_value() ) {
-			m_layout->paintHex( paint, m_selections->selected.value(), fggl::gfx::colours::YELLOW);
+			m_layout->paintHex( paint, m_selections->selected.value(), fggl::gfx::colours::YELLOW, m_camera->getFocusLocation());
 		}
 
 		if ( m_selections->hover.has_value() ) {
-			m_layout->paintHex( paint, m_selections->hover.value(), fggl::gfx::colours::BLANCHED_ALMOND);
+			m_layout->paintHex( paint, m_selections->hover.value(), fggl::gfx::colours::BLANCHED_ALMOND, m_camera->getFocusLocation());
 		}
 	}
 
diff --git a/demo/demo/hexboard/camera.cpp b/demo/demo/hexboard/camera.cpp
new file mode 100644
index 0000000..c87e703
--- /dev/null
+++ b/demo/demo/hexboard/camera.cpp
@@ -0,0 +1,34 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 02/01/23.
+//
+
+#include "hexboard/camera.hpp"
+
+#include "fggl/math/fmath.hpp"
+
+#include <cmath>
+
+namespace demo::hexboard {
+
+	void Camera2D::update(float delta) {
+		m_location = fggl::math::smooth_add( m_location, m_target, m_scale );
+		if ( m_trauma > 0 ) {
+			m_trauma = std::max( m_trauma - TRAUMA_DECAY, 0.0F );
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/demo/include/hexboard/camera.hpp b/demo/include/hexboard/camera.hpp
new file mode 100644
index 0000000..ccb0535
--- /dev/null
+++ b/demo/include/hexboard/camera.hpp
@@ -0,0 +1,140 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 02/01/23.
+//
+
+#ifndef FGGL_DEMO_INCLUDE_HEXBOARD_CAMERA_HPP
+#define FGGL_DEMO_INCLUDE_HEXBOARD_CAMERA_HPP
+
+#include "fggl/math/types.hpp"
+
+namespace demo::hexboard {
+
+	constexpr float TRAUMA_DECAY = 0.01F;
+	constexpr float TRAUMA_LARGE = 0.5F;
+	constexpr float TRAUMA_SMALL = 0.1F;
+
+	constexpr float SPEED_SLOW = 0.01F;
+	constexpr float SPEED_MEDIUM = 0.1F;
+	constexpr float SPEED_FAST = 0.5F;
+
+	constexpr fggl::math::vec2 ndc_to_screen(fggl::math::vec2 ndcPos, fggl::math::vec2 screenSize) {
+		return fggl::math::vec2(
+			fggl::math::rescale_ndc(ndcPos.x, 0, screenSize.x),
+			fggl::math::rescale_ndc(ndcPos.y, 0, screenSize.y)
+		);
+	}
+
+	/**
+	 * A 2D 'juiced' camera for grid worlds.
+	 *
+	 * This camera is based on HexBoard, and the GDC talk around this concept. Many of the ideas implemented here
+	 * come from that source.
+	 *
+	 * @see https://www.youtube.com/watch?v=tu-Qe66AvtY
+	 */
+	class Camera2D {
+		public:
+			using Point = fggl::math::vec2;
+
+			/**
+			 * Apply active camera effects.
+			 *
+			 * This method must be called once per frame of the camera's visual effects are to be animated.
+			 *
+			 * @param delta the amount of time that has passed
+			 */
+			void update(float delta);
+
+			inline fggl::math::vec2 unproject(fggl::math::vec2 screenPos) const {
+				auto location = screenPos;
+				location.x += 1920/2.0F;
+				location.y += 1080/2.0F;
+				return location + m_location;
+//				return screenPos + m_location;
+			}
+
+			inline fggl::math::vec2 project(fggl::math::vec2 worldPos) const {
+				return worldPos - m_location;
+			}
+
+			/**
+			 * Move the camera to a new location.
+			 *
+			 * This will apply any movement effects the camera has applied to it. As a result movement to the location
+			 * will not be instantaneous.
+			 *
+			 * @param newTarget the new target location
+			 */
+			inline void moveTo(Point newTarget) {
+				m_target = newTarget;
+			}
+
+			/**
+			 * Move the camera by a defined amount.
+			 *
+			 * This will apply any movement effects the camera has applied to it. As a result movement to the location
+			 * will not be instantaneous.
+			 *
+			 * @param delta the amount to add to the target, negative values will subject from the target location
+			 */
+			inline void moveBy(Point delta) {
+				m_target += delta;
+			}
+
+			/**
+			 * Instantaneously move the camera to a new location.
+			 *
+			 * @param the new camera location
+			 */
+			inline void teleportTo(Point newTarget) {
+				m_location = newTarget;
+				m_target = newTarget;
+			}
+
+			/**
+			 * Instantaneously adjust the camera location.
+			 *
+			 * @param delta the amount to adjust the camera location by
+			 */
+			inline void teleportBy(Point delta) {
+				m_location += delta;
+				m_target = m_location;
+			}
+
+			/**
+			 * Get the current view offset of this camera.
+			 *
+			 * @return the current location this camera is pointing at
+			 */
+			inline Point getFocusLocation() const {
+				auto offset = m_location;
+				offset.x += 1920/2.0F;
+				offset.y += 1080/2.0F;
+				return -offset;
+			}
+
+		private:
+			Point m_location;
+			Point m_target;
+			float m_trauma;
+			float m_scale = SPEED_MEDIUM;
+
+	};
+
+}
+
+#endif //FGGL_DEMO_INCLUDE_HEXBOARD_CAMERA_HPP
diff --git a/demo/include/hexboard/scene.hpp b/demo/include/hexboard/scene.hpp
index 0fe38cd..f49249a 100644
--- a/demo/include/hexboard/scene.hpp
+++ b/demo/include/hexboard/scene.hpp
@@ -26,6 +26,8 @@
 #include "fggl/grid/hexgrid.hpp"
 #include "fggl/grid/layout.hpp"
 
+#include "camera.hpp"
+
 namespace demo::hexboard {
 
 	struct SelectionModel {
@@ -47,6 +49,10 @@ namespace demo::hexboard {
 			std::unique_ptr<fggl::grid::HexGrid> m_board;
 			std::unique_ptr<fggl::grid::Layout> m_layout;
 			std::unique_ptr<SelectionModel> m_selections;
+			std::unique_ptr<Camera2D> m_camera;
+
+			fggl::math::vec2 m_screen;
+			std::optional<fggl::math::vec2> m_dragging;
 
 			void drawGrid(fggl::gfx::Paint&);
 			void drawSelections(fggl::gfx::Paint&);
diff --git a/include/fggl/grid/layout.hpp b/include/fggl/grid/layout.hpp
index 55a878e..94fca62 100644
--- a/include/fggl/grid/layout.hpp
+++ b/include/fggl/grid/layout.hpp
@@ -69,6 +69,9 @@ namespace fggl::grid {
 	};
 
 
+	constexpr int HEX_SIDES = 6;
+	constexpr int DEG_PER_HEX_SIDE = 360 / HEX_SIDES; // 60 degrees per side
+
 	struct Layout {
 		Orientation m_orientation;
 		math::vec2 m_size;
@@ -94,40 +97,40 @@ namespace fggl::grid {
 
 		[[nodiscard]]
 		inline math::vec2 toScreen(IntHex gridPos) const {
-			math::vec2 hexPoint{gridPos.q(), gridPos.r()};
-			auto p = (m_orientation.m_forward * hexPoint) * m_size;
-			return p + m_origin;
+			const math::vec2 hexPoint{gridPos.q(), gridPos.r()};
+			auto point = (m_orientation.m_forward * hexPoint) * m_size;
+			return point + m_origin;
 		}
 
 		[[nodiscard]]
 		inline FloatHex toGrid(math::vec2 screen) const {
 			auto point = (screen - m_origin) / m_size;
-			auto p = m_orientation.m_backward * point;
-			return {p.x, p.y};
+			auto hexPos = m_orientation.m_backward * point;
+			return {hexPos.x, hexPos.y};
 		}
 
 		[[nodiscard]]
 		inline math::vec2 cornerOffset(int corner) const {
-			int angInc = (360 / 6) * corner;
-			float angle = (angInc + m_orientation.m_angle) * ( fggl::math::PI / 180.0F);
+			const int angInc = DEG_PER_HEX_SIDE * corner;
+			const float angle = (angInc + m_orientation.m_angle) * ( fggl::math::PI / 180.0F);
 			return {
 				m_size.x * cosf(angle),
 				m_size.y * sinf(angle)
 			};
 		}
 
-		void paintHex(fggl::gfx::Paint& paint, IntHex pos, math::vec3 colour) const {
+		void paintHex(fggl::gfx::Paint& paint, IntHex pos, math::vec3 colour, math::vec2 offset) const {
 			const auto hexScreenCenter = toScreen(pos);
 
 			gfx::Path2D path({0,0});
 			path.colour(colour);
 
-			for (int i=0; i < 6; ++i) {
+			for (int i=0; i < HEX_SIDES; ++i) {
 				auto cornerPos =  hexScreenCenter + cornerOffset(i);
 				if ( i == 0) {
-					path.moveTo(cornerPos);
+					path.moveTo(cornerPos + offset);
 				} else {
-					path.pathTo(cornerPos);
+					path.pathTo(cornerPos + offset);
 				}
 			}
 			paint.stroke(path);
diff --git a/include/fggl/input/mouse.hpp b/include/fggl/input/mouse.hpp
index f2900e1..01f953b 100644
--- a/include/fggl/input/mouse.hpp
+++ b/include/fggl/input/mouse.hpp
@@ -19,17 +19,19 @@
 #include <array>
 #include <bitset>
 
+#include "fggl/math/types.hpp"
+
 namespace fggl::input {
 
 	enum class MouseButton {
-		LEFT,
-		MIDDLE,
-		RIGHT,
-		EXTRA_1,
-		EXTRA_2,
-		EXTRA_3,
-		EXTRA_4,
-		EXTRA_5
+		LEFT  = 0,
+		MIDDLE = 2,
+		RIGHT = 1,
+		EXTRA_1 = 3,
+		EXTRA_2 = 4,
+		EXTRA_3 = 5,
+		EXTRA_4 = 6,
+		EXTRA_5 = 7
 	};
 
 	enum class MouseAxis {
@@ -74,6 +76,7 @@ namespace fggl::input {
 		void operator=(const MouseState &rhs);
 	};
 
+
 	class MouseInput {
 		public:
 			MouseInput() = default;
@@ -121,6 +124,15 @@ namespace fggl::input {
 			MouseState m_prev;
 	};
 
+	// helpers
+	inline math::vec2 mouse_axis(const MouseInput& input) {
+		return { input.axis(MouseAxis::X), input.axis(MouseAxis::Y) };
+	}
+
+	inline math::vec2 mouse_axis_delta(const MouseInput& input) {
+		return { input.axisDelta(MouseAxis::X), input.axisDelta(MouseAxis::Y) };
+	}
+
 };
 
 #endif
diff --git a/include/fggl/math/fmath.hpp b/include/fggl/math/fmath.hpp
index f6683b6..d708837 100644
--- a/include/fggl/math/fmath.hpp
+++ b/include/fggl/math/fmath.hpp
@@ -92,6 +92,13 @@ namespace fggl::math {
 		return (1 - t) * start + t * end;
 	}
 
+	template<typename T>
+	[[nodiscard]]
+	constexpr T smooth_add(T first, T second, const float weight) {
+		const float other = 1 - weight;
+		return (first * other) + (second * weight);
+	}
+
 } // namespace fggl::math
 
 
-- 
GitLab