--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Two fingers camera manipulation, scroll and pinch to zoom
+#
+# Provides the main service `EulerCamera::accept_two_fingers_motion`.
+module android_two_fingers_motion
+
+# TODO add an abstraction when another platform supports it
+
+import gamnit
+import cameras
+import android
+
+redef class EulerCamera
+ # Smoothened history of pointers in the current motion event
+ private var last_motion_pointers = new HashMap[Int, Point[Float]] is lazy
+
+ # Start time of the current motion event
+ private var last_motion_start: Int = -1
+
+ # Move and zoom (set `position`) from a two finger pinch and slide option
+ #
+ # Returns `true` if the event is intercepted.
+ #
+ # Should be called from `App::accept_event` before accepting pointer events:
+ #
+ # ~~~nitish
+ # redef class App
+ # redef fun accept_event(event)
+ # do
+ # if world_camera.accept_two_fingers_motion(event) then return true
+ #
+ # # Handle other events...
+ # end
+ # end
+ # ~~~
+ fun accept_two_fingers_motion(event: InputEvent): Bool
+ do
+ if not event isa AndroidMotionEvent then return false
+
+ if event.pointers.length < 2 then
+ # Intercept leftovers of the last motion
+ return event.down_time == last_motion_start
+ end
+
+ # Collect active pointer and their world position
+ var new_motion_pointers = new HashMap[Int, Point[Float]]
+ var ids = new Array[Int]
+ for pointer in event.pointers do
+ var id = pointer.pointer_id
+ ids.add id
+ new_motion_pointers[id] = camera_to_world(pointer.x, pointer.y)
+ end
+
+ var last_motion_pointers = last_motion_pointers
+ if last_motion_start == event.down_time and
+ last_motion_pointers.keys.has(ids[0]) and last_motion_pointers.keys.has(ids[1]) then
+ # Continued motion event
+
+ # Get new and old position for 2 fingers
+ var new_motion_a = new_motion_pointers[ids[0]]
+ var new_motion_b = new_motion_pointers[ids[1]]
+ var prev_pos_a = last_motion_pointers[ids[0]]
+ var prev_pos_b = last_motion_pointers[ids[1]]
+
+ # Move camera
+ var prev_middle_pos = prev_pos_a.lerp(prev_pos_b, 0.5)
+ var new_middle_pos = new_motion_a.lerp(new_motion_b, 0.5)
+ position.x -= new_middle_pos.x - prev_middle_pos.x
+ position.y -= new_middle_pos.y - prev_middle_pos.y
+
+ # Zoom camera
+ var prev_dist = prev_pos_a.dist(prev_pos_b)
+ var new_dist = new_motion_a.dist(new_motion_b)
+
+ position.z = prev_dist * position.z / new_dist
+ else
+ # Prepare for a new motion event
+ last_motion_pointers.clear
+ last_motion_start = event.down_time
+ end
+
+ # Keep a smooth history
+ for i in [0..1] do
+ if last_motion_pointers.keys.has(ids[i]) then
+ last_motion_pointers[ids[i]] = last_motion_pointers[ids[i]]*0.5 +
+ new_motion_pointers[ids[i]]*0.5
+ else last_motion_pointers[ids[i]] = new_motion_pointers[ids[i]]
+ end
+
+ return true
+ end
+end
+
+redef class Point[N]
+ private fun *(scalar: Numeric): Point[N]
+ do return new Point[N](x.mul(scalar), y.mul(scalar))
+
+ private fun +(other: Point[N]): Point[N]
+ do return new Point[N](x.add(other.x), y.add(other.y))
+end