UNPKG

pencil.js

Version:

Nice modular interactive 2D drawing library.

306 lines (277 loc) 10 kB
import { constrain, equals, radianCircle, modulo } from "@pencil.js/math"; /** * @module Position */ /** * Pair of value in 2d space * @class */ export default class Position { /** * Position constructor * @param {Number} x - Vertical component * @param {Number} y - Horizontal component */ constructor (x = 0, y = 0) { this.x = +x; this.y = +y; } /** * Define this position value * @param {PositionDefinition|Number} definition - Horizontal position or another position * @param {Number} [diffY] - Vertical position if "definition" is a number * @return {Position} Itself */ set (definition, diffY) { let x; let y; if (typeof definition === "number") { x = definition; y = diffY === undefined ? definition : diffY; } else { const position = Position.from(definition); ({ x, y } = position); } this.x = x; this.y = y; return this; } /** * Create a copy of this position * @return {Position} New instance */ clone () { return new Position(this.x, this.y); } /** * Determine if is equal to another position * @param {PositionDefinition} positionDefinition - Any other position * @return {Boolean} */ equals (positionDefinition) { const position = Position.from(positionDefinition); return equals(this.x, position.x) && equals(this.y, position.y); } /** * Apply an operation to this position values * @param {Function} operation - Function to apply on value * @param {PositionDefinition|Number} definition - Another position or a number * @param {Number} [diffY] - Value to apply on "y" if "definition" is a number * @return {Position} Itself */ calc (operation, definition, diffY) { let x = 0; let y = 0; if (typeof definition === "number") { x = operation(this.x, definition); y = operation(this.y, diffY === undefined ? definition : diffY); } else { const position = Position.from(definition); x = operation(this.x, position.x); y = operation(this.y, position.y); } return this.set(x, y); } /** * Add another position or number * @param {PositionDefinition|Number} definition - Another position or a number * @param {Number} [y] - Value for "y" if "position" is a number * @return {Position} Itself */ add (definition, y) { return this.calc((self, other) => self + other, definition, y); } /** * Subtract another position or number * @param {PositionDefinition|Number} definition - Another position or a number * @param {Number} [y] - Value for "y" if "position" is a number * @return {Position} Itself */ subtract (definition, y) { return this.calc((self, other) => self - other, definition, y); } /** * Multiply by another position or number * @param {PositionDefinition|Number} definition - Another position or a number * @param {Number} [y] - Value for "y" if "position" is a number * @return {Position} Itself */ multiply (definition, y) { return this.calc((self, other) => self * other, definition, y); } /** * Divide by another position or number * @param {PositionDefinition|Number} definition - Another position or a number * @param {Number} [y] - Value for "y" if "position" is a number * @return {Position} Itself */ divide (definition, y) { return this.calc((self, other) => self / other, definition, y); } /** * Gives the modulo by another position or number * @param {PositionDefinition|Number} definition - Another position or a number * @param {Number} [y] - Value for "y" if "position" is a number * @return {Position} Itself */ modulo (definition, y) { return this.calc((self, other) => modulo(self, other), definition, y); } /** * Raise to a power * @param {PositionDefinition|Number} definition - Another position or a number * @param {Number} [y] - Value for "y" if "position" is a number * @return {Position} Itself */ power (definition, y) { return this.calc((self, other) => self ** other, definition, y); } /** * Rotate the position around the origin clockwise * @param {Number} [angle=0] - Angle of rotation in ratio of full circle * (0 means no rotation, 1 means go full circle back to same position) * @param {PositionDefinition} [originDefinition] - Point of origin to rotate around (by default (0, 0)) * @return {Position} Itself */ rotate (angle = 0, originDefinition) { const { cos, sin } = Math; const degree = angle * radianCircle; const origin = Position.from(originDefinition); const clone = this.clone().subtract(origin); const x = (clone.x * cos(degree)) - (clone.y * sin(degree)); const y = (clone.y * cos(degree)) + (clone.x * sin(degree)); return this.set(x, y).add(origin); } /** * Constrain the position to a rectangle define by two positions * @param {PositionDefinition} startDefinition - Starting position of the constrain (upper-left corner) * @param {PositionDefinition} endDefinition - Ending position of the constrain (lower-right corner) * @return {Position} Itself */ constrain (startDefinition, endDefinition) { const start = Position.from(startDefinition); const end = Position.from(endDefinition); const x = constrain(this.x, Math.min(start.x, end.x), Math.max(start.x, end.x)); const y = constrain(this.y, Math.min(start.y, end.y), Math.max(start.y, end.y)); return this.set(x, y); } /** * Move the position towards another by a ratio * @param {PositionDefinition} positionDefinition - Any other position * @param {Number} ratio - Ratio of distance to move, 0 mean no change, 1 mean arrive at position * @return {Position} Itself */ lerp (positionDefinition, ratio) { const difference = Position.from(positionDefinition).clone().subtract(this).multiply(ratio); return this.add(difference); } /** * Compute distance with another position * @param {PositionDefinition} positionDefinition - Any position * @return {Number} */ distance (positionDefinition) { const position = Position.from(positionDefinition); return Math.hypot(position.x - this.x, position.y - this.y); } /** * Dot product * @param {PositionDefinition} positionDefinition - Another position * @return {Number} */ dotProduct (positionDefinition) { const position = Position.from(positionDefinition); return (this.x * position.x) + (this.y * position.y); } /** * Cross product * @param {PositionDefinition} positionDefinition - Another position * @return {Number} */ crossProduct (positionDefinition) { const position = Position.from(positionDefinition); return (this.x * position.y) - (this.y * position.x); } /** * Define if this is on the same side of a vector as another position * @param {PositionDefinition} positionDefinition - Another position * @param {Vector} vector - Any vector * @return {Boolean} */ isOnSameSide (positionDefinition, vector) { const position = Position.from(positionDefinition); const thisMoved = this.clone().subtract(vector.start); const positionMoved = position.clone().subtract(vector.start); const delta = vector.getDelta(); const { sign } = Math; return sign(thisMoved.crossProduct(delta)) === sign(positionMoved.crossProduct(delta)); } /** * Get vector length * @return {Number} */ get length () { return this.distance(); } /** * Get the angle of a position relative to the horizontal axis * @return {Number} */ get angle () { if (this.x === 0 && this.y === 0) { return 0; } return (Math.atan(this.y / this.x) / radianCircle) + (this.x < 0 ? 0.75 : 0.25); } /** * Return a JSON ready Position definition * @return {Array<Number>} */ toJSON () { const { x, y } = this; return [ x, y, ]; } /** * @typedef {Object} AbstractPosition * @prop {Number} [x=0] - Vertical position * @prop {Number} [y=0] - Horizontal position */ /** * @typedef {Array<Number>|AbstractPosition} PositionDefinition */ /** * Create a Position from a generic definition or do nothing if already a Position * @param {PositionDefinition} [positionDefinition] - Position definition * @return {Position} */ static from (positionDefinition = new Position()) { if (positionDefinition instanceof Position) { return positionDefinition; } if (Array.isArray(positionDefinition)) { return new Position(...positionDefinition); } try { return new Position(positionDefinition.x, positionDefinition.y); } catch { throw TypeError(`Unexpected type for position: ${JSON.stringify(positionDefinition)}.`); } } /** * Compute the average for a set of positions * @param {...PositionDefinition} positionDefinitions - List of positions to average * @return {Position} */ static average (...positionDefinitions) { let result = new Position(); positionDefinitions.forEach(one => result = result.add(Position.from(one))); const nbPositions = positionDefinitions.length; return result.divide(nbPositions); } }