[gcompris] mining: add tutorial for mining activity



commit dd5ced1d97a4add52808399470985a138089b010
Author: Peter Albrecht <pa-dev gmx de>
Date:   Fri Sep 21 15:59:04 2012 +0200

    mining: add tutorial for mining activity
    
    This commit adds a tutorial to the mining activity. It contains
    animations, which demonstrate:
      - move cursor here
      - zoom in (by mouse and touchpad)
      - click (with mouse or touchpad)
      - zoom out (by mouse and touchpad)
    
    Based on certain conditions, the tutorial advances to its next step.
    
    The tutorial is only enabled for level 1.
    
    About the touchpad:
    I think, the touchpad is easier recognized as such, if the keyboard above
    is easier recognized. Therefor letters have been added to the former blank
    keys of the keyboard.
    I hope, having choosen a german keyboard layout is ok.
    
    A BlockingArea for the space occupied by the tutorial animations
    (mouse and touchpad) has been added, to avoid the nugget being placed
    behind the tutorial mouse or touchpad.

 src/mining-activity/Makefile.am                    |    6 +-
 src/mining-activity/mining.py                      |   92 ++-
 src/mining-activity/mining_tutorial.py             |  974 ++++++++++++++++++++
 src/mining-activity/resources/mining/Makefile.am   |    1 +
 src/mining-activity/resources/mining/tutorial.svgz |  Bin 0 -> 13858 bytes
 5 files changed, 1070 insertions(+), 3 deletions(-)
---
diff --git a/src/mining-activity/Makefile.am b/src/mining-activity/Makefile.am
index 642a17a..b9c9c1b 100644
--- a/src/mining-activity/Makefile.am
+++ b/src/mining-activity/Makefile.am
@@ -4,8 +4,10 @@ SUBDIRS = resources
 
 pythondir = $(PYTHON_PLUGIN_DIR)
 
-dist_python_DATA= 	mining.py
-
+dist_python_DATA = \
+	mining.py \
+	mining_tools.py \
+	mining_tutorial.py
 
 
 xmldir = $(pkgdatadir)/@PACKAGE_DATA_DIR@
diff --git a/src/mining-activity/mining.py b/src/mining-activity/mining.py
index f3574da..3651cb6 100644
--- a/src/mining-activity/mining.py
+++ b/src/mining-activity/mining.py
@@ -31,6 +31,7 @@ import goocanvas
 import pango
 import random
 import cairo
+import mining_tutorial
 
 from mining_tools import Area, BlockingArea
 
@@ -59,6 +60,14 @@ class Gcompris_mining:
   # so the image looks still nice, if we zoom in a bit.
   source_image_scale = 3.0
 
+  # handle to the tutorial object
+  tutorial = None
+  is_tutorial_enabled = False
+
+  # the distance, the mouse cursor has to approach the nugget, triggering the next tutorial step
+  # (in 800x520 coordinate space) (should be in sync with the graphics in tutorial.svgz)
+  min_nugget_approach = 50.0
+
 
   def __init__(self, gcomprisBoard):
     """ Constructor """
@@ -98,6 +107,7 @@ class Gcompris_mining:
     # automatically.
     self.rootitem = goocanvas.Group(parent = self.gcomprisBoard.canvas.get_root_item())
     self.rootitem.connect("button_press_event", self.on_button_press)
+    self.rootitem.connect("motion_notify_event", self.on_mouse_move)
 
     svghandle = gcompris.utils.load_svg("mining/rockwall.svgz")
 
@@ -124,6 +134,9 @@ class Gcompris_mining:
     # create sparkling last, so it is on above the nugget
     self.sparkling = Sparkling(svghandle, self.viewport.get_gc_group())
 
+    # prepare the tutorial
+    self.tutorial = mining_tutorial.MiningTutorial(self.rootitem)
+
     # initialize the level, to start with
     self.set_level(1)
 
@@ -143,11 +156,20 @@ class Gcompris_mining:
     if level == 1:
       self.nuggets_to_collect = 3
 
+      # Enable the tutorial for level 1
+      self.is_tutorial_enabled = True
+
+      # add the tutorials blocking area, to avoid the nugget being placed behind
+      # the tutorial mouse or touchpad
+      self.placer.add_blocker(self.tutorial.get_blocking_area())
+
     elif level == 2:
       self.nuggets_to_collect = 6
+      self.is_tutorial_enabled = False
 
     elif level == 3:
       self.nuggets_to_collect = 9
+      self.is_tutorial_enabled = False
 
     else:
       print("Warning: No level specific values defined for level %i! Keeping current settings." % level)
@@ -182,10 +204,58 @@ class Gcompris_mining:
     self.sparkling.animation_start()
     self.need_new_nugget = False
 
+    if self.is_tutorial_enabled:
+      self.tutorial.start()
+      nuggetArea = Area(self.nugget.get_bounds())
+      self.tutorial.set_tutorial_state('move to', nuggetArea.center_x, nuggetArea.center_y)
+
+
     # The following sound was copied form "Tuxpaint" (GPL)
     gcompris.sound.play_ogg("mining/realrainbow.ogg")
 
 
+  def on_mouse_move(self, item, target_item, event):
+    """
+    The user moved the mouse
+      item - The element connected with this callback function
+      target_item - The element under the cursor
+      event  - gtk.gdk.Event
+    """
+
+    if not self.is_tutorial_enabled:
+      return True
+
+
+    ##
+    # if we the mouse cursor is close enough to the nugget, switch to next tutorial step
+
+    # event.x / .y are based on the target_item, which may be the rockwall or a stone, ...
+    # so we can't use those, since the stones have another coordinate space  :(
+
+    # get the coordinates relative to the root of the screen (800 x 520)
+    x = event.x_root
+    y = event.y_root
+
+    # get_bounds() also gives us coordinates relative to the root of the screen (800 x 520)
+    nuggetArea = Area(self.nugget.get_bounds())
+    nx = nuggetArea.center_x
+    ny = nuggetArea.center_y
+
+    #          a^2         +         b^2         <=                         c^2
+    if (x - nx) * (x - nx) + (y - ny) * (y - ny) <= self.min_nugget_approach * self.min_nugget_approach:
+      # the mouse cursor is close enough, go to next tutorial step
+      self.tutorial.set_tutorial_state('zoom in')
+
+    else:
+      # if we still want to show the user, where to move the mouse pointer to, we need to
+      # update this animation now
+      if self.tutorial.get_tutorial_state() == 'move to':
+        self.tutorial.restart_tutorial_step(x, y, nx, ny)
+
+    # we processed this event
+    return True
+
+
   def on_zoom_change(self, state):
     """ Do something according to specific zoom states (E.g. the nugget is only visible at maximum zoom.) """
     # As of 2012-08-11 there seems to be no "gcomrpis way" to change the mouse cursor to
@@ -195,12 +265,19 @@ class Gcompris_mining:
       self.nugget.hide()
 
       if self.need_new_nugget:
+        if self.is_tutorial_enabled:
+          self.tutorial.stop()
+
         self.place_new_nugget()
 
     elif state == 'mid':
       self.nugget.hide()
 
     elif state == 'max':
+      if self.is_tutorial_enabled:
+        # proceed to next tutorial step
+        self.tutorial.set_tutorial_state('click')
+
       self.nugget.show()
 
     else:
@@ -259,6 +336,9 @@ class Gcompris_mining:
       # we need to collect more nuggets, so lets place a new one
       self.need_new_nugget = True
 
+      if self.is_tutorial_enabled:
+        self.tutorial.set_tutorial_state('zoom out')
+
 
   def update_lorry(self):
     """ Updates the nugget-collect-counter of the lorry in the lower right corner """
@@ -267,6 +347,10 @@ class Gcompris_mining:
 
   def on_level_won(self):
     """ The user collected enough nuggets """
+
+    if self.is_tutorial_enabled:
+      self.tutorial.stop()
+
     self.is_game_won = True;
     gcompris.bonus.display(gcompris.bonus.WIN, gcompris.bonus.LION)
 
@@ -305,6 +389,7 @@ class Gcompris_mining:
 
   def end_level(self):
     """ Terminate the current level """
+    self.tutorial.stop()
     self.sparkling.end()
     self.placer.remove_all_blocker()
     self.decorations.cleanup_viewport()
@@ -316,6 +401,8 @@ class Gcompris_mining:
 
     self.end_level()
 
+    self.tutorial.end()
+
     # Remove the root item removes all the others inside it
     self.rootitem.remove()
 
@@ -464,7 +551,10 @@ class Placer:
 
 
   def add_blocker(self, blocking_area):
-    """ Add a new blocking area to the internal list of blocking areas """
+    """
+    Add a new blocking area to the internal list of blocking areas
+      blocking_area: Object that implement method get_bounds() (like goocanvas.Item.get_bounds())
+    """
     self.blocking_areas.append(blocking_area)
 
 
diff --git a/src/mining-activity/mining_tutorial.py b/src/mining-activity/mining_tutorial.py
new file mode 100644
index 0000000..8ef6bf7
--- /dev/null
+++ b/src/mining-activity/mining_tutorial.py
@@ -0,0 +1,974 @@
+#  gcompris - mining_tutorial.py
+#
+# Copyright (C) 2012 Peter Albrecht
+#
+#   This program is free software; you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation; either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program 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 General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program; if not, see <http://www.gnu.org/licenses/>.
+#
+
+import math
+import gobject
+import gcompris
+import goocanvas
+import cairo
+
+from mining_tools import BlockingArea
+
+
+class MiningTutorial:
+  """ This class provides tutorial information to the user """
+
+  # the current tutorial state
+  current_state = None
+
+
+  def __init__(self, rootitem):
+    """
+    Constructor
+      rootitem: the root item, to add tutorial images to
+      (loads graphics)
+    """
+
+    # we create an own GooCanvas group to add tutorial items, so we can
+    # remove them easier
+    self.tutorial_rootitem = goocanvas.Group(parent = rootitem)
+
+    svghandle = gcompris.utils.load_svg("mining/tutorial.svgz")
+
+    self.mouse = TutorialMouse(self.tutorial_rootitem, svghandle, 300, 440)
+    self.cursor = TutorialCursor(self.tutorial_rootitem, svghandle)
+    self.touchpad = TutorialTouchpad(self.tutorial_rootitem, svghandle, 500, 440)
+
+
+  def get_blocking_area(self):
+    """
+    Returns the blocking area for the tutorial, to avoid placing the nugget behind
+    the tutorial mouse or the touchpad.
+    """
+    return BlockingArea(250, 360, 640, 520)
+
+
+  def start(self):
+    """ Start the tutorial (display graphics) """
+    self.current_state = 'start'
+
+
+  def get_tutorial_state(self):
+    """ Returns the current tutorial state """
+    return self.current_state
+
+
+  def set_tutorial_state(self, state, *special_state_arguments):
+    """
+    Set the current state of the tutorial
+      state: the state to set, valid values:
+        - 'move to'
+        - 'zoom in'
+        - 'click'
+        - 'zoom out'
+      special_state_arguments: special arguments for the particular state
+    """
+
+    if state == self.current_state:
+      return
+
+
+    # advance to next state, if current state matches
+    if state == 'move to':
+      if self.current_state == 'start':
+        # we should send the real mouse position, but best, I could get is
+        # the mouse position in screen pixels:
+        #   x, y = self.rootitem.get_canvas().get_pointer()
+        # And the documented function goocanvas.Canvas.convert_from_pixel() does not exist. :(
+        # So we take the middle of the screen as starting point for the animation.
+        self.cursor.start(gcompris.BOARD_WIDTH / 2, gcompris.BOARD_HEIGHT / 2, *special_state_arguments)
+
+        self.current_state = state
+
+    elif state == 'zoom in':
+      if self.current_state == 'move to':
+        self.cursor.stop()
+
+        self.mouse.start_zoom('in')
+        self.touchpad.start_zoom('in')
+
+        self.current_state = state
+
+    elif state == 'click':
+      if self.current_state == 'zoom in':
+        self.mouse.stop()
+        self.touchpad.stop()
+
+        self.mouse.start_click()
+        self.touchpad.start_click()
+
+        self.current_state = state
+
+    elif state == 'zoom out':
+      if self.current_state == 'click':
+        self.mouse.stop()
+        self.touchpad.stop()
+
+        self.mouse.start_zoom('out')
+        self.touchpad.start_zoom('out')
+
+        self.current_state = state
+
+    else:
+      # invalid state
+      assert(False)
+
+
+  def restart_tutorial_step(self, *special_state_arguments):
+    """
+    Restarts the current tutorial step
+      special_state_arguments: special arguments for the particular state
+    """
+    if self.current_state == 'move to':
+      self.cursor.stop()
+      self.cursor.start(*special_state_arguments)
+
+    else:
+      # restart not supported
+      assert(False)
+
+
+  def stop(self):
+    """ Stop the tutorial (hide graphics) """
+    self.cursor.stop()
+    self.mouse.stop()
+    self.touchpad.stop()
+
+
+  def end(self):
+    """ Our "destructor" (remove graphics) """
+    self.cursor.end()
+    self.mouse.end()
+    self.touchpad.end()
+
+    # remove all tutorial graphics from the canvas
+    self.tutorial_rootitem.remove()
+
+
+
+class TutorialCursor:
+  """ This class demonstrates to move the cursor to a specific position """
+
+  # position of the center of the cursor target in screen coordinates (800 x 520)
+  circle_x = 0.0
+  circle_y = 0.0
+
+  # position of the center of the ghost mouse cursor in screen coordinates (800 x 520)
+  cursor_x = 0.0
+  cursor_y = 0.0
+
+  # center of the cursor target in the svg file
+  pivot_circle_x = 400.0
+  pivot_circle_y = 370.0
+
+  # center of the mouse cursor in the svg file
+  pivot_cursor_x = 200.0
+  pivot_cursor_y = 370.0
+
+  # the number of milliseconds the total "move cursor to" animation lasts
+  animation_time_total = 3000
+
+  # the time between each animation step, in milliseconds
+  animation_time_step = 100
+
+
+  def __init__(self, rootitem, svghandle):
+    """
+    Constructor
+        rootitem    : The goocanvas item to add the mouse to
+        svghandle   : Handle of the svg file, containing the graphics
+    """
+
+    self.circle_img = goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#CURSOR_TARGET",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      )
+
+    self.cursor_img = goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#MOUSE_CURSOR",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      )
+
+
+  def start(self, cx, cy, tx, ty):
+    """
+    Start the cursor move tutorial: show graphics
+      cx, cy: x-, y-position of the mouse cursor
+      tx, ty: x-, y-position of the target
+    """
+
+    self.circle_x = tx
+    self.circle_y = ty
+
+    self.cursor_x = cx
+    self.cursor_y = cy
+
+    self.circle_img.props.visibility = goocanvas.ITEM_VISIBLE
+    self.cursor_img.props.visibility = goocanvas.ITEM_VISIBLE
+
+    self.__update_circle_transformation()
+    self.__update_cursor_transformation()
+
+    self.__animation_start()
+
+
+  def __update_circle_transformation(self):
+    """ Updates the transformation matrix of the cursor target """
+
+    matrix = cairo.Matrix(1, 0, 0, 1, self.circle_x - self.pivot_circle_x, self.circle_y - self.pivot_circle_y)
+    self.circle_img.set_transform(matrix)
+
+
+  def __update_cursor_transformation(self):
+    """ Updates the transformation matrix of the ghost mouse cursor """
+
+    matrix = cairo.Matrix(1, 0, 0, 1, self.cursor_x - self.pivot_cursor_x, self.cursor_y - self.pivot_cursor_y)
+    self.cursor_img.set_transform(matrix)
+
+
+  def __animation_start(self):
+    # start moving the ghost mouse cursor to the sparkle
+    self.cursor_img.animate(
+      self.circle_x - self.cursor_x, # destination x position in root coordinates (800x520)
+      self.circle_y - self.cursor_y, # destination y position in root coordinates (800x520)
+      1, 0, # no scale and no rotation
+      False, # destination position is relative
+      self.animation_time_total, # see variable definition
+      self.animation_time_step, # see variable definition
+      goocanvas.ANIMATE_RESTART        # loop the animation
+      )
+
+
+  def stop(self):
+    """ Stop the cursor move tutorial: hide graphics """
+
+    self.cursor_img.stop_animation()
+
+    self.circle_img.props.visibility = goocanvas.ITEM_INVISIBLE
+    self.cursor_img.props.visibility = goocanvas.ITEM_INVISIBLE
+
+
+  def end(self):
+    """ Our "destructor" """
+    pass
+
+
+
+class TutorialMouse:
+  """ Displays a mouse with mouse wheel animation """
+
+  # time period between two animation steps
+  timer_scroll_milliseconds = 300
+  timer_click_milliseconds = 800
+
+  # timer
+  timer_scroll = None
+  timer_click = None
+
+  # position of the center of the mouse in screen coordinates (800 x 520)
+  x = 0.0
+  y = 0.0
+
+  # center of the mouse in the svg file
+  pivot_mouse_x = 200
+  pivot_mouse_y = 220
+
+  # position of the mouse wheel center relative to the mouse center
+  center_mouse_to_center_wheel_x = 0
+  center_mouse_to_center_wheel_y = -14.2
+
+  # center of the mouse wheel pivots in the svg file
+  pivot_wheels = (
+    {
+      'pivot_x': 100,
+      'pivot_y': 220
+      },
+    {
+      'pivot_x': 120,
+      'pivot_y': 220
+      },
+    {
+      'pivot_x': 140,
+      'pivot_y': 220
+      }
+    )
+
+  # center of the mouse button in the svg file
+  pivot_button_x = 120
+  pivot_button_y = 270
+
+  # position of the mouse button center relative to the mouse center
+  center_mouse_to_center_button_x = -16
+  center_mouse_to_center_button_y = -12
+
+  # the current wheel displayed
+  current_wheel = None
+
+  # define the scroll direction:
+  #   +1: zoom in
+  #   -1: zoom out
+  scroll_direction = None
+
+  # the number of different wheels
+  number_of_wheels = 3
+
+  # list of mouse wheel images
+  wheel_imgs = []
+
+
+  def __init__(self, rootitem, svghandle, x, y):
+    """
+    Constructor
+        rootitem    : The goocanvas item to add the mouse to
+        svghandle   : Handle of the svg file, containing the graphics
+        x, y        : Position of the center of the mouse in screen coordinates (800 x 520)
+    """
+
+    self.x = x
+    self.y = y
+
+    self.mouse_img = goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#MOUSE",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      )
+
+    self.button_img = goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#MOUSE_BUTTON",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      )
+
+    # GooCanvas does not support to add SVG-items to other SVG-items, so we have to add
+    # the wheels to the rootitem.
+
+    self.wheel_imgs.append(goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#MOUSEWHEEL01",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      ))
+
+    self.wheel_imgs.append(goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#MOUSEWHEEL02",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      ))
+
+    self.wheel_imgs.append(goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#MOUSEWHEEL03",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      ))
+
+    self.current_wheel = 0
+    self.__update_transformation()
+
+
+  def start_zoom(self, zoom_direction):
+    """
+    Start the mouse zoom tutorial: show graphics, start animation
+      zoom_direction: "in" or "out"
+    """
+
+    self.mouse_img.props.visibility = goocanvas.ITEM_VISIBLE
+
+    self.current_wheel = 0
+    self.__display_current_wheel()
+
+    if zoom_direction == 'in':
+      self.scroll_animation_start(+1)
+
+    elif zoom_direction == 'out':
+      self.scroll_animation_start(-1)
+
+    else:
+      # invalid zoom direction
+      assert(False)
+
+
+  def start_click(self):
+    """ Start the mouse click tutorial: show graphics, start animation """
+
+    self.mouse_img.props.visibility = goocanvas.ITEM_VISIBLE
+
+    self.current_wheel = 0
+    self.__display_current_wheel()
+
+    self.click_animation_start()
+
+
+  def stop(self):
+    """ Stop the mouse tutorial: hide graphics, stop animation """
+
+    if self.__is_scroll_animation_playing():
+      self.scroll_animation_stop()
+
+    if self.__is_click_animation_playing():
+      self.click_animation_stop()
+
+    self.button_img.props.visibility = goocanvas.ITEM_INVISIBLE
+    self.mouse_img.props.visibility = goocanvas.ITEM_INVISIBLE
+    self.__hide_current_wheel()
+
+
+  def end(self):
+    """ Our "destructor" """
+    pass
+
+
+  def __display_current_wheel(self):
+    """ Displays the current mouse wheel """
+    self.wheel_imgs[self.current_wheel].props.visibility = goocanvas.ITEM_VISIBLE
+
+
+  def __hide_current_wheel(self):
+    """ Hides the current mouse wheel """
+    self.wheel_imgs[self.current_wheel].props.visibility = goocanvas.ITEM_INVISIBLE
+
+
+  def __display_next_wheel(self):
+    """ Displays the next mouse wheel on top of the mouse """
+
+    # hide the old one
+    self.__hide_current_wheel()
+
+    # determine next mouse wheel to display
+    self.current_wheel += self.scroll_direction
+    if self.current_wheel >= self.number_of_wheels:
+      self.current_wheel = 0
+    elif self.current_wheel < 0:
+      self.current_wheel = self.number_of_wheels - 1
+
+    # show the new one
+    self.__display_current_wheel()
+
+
+  def __update_transformation(self):
+    """ Updates the transformation matrix of the mouse, all wheels and the button """
+
+    matrix = cairo.Matrix(1, 0, 0, 1, self.x - self.pivot_mouse_x, self.y - self.pivot_mouse_y)
+    self.mouse_img.set_transform(matrix)
+
+    for i in range(self.number_of_wheels):
+      wheel = self.pivot_wheels[i]
+      matrix = cairo.Matrix(1, 0, 0, 1,
+        self.x - wheel['pivot_x'] + self.center_mouse_to_center_wheel_x,
+        self.y - wheel['pivot_y'] + self.center_mouse_to_center_wheel_y
+        )
+      self.wheel_imgs[i].set_transform(matrix)
+
+    matrix = cairo.Matrix(1, 0, 0, 1,
+      self.x - self.pivot_button_x + self.center_mouse_to_center_button_x,
+      self.y - self.pivot_button_y + self.center_mouse_to_center_button_y
+      )
+    self.button_img.set_transform(matrix)
+
+
+  def __scroll_animate(self):
+    """ Displays the next scrolling animation step """
+
+    self.__display_next_wheel()
+
+    # call timeout again
+    return True
+
+
+  def __is_scroll_animation_playing(self):
+    """ Tells us, if there is a scroll animation running at the moment """
+    return self.timer_scroll != None
+
+
+  def scroll_animation_start(self, scroll_direction):
+    """
+    Starts the mouse wheel scroll animation
+      scroll_direction:
+         +1: zoom in
+         -1: zoom out
+    """
+    assert(not self.__is_scroll_animation_playing())
+
+    self.scroll_direction = scroll_direction
+
+    self.timer_scroll = gobject.timeout_add(self.timer_scroll_milliseconds, self.__scroll_animate)
+
+
+  def scroll_animation_stop(self):
+    """ Stops the mouse wheel scroll animation """
+    assert(self.__is_scroll_animation_playing())
+
+    gobject.source_remove(self.timer_scroll)
+    self.timer_scroll = None
+
+
+  def __click_animate(self):
+    """ Displays the click animation step """
+
+    if self.click_visible:
+      # switch to invisible
+      self.click_visible = False
+      self.button_img.props.visibility = goocanvas.ITEM_INVISIBLE
+
+    else:
+      # switch to visible
+      self.click_visible = True
+      self.button_img.props.visibility = goocanvas.ITEM_VISIBLE
+
+    # call timeout again
+    return True
+
+
+  def __is_click_animation_playing(self):
+    """ Tells us, if there is a click animation running at the moment """
+    return self.timer_click != None
+
+
+  def click_animation_start(self):
+    """ Starts the mouse click animation """
+    assert(not self.__is_click_animation_playing())
+    self.click_visible = False
+    self.timer_click = gobject.timeout_add(self.timer_click_milliseconds, self.__click_animate)
+
+
+  def click_animation_stop(self):
+    """ Stops the mouse click animation """
+    assert(self.__is_click_animation_playing())
+
+    gobject.source_remove(self.timer_click)
+    self.timer_click = None
+
+
+
+class TutorialTouchpad:
+  """ Displays a touchpad showing the zoom animation """
+
+  # position of the center of the touchpad in screen coordinates (800 x 520)
+  touchpad_x = 0.0
+  touchpad_y = 0.0
+
+  # center of the touchpad in the svg file
+  pivot_touchpad_x = 400
+  pivot_touchpad_y = 220
+
+  # center of the finger in the svg file
+  pivot_finger_x = 600
+  pivot_finger_y = 220
+
+  # center of the touch effect in the svg file
+  pivot_touch_effect_x = 700
+  pivot_touch_effect_y = 220
+
+  # position of the touch effect center relative to the touchpad center
+  center_touchpad_to_center_touch_effect_x = -3
+  center_touchpad_to_center_touch_effect_y = 0
+
+  # the number of milliseconds one zoom finger animation lasts
+  zoom_animation_time_total = 3000
+
+  # the time between each zoom animation step, in milliseconds
+  zoom_animation_time_step = 100
+
+  # 1: show "1"-finger animation
+  # 2: show "2"-finger animation
+  zoom_number_of_fingers = None
+
+  # zoom "in" or "out"
+  zoom_direction = None
+
+  # the finger's movement on the y-axis during one finger animation
+  zoom_finger_movement_y = 40
+
+  # one time timer to start the zoom animation again
+  zoom_animation_start_timer = None
+
+  # we remember the handler id to be able to disconnect it again
+  zoom_animation_finished_handler_id = None
+
+  # the number of milliseconds one finger click animation lasts (only the lowering part)
+  click_animation_time_total = 1000
+
+  # the time between each click animation step, in milliseconds
+  click_animation_time_step = 100
+
+  # the factor, the finger is scaled down by, during the click animation
+  click_animation_finger_scale_down = 0.667
+
+  # the number of milliseconds, the touch-effect is shown during a click animation
+  click_show_effect_milliseconds = 750
+
+  # one time timer to restart the click animation, after showing the touch effect
+  click_animation_show_effect_timer = None
+
+  # we remember the handler id to be able to disconnect it again
+  click_animation_finished_handler_id = None
+
+
+  def __init__(self, rootitem, svghandle, x, y):
+    """
+    Constructor
+        rootitem    : The goocanvas item to add the mouse to
+        svghandle   : Handle of the svg file, containing the graphics
+        x, y        : Position of the center of the touchpad in screen coordinates (800 x 520)
+    """
+
+    self.touchpad_x = x
+    self.touchpad_y = y
+
+    self.touchpad_img = goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#TOUCHPAD",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      )
+
+    self.finger01_img = goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#FINGER",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      )
+
+    self.finger02_img = goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#FINGER",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      )
+
+    self.touch_effect_img = goocanvas.Svg(
+      parent = rootitem,
+      svg_handle = svghandle,
+      svg_id = "#TOUCHPAD_PRESS_EFFECT",
+      visibility = goocanvas.ITEM_INVISIBLE,
+      pointer_events = goocanvas.EVENTS_NONE
+      )
+
+    self.__update_touchpad_transformation()
+
+
+  def start_zoom(self, zoom_direction):
+    """
+    Start the touchpad zoom tutorial: show graphics, start animation
+      zoom_direction: "in" or "out"
+    """
+
+    self.zoom_animation_finished_handler_id = self.finger01_img.connect("animation-finished", self.__zoom_animation_finished)
+
+    self.zoom_direction = zoom_direction
+    self.touchpad_img.props.visibility = goocanvas.ITEM_VISIBLE
+    self.zoom_number_of_fingers = 1
+    self.__zoom_animation_start()
+
+
+  def start_click(self):
+    """ Start the touchpad click tutorial: show graphics, start animation """
+
+    self.click_animation_finished_handler_id = self.finger01_img.connect("animation-finished", self.__click_animation_finished)
+
+    self.touchpad_img.props.visibility = goocanvas.ITEM_VISIBLE
+    self.finger01_img.props.visibility = goocanvas.ITEM_VISIBLE
+
+    self.__click_animation_start()
+
+
+  def stop(self):
+    """ Stop the touchpad tutorials: hide graphics, stop animation """
+
+    self.finger01_img.stop_animation()
+    self.finger02_img.stop_animation()
+
+    if self.zoom_animation_finished_handler_id is not None:
+      self.finger01_img.disconnect(self.zoom_animation_finished_handler_id)
+      self.zoom_animation_finished_handler_id = None
+
+    if self.click_animation_finished_handler_id is not None:
+      self.finger01_img.disconnect(self.click_animation_finished_handler_id)
+      self.click_animation_finished_handler_id = None
+
+    if self.zoom_animation_start_timer is not None:
+      # do not start another zoom animation
+      gobject.source_remove(self.zoom_animation_start_timer)
+      self.zoom_animation_start_timer = None
+
+    if self.click_animation_show_effect_timer is not None:
+      # do not continue the click animation
+      gobject.source_remove(self.click_animation_show_effect_timer)
+      self.click_animation_show_effect_timer = None
+
+    self.touch_effect_img.props.visibility = goocanvas.ITEM_INVISIBLE
+    self.touchpad_img.props.visibility = goocanvas.ITEM_INVISIBLE
+    self.__hide_fingers()
+
+
+  def end(self):
+    """ Our "destructor" """
+    pass
+
+
+  def __hide_fingers(self):
+    """ Hide all fingers """
+    self.finger01_img.props.visibility = goocanvas.ITEM_INVISIBLE
+    self.finger02_img.props.visibility = goocanvas.ITEM_INVISIBLE
+
+
+  def __zoom_animation_start(self):
+    """ Starts the touchpad zoom animation """
+
+    self.__zoom_position_fingers()
+
+
+    if self.zoom_direction == 'in':
+      destination_y = -self.zoom_finger_movement_y
+
+    elif self.zoom_direction == 'out':
+      destination_y = +self.zoom_finger_movement_y
+
+    else:
+      # wrong zoom direction
+      assert(False)
+
+
+    self.finger01_img.props.visibility = goocanvas.ITEM_VISIBLE
+
+    if self.zoom_number_of_fingers == 1:
+      self.finger02_img.props.visibility = goocanvas.ITEM_INVISIBLE
+
+      self.finger01_img.animate(
+        0, # destination x position in root coordinates (800x520)
+        destination_y, # destination y position in root coordinates (800x520)
+        1, 0, # no scale and no rotation
+        False, # destination position is relative
+        self.zoom_animation_time_total, # see variable definition
+        self.zoom_animation_time_step, # see variable definition
+        goocanvas.ANIMATE_FREEZE  # stop at animation end
+        )
+
+    elif self.zoom_number_of_fingers == 2:
+      self.finger02_img.props.visibility = goocanvas.ITEM_VISIBLE
+
+      self.finger01_img.animate(
+        0,
+        destination_y,
+        1, 0,
+        False,
+        self.zoom_animation_time_total,
+        self.zoom_animation_time_step,
+        goocanvas.ANIMATE_FREEZE
+        )
+
+      self.finger02_img.animate(
+        0,
+        destination_y,
+        1, 0,
+        False,
+        self.zoom_animation_time_total,
+        self.zoom_animation_time_step,
+        goocanvas.ANIMATE_FREEZE
+        )
+
+    else:
+      # Wrong number of fingers!
+      assert(False)
+
+
+  def __zoom_animation_finished(self, item, stopped):
+    """
+    Called, when the zoom finger animation is finished
+      item - The element connected with this callback function
+      stopped - True if the animation was stopped explicitly
+    """
+
+    if stopped:
+      # animation has been stopped explicitly (tutorial.stop())
+      # => do not start another one
+      return True
+
+
+    ##
+    # start animation with the other number of fingers
+
+    # switch number of fingers
+    if self.zoom_number_of_fingers == 1:
+      self.zoom_number_of_fingers = 2
+
+    elif self.zoom_number_of_fingers == 2:
+      self.zoom_number_of_fingers = 1
+
+    else:
+      # Wrong number of fingers!
+      assert(False)
+
+    # Restart the animation again after a short period, to make sure the other animation
+    # has finished, too. And I would have a bad feeling, starting a new animation while being
+    # in the animation-finished function of the same object.
+    self.zoom_animation_start_timer = gobject.timeout_add(int(self.zoom_animation_time_step * 1.5), self.__zoom_animation_start_by_onetime_timer)
+
+    return True
+
+
+  def __zoom_animation_start_by_onetime_timer(self):
+    """ Start the finger zoom animation again, called by a timer, once """
+
+    # this timer is consumed
+    self.zoom_animation_start_timer = None
+
+    self.__zoom_animation_start()
+
+    # do not call this timer again (this return is the reason for this function)
+    return False
+
+
+  def __update_touchpad_transformation(self):
+    """ Updates the transformation matrix of the touchpad and the touch effect """
+
+    matrix = cairo.Matrix(1, 0, 0, 1, self.touchpad_x - self.pivot_touchpad_x, self.touchpad_y - self.pivot_touchpad_y)
+    self.touchpad_img.set_transform(matrix)
+
+    matrix = cairo.Matrix(1, 0, 0, 1,
+      self.touchpad_x - self.pivot_touch_effect_x + self.center_touchpad_to_center_touch_effect_x,
+      self.touchpad_y - self.pivot_touch_effect_y + self.center_touchpad_to_center_touch_effect_y
+      )
+    self.touch_effect_img.set_transform(matrix)
+
+
+  def __zoom_position_fingers(self):
+    """ Updates the position of the fingers for zoom animation to start """
+
+    if self.zoom_direction == 'in':
+      finger_y = 85
+
+    elif self.zoom_direction == 'out':
+      finger_y = 45
+
+    else:
+      # wrong zoom direction
+      assert(False)
+
+
+    if self.zoom_number_of_fingers == 1:
+      self.__update_finger_transformation(self.finger01_img, 35, finger_y, 1.0, -1.0)
+
+    elif self.zoom_number_of_fingers == 2:
+      self.__update_finger_transformation(self.finger01_img, -20, finger_y, 1.0, -1.0)
+      self.__update_finger_transformation(self.finger02_img, 2, finger_y - 2, 1.0, -2.5)
+
+    else:
+      # Wrong number of fingers!
+      assert(False)
+
+
+  def __click_animation_start(self):
+    """ Starts the touchpad click animation """
+
+    # position finger (with reverse "scale down", so at the end of the animation, we have scale = 1.0)
+    self.__update_finger_transformation(self.finger01_img, -2.1, 68, 1.0 / self.click_animation_finger_scale_down, -1.0)
+
+    # start "lowering finger" animation
+    self.finger01_img.animate(
+      # shrinking (scale down) affects our pivot correction, so we have to compensate this
+      # by relative movement:
+      self.pivot_finger_x * (1 - self.click_animation_finger_scale_down), # position correction on x axis
+      self.pivot_finger_y * (1 - self.click_animation_finger_scale_down), # position correction on y axis
+
+      self.click_animation_finger_scale_down, # scale to 1.0
+      0, # no rotation
+      False, # destination position is relative
+      self.click_animation_time_total, # see variable definition
+      self.click_animation_time_step, # see variable definition
+      goocanvas.ANIMATE_FREEZE  # stop at animation end
+      )
+
+
+  def __click_animation_finished(self, item, stopped):
+    """
+    Called, when the click finger animation is finished
+      item - The element connected with this callback function
+      stopped - True if the animation was stopped explicitly
+    """
+
+    if stopped:
+      # animation has been stopped explicitly (tutorial.stop())
+      # => do not continue this animation
+      return True
+
+    # show "touch"-effect
+    self.touch_effect_img.props.visibility = goocanvas.ITEM_VISIBLE
+
+    # start with the "lowering finger" animation again, in a few milliseconds
+    self.click_animation_show_effect_timer = gobject.timeout_add(self.click_show_effect_milliseconds, self.__click_animation_start_by_onetime_timer)
+
+    return True
+
+
+  def __click_animation_start_by_onetime_timer(self):
+    """ Start the finger click animation again, called by a timer, once """
+
+    # this timer is consumed
+    self.click_animation_show_effect_timer = None
+
+    # hide the "touch"-effect again for new animation
+    self.touch_effect_img.props.visibility = goocanvas.ITEM_INVISIBLE
+
+    self.__click_animation_start()
+
+    # do not call this timer again (this return is the reason for this function)
+    return False
+
+
+  def __update_finger_transformation(self, finger_img, x, y, scale, angle):
+    """
+    Updates the transformation matrix of the given finger
+      finger_img: the finger to move
+      x, y: position of the finger, relative to the touchpad center
+      scale: finger scale
+      angle: finger rotation in degrees
+    """
+
+    # we need those values more than once, so lets remember them
+    a = math.radians(angle)
+    cos_a = math.cos(a)
+    sin_a = math.sin(a)
+
+    # create the transformation matrices
+    m_center_to_origin = cairo.Matrix(1, 0, 0, 1, -self.pivot_finger_x, -self.pivot_finger_y)
+    m_scale = cairo.Matrix(scale, 0, 0, scale, 0, 0)
+    m_rotate = cairo.Matrix(cos_a, sin_a, -sin_a, cos_a, 0, 0)
+    m_to_destination = cairo.Matrix(1, 0, 0, 1, x + self.touchpad_x, y + self.touchpad_y)
+
+    # combine all transformation matrices to one
+    matrix = m_center_to_origin * m_scale * m_rotate * m_to_destination
+
+    finger_img.set_transform(matrix)
diff --git a/src/mining-activity/resources/mining/Makefile.am b/src/mining-activity/resources/mining/Makefile.am
index bb4e76a..0e26e0d 100644
--- a/src/mining-activity/resources/mining/Makefile.am
+++ b/src/mining-activity/resources/mining/Makefile.am
@@ -1,6 +1,7 @@
 imgdir = $(pkgdatadir)/@PACKAGE_DATA_DIR@/mining
 img_DATA = \
 	rockwall.svgz \
+	tutorial.svgz \
 	pickaxe.ogg \
 	realrainbow.ogg
 
diff --git a/src/mining-activity/resources/mining/tutorial.svgz b/src/mining-activity/resources/mining/tutorial.svgz
new file mode 100644
index 0000000..dcdbfd2
Binary files /dev/null and b/src/mining-activity/resources/mining/tutorial.svgz differ



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]