开发者

Constrain MovieClip drag to a circle

...well, to an incomplete circle.

I have a draggable slider that looks like this:

Constrain MovieClip drag to a circle

The blue bar has the instance name track and the pink dot has the instance name puck.

I need the puck to be constrained within the blue area at all times, and this is where my maths failings work against me! So far I have the puck moving along the x axis only like this:

private function init():void
{
    zeroPoint = track.x + (track.width/2);
    puck.x = zeroPoint-(puck.width/2);
    puck.buttonMode = true;
    puck.addEventListener(MouseEvent.MOUSE_DOWN,onMouseDown);
}

private function onMouseDown(evt:MouseEvent):void
{
    this.stage.addEventListener(MouseEvent.MOUSE_MOVE,onMouseMove);
    this.stage.addEventListener(MouseEvent.MOUSE_UP,onMouseUp);
}

private function onMouseUp(evt:MouseEvent):void
{
    this.stage.removeEventListener(MouseEvent.MOUSE_MOVE,onMouseMove);
}

private function onMouseMove(evt:MouseEvent):void
{
    puck.x = mouseX-(puck.width/2);
    //need to plot puck.y using trig magic...
}

My thinking is currently that I can use the radius of the incomplete circle (50) and the mouseX relative to the top of the arc to calculate a triangle, and from there I can calculate the required y position. Problem is, I'm reading various trigonometry sites and still have no idea where to begin. Could someone explain what I need to do as if speaking to a child please?

Edit: The fact that the circle is broken shouldn't be an issue, I can cap the movement to a certain number of degrees in each direction easily, it's getting the degrees in the first place that I can't get my head around!

Edit2: I'm trying to follow Bosworth99's answer, and this is the function I've come up with for calculating a radian to put into his function:

private func开发者_C百科tion getRadian():Number
{
    var a:Number = mouseX - zeroPoint;
    var b:Number = 50;
    var c:Number = Math.sqrt((a^2)+(b^2));
    return c;
}


As I see it, the problem you solve is finding the closest point on a circle. Google have a lot of suggestions on this subject.

You can optimise it by first detecting an angle between mouse position and circle center. Use Math.atan2() for that. If the angle is in a gap range, just choose the closest endpoint: left or right.

EDIT1 Here is a complete example of this strategy.

Hope that helps.

import flash.geom.Point;
import flash.events.Event;
import flash.display.Sprite;

var center:Point = new Point(200, 200);
var radius:uint = 100;

var degreesToRad:Number = Math.PI/180;

// gap angles. degrees are used here just for the sake of simplicity.
// what we use here are stage angles, not the trigonometric ones.
var gapFrom:Number = 45; // degrees
var gapTo:Number = 135; // degrees

// calculate endpoints only once

var endPointFrom:Point = new Point();
endPointFrom.x = center.x+Math.cos(gapFrom*degreesToRad)*radius;
endPointFrom.y = center.y+Math.sin(gapFrom*degreesToRad)*radius;

var endPointTo:Point = new Point();
endPointTo.x = center.x+Math.cos(gapTo*degreesToRad)*radius;
endPointTo.y = center.y+Math.sin(gapTo*degreesToRad)*radius;

// just some drawing
graphics.beginFill(0);
graphics.drawCircle(center.x, center.y, radius);
graphics.moveTo(center.x, center.y);
graphics.lineTo(endPointFrom.x, endPointFrom.y);
graphics.lineTo(endPointTo.x, endPointTo.y);
graphics.lineTo(center.x, center.y);
graphics.endFill();

// something to mark the closest point
var marker:Sprite = new Sprite();
marker.graphics.lineStyle(20, 0xFF0000);
marker.graphics.lineTo(0, 1);
addChild(marker);

var onEnterFrame:Function = function (event:Event) : void
{
    // circle intersection goes here
    var mx:int = stage.mouseX;
    var my:int = stage.mouseY;

    var angle:Number = Math.atan2(center.y-my, center.x-mx);
    // NOTE: in flash rotation is increasing clockwise, 
    // while in trigonometry angles increase counter clockwise
    // so we handle this difference
    angle += Math.PI;

    // calculate the stage angle in degrees
    var clientAngle:Number = angle/Math.PI*180

    // check if we are in a gap
    if (clientAngle >= gapFrom && clientAngle <= gapTo) {
        // we are in a gap, no sines or cosines needed
        if (clientAngle-gapFrom < (gapTo-gapFrom)/2) {        
            marker.x = endPointFrom.x;
            marker.y = endPointFrom.y;
        } else {
            marker.x = endPointTo.x;
            marker.y = endPointTo.y;
        }
        // we are done here
        return;
    }

    // we are not in a gp, calculate closest position on a circle
    marker.x = center.x + Math.cos(angle)*radius;
    marker.y = center.y + Math.sin(angle)*radius;
}
addEventListener(Event.ENTER_FRAME, onEnterFrame);

EDIT2 Some links

Here are some common problems explained and solved in a brilliantly clear and concise manner: http://paulbourke.net/geometry/ This resource helped me a lot days ago.

Intersection of a line and a circle is a bit of an overkill here, but here it is: http://paulbourke.net/geometry/sphereline/


Rather than trying to move the point along the partial path of the circle, why not fake it and use a knob/dial? Skin it to look like the dot is moving along the path.

Then just set the rotation of the knob to:

var deg:Number = Math.atan2(stage.mouseY - knob.y,stage.mouseX - knob.x) / (Math.PI/180);
// code to put upper/lower bounds on degrees    
knob.rotation = deg;

You can test this by throwing it in an enter frame event, but you'll obviously want to put some logic in to control how the knob starts moving and when it should stop.


100% working code.

enter code here

const length:int = 100;

var dragging:Boolean = false;
var tx:int;
var ty:int;



var p1:Sprite = new Sprite();
var p2:Sprite = new Sprite();

p1.graphics.beginFill(0);
p1.graphics.drawCircle(0, 0, 10);
p1.graphics.endFill();

p2.graphics.copyFrom(p1.graphics);

p1.x = stage.stageWidth / 2;
p1.y = stage.stageHeight / 2;

p2.x = p1.x + length;
p2.y = p1.y;

addChild(p1);
addChild(p2);

p2.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown);
stage.addEventListener(MouseEvent.MOUSE_UP, mouseUp);
stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove);

function mouseDown(event:MouseEvent):void
{
    dragging = true;
}

function mouseUp(event:MouseEvent):void
{
    dragging = false;
}

function mouseMove(event:MouseEvent):void
{
if (dragging)
{
    tx = event.stageX - p1.x;
    ty = event.stageY - p1.y;
    if (tx * tx + ty * ty > length * length)
    {
        p2.x = p1.x + tx / Math.sqrt(tx * tx + ty * ty) * length;
        p2.y = p1.y + ty / Math.sqrt(tx * tx + ty * ty) * length;
    }
    else
    {
        p2.x = event.stageX;
        p2.y = event.stageY;
    }
}
}


Something like this ought to work out:

private function projectLocation(center:point, radius:uint, radian:Number):Point 
    {
        var result:Point = new Point();

        //obtain X
        result.x = center.x + radius * Math.cos(radian));

        //obtain Y
        result.y = center.y + radius * Math.sin(radian));

        return result;
    }

Obviously, modify as needed, but you just need to send in a center point, radius and then a radian (you can obtain with angle * (Math.PI / 180)). You could easily hard code in the first two params if they don't change. What does change is the radian, and that is what you will need to change over time, as your mouse is dragging (defined by the mouseX distance to the center point - positive or negative).

Hopefully that helps get you started -

update

This was how I was working this out - tho its a tad buggy, in that the puck resets to 0 degrees when the sequence starts. That being said, I just saw that - @Nox got this right. I'll post what I was arriving at using my projectLocation function anyways ;)

package com.b99.testBed.knob 
{
    import com.b99.testBed.Main;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.geom.Point;

    /**
     * ...
     * @author bosworth99
     */
    public class Knob extends Sprite
    {

        private var _puck       :Sprite;
        private var _track      :Sprite;
        private const DIAMETER  :uint = 100;
        private const RADIUS    :uint = DIAMETER / 2;

        public function Knob() 
        {
            super();
            init();
        }

        private function init():void
        {
            assembleDisplayObjects();
            addEventHandlers();
        }

        private function assembleDisplayObjects():void
        {
            _track = new Sprite();
            with (_track) 
            {
                graphics.beginFill(0xffffff, 1);
                graphics.lineStyle(1, 0x000000);
                graphics.drawEllipse(-RADIUS, -RADIUS, DIAMETER, DIAMETER);
                graphics.endFill();
            }
            this.addChild(_track);
            _track.x = Main.stage.stageWidth / 2;
            _track.y = Main.stage.stageHeight / 2;

            _puck = new Sprite();
            with (_puck) 
            {
                graphics.beginFill(0x2DFE07, 1);
                graphics.drawEllipse(-8, -8, 16, 16);
                graphics.endFill();
                x   = _track.x;
                y   = _track.y - _track.width / 2;
                buttonMode = true;
            }
            this.addChild(_puck);
        }

        private function addEventHandlers():void
        {
            Main.stage.addEventListener(MouseEvent.MOUSE_DOWN, activate);
            Main.stage.addEventListener(MouseEvent.MOUSE_UP, deactivate);
        }

        private function deactivate(e:MouseEvent):void 
        {
            Main.stage.removeEventListener(MouseEvent.MOUSE_MOVE, update);
        }

        private var _origin:uint;
        private function activate(e:MouseEvent):void 
        {
            Main.stage.addEventListener(MouseEvent.MOUSE_MOVE, update);
            _origin = mouseX;
        }

        private function update(e:MouseEvent):void 
        {
            var distance:Number;
            (mouseX < _origin)? distance = -(_origin - mouseX) : distance = mouseX - _origin;
            if(distance > 40){distance = 40};
            if(distance < -220){distance = -220};

            var angle:Number  = distance;  //modify?
            var radian:Number = angle * (Math.PI / 180);
            var center:Point  = new Point(_track.x, _track.y);
            var loc:Point     = projectLocation(center, RADIUS, radian);

            _puck.x = loc.x;
            _puck.y = loc.y;
        }

        private function projectLocation(center:Point, radius:uint, radian:Number):Point 
        {
            var result:Point = new Point();

            //obtain X
            result.x = center.x + radius * Math.cos(radian);

            //obtain Y
            result.y = center.y + radius * Math.sin(radian);

            return result;
        }

    }

}

Main difference is that I'm obtaining the angle via horizontal (x) movement, and not checking against the cursors angle. Thos, trapping the values manually feels kinda hacky compared to @Nox very good soution. Would of cleaned up had I kept going;)

Nice question - Cheers

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜