AS3: Drag a Clip Along an Arc

View Example
Download Source

A while ago I was creating a video player for a project that needed to have the scrubber sit on an arc and be draggable. I asked around on Twitter if anyone knew of something that had this type of functionality already built (because it seems like something I shouldn't have to write from the ground up) and someone pointed me to senocular's Path class. It seemed promising and turned out to be the perfect class I needed to make this work. I included the class in the download files just because I wanted to make sure all files are provided but if you're going to use this make sure you grab it from his site as it's probably the latest version and all credit goes to him for making the class.

Here is the code to make the example run properly:

Actionscript:
  1. package
  2. {
  3.     import flash.text.TextField;
  4.     import flash.geom.Point;
  5.     import com.senocular.drawing.Path;
  6.  
  7.     import flash.display.Sprite;
  8.     import flash.events.MouseEvent;
  9.     import flash.utils.getQualifiedClassName;
  10.    
  11.     /**
  12.     * @author Matt Przybylski [http://www.reintroducing.com]
  13.     * @version 1.0
  14.     */
  15.     public class Main extends Sprite
  16.     {
  17. //- PRIVATE & PROTECTED VARIABLES -------------------------------------------------------------------------
  18.  
  19.         private var _point:Point = new Point();
  20.         private var _localPoint:Point = new Point();
  21.        
  22.         private var _percent:Number;
  23.        
  24. //- PUBLIC & INTERNAL VARIABLES ---------------------------------------------------------------------------
  25.        
  26.         public var scrubPath:Path = new Path();
  27.         public var pathHolder:Sprite = new Sprite();
  28.         public var scrubber:Sprite = new Sprite();
  29.         public var tf:TextField = new TextField();
  30.        
  31. //- CONSTRUCTOR -------------------------------------------------------------------------------------------
  32.    
  33.         public function Main()
  34.         {
  35.             super();
  36.            
  37.             init();
  38.         }
  39.        
  40. //- PRIVATE & PROTECTED METHODS ---------------------------------------------------------------------------
  41.        
  42.         /**
  43.          *
  44.          */
  45.         protected function init():void
  46.         {
  47.             pathHolder.graphics.lineStyle(1, 0x000000, .1);
  48.             pathHolder.x = pathHolder.y = 100;
  49.            
  50.             scrubPath.moveTo(0, 0);
  51.             scrubPath.curveTo(300, 100, 600, 0);
  52.             scrubPath.draw(pathHolder.graphics);
  53.            
  54.             scrubber.graphics.beginFill(0xFF0000, .5);
  55.             scrubber.graphics.drawCircle(0, 0, 10);
  56.             scrubber.graphics.endFill();
  57.             scrubber.buttonMode = true;
  58.             scrubber.addEventListener(MouseEvent.MOUSE_DOWN, handleScrubberPressed);
  59.             scrubber.addEventListener(MouseEvent.MOUSE_UP, handleScrubberReleased);
  60.            
  61.             tf.width = 800;
  62.             tf.x = 20;
  63.             tf.y = 200;
  64.            
  65.             pathHolder.addChild(scrubber);
  66.             addChild(pathHolder);
  67.             addChild(tf);
  68.         }
  69.        
  70. //- PUBLIC & INTERNAL METHODS -----------------------------------------------------------------------------
  71.    
  72.        
  73.    
  74. //- EVENT HANDLERS ----------------------------------------------------------------------------------------
  75.    
  76.         /**
  77.          *
  78.          */
  79.         private function handleScrubberPressed($evt:MouseEvent):void
  80.         {
  81.             stage.addEventListener(MouseEvent.MOUSE_MOVE, handleMouseMoved);
  82.             stage.addEventListener(MouseEvent.MOUSE_UP, handleScrubberReleased);
  83.         }
  84.        
  85.         /**
  86.          *
  87.          */
  88.         private function handleScrubberReleased($evt:MouseEvent):void
  89.         {
  90.             stage.removeEventListener(MouseEvent.MOUSE_MOVE, handleMouseMoved);
  91.             stage.removeEventListener(MouseEvent.MOUSE_UP, handleScrubberReleased);
  92.         }
  93.        
  94.         /**
  95.          *
  96.          */
  97.         private function handleMouseMoved($evt:MouseEvent):void
  98.         {
  99.             _localPoint = pathHolder.globalToLocal(new Point($evt.stageX, $evt.stageY));
  100.             _percent = (_localPoint.x / scrubPath.length);
  101.             _point = scrubPath.pointAt(_percent);
  102.            
  103.             if ((_localPoint.x>= 0) && (_localPoint.x <= scrubPath.length))
  104.             {
  105.                 scrubber.x = _point.x;
  106.                 scrubber.y = _point.y;
  107.                
  108.                 tf.text = String("PERCENT: " + Math.round(_percent * 100) + "%\nPOINT: " + _point);
  109.             }
  110.            
  111.             $evt.updateAfterEvent();
  112.         }
  113.    
  114. //- GETTERS & SETTERS -------------------------------------------------------------------------------------
  115.    
  116.        
  117.    
  118. //- HELPERS -----------------------------------------------------------------------------------------------
  119.    
  120.         /**
  121.          * @private
  122.          */
  123.         override public function toString():String
  124.         {
  125.             return getQualifiedClassName(this);
  126.         }
  127.    
  128. //- END CLASS ---------------------------------------------------------------------------------------------
  129.     }
  130. }

The magic happens at this stage:

Actionscript:
  1. scrubPath.moveTo(0, 0);
  2. scrubPath.curveTo(300, 100, 600, 0);
  3. scrubPath.draw(pathHolder.graphics);

Basically what we're doing here is using an API similar to the drawing API in the native graphics class which we're all accustomed to. Once the curve is made, it's drawn into the holder sprite's graphics. Later, we can grab the point while we are scrubbing it in a MOUSE_MOVE event.

Actionscript:
  1. private function handleMouseMoved($evt:MouseEvent):void
  2. {
  3.     _localPoint = pathHolder.globalToLocal(new Point($evt.stageX, $evt.stageY));
  4.     _percent = (_localPoint.x / scrubPath.length);
  5.     _point = scrubPath.pointAt(_percent);
  6.            
  7.     if ((_localPoint.x>= 0) && (_localPoint.x <= scrubPath.length))
  8.     {
  9.         scrubber.x = _point.x;
  10.         scrubber.y = _point.y;
  11.                
  12.         tf.text = String("PERCENT: " + Math.round(_percent * 100) + "%\nPOINT: " + _point);
  13.     }
  14.            
  15.     $evt.updateAfterEvent();
  16. }

Here I'm grabbing the point at which the mouse currently sits, converting it to a local point and calculating the percent that it's at along the path. I can then use the pointAt() method of the Path class to figure out at which point on the path I currently need to be and move the scrubber to that point. I'm also checking if I'm between the start and end points on the path so that I'm only dragging along the path and not past that area to the left or the right.

Turned out to be a very simple solution thanks to senocular's Path class and I hope it helps someone out in the future.

If you found this post useful, please consider leaving a comment, subscribing to the feed, or making a small donation.

4 Comments

Sweet!...did not even know that class existed. Perfect and simple solution to pathing.

Hi !
Your tutorial is very useful. I have been searching for days how to do a « kind, of drag and drop along a curve ». Thanks to your post, I’m about to solve the problem. Unfortunately, I’m not really used with classes. I have applied your code to a customised path. It nearly works but as soon as I click on the red circle, it follows a copy of my path on the bottom of the scene. I most probably have made a mistake, but can’t find where. Could you please help me?
Here’s my code.
Again, Thank you for sharing your knowledge.

MelodiaNoUta


package
{
import flash.text.TextField;
import flash.geom.Point;
import com.senocular.drawing.Path;

import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.utils.getQualifiedClassName;

public class Main extends Sprite
{
//- PRIVATE & PROTECTED VARIABLES -------------------------------------------------------------------------

private var _point:Point = new Point();
private var _localPoint:Point = new Point();

private var _percent:Number;

//- PUBLIC & INTERNAL VARIABLES ---------------------------------------------------------------------------

public var myTrajectoire:Path = new Path();
public var pathHolder:Sprite = new Sprite();
public var scrubber:Sprite = new Sprite();
public var tf:TextField = new TextField();

//- CONSTRUCTOR -------------------------------------------------------------------------------------------

public function Main()
{
super();

init();
}

//- PRIVATE & PROTECTED METHODS ---------------------------------------------------------------------------

/**
*
*/
protected function init():void
{

pathHolder.graphics.lineStyle(1, 0x000000, .1);
pathHolder.x = 0;
pathHolder.y = 0;

//définition de la trajectoire
myTrajectoire.moveTo(166, 202);
myTrajectoire.curveTo(182, 134, 302, 143);
myTrajectoire.moveTo(302, 143);
myTrajectoire.curveTo(335, 157, 363, 172);
myTrajectoire.moveTo(363, 172);
myTrajectoire.curveTo(406, 211, 370, 258);

//définition du curseur
scrubber.graphics.beginFill(0xFF0000, .5);
scrubber.graphics.drawCircle(166, 202, 10);
scrubber.graphics.endFill();
scrubber.buttonMode = true;

//detection du clic sur le curseur
scrubber.addEventListener(MouseEvent.MOUSE_DOWN, handleScrubberPressed);
scrubber.addEventListener(MouseEvent.MOUSE_UP, handleScrubberReleased);

myTrajectoire.draw(pathHolder.graphics);

tf.width = 800;
tf.x = 20;
tf.y = 200;

//ajoute les éléments créés sur la scène
pathHolder.addChild(scrubber);
addChild(pathHolder);
addChild(tf)
}

//- PUBLIC & INTERNAL METHODS -----------------------------------------------------------------------------

//- EVENT HANDLERS ----------------------------------------------------------------------------------------

/**
*
*/
private function handleScrubberPressed($evt:MouseEvent):void
{
stage.addEventListener(MouseEvent.MOUSE_MOVE, handleMouseMoved);
stage.addEventListener(MouseEvent.MOUSE_UP, handleScrubberReleased);
}

/**
*
*/
private function handleScrubberReleased($evt:MouseEvent):void
{
stage.removeEventListener(MouseEvent.MOUSE_MOVE, handleMouseMoved);
stage.removeEventListener(MouseEvent.MOUSE_UP, handleScrubberReleased);
}

/**
*
*/
private function handleMouseMoved($evt:MouseEvent):void
{

_localPoint = pathHolder.globalToLocal(new Point($evt.stageX , $evt.stageY));
_percent = (_localPoint.x / myTrajectoire.length);
_point = myTrajectoire.pointAt(_percent);

if ((_localPoint.x >= 0) && (_localPoint.x <= myTrajectoire.length))
{
scrubber.x = _point.x ;
scrubber.y = _point.y ;
trace(mouseY+ " " +scrubber.x);

tf.text = String("PERCENT: " + Math.round(_percent * 100) + "%\nPOINT: " + _point);
}

$evt.updateAfterEvent();
}

//- GETTERS & SETTERS -------------------------------------------------------------------------------------

//- HELPERS -----------------------------------------------------------------------------------------------

/**
* @private
*/
override public function toString():String
{
return getQualifiedClassName(this);
}

//- END CLASS ---------------------------------------------------------------------------------------------
}
}

@MelodiaNoUta: What you've got drawn is not really an arc rather an odd shape. I don't think this will work with that though I could be mistaken without playing with it. Also, you're moving the cursor before drawing inside of pathHolder so make sure you're taking those offsets into account when calculating the percentage. Hope that helps.

I had forgotten to thank you, for your answer. I reduce the number of points on my path and now it works.
In fact, when more than 3 points are added to the curve, bugs appear. I'm convinced that someone more experienced than me, could make it works, cause the movement of the point is still done.

Leave a comment

(required)

(required)