'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 segment of a `Ray`.
*
* Uses a `Ray` as `[anchor]{@link Rac.RayControl#anchor}`, which defines
* the position where the control is drawn.
*
* `[length]{@link Rac.RayControl#length}` defines the length of the
* segment in the `anchor` ray which is available for user interaction.
* Within this segment the user can slide the control knob to select a
* value.
*
* @alias Rac.RayControl
* @extends Rac.Control
*/
class RayControl extends Rac.Control {
/**
* Creates a new `RayControl` instance with the starting `value` and the
* interactive `length`.
*
* @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 {number} length - The length of the `anchor` ray available for
* user interaction
*/
constructor(rac, value, length) {
utils.assertExists(rac, value, length);
utils.assertNumber(value, length);
super(rac, value);
/**
* Length of the `anchor` ray available for user interaction.
* @type {number}
*/
this.length = length;
/**
* `Ray` to which the control will be anchored. Defines the location
* where the control is drawn.
*
* Along with `[length]{@link Rac.RayControl#length}` defines the
* segment available for user interaction.
*
* The control cannot be drawn or selected until this property is set.
*
* @type {Rac.Ray?}
* @default null
*/
this.anchor = null;
if (rac.controller.autoAddControls) {
rac.controller.add(this);
}
}
/**
* Sets `value` using the projection of `lengthValue` in the `[0,length]`
* range.
*
* @param {number} lengthValue - The length at which to set the current
* value
*/
setValueWithLength(lengthValue) {
let lengthRatio = lengthValue / this.length;
this.value = lengthRatio;
}
/**
* Sets both `startLimit` and `endLimit` with the given insets from `0`
* and `length`, correspondingly, both projected in the `[0,length]`
* range.
*
* > E.g.
* > ```
* > // For a RayControl with length of 100
* > control.setLimitsWithLengthInsets(10, 20)
* > // sets startLimit as 0.1 which is at length 10
* > // sets endLimit as 0.8 which is at length 80 from 100
* > // 10 inset from 0 = 10
* > // 20 inset from 100 = 80
* > ```
*
* @param {number} startInset - The inset from `0` in the range
* `[0,length]` to use for `startLimit`
* @param {number} endInset - The inset from `length` in the range
* `[0,length]` to use for `endLimit`
*/
setLimitsWithLengthInsets(startInset, endInset) {
this.startLimit = startInset / this.length;
this.endLimit = (this.length - endInset) / this.length;
}
/**
* Returns the distance between the `anchor` ray `start` and the control
* knob.
*
* Equivalent to the control `value` projected to the range `[0,length]`.
*
* @returns {number}
*/
distance() {
return this.length * 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 the knob
return null;
}
return this.anchor.pointAtDistance(this.distance());
}
/**
* Returns a new `Segment` produced with the `anchor` ray with `length`,
* to be persisted during user interaction.
*
* An error is thrown if `anchor` is not set.
*
* @returns {Rac.Segment}
*/
affixAnchor() {
if (this.anchor === null) {
throw Rac.Exception.invalidObjectConfiguration(
`Expected anchor to be set, null found instead`);
}
return this.anchor.segment(this.length);
}
/**
* 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;
fixedAnchor.draw(controlStyle);
let knob = this.knob();
let angle = fixedAnchor.angle();
this.rac.pushComposite();
// Value markers
this.markers.forEach(item => {
if (item < 0 || item > 1) { return }
let point = fixedAnchor.startPoint().pointToAngle(angle, this.length * item);
Rac.Control.makeValueMarker(this.rac, point, angle)
.attachToComposite();
}, this);
// Control knob
knob.arc(this.rac.controller.knobRadius)
.attachToComposite();
// Negative arrow
if (this.value >= this.startLimit + this.rac.unitaryEqualityThreshold) {
Rac.Control.makeArrowShape(this.rac, knob, angle.inverse())
.attachToComposite();
}
// Positive arrow
if (this.value <= this.endLimit - this.rac.unitaryEqualityThreshold) {
Rac.Control.makeArrowShape(this.rac, knob, angle)
.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.Segment} fixedAnchor - `Segment` produced with `affixAnchor`
* when user interaction started
*/
updateWithPointer(pointerKnobCenter, fixedAnchor) {
let length = fixedAnchor.length;
let startInset = length * this.startLimit;
let endInset = length * (1 - this.endLimit);
// New value from the current pointer position, relative to fixedAnchor
let newDistance = fixedAnchor
.ray.distanceToProjectedPoint(pointerKnobCenter);
// Clamping value (javascript has no Math.clamp)
newDistance = fixedAnchor.clampToLength(newDistance,
startInset, endInset);
// Update control with new distance
let lengthRatio = newDistance / length;
this.value = lengthRatio;
}
/**
* Draws the selection state along with pointer interaction visuals.
*
* @param {Rac.Point} pointerCenter - The position of the user pointer
* @param {Rac.Segment} fixedAnchor - `Segment` 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; }
this.rac.pushComposite();
fixedAnchor.attachToComposite();
let angle = fixedAnchor.angle();
let length = fixedAnchor.length;
// Value markers
this.markers.forEach(item => {
if (item < 0 || item > 1) { return }
let markerPoint = fixedAnchor.startPoint().pointToAngle(angle, length * item);
Rac.Control.makeValueMarker(this.rac, markerPoint, angle)
.attachToComposite();
});
// Limit markers
if (this.startLimit > 0) {
let minPoint = fixedAnchor.startPoint().pointToAngle(angle, length * this.startLimit);
Rac.Control.makeLimitMarker(this.rac, minPoint, angle)
.attachToComposite();
}
if (this.endLimit < 1) {
let maxPoint = fixedAnchor.startPoint().pointToAngle(angle, length * this.endLimit);
Rac.Control.makeLimitMarker(this.rac, maxPoint, angle.inverse())
.attachToComposite();
}
// Segment from pointer to control dragged center
let draggedCenter = pointerToKnobOffset
.withStartPoint(pointerCenter)
.endPoint();
// Control dragged center, attached to pointer
draggedCenter.arc(2)
.attachToComposite();
// Constrained length clamped to limits
let constrainedLength = fixedAnchor
.ray.distanceToProjectedPoint(draggedCenter);
let startInset = length * this.startLimit;
let endInset = length * (1 - this.endLimit);
constrainedLength = fixedAnchor.clampToLength(constrainedLength,
startInset, endInset);
let constrainedAnchorCenter = fixedAnchor
.withLength(constrainedLength)
.endPoint();
// Control center constrained to anchor
constrainedAnchorCenter.arc(this.rac.controller.knobRadius)
.attachToComposite();
// Dragged shadow center, semi attached to pointer
// always perpendicular to anchor
let draggedShadowCenter = draggedCenter
.segmentToProjectionInRay(fixedAnchor.ray)
// reverse and translated to constraint to anchor
.reverse()
.withStartPoint(constrainedAnchorCenter)
// Segment from constrained center to shadow center
.attachToComposite()
.endPoint();
// Control shadow center
draggedShadowCenter.arc(this.rac.controller.knobRadius / 2)
.attachToComposite();
// Ease for segment to dragged shadow center
let easeOut = Rac.EaseFunction.makeEaseOut();
easeOut.postBehavior = Rac.EaseFunction.Behavior.clamp;
// Tail will stop stretching at 2x the max tail length
let maxDraggedTailLength = this.rac.controller.knobRadius * 5;
easeOut.inRange = maxDraggedTailLength * 2;
easeOut.outRange = maxDraggedTailLength;
// Segment to dragged shadow center
let draggedTail = draggedShadowCenter
.segmentToPoint(draggedCenter);
let easedLength = easeOut.easeValue(draggedTail.length);
draggedTail.withLength(easedLength).attachToComposite();
// Draw all!
this.rac.popComposite().draw(pointerStyle);
}
} // class RayControl
module.exports = RayControl;