Knob AS3 drag on circle or ellipse

| 3 min read

I needed some knobs for a debug interface. I share this very simple class to use so you don't have to rebuild it from scratch, it should be pretty flexible.

preview

Click here to see the demo.

Download source.

The knob can be used as circle or ellipse, here is how to create one:

var knob:Knob = new Knob();
knob.addEventListener(Event.CHANGE, changeHandler);
addChild(knob);

function changeHandler(event:Event):void {
var knob:Knob = event.currentTarget as Knob;
trace(knob.value);
trace(knob.valuePercent);
}

Set the radius, a custom handler and a custom graphic:

var customHandler:Sprite = new Sprite();
customHandler.graphics.beginFill(0xFF0000);
customHandler.graphics.drawRect(-10, -10, 20, 20);
customHandler.graphics.endFill();
var knob:Knob = = new Knob(100, 50, customHandler);
knob.stylePathColor = 0x00FF00;
knob.stylePathThickness = 3;
knob.stylePathAlpha = 0.3;

That's pretty much it, here is the class:

package {

import flash.display.DisplayObject;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;

/**
* @author Romuald Quantin
*/

public class Knob extends Sprite {

private var _radiusX:Number;
private var _radiusY:Number;
private var _handler:DisplayObject;
private var _sector:Sector;

private var _stylePathColor:uint = 0xCCCCCC;
private var _stylePathAlpha:Number = 1;
private var _stylePathThickness:Number = 0;

public function Knob(radiusX:Number = 100, radiusY:Number = 100, handler:DisplayObject = null) {
setRadius(radiusX, radiusY);
setHandler(handler);
updatePositionFromDegree(-90);
}

private function setRadius(radiusX:Number, radiusY:Number):void {
_radiusX = radiusX;
_radiusY = radiusY;
if (!_sector) _sector = new Sector(0, _radiusX, _radiusY);
draw();
updatePosition();
}

private function setHandler(value:DisplayObject):void {
disposeDragabble();
_handler = (!value) ? createNewHandler() : value;
setHandlerListeners();
addChild(_handler);
updatePosition();
}

private function setHandlerListeners():void {
_handler.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
}

private function removeHandlerListeners():void {
_handler.removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
stage.removeEventListener(Event.MOUSE_LEAVE, mouseLeaveHandler);
}

private function mouseLeaveHandler(event:Event):void {
stopDragging();
}

private function mouseDownHandler(event:MouseEvent):void {
startDragging();
}

private function mouseUpHandler(event:MouseEvent):void {
stopDragging();
}

private function startDragging():void {
stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
stage.removeEventListener(Event.MOUSE_LEAVE, mouseLeaveHandler);
stage.addEventListener(Event.MOUSE_LEAVE, mouseLeaveHandler);
}

private function stopDragging():void {
stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
stage.removeEventListener(Event.MOUSE_LEAVE, mouseLeaveHandler);
}

private function mouseMoveHandler(event:MouseEvent):void {
updatePosition();
}

private function updatePosition():void {
var ratio:Number = _radiusX / _radiusY;
var angle:Number = Math.atan2(mouseX, mouseY);
updatePositionFromDegree(90 - (Math.atan2(Math.sin(angle), Math.cos(angle) * ratio)) * (180 / Math.PI));
dispatchEvent(new Event(Event.CHANGE));
}

private function updatePositionFromDegree(value:Number):void {
_sector.degree = value;
_sector.radiusX = _radiusX;
_sector.radiusY = _radiusY;
if (_handler) {
_handler.x = _sector.x;
_handler.y = _sector.y;
}
}

private function createNewHandler() : DisplayObject {
var circle:Sprite = new Sprite();
circle.graphics.beginFill(0x434F6C);
circle.graphics.drawCircle(0, 0, 5);
return circle;
}

private function draw():void {
graphics.clear();
graphics.lineStyle(_stylePathThickness, _stylePathColor, _stylePathAlpha);
graphics.moveTo(_sector.x, _sector.y);
var i:Number = 0;
var l:Number = 360;
for (; i <= l; ++i) {
_sector.degree = i;
_sector.radiusX = _radiusX;
_sector.radiusY = _radiusY;
graphics.lineTo(_sector.x, _sector.y);
}
graphics.endFill();
}

public function get radiusX():Number {
return _radiusX;
}

public function set radiusX(value:Number):void {
setRadius(value, _radiusY);
}

public function get radiusY():Number {
return _radiusY;
}

public function set radiusY(value:Number):void {
setRadius(_radiusX, value);
}

public function get handler() : DisplayObject {
return _handler;
}

public function set handler(value:DisplayObject):void {
setHandler(value);
}

public function get value():Number {
return _sector.degree + 90;
}

public function set value(value:Number):void {
updatePositionFromDegree(value - 90);
}

public function get valuePercent():Number {
return value / 360;
}

public function set valuePercent(value:Number):void {
this.value = 360 * Math.min(1, Math.max(0, value));
}

public function disposeDragabble():void {
if (!_handler) return;
removeHandlerListeners();
if (_handler.hasOwnProperty("dispose")) {
_handler['dispose']();
}
removeChild(_handler);
_handler = null;
}

public function dispose():void {
disposeDragabble();
_sector = null;
}

public function get stylePathColor():uint {
return _stylePathColor;
}

public function set stylePathColor(value:uint):void {
_stylePathColor = value;
draw();
}

public function get stylePathAlpha():Number {
return _stylePathAlpha;
}

public function set stylePathAlpha(value:Number):void {
_stylePathAlpha = value;
draw();
}

public function get stylePathThickness():Number {
return _stylePathThickness;
}

public function set stylePathThickness(value:Number):void {
_stylePathThickness = value;
draw();
}
}
}

class Sector {

public var degree:Number;
public var radiusX:Number;
public var radiusY:Number;

public function Sector(degree:Number = 0, radiusX:Number = 10, radiusY:Number = 10) {
this.degree = degree;
this.radiusX = radiusX;
this.radiusY = radiusY;
}

public function get x():Number {
return radiusX * Math.cos(degree * Math.PI / 180);
}

public function get y():Number {
return radiusY * Math.sin(degree * Math.PI / 180);
}

public function toString():String {
return "[Sector] degree: " + degree + ", radiusX: " + radiusX + ", radiusY: " + radiusY + ", x: " + x + ", y: " + y;
}
}