← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/animation_manager into lp:widelands

 

SirVer has proposed merging lp:~widelands-dev/widelands/animation_manager into lp:widelands.

Requested reviews:
  Widelands Developers (widelands-dev)

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/animation_manager/+merge/313879

Request for testing help!!!

This code provides a new way to "jump" to events in the map. This is not yet done - there are some NOCOMs in the code. But I am kinda tired with tweaking and wanted some input on the current parameter set.

For testing: 
1) Tweak constants at the top of src/wui/map_view.cc and compile.  
2) Load a game that spans a big map.
3) Open the message window and jump to the event location. Try various locations.

Please let me know how this "feels". I can elaborate on the choices I made and the things I tried out if that is desired. 



-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/animation_manager into lp:widelands.
=== modified file 'data/scripting/ui.lua'
--- data/scripting/ui.lua	2016-01-28 05:24:34 +0000
+++ data/scripting/ui.lua	2016-12-26 21:45:30 +0000
@@ -67,6 +67,7 @@
 --                             PUBLIC FUNCTIONS
 -- =======================================================================
 
+-- NOCOM(#sirver): replace everything in this file through engine functions.
 -- RST
 -- .. function:: timed_scroll(pts[, dt = 20])
 --

=== modified file 'src/base/math.h'
--- src/base/math.h	2016-11-03 07:20:57 +0000
+++ src/base/math.h	2016-12-26 21:45:30 +0000
@@ -40,6 +40,11 @@
 	return val;
 }
 
+// A simple square function.
+template <typename T> T sqr(const T& a) {
+	return a * a;
+}
+
 }  // namespace math
 
 #endif  // end of include guard: WL_BASE_MATH_H

=== modified file 'src/game_io/game_preload_packet.cc'
--- src/game_io/game_preload_packet.cc	2016-12-10 09:34:41 +0000
+++ src/game_io/game_preload_packet.cc	2016-12-26 21:45:30 +0000
@@ -126,7 +126,7 @@
 		std::unique_ptr<Texture> texture;
 		if (ipl != nullptr) {  // Player
 			texture = draw_minimap(
-			   game, &ipl->player(), ipl->get_view_area(), MiniMapType::kStaticViewWindow, layers);
+			   game, &ipl->player(), ipl->view_area(), MiniMapType::kStaticViewWindow, layers);
 		} else {  // Observer
 			texture = draw_minimap(game, nullptr, Rectf(), MiniMapType::kStaticMap, layers);
 		}

=== modified file 'src/logic/cmd_luacoroutine.cc'
--- src/logic/cmd_luacoroutine.cc	2016-08-04 15:49:05 +0000
+++ src/logic/cmd_luacoroutine.cc	2016-12-26 21:45:30 +0000
@@ -34,16 +34,16 @@
 
 namespace Widelands {
 
+CmdLuaCoroutine::~CmdLuaCoroutine() {}
+
 void CmdLuaCoroutine::execute(Game& game) {
 	try {
 		int rv = cr_->resume();
 		const uint32_t sleeptime = cr_->pop_uint32();
 		if (rv == LuaCoroutine::YIELDED) {
-			game.enqueue_command(new Widelands::CmdLuaCoroutine(sleeptime, cr_));
-			cr_ = nullptr;  // Remove our ownership so we don't delete.
+			game.enqueue_command(new Widelands::CmdLuaCoroutine(sleeptime, std::move(cr_)));
 		} else if (rv == LuaCoroutine::DONE) {
-			delete cr_;
-			cr_ = nullptr;
+			cr_.reset();
 		}
 	} catch (LuaError& e) {
 		log("Error in Lua Coroutine\n");
@@ -90,6 +90,6 @@
 	upcast(LuaGameInterface, lgi, &egbase.lua());
 	assert(lgi);  // If this is not true, this is not a game.
 
-	lgi->write_coroutine(fw, cr_);
+	lgi->write_coroutine(fw, cr_.get());
 }
 }

=== modified file 'src/logic/cmd_luacoroutine.h'
--- src/logic/cmd_luacoroutine.h	2016-08-04 15:49:05 +0000
+++ src/logic/cmd_luacoroutine.h	2016-12-26 21:45:30 +0000
@@ -20,6 +20,7 @@
 #ifndef WL_LOGIC_CMD_LUACOROUTINE_H
 #define WL_LOGIC_CMD_LUACOROUTINE_H
 
+#include <memory>
 #include <string>
 
 #include "logic/cmd_queue.h"
@@ -28,15 +29,13 @@
 namespace Widelands {
 
 struct CmdLuaCoroutine : public GameLogicCommand {
-	CmdLuaCoroutine() : GameLogicCommand(0), cr_(nullptr) {
+	CmdLuaCoroutine() : GameLogicCommand(0) {
 	}  // For savegame loading
-	CmdLuaCoroutine(uint32_t const init_duetime, LuaCoroutine* const cr)
-	   : GameLogicCommand(init_duetime), cr_(cr) {
+	CmdLuaCoroutine(uint32_t const init_duetime, std::unique_ptr<LuaCoroutine> cr)
+	   : GameLogicCommand(init_duetime), cr_(std::move(cr)) {
 	}
 
-	~CmdLuaCoroutine() {
-		delete cr_;
-	}
+	~CmdLuaCoroutine();
 
 	// Write these commands to a file (for savegames)
 	void write(FileWrite&, EditorGameBase&, MapObjectSaver&) override;
@@ -49,7 +48,7 @@
 	void execute(Game&) override;
 
 private:
-	LuaCoroutine* cr_;
+	std::unique_ptr<LuaCoroutine> cr_;
 };
 }
 

=== modified file 'src/logic/game.cc'
--- src/logic/game.cc	2016-10-15 16:36:12 +0000
+++ src/logic/game.cc	2016-12-26 21:45:30 +0000
@@ -306,7 +306,7 @@
 		table->do_not_warn_about_unaccessed_keys();
 		win_condition_displayname_ = table->get_string("name");
 		std::unique_ptr<LuaCoroutine> cr = table->get_coroutine("func");
-		enqueue_command(new CmdLuaCoroutine(get_gametime() + 100, cr.release()));
+		enqueue_command(new CmdLuaCoroutine(get_gametime() + 100, std::move(cr)));
 	} else {
 		win_condition_displayname_ = "Scenario";
 	}

=== modified file 'src/logic/player.cc'
--- src/logic/player.cc	2016-12-18 17:02:44 +0000
+++ src/logic/player.cc	2016-12-26 21:45:30 +0000
@@ -200,7 +200,7 @@
 		table->do_not_warn_about_unaccessed_keys();
 		std::unique_ptr<LuaCoroutine> cr = table->get_coroutine("func");
 		cr->push_arg(this);
-		game.enqueue_command(new CmdLuaCoroutine(game.get_gametime(), cr.release()));
+		game.enqueue_command(new CmdLuaCoroutine(game.get_gametime(), std::move(cr)));
 
 		// Check if other starting positions are shared in and initialize them as well
 		for (uint8_t n = 0; n < further_shared_in_player_.size(); ++n) {
@@ -213,7 +213,7 @@
 			      ->get_coroutine("func");
 			ncr->push_arg(this);
 			ncr->push_arg(further_pos);
-			game.enqueue_command(new CmdLuaCoroutine(game.get_gametime(), ncr.release()));
+			game.enqueue_command(new CmdLuaCoroutine(game.get_gametime(), std::move(ncr)));
 		}
 	} else
 		throw WLWarning(_("Missing starting position"),

=== modified file 'src/scripting/logic.cc'
--- src/scripting/logic.cc	2016-08-04 15:49:05 +0000
+++ src/scripting/logic.cc	2016-12-26 21:45:30 +0000
@@ -146,8 +146,8 @@
 LuaGameInterface::~LuaGameInterface() {
 }
 
-LuaCoroutine* LuaGameInterface::read_coroutine(FileRead& fr) {
-	LuaCoroutine* rv = new LuaCoroutine(nullptr);
+std::unique_ptr<LuaCoroutine> LuaGameInterface::read_coroutine(FileRead& fr) {
+	std::unique_ptr<LuaCoroutine> rv(new LuaCoroutine(nullptr));
 	rv->read(lua_state_, fr);
 	return rv;
 }

=== modified file 'src/scripting/logic.h'
--- src/scripting/logic.h	2016-08-04 15:49:05 +0000
+++ src/scripting/logic.h	2016-12-26 21:45:30 +0000
@@ -51,7 +51,7 @@
 	std::unique_ptr<LuaTable> run_script(const std::string& script) override;
 
 	// Input/output for coroutines.
-	LuaCoroutine* read_coroutine(FileRead&);
+	std::unique_ptr<LuaCoroutine> read_coroutine(FileRead&);
 	void write_coroutine(FileWrite&, LuaCoroutine*);
 
 	// Input output for the global game state.

=== modified file 'src/scripting/lua_root.cc'
--- src/scripting/lua_root.cc	2016-11-02 05:48:00 +0000
+++ src/scripting/lua_root.cc	2016-12-26 21:45:30 +0000
@@ -19,6 +19,8 @@
 
 #include "scripting/lua_root.h"
 
+#include <memory>
+
 #include <boost/format.hpp>
 
 #include "logic/cmd_luacoroutine.h"
@@ -195,16 +197,17 @@
 	int nargs = lua_gettop(L);
 	uint32_t runtime = get_game(L).get_gametime();
 	if (nargs < 2)
-		report_error(L, "Too little arguments!");
+		report_error(L, "Too few arguments!");
 	if (nargs == 3) {
 		runtime = luaL_checkuint32(L, 3);
 		lua_pop(L, 1);
 	}
 
-	LuaCoroutine* cr = new LuaCoroutine(luaL_checkthread(L, 2));
+	std::unique_ptr<LuaCoroutine> cr(new LuaCoroutine(luaL_checkthread(L, 2)));
 	lua_pop(L, 2);  // Remove coroutine and Game object from stack
 
-	get_game(L).enqueue_command(new Widelands::CmdLuaCoroutine(runtime, cr));
+	get_game(L).enqueue_command(
+	   new Widelands::CmdLuaCoroutine(runtime, std::move(cr)));
 
 	return 0;
 }

=== modified file 'src/wui/interactive_base.cc'
--- src/wui/interactive_base.cc	2016-11-23 08:31:25 +0000
+++ src/wui/interactive_base.cc	2016-12-26 21:45:30 +0000
@@ -378,7 +378,7 @@
 
 void InteractiveBase::mainview_move() {
 	if (m->minimap.window) {
-		m->mm->set_view(get_view_area());
+		m->mm->set_view(view_area());
 	}
 }
 

=== modified file 'src/wui/mapview.cc'
--- src/wui/mapview.cc	2016-11-16 10:01:52 +0000
+++ src/wui/mapview.cc	2016-12-26 21:45:30 +0000
@@ -19,6 +19,8 @@
 
 #include "wui/mapview.h"
 
+#include <SDL.h>
+
 #include "base/macros.h"
 #include "base/math.h"
 #include "graphic/game_renderer.h"
@@ -34,6 +36,27 @@
 
 namespace {
 
+// NOCOM(#sirver): maybe replace set_zoom through set_view.
+// NOCOM(#sirver): how to 'reverse' a plan?
+
+// Number of keyframes to generate for a plan. The more points, the smoother
+// the animation (though we also lineraly interpolate between keyframes) and
+// the more work.
+constexpr int kNumKeyFrames = 102;
+
+// The maximum zoom to use in moving animations.
+constexpr float kMaxAnimationZoom = 8.f;
+
+// The time used for paning only automated map movement.
+constexpr float kPanOnlyAnimationTimeMs = 500.f;
+
+// The time used for zooming and paning automated map movement.
+constexpr float kPanAndZoomAnimationTimeMs = 1500.f;
+
+// If the difference between the current zoom and the target zoom in an
+// animation plan is smaller than this value, we will do a pan-only movement.
+constexpr float kPanOnlyZoomThreshold = 0.25f;
+
 // Given 'p' on a torus of dimension ('h', 'h') and 'r' that contains this
 // point, change 'p' so that r.x < p.x < r.x + r.w and similar for y.
 // Containing is defined as such that the shortest distance between the center
@@ -55,6 +78,194 @@
 	return p;
 }
 
+// Returns the view area, i.e. the currently visible rectangle in map pixel
+// space for the given 'view'.
+Rectf get_view_area(const MapView::View& view, const int width, const int height) {
+	return Rectf(view.viewpoint, width * view.zoom, height * view.zoom);
+}
+
+template <typename T>
+struct KeyFrame {
+	float time_ms;
+	T value;
+};
+
+constexpr float pow2(float t) {
+	return t * t;
+}
+constexpr float pow3(float t) {
+	return t * t * t;
+}
+
+// NOCOM(#sirver): Explain interpolator properties, i.e number of required keypoints
+template <typename T>
+class Interpolator {
+public:
+	Interpolator(std::vector<KeyFrame<T>> keyframes)
+	   : keyframes_(std::move(keyframes)) {
+#ifndef NDEBUG
+		assert(keyframes_.size() >= 4);
+		for (size_t i = 1; i < keyframes_.size(); ++i) {
+			assert(keyframes_[i-1].time_ms < keyframes_[i].time_ms);
+		}
+#endif
+	}
+
+	T value(const float time_ms) {
+		if (time_ms <= keyframes_.front().time_ms) {
+			return keyframes_.front().value;
+		}
+		if (time_ms >= keyframes_.back().time_ms) {
+			return keyframes_.back().value;
+		}
+
+		size_t i = 0;
+		while (i < keyframes_.size() && keyframes_[i].time_ms <= time_ms) {
+			++i;
+		}
+
+		// NOCOM(#sirver): are these assumptions right?
+		// The interpolated point must be in the second interval or later.
+		assert(i > 1);
+		// The interpolated point must not in the last interval.
+		assert(i <= keyframes_.size() - 1);
+		assert(keyframes_[i-1].time_ms <= time_ms);
+		assert(time_ms <= keyframes_[i].time_ms);
+
+		// const float ts0 = keyframes_[i-2].time_ms;
+		const float ts1 = keyframes_[i-1].time_ms;
+		const float ts2 = keyframes_[i].time_ms;
+		// const float ts3 = keyframes_[i+1].time_ms;
+
+		const T& p0 = keyframes_[i-2].value;
+		const T& p1 = keyframes_[i-1].value;
+		const T& p2 = keyframes_[i].value;
+		const T& p3 = keyframes_[i+1].value;
+
+		float u = (time_ms - ts1) / (ts2 - ts1);
+
+		const Vector2f c0 = p1;
+		const Vector2f c1 = -p0 / 2.0f + p2 / 2.0f;
+		const Vector2f c2 = p0 - p1 * (5.0f / 2.0f) + p2 * 2 - p3 / 2.0f;
+		const Vector2f c3 = -p0 / 2.0f + p1 * (3.0f / 2.0f) - p2 * (3.0f / 2.0f) + p3 / 2.0f;
+
+		return c0 + c1 * u + c2 * pow2(u) + c3 * pow3(u);
+	}
+
+private:
+	std::vector<KeyFrame<T>> keyframes_;
+
+	DISALLOW_COPY_AND_ASSIGN(Interpolator);
+};
+
+
+template <typename T>
+T mix(float t, const T& a, const T& b) {
+	return a * (1.f - t) + b * t;
+}
+
+// https://en.wikipedia.org/wiki/Smoothstep
+template <typename T>
+class SmoothstepInterpolator {
+public:
+	SmoothstepInterpolator(const T& start, const T& end, float dt)
+	   : start_(start), end_(end), dt_(dt) {
+	}
+
+	T value(const float time_ms) {
+		const float t = math::clamp(time_ms / dt_, 0.f, 1.f);
+		return mix(pow2(t) * (3.f - 2.f * t), start_, end_);
+		// NOCOM(#sirver): Smootherstep - maybe accelerations too slowly, but definitvely smoother.
+		// return mix(pow3(t) * (t * (t * 6.f - 15.f) + 10.f), start_, end_);
+	}
+
+private:
+	T start_, end_;
+	float dt_;
+
+	DISALLOW_COPY_AND_ASSIGN(SmoothstepInterpolator);
+};
+
+template <typename T, typename P>
+class SymmetricInterpolator {
+public:
+	SymmetricInterpolator(const T& start, const T& end, float dt)
+	   : inner_(start, end, dt / 2.f), dt_(dt) {
+	}
+
+	T value(const float time_ms) {
+		const float t = math::clamp(time_ms / dt_, 0.f, 1.f);
+		if (t < 0.5f) {
+			return inner_.value(t * dt_);
+		} else {
+			return inner_.value((1.f - t) * dt_);
+		}
+	}
+
+private:
+	P inner_;
+	float dt_;
+
+	DISALLOW_COPY_AND_ASSIGN(SymmetricInterpolator);
+};
+
+
+
+// Calculates a animation plan from 'start' to 'end_viewpoint' - both at 'zoom',
+// taking 'duraction_ms'. The animation is assumed to start at the
+// time of calling the function
+// NOCOM(#sirver): do everything in floats, only convert at the end.
+std::vector<MapView::TimestampedView> plan_animation(const Widelands::Map& map,
+                                                     const Vector2f& start,
+                                                     const Vector2f& end,
+                                                     const float start_zoom,
+                                                     const int width,
+                                                     const int height) {
+	const Vector2f start_center =
+	   get_view_area(MapView::View{start, start_zoom}, width, height).center();
+	const Vector2f end_center =
+	   get_view_area(MapView::View{end, start_zoom}, width, height).center();
+	const Vector2f center_point_change =
+	   MapviewPixelFunctions::calc_pix_difference(map, end_center, start_center);
+
+	// Heuristic: How many screens is the target point away from the current
+	// viewpoint? We use it to decide the zoom out factor and scroll speed.
+	float num_screens = std::max(std::abs(center_point_change.x) / (width * start_zoom),
+	                             std::abs(center_point_change.y) / (height * start_zoom));
+
+	// If the target is 4 screens away, we zoom out to x4. If we would not zoom
+	// out, we do not interpolate the zoom at all. This avoids rounding errors.
+	const float target_zoom = math::clamp(num_screens, start_zoom, kMaxAnimationZoom);
+	const float delta_zoom = target_zoom - start_zoom;
+	const bool pan_and_zoom_animation = delta_zoom > kPanOnlyZoomThreshold;
+	const float duration_ms =
+	   pan_and_zoom_animation ? kPanAndZoomAnimationTimeMs : kPanOnlyAnimationTimeMs;
+
+	SymmetricInterpolator<float, SmoothstepInterpolator<float>> zoom_t(
+	   start_zoom, target_zoom, static_cast<float>(duration_ms));
+
+	SmoothstepInterpolator<Vector2f> center_point_t(
+	   start_center, start_center + center_point_change, duration_ms);
+	const uint32_t start_time = SDL_GetTicks();
+	std::vector<MapView::TimestampedView> plan;
+	plan.push_back(MapView::TimestampedView{start_time, MapView::View{start, start_zoom}});
+	for (int i = 1; i < kNumKeyFrames - 2; i++) {
+		float dt = (duration_ms / kNumKeyFrames) * i;
+		const float zoom = pan_and_zoom_animation ? zoom_t.value(dt) : start_zoom;
+		const Vector2f center_point = center_point_t.value(dt);
+		const Vector2f viewpoint = center_point - Vector2f(width * zoom / 2.f, height * zoom / 2.f);
+		plan.push_back(MapView::TimestampedView{
+		   static_cast<uint32_t>(std::lround(start_time + dt)), MapView::View{viewpoint, zoom}});
+	}
+	// Correct numeric instabilities. We want to land precisely at 'end'.
+	const Vector2f end_viewpoint = (start_center + center_point_change) -
+	                               Vector2f(width * start_zoom / 2.f, height * start_zoom / 2.f);
+	plan.push_back(
+	   MapView::TimestampedView{static_cast<uint32_t>(std::lround(start_time + duration_ms)),
+	                            MapView::View{end_viewpoint, start_zoom}});
+	return plan;
+}
+
 }  // namespace
 
 MapView::MapView(
@@ -62,8 +273,7 @@
    : UI::Panel(parent, x, y, w, h),
      renderer_(new GameRenderer()),
      intbase_(player),
-     viewpoint_(0.f, 0.f),
-     zoom_(1.f),
+     view_{Vector2f(0.f, 0.f), 1.f},
      dragging_(false) {
 }
 
@@ -71,15 +281,15 @@
 }
 
 Vector2f MapView::get_viewpoint() const {
-	return viewpoint_;
+	return view_.viewpoint;
 }
 
 Vector2f MapView::to_panel(const Vector2f& map_pixel) const {
-	return MapviewPixelFunctions::map_to_panel(viewpoint_, zoom_, map_pixel);
+	return MapviewPixelFunctions::map_to_panel(view_.viewpoint, view_.zoom, map_pixel);
 }
 
 Vector2f MapView::to_map(const Vector2f& panel_pixel) const {
-	return MapviewPixelFunctions::panel_to_map(viewpoint_, zoom_, panel_pixel);
+	return MapviewPixelFunctions::panel_to_map(view_.viewpoint, view_.zoom, panel_pixel);
 }
 
 /// Moves the mouse cursor so that it is directly above the given node
@@ -105,18 +315,18 @@
 
 	const Widelands::Map& map = intbase().egbase().map();
 	const Vector2f map_pixel = MapviewPixelFunctions::to_map_pixel_with_normalization(map, c);
-	const Rectf view_area = get_view_area();
+	const Rectf area = view_area();
 
-	const Vector2f view_center = view_area.center();
-	const int w = MapviewPixelFunctions::get_map_end_screen_x(map);
-	const int h = MapviewPixelFunctions::get_map_end_screen_y(map);
+	const Vector2f view_center = area.center();
 	const Vector2f dist = MapviewPixelFunctions::calc_pix_difference(map, view_center, map_pixel);
 
 	// Check if the point is visible on screen.
-	if (dist.x > view_area.w / 2.f || dist.y > view_area.h / 2.f) {
+	if (std::abs(dist.x) > area.w / 2.f || std::abs(dist.y) > area.h / 2.f) {
 		return;
 	}
-	const Vector2f in_panel = to_panel(move_inside(map_pixel, view_area, w, h));
+	const Vector2f in_panel =
+	   to_panel(move_inside(map_pixel, area, MapviewPixelFunctions::get_map_end_screen_x(map),
+	                        MapviewPixelFunctions::get_map_end_screen_y(map)));
 	set_mouse_pos(round(in_panel));
 	track_sel(in_panel);
 }
@@ -124,6 +334,28 @@
 void MapView::draw(RenderTarget& dst) {
 	Widelands::EditorGameBase& egbase = intbase().egbase();
 
+	if (!current_plan_.empty()) {
+		uint32_t now = SDL_GetTicks();
+		size_t i = 0;
+		while (i < current_plan_.size() && current_plan_[i].t < now) {
+			++i;
+		}
+		if (i == current_plan_.size()) {
+			current_plan_.clear();
+		} else if (i > 0) {
+			// Linearly interpolate between the next and the last.
+			float t = (now - current_plan_[i - 1].t) /
+			          static_cast<float>(current_plan_[i].t - current_plan_[i - 1].t);
+			const float zoom =
+			   mix(t, current_plan_[i - 1].view.zoom, current_plan_[i].view.zoom);
+			set_zoom(zoom);
+			const Vector2f viewpoint = mix(
+			   t, current_plan_[i - 1].view.viewpoint, current_plan_[i].view.viewpoint);
+			set_viewpoint(viewpoint, false);
+			// log("#sirver %d,%.4f,%.4f,%.4f\n", now, viewpoint.x, viewpoint.y, zoom);
+		}
+	}
+
 	if (upcast(Widelands::Game, game, &egbase)) {
 		// Bail out if the game isn't actually loaded.
 		// This fixes a crash with displaying an error dialog during loading.
@@ -142,18 +374,18 @@
 
 	if (upcast(InteractivePlayer const, interactive_player, &intbase())) {
 		renderer_->rendermap(
-		   egbase, viewpoint_, zoom_, interactive_player->player(), draw_text, &dst);
+		   egbase, view_.viewpoint, view_.zoom, interactive_player->player(), draw_text, &dst);
 	} else {
-		renderer_->rendermap(egbase, viewpoint_, zoom_, static_cast<TextToDraw>(draw_text), &dst);
+		renderer_->rendermap(egbase, view_.viewpoint, view_.zoom, static_cast<TextToDraw>(draw_text), &dst);
 	}
 }
 
 float MapView::get_zoom() const {
-	return zoom_;
+	return view_.zoom;
 }
 
 void MapView::set_zoom(const float zoom) {
-	zoom_ = zoom;
+	view_.zoom = zoom;
 }
 
 /*
@@ -162,9 +394,9 @@
 ===============
 */
 void MapView::set_viewpoint(const Vector2f& viewpoint, bool jump) {
-	viewpoint_ = viewpoint;
+	view_.viewpoint = viewpoint;
 	const Widelands::Map& map = intbase().egbase().map();
-	MapviewPixelFunctions::normalize_pix(map, &viewpoint_);
+	MapviewPixelFunctions::normalize_pix(map, &view_.viewpoint);
 	changeview(jump);
 }
 
@@ -180,16 +412,19 @@
 }
 
 void MapView::center_view_on_map_pixel(const Vector2f& pos) {
-	const Rectf view_area = get_view_area();
-	set_viewpoint(pos - Vector2f(view_area.w / 2.f, view_area.h / 2.f), true);
+	const Rectf area = view_area();
+	const Vector2f target_view = pos - Vector2f(area.w / 2.f, area.h / 2.f);
+	const Widelands::Map& map = intbase().egbase().map();
+	current_plan_ =
+	   plan_animation(map, view_.viewpoint, target_view, view_.zoom, get_w(), get_h());
 }
 
-Rectf MapView::get_view_area() const {
-	return Rectf(viewpoint_, get_w() * zoom_, get_h() * zoom_);
+Rectf MapView::view_area() const {
+	return get_view_area(view_, get_w(), get_h());
 }
 
 void MapView::pan_by(Vector2i delta_pixels) {
-	set_viewpoint(get_viewpoint() + delta_pixels.cast<float>() * zoom_, false);
+	set_viewpoint(get_viewpoint() + delta_pixels.cast<float>() * view_.zoom, false);
 }
 
 void MapView::stop_dragging() {
@@ -248,7 +483,7 @@
 	}
 
 	constexpr float kPercentPerMouseWheelTick = 0.02f;
-	float zoom = zoom_ * static_cast<float>(
+	float zoom = view_.zoom * static_cast<float>(
 	                        std::pow(1.f - math::sign(y) * kPercentPerMouseWheelTick, std::abs(y)));
 	zoom_around(zoom, last_mouse_pos_.cast<float>());
 	return true;
@@ -263,9 +498,9 @@
 	// Zoom around the current mouse position. See
 	// http://stackoverflow.com/questions/2916081/zoom-in-on-a-point-using-scale-and-translate
 	// for a good explanation of this math.
-	const Vector2f offset = -panel_pixel * (new_zoom - zoom_);
-	zoom_ = new_zoom;
-	set_viewpoint(viewpoint_ + offset, false);
+	const Vector2f offset = -panel_pixel * (new_zoom - view_.zoom);
+	view_.zoom = new_zoom;
+	set_viewpoint(view_.viewpoint + offset, false);
 }
 
 /*
@@ -293,10 +528,10 @@
 	constexpr float kPercentPerKeyPress = 0.10f;
 	switch (code.sym) {
 	case SDLK_PLUS:
-		zoom_around(zoom_ - kPercentPerKeyPress, Vector2f(get_w() / 2.f, get_h() / 2.f));
+		zoom_around(view_.zoom - kPercentPerKeyPress, Vector2f(get_w() / 2.f, get_h() / 2.f));
 		return true;
 	case SDLK_MINUS:
-		zoom_around(zoom_ + kPercentPerKeyPress, Vector2f(get_w() / 2.f, get_h() / 2.f));
+		zoom_around(view_.zoom + kPercentPerKeyPress, Vector2f(get_w() / 2.f, get_h() / 2.f));
 		return true;
 	case SDLK_0:
 		zoom_around(1.f, Vector2f(get_w() / 2.f, get_h() / 2.f));

=== modified file 'src/wui/mapview.h'
--- src/wui/mapview.h	2016-11-03 07:20:57 +0000
+++ src/wui/mapview.h	2016-12-26 21:45:30 +0000
@@ -37,6 +37,22 @@
  * Implements a view of a map. It is used to render a valid map on the screen.
  */
 struct MapView : public UI::Panel {
+	struct View {
+		// Mappixel of top-left pixel of this MapView.
+		Vector2f viewpoint;
+
+		// Current zoom value.
+		float zoom;
+	};
+
+	struct TimestampedView {
+		// Time in milliseconds since the game was launched. Animations always
+		// happen in real-time, not in gametime. Therefore they are also not
+		// affected by pause.
+		uint32_t t;
+		View view;
+	};
+
 	MapView(UI::Panel* const parent,
 	        const int32_t x,
 	        const int32_t y,
@@ -60,7 +76,7 @@
 	void center_view_on_map_pixel(const Vector2f& pos);
 
 	Vector2f get_viewpoint() const;
-	Rectf get_view_area() const;
+	Rectf view_area() const;
 	float get_zoom() const;
 
 	// Set the zoom to the new value without changing view_point. For the user
@@ -104,10 +120,12 @@
 
 	std::unique_ptr<GameRenderer> renderer_;
 	InteractiveBase& intbase_;
-	Vector2f viewpoint_;
-	float zoom_;
+	View view_;
 	Vector2i last_mouse_pos_;
 	bool dragging_;
+
+	// If not empty(), the MapView is currently following this animation plan.
+	std::vector<TimestampedView> current_plan_;
 };
 
 #endif  // end of include guard: WL_WUI_MAPVIEW_H

=== modified file 'src/wui/quicknavigation.cc'
--- src/wui/quicknavigation.cc	2016-11-23 08:31:25 +0000
+++ src/wui/quicknavigation.cc	2016-12-26 21:45:30 +0000
@@ -41,7 +41,7 @@
 }
 
 void QuickNavigation::view_changed(bool jump) {
-	const Rectf view_area = map_view_->get_view_area();
+	const Rectf view_area = map_view_->view_area();
 	if (havefirst_ && update_) {
 		if (!jump) {
 			// Check if the anchor is moved outside the screen. If that is the

=== modified file 'src/wui/watchwindow.cc'
--- src/wui/watchwindow.cc	2016-10-24 14:07:28 +0000
+++ src/wui/watchwindow.cc	2016-12-26 21:45:30 +0000
@@ -295,7 +295,7 @@
  * Cause the main mapview_ to jump to our current position.
  */
 void WatchWindow::do_goto() {
-	warp_mainview(mapview_.get_view_area().center());
+	warp_mainview(mapview_.view_area().center());
 }
 
 /**


Follow ups