'use strict';
let Rac = require('../Rac');
let utils = require('../util/utils');
/**
* Abstract class for controls that select a value within a range.
*
* Controls may use an `anchor` object to determine the visual position of
* the control's interactive elements. Each implementation determines the
* class used for this `anchor`, for example
* `[ArcControl]{@link Rac.ArcControl}` uses an `[Arc]{@link Rac.Arc}` as
* anchor, which defines where the control is drawn, what orientation it
* uses, and the position of the control knob through the range of possible
* values.
*
* A control keeps a `value` property in the range *[0,1]* for the currently
* selected value.
*
* The `projectionStart` and `projectionEnd` properties can be used to
* project `value` into the range `[projectionStart,projectionEnd]` by using
* the `projectedValue()` method. By default set to *[0,1]*.
*
* The `startLimit` and `endLimit` can be used to restrain the allowable
* values that can be selected through user interaction. By default set to
* *[0,1]*.
*
* @alias Rac.Control
*/
class Control {
/**
* Creates a new `Control` instance.
*
* @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
*/
constructor(rac, value) {
utils.assertExists(rac);
utils.assertNumber(value);
/**
* Instance of `Rac` used for drawing and passed along to any created
* object.
*
* @type {Rac}
*/
this.rac = rac;
/**
* Current selected value, in the range *[0,1]*.
*
* May be further constrained to `[startLimit,endLimit]`.
*
* @type {number}
*/
this.value = value;
/**
* [Projected value]{@link Rac.Control#projectedValue} to use when
* `value` is `0`.
*
* @type {number}
* @default 0
*/
this.projectionStart = 0;
/**
* [Projected value]{@link Rac.Control#projectedValue} to use when
* `value` is `1`.
*
* @type {number}
* @default 1
*/
this.projectionEnd = 1;
/**
* Minimum `value` that can be selected through user interaction.
*
* @type {number}
* @default 0
*/
this.startLimit = 0;
/**
* Maximum `value` that can be selected through user interaction.
*
* @type {number}
* @default 1
*/
this.endLimit = 1;
/**
* Collection of values at which visual markers are drawn.
*
* @type {number[]}
* @default []
*/
this.markers = [];
/**
* Style to apply when drawing. This style gets applied after
* `[rac.controller.controlStyle]{@link Rac.Controller#controlStyle}`.
*
* @type {?Rac.Stroke|Rac.Fill|Rac.StyleContainer} [style=null]
* @default null
*/
this.style = null;
}
/**
* Returns `value` projected into the range
* `[projectionStart,projectionEnd]`.
*
* By default the projection range is *[0,1]*, in which case `value` and
* `projectedValue()` are equal.
*
* Projection ranges with a negative direction (E.g. *[50,30]*, when
* `projectionStart` is greater that `projectionEnd`) are supported. As
* `value` increases, the projection returned decreases from
* `projectionStart` until reaching `projectionEnd`.
*
* > E.g.
* > ```
* > For a control with a projection range of [100,200]
* > + when value is 0, projectionValue() is 100
* > + when value is 0.5, projectionValue() is 150
* > + when value is 1, projectionValue() is 200
* >
* > For a control with a projection range of [50,30]
* > + when value is 0, projectionValue() is 50
* > + when value is 0.5, projectionValue() is 40
* > + when value is 1, projectionValue() is 30
* > ```
*
* @returns {number}
*/
projectedValue() {
let projectionRange = this.projectionEnd - this.projectionStart;
return (this.value * projectionRange) + this.projectionStart;
}
// TODO: reintroduce when tested
// Returns the corresponding value in the range *[0,1]* for the
// `projectedValue` in the range `[projectionStart,projectionEnd]`.
// valueOfProjected(projectedValue) {
// let projectionRange = this.projectionEnd - this.projectionStart;
// return (projectedValue - this.projectionStart) / projectionRange;
// }
/**
* Sets both `startLimit` and `endLimit` with the given insets from `0`
* and `1`, correspondingly.
*
* > E.g.
* > ```
* > control.setLimitsWithInsets(0.1, 0.2)
* > // sets startLimit as 0.1
* > // sets endLimit as 0.8
* > ```
*
* @param {number} startInset - The inset from `0` to use for `startLimit`
* @param {number} endInset - The inset from `1` to use for `endLimit`
*/
setLimitsWithInsets(startInset, endInset) {
this.startLimit = startInset;
this.endLimit = 1 - endInset;
}
// TODO: reintroduce when tested
// Sets `startLimit` and `endLimit` with two inset values relative to the
// [0,1] range.
// setLimitsWithProjectionInsets(startInset, endInset) {
// this.startLimit = this.valueOf(startInset);
// this.endLimit = this.valueOf(1 - endInset);
// }
/**
* Adds a marker at the current `value`.
*/
addMarkerAtCurrentValue() {
this.markers.push(this.value);
}
/**
* Returns `true` when this control is the currently selected control.
*
* @returns {boolean}
*/
isSelected() {
if (this.rac.controller.selection === null) {
return false;
}
return this.rac.controller.selection.control === this;
}
/**
* Returns a `Point` at the center of the control knob.
*
* > ⚠️ This method must be overriden by an extending class. Calling this
* > implementation throws an error.
*
* @abstract
* @return {Rac.Point}
*/
knob() {
throw Rac.Exception.abstractFunctionCalled(
`this-type:${utils.typeName(this)}`);
}
/**
* Returns a copy of the anchor to be persited during user interaction.
*
* Each implementation determines the type used for `anchor` and
* `affixAnchor()`.
*
* This fixed anchor is passed back to the control through
* `[updateWithPointer]{@link Rac.Control#updateWithPointer}` and
* `[drawSelection]{@link Rac.Control#drawSelection}` during user
* interaction.
*
* > ⚠️ This method must be overriden by an extending class. Calling this
* > implementation throws an error.
*
* @abstract
* @return {object}
*/
affixAnchor() {
throw Rac.Exception.abstractFunctionCalled(
`this-type:${utils.typeName(this)}`);
}
/**
* Draws the current state.
*
* > ⚠️ This method must be overriden by an extending class. Calling this
* > implementation throws an error.
*
* @abstract
*/
draw() {
throw Rac.Exception.abstractFunctionCalled(
`this-type:${utils.typeName(this)}`);
}
/**
* Updates `value` using `pointerKnobCenter` in relation to `fixedAnchor`.
* Called by `[rac.controller.pointerDragged]{@link Rac.Controller#pointerDragged}`
* as the user interacts with the control.
*
* Each implementation interprets `pointerKnobCenter` against `fixedAnchor`
* to update its own value. The current `anchor` is not used for this
* update since `anchor` could change during redraw in response to updates
* in `value`.
*
* Each implementation is also responsible of keeping the updated `value`
* within the range `[startLimit,endLimit]`. This method is the only path
* for updating the control through user interaction, and thus the only
* place where each implementation must enforce a valid `value` within
* *[0,1]* and `[startLimit,endLimit]`.
*
* > ⚠️ This method must be overriden by an extending class. Calling this
* > implementation throws an error.
*
* @abstract
* @param {Rac.Point} pointerKnobCenter - The position of the knob center
* as interacted by the user pointer
* @param {object} fixedAnchor - Anchor produced when user interaction
* started
*/
updateWithPointer(pointerKnobCenter, fixedAnchor) {
throw Rac.Exception.abstractFunctionCalled(
`this-type:${utils.typeName(this)}`);
}
/**
* Draws the selection state along with pointer interaction visuals.
* Called by `[rac.controller.drawControls]{@link Rac.Controller#drawControls}`
* only for the selected control.
*
* > ⚠️ This method must be overriden by an extending class. Calling this
* > implementation throws an error.
*
* @abstract
* @param {Rac.Point} pointerCenter - The position of the user pointer
* @param {object} fixedAnchor - Anchor of the control produced 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) {
throw Rac.Exception.abstractFunctionCalled(
`this-type:${utils.typeName(this)}`);
}
} // class Control
module.exports = Control;
// Controls shared drawing elements
Control.makeArrowShape = function(rac, center, angle) {
// Arc
let angleDistance = rac.Angle.from(1/22);
let arc = center.arc(rac.controller.knobRadius * 1.5,
angle.subtract(angleDistance), angle.add(angleDistance));
// Arrow walls
let pointAngle = rac.Angle.from(1/8);
let rightWall = arc.startPoint().ray(angle.add(pointAngle));
let leftWall = arc.endPoint().ray(angle.subtract(pointAngle));
// Arrow point
let point = rightWall.pointAtIntersection(leftWall);
// Shape
rac.pushShape();
point.segmentToPoint(arc.startPoint())
.attachToShape();
arc.attachToShape();
arc.endPoint().segmentToPoint(point)
.attachToShape();
return rac.popShape();
};
Control.makeLimitMarker = function(rac, point, angle) {
angle = rac.Angle.from(angle);
let perpendicular = angle.perpendicular(false);
let composite = new Rac.Composite(rac);
point.segmentToAngle(perpendicular, 4)
.withStartExtension(4)
.attachTo(composite);
point.pointToAngle(perpendicular, 8).arc(3)
.attachTo(composite);
return composite;
};
Control.makeValueMarker = function(rac, point, angle) {
angle = rac.Angle.from(angle);
return point.segmentToAngle(angle.perpendicular(), 3)
.withStartExtension(3);
};