'use strict';
let Rac = require('../Rac');
let utils = require('../util/utils');
/**
* Control that allows the selection of a value with a knob that slides
* through the section of an `Arc`.
*
* Uses an `Arc` as `[anchor]{@link Rac.ArcControl#anchor}`, which defines
* the position where the control is drawn.
*
* `[angleDistance]{@link Rac.ArcControl#angleDistance}` defines the
* section of the `anchor` arc which is available for user interaction.
* Within this section the user can slide the control knob to select a
* value.
*
* @alias Rac.ArcControl
* @extends Rac.Control
*/
class ArcControl extends Rac.Control {
/**
* Creates a new `ArcControl` instance with the starting `value` and the
* interactive `angleDistance`.
*
* @param {Rac} rac - Instance to use for drawing and creating other objects
* @param {Number} value - The initial value of the control, in the
* *[0,1]* range
* @param {Rac.Angle} angleDistance - The angleDistance of the `anchor`
* arc available for user interaction
*/
constructor(rac, value, angleDistance) {
utils.assertExists(rac);
utils.assertNumber(value);
utils.assertType(Rac.Angle, angleDistance);
super(rac, value);
/**
* Angle distance of the `anchor` arc available for user interaction.
* @type {Number}
*/
this.angleDistance = Rac.Angle.from(rac, angleDistance);
/**
* `Arc` to which the control will be anchored. Defines the location
* where the control is drawn.
*
* Along with `[angleDistance]{@link Rac.ArcControl#angleDistance}`
* defines the section available for user interaction.
*
* The control cannot be drawn or selected until this property is set.
*
* @type {?Rac.Arc}
* @default null
*/
this.anchor = null;
if (rac.controller.autoAddControls) {
rac.controller.add(this);
}
}
/**
* Sets `value` using the projection of `valueAngleDistance.turn` in the
* `[0,angleLength.turn]` range.
*
* @param {Rac.Angle|Number} valueAngleDistance - The angle distance at
* which to set the current value
*/
setValueWithAngleDistance(valueAngleDistance) {
valueAngleDistance = Rac.Angle.from(this.rac, valueAngleDistance)
let distanceRatio = valueAngleDistance.turn / this.angleDistance.turnOne();
this.value = distanceRatio;
}
// TODO: this example/code may not be working or be innacurrate
// check RayControl:setLimitsWithLengthInsets for a better example
/**
* Sets both `startLimit` and `endLimit` with the given insets from `0`
* and `angleDistance.turn`, correspondingly, both projected in the
* `[0, angleDistance.turn]` range.
*
* @example
* <caption>For an ArcControl with angleDistance of 0.5 turn</caption>
* let control = new Rac.ArcControl(rac, 0, rac.Angle(0.5))
* // sets startLimit as 0.1, since 0 + 0.2 * 0.5 = 0.1
* // sets endLimit as 0.3, since 0.5 - 0.4 * 0.5 = 0.3
* control.setLimitsWithAngleDistanceInsets(0.2, 0.4)
*
* @param {Rac.Angle|Number} startInset - The inset from `0` in the range
* `[0,angleDistance.turn]` to use for `startLimit`
* @param {Rac.Angle|Number} endInset - The inset from `angleDistance.turn`
* in the range `[0,angleDistance.turn]` to use for `endLimit`
*/
setLimitsWithAngleDistanceInsets(startInset, endInset) {
startInset = Rac.Angle.from(this.rac, startInset);
endInset = Rac.Angle.from(this.rac, endInset);
this.startLimit = startInset.turn / this.angleDistance.turnOne();
this.endLimit = (this.angleDistance.turnOne() - endInset.turn) / this.angleDistance.turnOne();
}
/**
* Returns the [angle `distance`]{@link Rac.Angle#distance} between the
* `anchor` arc `start` and the control knob.
*
* The `turn` of the returned `Angle` is equivalent to the control `value`
* projected to the range `[0,angleDistance.turn]`.
*
* @returns {Rac.Angle}
*/
distance() {
return this.angleDistance.multOne(this.value);
}
/**
* Returns a `Point` at the center of the control knob.
*
* When `anchor` is not set, returns `null` instead.
*
* @return {?Rac.Point}
*/
knob() {
if (this.anchor === null) {
// Not posible to calculate knob
return null;
}
return this.anchor.pointAtAngleDistance(this.distance());
}
/**
* Returns a new `Arc` produced with the `anchor` arc with
* `angleDistance`, to be persisted during user interaction.
*
* An error is thrown if `anchor` is not set.
*
* @returns {Rac.Arc}
*/
affixAnchor() {
if (this.anchor === null) {
throw Rac.Exception.invalidObjectConfiguration(
`Expected anchor to be set, null found instead`);
}
return this.anchor.withAngleDistance(this.angleDistance);
}
/**
* Draws the current state.
*/
draw() {
if (this.anchor === null) {
// Unable to draw without anchor
return;
}
let fixedAnchor = this.affixAnchor();
let controllerStyle = this.rac.controller.controlStyle;
let controlStyle = controllerStyle !== null
? controllerStyle.appendStyle(this.style)
: this.style;
// Arc anchor is always drawn without fill
let anchorStyle = controlStyle !== null
? controlStyle.appendStyle(this.rac.Fill.none)
: this.rac.Fill.none;
fixedAnchor.draw(anchorStyle);
let knob = this.knob();
let angle = fixedAnchor.center.angleToPoint(knob);
this.rac.pushComposite();
// Value markers
this.markers.forEach(item => {
if (item < 0 || item > 1) { return }
let markerAngleDistance = this.angleDistance.multOne(item);
let markerAngle = fixedAnchor.shiftAngle(markerAngleDistance);
let point = fixedAnchor.pointAtAngle(markerAngle);
Rac.Control.makeValueMarker(this.rac, point, markerAngle.perpendicular(!fixedAnchor.clockwise))
.attachToComposite();
}, this);
// Control knob
knob.arc(this.rac.controller.knobRadius)
.attachToComposite();
let isCircleControl = this.angleDistance.equals(this.rac.Angle.zero)
&& this.startLimit == 0
&& this.endLimit == 1
let hasNegativeRange = isCircleControl
|| this.value >= this.startLimit + this.rac.unitaryEqualityThreshold
let hasPositiveRange = isCircleControl
|| this.value <= this.endLimit - this.rac.unitaryEqualityThreshold
// Negative arrow
if (hasNegativeRange) {
let negAngle = angle.perpendicular(fixedAnchor.clockwise).inverse();
Rac.Control.makeArrowShape(this.rac, knob, negAngle)
.attachToComposite();
}
// Positive arrow
if (hasPositiveRange) {
let posAngle = angle.perpendicular(fixedAnchor.clockwise);
Rac.Control.makeArrowShape(this.rac, knob, posAngle)
.attachToComposite();
}
this.rac.popComposite().draw(controlStyle);
// Selection
if (this.isSelected()) {
let pointerStyle = this.rac.controller.pointerStyle;
if (pointerStyle !== null) {
knob.arc(this.rac.controller.knobRadius * 1.5).draw(pointerStyle);
}
}
}
/**
* Updates `value` using `pointerKnobCenter` in relation to `fixedAnchor`.
*
* `value` is always updated by this method to be within *[0,1]* and
* `[startLimit,endLimit]`.
*
* @param {Rac.Point} pointerKnobCenter - The position of the knob center
* as interacted by the user pointer
* @param {Rac.Arc} fixedAnchor - Anchor produced with `affixAnchor` when
* user interaction started
*/
updateWithPointer(pointerKnobCenter, fixedAnchor) {
let angleDistance = fixedAnchor.angleDistance();
let startInset = angleDistance.multOne(this.startLimit);
let endInset = angleDistance.multOne(1 - this.endLimit);
let selectionAngle = fixedAnchor.center
.angleToPoint(pointerKnobCenter);
selectionAngle = fixedAnchor.clampToAngles(selectionAngle,
startInset, endInset);
let newDistance = fixedAnchor.distanceFromStart(selectionAngle);
// Update control with new distance
let distanceRatio = newDistance.turn / this.angleDistance.turnOne();
this.value = distanceRatio;
}
/**
* Draws the selection state along with pointer interaction visuals.
*
* @param {Rac.Point} pointerCenter - The position of the user pointer
* @param {Rac.Arc} fixedAnchor - `Arc` produced with `affixAnchor` when
* user interaction started
* @param {Rac.Segment} pointerToKnobOffset - A `Segment` that represents
* the offset from `pointerCenter` to the control knob when user
* interaction started.
*/
drawSelection(pointerCenter, fixedAnchor, pointerToKnobOffset) {
let pointerStyle = this.rac.controller.pointerStyle;
if (pointerStyle === null) { return; }
// Arc anchor is always drawn without fill
let anchorStyle = pointerStyle.appendStyle(this.rac.Fill.none);
fixedAnchor.draw(anchorStyle);
let angleDistance = fixedAnchor.angleDistance();
this.rac.pushComposite();
// Value markers
this.markers.forEach(item => {
if (item < 0 || item > 1) { return }
let markerAngle = fixedAnchor.shiftAngle(angleDistance.multOne(item));
let markerPoint = fixedAnchor.pointAtAngle(markerAngle);
Rac.Control.makeValueMarker(this.rac, markerPoint, markerAngle.perpendicular(!fixedAnchor.clockwise))
.attachToComposite();
});
// Limit markers
if (this.startLimit > 0) {
let minAngle = fixedAnchor.shiftAngle(angleDistance.multOne(this.startLimit));
let minPoint = fixedAnchor.pointAtAngle(minAngle);
let markerAngle = minAngle.perpendicular(fixedAnchor.clockwise);
Rac.Control.makeLimitMarker(this.rac, minPoint, markerAngle)
.attachToComposite();
}
if (this.endLimit < 1) {
let maxAngle = fixedAnchor.shiftAngle(angleDistance.multOne(this.endLimit));
let maxPoint = fixedAnchor.pointAtAngle(maxAngle);
let markerAngle = maxAngle.perpendicular(!fixedAnchor.clockwise);
Rac.Control.makeLimitMarker(this.rac, maxPoint, markerAngle)
.attachToComposite();
}
// Segment from pointer to control dragged center
let draggedCenter = pointerToKnobOffset
.withStartPoint(pointerCenter)
.endPoint();
// Control dragged center, attached to pointer
draggedCenter.arc(2)
.attachToComposite();
this.rac.popComposite().draw(pointerStyle);
// TODO: implement arc control dragging visuals!
}
} // class ArcControl
module.exports = ArcControl;