Parse
File Parse point.js
This runs the server-side parser and regenerates the documentation tree for this source file.
Source
/*
files:
relative-xy.js
pointcast.js
categories: primary
point
doc_readme: point/readme.md
doc_content: point/*.md
doc_imports: point
doc_loader: loadDocInfo
*/
const isPoint = function(value) {
return value.constructor == Point
}
const isFunction = function(value) {
return (typeof(value) == 'function')
}
const point = function(p, b) {
if(p.constructor == Point) {
return p
}
if(Array.isArray(p)) {
return new Point({x: p[0], y:p[1]})
}
if(b !== undefined) {
return new Point({x: p, y: b})
}
return p
}
function getVector(radians, multiplier=1) {
/* Return the 2D XY vector of this point using `this.radians`.
point.getVector()
{ x: 1.2, y: 0.3 }
Provide a multuplier, for example to create an incident angle:
point.getVector(-1)
{ x: -1.2, y: -0.3 }
*/
return {
// Step 1: Convert the incident angle (from radians) to a vector
x: multiplier * Math.cos(radians)
, y: multiplier * Math.sin(radians)
}
}
window.loadDocInfo = function() {
/* Return the think to resolve */
console.log('loadDocInfo called')
return Point;
}
class Positionable extends Relative {
/* The base `Positionable` class provides functionality for
plotting the X and Y of an entity. This includes any fundamental
methods such as `multiply()`.
*/
/* ABOVE X method
Multiline - touching.
*/
set x(value) {
/* Set the _X_ (horizontal | latitude | across) value of the positionable.
From cooridinate top left `(0,0)``
point.x = 100
If the given value is a function, the function is called immediately,
with _this_ positionable as the first argument.
stage.center.x = (p)=>200
This property is known as a _special_ prop, and will actuate the
`setSpecial` method and dirty flag.
*/
// this._opts.x = isFunction(v)? v(this, 'x'): v
return this.setSpecial('x', value)
}
set y(value) {
/* Set the _Y_ (vertical | height | longtitude) value of the positionable.
From cooridinate top left `(0,0)``
point.x = 100
If the given value is a function, the function is called immediately,
with _this_ positionable as the first argument.
stage.center.x = (p)=>200
This property is known as a _special_ prop, and will actuate the
`setSpecial` method and dirty flag.
*/
// this._opts.y = isFunction(v)? v(this, 'y'): v
return this.setSpecial('y', value)
}
get x() {
// const _x = this._opts.x;
// let r = _x == undefined? 0: _x
// let v = this._relativeData[0]
// r = isFunction(r)? r(this, 'x'): r
// return r + v
return this.getSpecial('x', 0)
}
get y() {
// const _y = this._opts.y;
// let r = _y == undefined? 0: _y
// r = isFunction(r)? r(this, 'y'): r
// return r + this._relativeData[1]
return this.getSpecial('y', 1)
}
set radius(v) {
// this._opts.radius = isFunction(v)? v(this, 'radius'): v
return this.setSpecial('radius', v)
}
get radius() {
/*
Return the _radius_ of this point in base units (pixels)
const point = new Point(100,300, 20)
point.radius == 20
*/
// const _radius = this._opts.radius
// let r = _radius == undefined? 0: _radius;
// r = isFunction(r)? r(this, 'radius'): r;
// return r + this._relativeData[2]
return this.getSpecial('radius', 2, 5)
}
setSpecial(key, value) {
/* Set the given `key`, `value`, assuming the given key is a "special"
string, such as "radius".
This method is called by the special getters and setters for this point:
const point = new Point(100,300, 20)
point.setSpecial('radius', 55)
Synonymous to:
point.radius = 55
*/
let rV = isFunction(value)? value(this, key): value
const v = this.onSpecialSet(key, rV)
this._opts[key] = v === undefined? rV: v
return true
}
onSpecialSet(key, value) {
/* A callback executed by setSpecial when a "special" property is called.
If a method on this point exists matching the _set method_ pattern,
the method is called with the given `value`:
class MyPoint extends Point {
radiusSet(value) {
console.log('New Radius value is', value)
}
}
*/
this._dirty = true
let name = `${key}Set`
let r = this[name] && this[name](value)
this._onDirty?.forEach(f=>f(name, r))
// console.log(name)
return r
}
onDirty(func) {
/* Assign a function to call when a special value on this node is
changed.
let p = new Point(10, 10)
p.onDirty((key) => {
console.log('set', key)
})
p.y = 100
// set y
This provides a more _reactive_ method of dirty test, as this is called
immediately after the special key is changed.
Returns nothing.
*/
if(!this._onDirty) { this._onDirty = [] }
this._onDirty.push(func)
}
get dirty() {
/* Return the _is_ `dirty` value. */
return this._dirty
}
set dirty(v) {
/* Typically we should allow dirty to be auto managed,
however it can be overridden with custom functionality.
At the moment we simply accept and store the given value.
This may affect the running code, as dirty is reflagged by the
internal point. In the future turning off auto dirty may be an option..*/
this._dirty = v;
}
get wasDirty() {
/* Return the _is_ `dirty` value, but also remove the dirty flag.
This is useful for dirty tests within the active render loop:
if(myPoint.wasDirty) {
// will call.
console.log('Dirty')
}
myPoint.dirty
// false
*/
let r = this._dirty
this._dirty = false;
return r
}
getSpecial(key, relIndex=undefined, defaultValue=0) {
/* Return a stored _special_ value given a `key`.
If the `relIndex` property is not None, the relative value found
at the index (within this points relative data Array),
add the stashed relative value to the result.
const point = new Point(100,300, 20)
point.getSpecial('radius', 2)
// 20
if a default value is given, and the internal `key` value does not exist,
return the default value:
point.getSpecial('banana', 'yellow')
'yellow'
*/
const internalValue = this._opts[key];
let r = internalValue == undefined? defaultValue: internalValue
r = isFunction(r)? r(this, key): r
let relVal = relIndex != undefined? this.getRelativeData()[relIndex]: 0
return r + relVal
}
set(x, y, radius, rotation) {
/*
set(x, y, radius, rotation)
set(100) => x,y
set([]) => [x,y,radius, rotation]
set({}) => *
*/
const isUndefined = function(v) {
return v === undefined
}
const ifUndefined = (v, d) => v === undefined? d: v
if(isUndefined(y)) {
if(Array.isArray(x)) {
this.fromArray
let lmap = {
1: () => {
/* An array of one
set([200])
*/
}
, 2: ()=> {
[x,y] = x
}
, 3: ()=> {
[x,y, radius] = x
}
, 4: ()=> {
[x,y, radius, rotation] = x
}
}
lmap[x.length]()
} else if(typeof(x)=='number') {
y = x
x = x
}else{
// object
for(let k in x) {
this[k] = x[k]
}
y = x?.y
x = x?.x
}
}
/* XY Are absolutely mandatory. TODO: pickup from _defaults_ */
this.x = ifUndefined(x, 0)
this.y = ifUndefined(y, 0)
if(!isUndefined(radius)) {
/* We don't have to worry about object.radius here
as that will already be set. */
this.radius = radius
}
if(!isUndefined(rotation)) {
/* same as above. */
this.rotation = rotation
}
}
_cast(other, _2=other) {
if(typeof(other) == 'number') { other = point(other, _2) }
if(Array.isArray(other)){ return point(other, _2) }
return other
}
subtract(other, _2=other){
/* "subtract" this point to the _other_ point, returning a new point. */
if(typeof(other) == 'number') {
other = point(other, _2)
}
return new Point(this.x - other.x, this.y - other.y)
}
add(other, _b,) {
/* "Add" this point to the _other_ point, returning a new point. */
other = this._cast(other, _b)
return new Point(
this.x + other.x,
this.y + other.y
)
}
divide(other) {
/* "Divide" this point to the _other_ point, returning a new point. */
if(typeof(other) == 'number') {
other = point(other, other)
}
let nNaN = v => isNaN(v)? 0: v;
return new Point(
nNaN(this.x / other.x)
, nNaN(this.y / other.y)
)
}
multiply(other) {
/* "Multiply" this point to the _other_ point, returning a new point. */
if(typeof(other) == 'number') {
other = point(other, other)
}
return new Point(
this.x * other.x,
this.y * other.y
)
}
// _midpoint(other, offset=.5) {
// /*return a new point, with the XY set at the _mid point_ between
// this point and the given*/
// let p = this.copy()
// p.x = (p.x + other.x) * offset
// p.y = (p.y + other.y) * offset
// return p
// }
midpoint(other, offset=0.5) {
/*
Returns a new point, with the XY set at the point that is `offset` times
the distance from the current point to the other point.
this function is also `lerp` for linear interpolation
*/
let p = this.copy();
p.x = p.x + (other.x - p.x) * offset;
p.y = p.y + (other.y - p.y) * offset;
return p;
}
lerp = this.midpoint
}
class Rotation extends Positionable {
set rotation(value){
/* Set the rotation in degrees (0 to 360). If `this.modulusRotate` is
`true` (default), the given value is fixed through modulus 360
point.rotation = 600
// 240
point.rotation += 1
// 241
To set the rotation (degrees) whilst accounting for the _UP_ vector,
consider the `rotate()` method.
*/
if(this.modulusRotate == false) {
return this.setSpecial('rotation', value)
}
return this.setSpecial('rotation', value % 360)
}
rotate(degrees) {
this.rotation = this.UP + degrees
return this
}
get rotation() {
return this.getSpecial('rotation', 3)
}
get radians() {
/*Return the _radians_ of the current rotation, where _rotation returns
the degrees*/
return degToRad(this.getSpecial('rotation', 3))
}
set radians(angle) {
/* by pushing through the existing rotations, we account for the _up_
vector. */
this.rotation = radiansToDegrees(angle)
}
lookAt(otherPoint, add=0, rotationMultiplier=undefined) {
/* Rotate the point such that the angle relative to the
`otherPoint` is `0`, essentially _looking at_ the other point.
point.lookAt(otherPoint)
Return the angle in radians.
*/
return this.radians = this.directionTo(otherPoint, rotationMultiplier, add)// + add
}
directionTo(otherPoint, rotationMultiplier=undefined, addRad=0) {
// Calculate the differences in x and y coordinates
let delta = 0
try {
delta = otherPoint.subtract(this);
} catch(e) {
if(!isPoint(otherPoint)) {
otherPoint = new Point(otherPoint)
delta = otherPoint.subtract(this);
} else {
throw e
}
}
if(rotationMultiplier != undefined) {
let normRad = this._normalizedRadians(otherPoint, rotationMultiplier, addRad)
return normRad
}
// Calculate the angle in radians
const angleRadians = delta.atan2()
return angleRadians + addRad
}
_normalizedRadians(otherPoint, rotationMultiplier, addRad=0) {
const delta = otherPoint.subtract(this);
const targetRad = delta.atan2() + addRad;
const currentRad = this.radians;
let radDiff = targetRad - currentRad;
// Normalize the angle difference to be within the range -PI to PI
radDiff = Math.atan2(Math.sin(radDiff), Math.cos(radDiff));
const newAngleRadians = currentRad + radDiff * rotationMultiplier;
const normRad = Math.atan2(
Math.sin(newAngleRadians),
Math.cos(newAngleRadians)
);
return normRad;
}
turnTo(otherPoint, rotationMultiplier=1){
let normRad = this._normalizedRadians(otherPoint, rotationMultiplier)
this.radians = normRad;
return normRad
}
getTheta(other, direction=undefined) {
/* Return the calculated theta value through atan2 and built to offload
some of the boring.
The _direction_ denotes the "gravity" pull. Generally this is `DOWN`.
Synonymous to:
let theta = Math.atan2(point.y,
point.x);
let theta = Math.atan2(point.y - other.y,
point.x - other.x);
let theta = Math.atan2(point.y - other.y,
point.x - other.x) - DOWN;
*/
let x = this.x
, y = this.y
;
if(other) {
let _p = this.subtract(other)
x = _p.x
y = _p.y
}
// Gravity is generally DOWN by default
// perhaps this should be -rotation.
// direction = this.resolveStringOrFunction(direction, DOWN)
let theta = Math.atan2(y, x) - direction
return theta
}
}
class Tooling extends Rotation {
resolveStringOrFunction(direction, defaultValue) {
let res = defaultValue;
if(typeof(direction) == 'string') {
// str such as this["rotation"]
res = this[direction]
}
if(typeof(direction) == 'function') {
return direction()
}
return res
}
atan2() {
/* Return the theta value through the atan2 function for this point.*/
let x = this.x
, y = this.y
;
let thetaRadians = Math.atan2(y, x);
return thetaRadians
}
project(distance, rotation, relative=true) {
if(rotation !== undefined && relative == true) {
rotation = (this.UP + rotation) % 360
}
let np = new this.constructor(projectFrom(this, distance, rotation))
np.rotation = this.rotation
return np
}
getTip() {
const distance = this.radius
const rads = this.radians
const vector = this.vector2D(distance)
// Calculate the new x and y coordinates
const x = this.x + vector.x //distance * Math.cos(rads);
const y = this.y + vector.y //distance * Math.sin(rads);
// const x = this.x + distance * Math.cos(rads);
// const y = this.y + distance * Math.sin(rads);
return { x, y, };
}
vector2D(multiplier=1) {
/* Return the 2D XY vector of this point using `this.radians`.
point.vector2D()
{ x: 1.2, y: 0.3 }
Provide a multuplier, for example to create an incident angle:
point.vector2D(-1)
{ x: -1.2, y: -0.3 }
*/
const rads = this.radians
return {
// Step 1: Convert the incident angle (from radians) to a vector
x: multiplier * Math.cos(rads)
, y: multiplier * Math.sin(rads)
}
}
copy(position, deep=false) {
/*
Copy this point as a new Point, or digest the given `position`.
// new point.
let p = stage.center.copy()
Given another point:
// drink options.
p.copy(new Point(100, 100))
Notably `copy(other)` is mutative and calls `set` on this point.
*/
if(position) {
this.set(position.x, position.y)
if(position?.radians){
this.radians = position.radians
}
/* Deep should inspect all given options to a point, and apply them all. */
if(deep==true && position.radius){
this.radius = position.radius
}
return this;
}
return new Point(this.x, this.y, this.radius, this.rotation)
}
magnitude() {
let x = this.x;
let y = this.y;
return Math.sqrt(x * x + y * y);
}
normalized(magnitude=this.magnitude()) {
/*
Synonymous to:
{
x: AB.x / magnitudeAB,
y: AB.y / magnitudeAB
};
*/
return this.divide(magnitude)
// return this.divide(this.magnitude())
}
interpolateTo(other, offset, pointIndex=0) {
/* return a point relative from _this_ point towards the `other`,
offset by the given number `offset` value.
The `pointIndex` (default 0) identifies from which point [this, other]
to offset.*/
return getPointOffsetAbsolute(this, other, offset, pointIndex)
}
interpolateFrom(other, offset, pointIndex=0) {
return getPointOffsetAbsolute(other, this, offset, pointIndex)
}
static distance(a, b){
return Math.hypot(b.x - a.x, b.y - a.y);
}
quantize(amount=1) {
let q = quantizeNumber
return new this.constructor({
x: q(this.x, amount)
, y: q(this.y, amount)
})
}
protractorAngleTo(other, referencePoint) {
let value = calculateAngleWithRefWithNeg(this, other, referencePoint)
return new Angle(value)
}
lerpPixel(other, pixelDistance) {
/*
Returns a new point, offset by `pixelDistance` pixels in the direction
from this point to the other point.
*/
// Calculate the direction vector from this point to the other point
let directionX = other.x - this.x;
let directionY = other.y - this.y;
let dirV = this.distance2D(other)
let distance = this.distanceTo(other)
// Normalize the direction vector (to unit length)
let unitX = dirV.x / distance;
let unitY = dirV.y / distance;
// Scale the unit vector by the desired pixel distance
let offsetX = unitX * pixelDistance;
let offsetY = unitY * pixelDistance;
// Create a new point at the scaled offset from the original point
let p = this.copy();
p.x = this.x + offsetX;
p.y = this.y + offsetY;
return p;
}
}
class Point extends Tooling {
/*The `Point` is the primary class for manipulating XY 2D points.
Arguments:
new Point(100, 200)
Object | Array:
new Point({x: 100, y: 200})
new Point([100, 200])
Properties:
point = new Point
point.x = 100
point.y = 200
*/
// x=0
// y=0
// radius = 5
UP = UP_DEG
_rotationDegrees = UP_DEG
constructor(opts={}){
/* A new point accepts arguments, an object, or an array.
By default the `Point` accepts up to four arguments
new Point(x, y, radius, rotation)
The same properties may be supplied as a single `Array`:
new Point([x, y, radius, rotation])
If the given object is an `object`, we can assign properties
immediately:
point = new Point({x, y, radius, rotation, other:100})
point.other
// 100
*/
super(opts)
/* Ensure _opts_ is something. Default; a dict. */
opts = arguments[0] || {}
/* If given a Point instance, return the given point instance */
if(opts && (opts.constructor == this.constructor) ){ return opts }
// new Point(x,y, ...) // reset the opts obj.
if(arguments.length > 1 || typeof(arguments[0] == 'number')){ opts = {} }
this.modulusRotate = undefined
this._opts = Object.assign({relX: 0, relY: 0 }, opts)
// set 0 or more object
this.set.apply(this, arguments)
this.created()
}
created() {
// api hook for life.
}
update(data) {
/*
Perform an _update_ given a dictionary of other properties.
point.update({ x: 200, color: 'red' })
*/
for(let k in data) {
this[k] = data[k]
}
return this
}
get uuid() {
/* Get or create the random `_id` for this point.
Return a unique string.
*/
let r = this._id;
if(r == undefined) {
this._id = r = (~~(Math.random() * 10000)).toString(32)
}
return r
}
set uuid(v) {
this._id = v
}
get [0]() {
/* return the X value of the point:
point[0]
Note:
point[0] == point.x
*/
return this.x
}
set [0](value) {
/*
Sugar function to apply `this.x`:
point[0] = 100
point.x == 100
*/
this.x = value
}
get [1]() {
/*
return the Y value of the point:
point[0]
Note:
point[0] == point.y
*/
return this.y
}
set [1](value) {
/*
Sugar function to apply `this.y`:
point[0] = 300
point.y == 300
*/
this.y = value
}
get [Symbol.toStringTag]() {
return this.toString()
}
[Symbol.toPrimitive](hint) {
if (hint === 'string') {
return this.toString()
}
return null
// return Reflect.apply(...arguments)
}
toString(){
let name = 'point'
return `${name}({x:${this.x}, y:${this.y}})`;
}
get _liveProps() { return true }
asArray(fix=false) {
/*
Return this point as an Array of 4 values:
const point = new Point(1,2,3,4);
point.asArray();
// [1,2,3,4]
Keys:
+ x
+ y
+ radius
+ rotation
*/
// let r = [this.x, this.y, this.radius, this.rotation]
let r = Object.values(this.asObject())
if(fix) {
let int = (x)=> Number( x.toFixed(Number(fix)) )
return r.map(v=>int(v))
}
return r
}
asObject() {
/* Return the important information about this node,
used for _save_ or copy methods. */
return {
x: this.x
, y: this.y
, radius: this.radius
, rotation: this.rotation
}
}
}
Polypoint.head.install(Point)
Polypoint.head.mixin('Point', {
isNaN: {
value(any=false) {
let r = 0;
r += +isNaN(this.x)
r += +isNaN(this.y)
if(r==0) { return false }
if(r > 0) {
if(any) { return true }
// two NaNs, is always isNaN == true
if(r >= 2) { return true }
}
return false
}
, writable: true
}
})
;Object.defineProperty(Point, 'from', {
value: function(a,b, c, d){
if(a.offsetX && b==undefined) {
// is an event
return new Point(a.offsetX, a.offsetY)
}
return new Point(a,b, c, d)
}
});
copy