diff --git a/demo/main.cpp b/demo/main.cpp
index b706e42b6b3cabe49a23808b97ff8c1917806e49..83211bb2b92d142e4d0c646fad77b414952288c7 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -1,4 +1,5 @@
 #include <filesystem>
+#include <glm/geometric.hpp>
 #include <glm/trigonometric.hpp>
 #include <iostream>
 
@@ -12,6 +13,10 @@
 #include <fggl/debug/debug.h>
 #include <fggl/data/storage.hpp>
 
+template <typename T> int sgn(T val) {
+    return (T(0) < val) - (val < T(0));
+}
+
 // prototype of resource discovery
 void discover(std::filesystem::path base) {
 
@@ -36,6 +41,83 @@ void discover(std::filesystem::path base) {
 
 }
 
+struct InputState {
+	double currCursor[2];
+	double lastCursor[2];
+	bool buttons[3];
+};
+
+void process_inputs(fggl::gfx::Window& window, InputState& state) {
+	state.lastCursor[0] = state.currCursor[0];
+	state.lastCursor[1] = state.currCursor[1];
+	glfwGetCursorPos(window.handle(), &state.currCursor[0], &state.currCursor[1]);
+
+	state.buttons[0] = glfwGetMouseButton(window.handle(), GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
+	state.buttons[1] = glfwGetMouseButton(window.handle(), GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS;
+	state.buttons[2] = glfwGetMouseButton(window.handle(), GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS;
+}
+
+//TODO proper input system
+void process_camera(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputState& state, fggl::ecs::entity_t cam) {
+	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
+	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);
+	float moveSpeed = 1.0f;
+
+	glm::vec3 dir = ( camTransform->origin() - camComp->target );
+	glm::vec3 forward = glm::normalize( dir );
+
+	glm::vec3 motion(0.0f);
+	if ( glfwGetKey(window.handle(), GLFW_KEY_W) == GLFW_PRESS ) {
+		if ( glm::length( dir ) > 2.5f ) {
+		  motion -= (forward * moveSpeed);
+		}
+	}
+	if ( glfwGetKey(window.handle(), GLFW_KEY_S) == GLFW_PRESS ) {
+		if ( glm::length( dir ) < 25.0f ) {
+		  motion += (forward * moveSpeed);
+		}
+	}
+	if ( glfwGetKey( window.handle(), GLFW_KEY_A ) == GLFW_PRESS ) {
+		
+	}
+
+	camTransform->origin( camTransform->origin() + motion );
+}
+
+void process_arcball(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputState& state, fggl::ecs::entity_t cam) {
+	// see https://asliceofrendering.com/camera/2019/11/30/ArcballCamera/
+	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
+	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);
+
+	glm::vec4 position(camTransform->origin(), 1.0f);
+	glm::vec4 pivot(camComp->target, 1.0f);
+	glm::mat4 view = glm::lookAt( camTransform->origin(), camComp->target, camTransform->up() );
+	glm::vec3 viewDir = -glm::transpose(view)[2];
+	glm::vec3 rightDir = glm::transpose(view)[0];
+
+	float deltaAngleX = ( 2 * M_PI / window.width() ); 
+	float deltaAngleY = ( M_PI / window.height() );
+	float xAngle = ( state.lastCursor[0] - state.currCursor[0] ) * deltaAngleX;
+	float yAngle = ( state.lastCursor[1] - state.currCursor[1] ) * deltaAngleY;
+
+	auto cosAngle = glm::dot( viewDir, fggl::math::UP );
+	if ( cosAngle * sgn(deltaAngleY) > 0.99f ) {
+		deltaAngleY = 0;
+	}
+
+	// rotate the camera around the pivot on the first axis
+	glm::mat4x4 rotationMatrixX(1.0f);
+	rotationMatrixX = glm::rotate( rotationMatrixX, xAngle, fggl::math::UP );
+	position = ( rotationMatrixX * ( position - pivot ) ) + pivot;
+
+	// rotate the camera aroud the pivot on the second axis
+	glm::mat4x4 rotationMatrixY(1.0f);
+	rotationMatrixY = glm::rotate(rotationMatrixY, yAngle, rightDir );
+	glm::vec3 finalPos = ( rotationMatrixY * ( position - pivot ) ) + pivot;
+
+	camTransform->origin( finalPos );
+}
+
 int main(int argc, char* argv[]) {
 	fggl::gfx::Context ctx;
 
@@ -124,6 +206,7 @@ int main(int argc, char* argv[]) {
 		ecs.addComponent<fggl::gfx::MeshToken>(entity, token);
 	}
 
+	InputState inputs;
 
 	float time = 0.0f;
 	float dt = 16.0f;
@@ -134,6 +217,12 @@ int main(int argc, char* argv[]) {
 		// update step
 		time += dt;
 
+		process_inputs(win, inputs);
+		process_camera(win, ecs, inputs, camEnt);
+		if ( inputs.buttons[2] ) {
+			process_arcball(win, ecs, inputs, camEnt);
+		}
+
 /*		float amount = glm::radians( time / 2048.0f * 360.0f );
 		auto spinners = ecs.getEntityWith<fggl::math::Transform>();
 		for ( auto entity : spinners ) {
diff --git a/fggl/gfx/window.cpp b/fggl/gfx/window.cpp
index 044f1d9b33ae0c6349c87460ae3293382a0cbdbe..f160f039769c087c06dbdb5f71c7437372197280 100644
--- a/fggl/gfx/window.cpp
+++ b/fggl/gfx/window.cpp
@@ -16,6 +16,9 @@ static void glfw_error(int code, const char* description) {
 static void framebuffer_resize(GLFWwindow* window, int width, int height) {
 	glfwMakeContextCurrent( window );
 	glViewport(0, 0, width, height);
+
+	auto fgglWindow = reinterpret_cast<Window*>(glfwGetWindowUserPointer( window ));
+	fgglWindow->framesize( width, height );
 }
 
 Context::Context() {
@@ -46,6 +49,7 @@ Window::Window() : m_window(nullptr), m_input(nullptr) {
 		return;
 	}
 
+	m_framesize = glm::vec2(1920, 1080);
 	glfwSetWindowUserPointer(m_window, this);
 	glfwSetFramebufferSizeCallback( m_window, framebuffer_resize );
 }
diff --git a/fggl/gfx/window.hpp b/fggl/gfx/window.hpp
index 0afc6f1255d146cb92fe95bba54763ac77a89465..3045983f31f28e101d85eed99f9f8caf241ee19e 100644
--- a/fggl/gfx/window.hpp
+++ b/fggl/gfx/window.hpp
@@ -4,6 +4,7 @@
 #include <cassert>
 #include <string>
 
+#include <fggl/math/types.hpp>
 #include <fggl/gfx/rendering.hpp>
 
 namespace fggl::gfx {
@@ -48,6 +49,18 @@ namespace fggl::gfx {
 			void activate() const;
 			void swap();
 
+			inline float width() const {
+				return m_framesize.x;
+			}
+
+			inline float height() const {
+				return m_framesize.y;
+			}
+
+			inline void framesize(int width, int height) {
+				m_framesize = math::vec2( width, height );
+			}
+
 			// window manager stuff
 			[[nodiscard]]
 			inline bool closeRequested() const {
@@ -113,6 +126,7 @@ namespace fggl::gfx {
 		private:
 			GLFWwindow* m_window;
 			Input* m_input;
+			math::vec2 m_framesize;
 
 			inline void set_hint(int hint, bool state) const {
 				assert( m_window != nullptr );