UNPKG

@obliczeniowo/elementary

Version:
768 lines (754 loc) 25.5 kB
import { ElementaryMath } from '@obliczeniowo/elementary/math'; class ColorRGB { r = 0; g = 0; b = 0; a = 255; /** * Calculate color with linear interpolation using factor parameter */ static proportionalColor(firstColor, secondColor, factor) { factor = Math.min(1, Math.max(0, factor)); return new ColorRGB(firstColor.red * factor + secondColor.red * (1 - factor), firstColor.green * factor + secondColor.green * (1 - factor), firstColor.blue * factor + secondColor.blue * (1 - factor)); } static fromHex(hexColor) { const color = hexColor.slice(1); const red = parseInt(color.substr(0, 2), 16); const green = parseInt(color.substr(2, 2), 16); const blue = parseInt(color.substr(4, 2), 16); return new ColorRGB(red, green, blue); } static fromHtmlColor(htmlColor) { const d = document.createElement('div'); d.style.color = htmlColor; window.document.body.appendChild(d); const parts = (window.getComputedStyle(d).color || '').match(/\d+/g) || ['00', '00', '00']; const f = (n) => { return parseInt(n, 10); }; window.document.body.removeChild(d); return new ColorRGB(f(parts[0]), f(parts[1]), f(parts[2])); } constructor(red, green, blue, alpha = 255) { this.red = +red; this.green = +green; this.blue = +blue; this.a = +alpha; } getHex(value) { return ('0' + value.toString(16)).substr(-2); } /** set red value of color with handling min/max */ set red(red) { this.r = ElementaryMath.minmax(Math.round(red), 0, 255); } get red() { return this.r; } /** set green value of color with handling min/max */ set green(green) { this.g = ElementaryMath.minmax(Math.round(green), 0, 255); } get green() { return this.g; } /** set blue value of color with handling min/max */ set blue(blue) { this.b = ElementaryMath.minmax(Math.round(blue), 0, 255); } get blue() { return this.b; } get alpha() { return this.a; } /** set alpha value of color with handling min/max */ set alpha(alpha) { this.a = ElementaryMath.minmax(Math.floor(alpha), 0, 255); } /** * Return color in hex format including transparency * @return - format #ffccbbaa (last one is alpha) */ get getHexColor() { return this.getHexColorBase + this.getHex(Math.round(this.a)); } /** * Return hex format of color without alpha * @return color in format: #ff00bb */ get getHexColorBase() { return '#' + this.getHex(this.r) + this.getHex(this.g) + this.getHex(this.b); } /** * Return rgba(100, 200, 255, 0.5) color format as string */ getRGBAColor() { return 'rgba(' + this.r + ', ' + this.g + ', ' + this.b + ', ' + (this.a / 255).toString().substring(0, 5) + ')'; } toString() { return this.getHexColorBase; } /** * create object copy */ copy() { return new ColorRGB(this.red, this.green, this.blue, this.alpha); } /** * Multiply every single value of colors by factor parameter with handling min/max cases */ multiply(factor) { return new ColorRGB(this.r * factor, this.g * factor, this.b * factor, this.alpha); } negative() { return new ColorRGB(255 - this.r, 255 - this.b, 255 - this.g, this.alpha); } } class ColorHSV { h; s; v; constructor(h, s, v) { this.h = Math.max(Math.min(ColorHSV.fmod(h, 360), 360), 0); this.s = Math.max(Math.min(s, 1), 0); this.v = Math.max(Math.min(v, 255), 0); } /** * Calculate color with linear interpolation using factor parameter */ static proportionalColor(firstColor, secondColor, factor) { factor = Math.min(1, Math.max(0, factor)); return ColorHSV.createColorHSV(firstColor.h * factor + secondColor.h * (1 - factor), firstColor.s * factor + secondColor.s * (1 - factor), firstColor.v * factor + secondColor.v * (1 - factor)); } /** * Static constructor for creating ColorHSV object * @param h hue - value from 0 - 360 degrees * @param s saturation - value from 0 - 1 * @param v value 0 - 255 * @returns ColorHSV object */ static createColorHSV(h, s, v) { return new ColorHSV(h, s, v); } static fmod(a, b) { return a - Math.floor(a / b) * b; } /** * Static constructor for creating ColorHSV object * @param colorRGB object of RGB color * @returns brand, brand new object of ColorHSV */ static createColorHSVfromRGB(colorRGB) { const x = Math.min(Math.min(colorRGB.red, colorRGB.green), colorRGB.blue); let f; let i; const v = Math.max(Math.max(colorRGB.red, colorRGB.green), colorRGB.blue); let h; let s; if (x === v) { h = 0; s = 0; } else { f = colorRGB.red === x ? colorRGB.green - colorRGB.blue : colorRGB.green === x ? colorRGB.blue - colorRGB.red : colorRGB.red - colorRGB.green; i = colorRGB.red === x ? 3 : colorRGB.green === x ? 5 : 1; h = ColorHSV.fmod((i - f / (v - x)) * 60, 360); s = (v - x) / v; } return new ColorHSV(h, s, v); } convertToRGB() { let i; let f; let p; let q; let t; let r = 0; let g = 0; let b = 0; let h = this.h; const v = this.v; const s = this.s; if (v === 0) { r = 0; g = 0; } else { h /= 60; i = Math.floor(h); f = h - i; p = v * (1 - s); q = v * (1 - s * f); t = v * (1 - s * (1 - f)); if (i === 0) { r = v; g = t; b = p; } else if (i === 1) { r = q; g = v; b = p; } else if (i === 2) { r = p; g = v; b = t; } else if (i === 3) { r = p; g = q; b = v; } else if (i === 4) { r = t; g = p; b = v; } else if (i === 5) { r = v; g = p; b = q; } } return new ColorRGB(r, g, b); } addHue(hue) { return ColorHSV.createColorHSV(this.h + hue, this.s, this.v); } toString() { return this.convertToRGB().toString(); } } class Dates { static countDays(start, end) { let iterator = new Date(start); let i = 0; if (start.getTime() > end.getTime()) { throw new Error('start date must be smaller then end date'); } while (!Dates.equalToDayLevel(iterator, end)) { // (24 hours * 60 min * 60 sec * 1000 ms * 2) in dividing part const dt = Math.ceil((end.getTime() - iterator.getTime()) / 172800000); iterator.setDate(iterator.getDate() + dt); i += dt; } return i; } static countWeeks(start, end) { return (end.getTime() - start.getTime()) / 604800000; } static getWeekDate(date, type = 'first', offset = 0) { const weekDay = (date.getDay() + offset) % 7; return new Date(date.getTime() + (type === 'first' ? -1 : 1) * 86400000 * weekDay); } static equalToDayLevel(first, last) { return first.getMonth() === last.getMonth() && first.getDate() === last.getDate() && first.getFullYear() === last.getFullYear(); } static equalToTimeLevel(first, last) { return first.getHours() === last.getHours() && first.getMinutes() === last.getMinutes() && first.getSeconds() === last.getSeconds(); } static compare(first, last, operator, error) { switch (operator) { case '<': return first < last; case '<=': return first <= last; case '=': return first === last; case '>=': return first >= last; case '>': return first > last; case '!=': return first !== last; default: throw error; } } static compareToTimeLevel(first, last, operator) { const f = Dates.setDateToZero(new Date(first)).getTime(); const l = Dates.setDateToZero(new Date(last)).getTime(); return Dates.compare(f, l, operator, new Error('Wrong operator in Dates.compareToTimeLevel method')); } /** * Compare two dates on time level that consider only hours, minutes and seconds (no milliseconds) */ static compareToDateLevel(first, last, operator) { const f = Dates.setTimeToZero(new Date(first)).getTime(); const l = Dates.setTimeToZero(new Date(last)).getTime(); return Dates.compare(f, l, operator, new Error('Wrong operator in Dates.compareToDateLevel method')); } /** * set date */ static setTimeToZero(date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } /** * clear date part and milliseconds part */ static setDateToZero(date) { const copy = new Date(date); copy.setTime(Math.floor((date.getTime() % 86400000) / 1000) * 1000); return copy; } static timeFromArray(time) { const date = new Date(); date.setTime(0); date.setHours(time[0], time[1], time[2]); return date; } } class Point2D { x; y; static isCorrectPoint(point) { return ( // eslint-disable-next-line @typescript-eslint/prefer-optional-chain point && point.x !== undefined && point.y !== undefined && point.x !== null && point.y !== null); } static fromInterface(iPoint) { return new Point2D(iPoint.x, iPoint.y); } /** * Check if two lines described by pairs of points intersects with each other * @param pt1 first line start point * @param pt2 first line end point * @param pt3 second line start point * @param pt4 second line end point * @returns true if intersection exist */ static linesIntersect(pt1, pt2, pt3, pt4) { if (Math.sign(pt1.subtract(pt2).determinant(pt3.subtract(pt2))) !== Math.sign(pt1.subtract(pt2).determinant(pt4.subtract(pt2))) && Math.sign(pt3.subtract(pt4).determinant(pt1.subtract(pt4))) !== Math.sign(pt3.subtract(pt4).determinant(pt2.subtract(pt4)))) { return true; } return false; } constructor(x = 0, y = 0) { this.x = x; this.y = y; } get isZeroLength() { return this.x === 0 && this.y === 0; } isPtInCircle(centralPoint, ray) { return this.subtract(centralPoint).sickLength() < ray * ray; } isPtInRect(x, y, width, height) { return this.x >= x && this.x <= x + width && this.y >= y && this.y <= y + height; } isEqual(point2D) { return this.x === point2D.x && this.y === point2D.y; } multiply(value) { return new Point2D(value * this.x, value * this.y); } scalarMultiple(other) { return this.x * other.x + this.y * other.y; } determinant(other) { return this.x * other.y - this.y * other.x; } scale(xs, ys) { return new Point2D(this.x * xs, this.y * ys); } add(point) { return new Point2D(this.x + point.x, this.y + point.y); } subtract(point) { return new Point2D(this.x - point.x, this.y - point.y); } length() { return Math.sqrt(this.scalarMultiple(this)); } /** * Set vector length * @param length new length to set * @returns new instance of Point2D with new length if current length of vector is !== 0 in this case * return degenerated { x: 0, y: 0 } vector */ setLength(length) { const k = length / (this.length() || 1); return new Point2D(k * this.x, k * this.y); } /** * Calculate x * x + y * y = scalarMultiple(this) * @returns return sick length that can be useful to compare distance between points without calculate sqrt. * for example if you have two points P1, P2 then P1.sickLength() < P2.sickLength() means P1 is closer to beginning of * x = 0, y = 0 point then P2 (not require calculate sqrt) */ sickLength() { return this.scalarMultiple(this); } angle(reverseX = false) { return Math.atan2(this.y, reverseX ? -this.x : this.x); } /** * rotate point by angle * @param angle - in radians * @returns new Instance of Point2D */ rotate(angle) { const sin = Math.sin(angle); const cos = Math.cos(angle); return new Point2D(this.x * cos - this.y * sin, this.x * sin + this.y * cos); } /** * Calculate Point2D containing coordinates of point thrown perpendicularly on line * @param startLinePoint first point of line * @param endLinePoint last point of line * @returns if points are equal then null, if not then new instance of Point2D containing coordinates of point thrown * perpendicularly on line */ getPointOnLine(startLinePoint, endLinePoint) { if (startLinePoint.isEqual(endLinePoint)) { return null; } const lineVector = startLinePoint.subtract(endLinePoint); const u = this.subtract(startLinePoint).scalarMultiple(startLinePoint.subtract(endLinePoint)) / lineVector.scalarMultiple(lineVector); return startLinePoint.add(lineVector.multiply(u)); } /** * Method to draw point position using IDrawingInterface (require only DrawText method) * @param ctx can be any IDrawingInterface or your own class containing drawing method * @param options options to control drawing: * x, y - position coordinates to display (can be different then coordinates) * precision - 2 for value 2.3333 display 2.33 * color - of text * angle - of text */ drawPointPos(ctx, options) { ctx.setFontSize(options?.fontSize || 5); ctx.drawText(`X: ${this.x.toFixed(options?.precision || 3)}; Y: ${this.y.toFixed(options?.precision || 3)}`, new Point2D(options?.x || this.x, options?.y || this.y), options?.color || 'black', options?.angle || 0); } /** * Make brand brand new copy of object * @returns new instance */ copy() { return new Point2D(this.x, this.y); } /** * Simplify to interface * * @returns IPoint2D interface */ toInterface() { return { x: this.x, y: this.y }; } /** * Convert to string * @param separator separator to use for coordinates * @returns string in format: ' 10, 23.5' */ toString(separator = ',') { return ' ' + this.x + separator + this.y; } toCssTranslate(unit = '') { return `translate(${this.x}${unit}, ${this.y}${unit})`; } } class Point2DData extends Point2D { extData; constructor(x = 0, y = 0, extData) { super(x, y); this.extData = extData; } } class Point3D { x = 0; y = 0; z = 0; constructor(x = 0, y = 0, z = 0) { this.x = x; this.y = y; this.z = z; } add(point) { return new Point3D(this.x + point.x, this.y + point.y, this.z + point.z); } subtract(point) { return new Point3D(this.x - point.x, this.y - point.y, this.z - point.z); } sickLength() { return this.x * this.x + this.y * this.y + this.z * this.z; } length() { return Math.sqrt(this.sickLength()); } copy(p3d) { return new Point3D(p3d.x, p3d.y, p3d.z); } } /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */ class TextTransform { /** * Split text only on space sign and if length of more then 1 word in one line is greater then maxLength * @param text - text to split * @param maxLength - max length of line * @returns table of splitted text */ static splitByLengthAndWords(text, maxLength) { const strings = text.split(' '); const arr = []; arr[0] = strings.shift() || ''; while (strings.length) { if (arr[arr.length - 1].length + 1 + strings[0].length < maxLength) { arr[arr.length - 1] = arr[arr.length - 1] + strings.shift(); } else { arr.push(strings.shift()); } } return arr; } /** * Transform text `some text {to-replace-1}, by using other object {to-replace-2}` using object properties keys * to replace in text by given properties object * @param text - text with hidden {key} value * @param properties - object properties * @returns text with replaced by keys part, if key not exist in properties object then matching text is put there */ static prepare(text, properties, prepareProps = (key, properties) => properties[key]?.toString()) { const pattern = /{(\w+)}/g; const replacePlaceholders = (match, fieldName) => { const fieldValue = prepareProps(fieldName, properties); return fieldValue !== undefined ? String(fieldValue) : match; }; return text.replace(pattern, replacePlaceholders); } static splitByMathOperator(text) { const operators = ['+', '-', '*', '/', '%', '^', '(', ')']; const operatorTable = []; let currentFragment = ''; for (let i = 0; i < text.length; i++) { const char = text.charAt(i); if (operators.includes(char)) { if (currentFragment !== '') { operatorTable.push(currentFragment); currentFragment = ''; } operatorTable.push(char); } else { currentFragment += char; } } if (currentFragment !== '') { operatorTable.push(currentFragment); } return operatorTable; } } class LinearFunction2D { a; b; static fromTwoPoints(p1, p2) { // if pt.x === p2.x then there is no way to define proper f(x) function if (p1.x === p2.x) { return undefined; } const a = (p2.y - p1.y) / (p2.x - p1.x); const b = p1.y - a * p1.x; return new LinearFunction2D(a, b); } constructor(a, b) { this.a = a; this.b = b; } zeroPoint() { const { a, b } = this; return -b / a; } xPoint(y) { const { a, b } = this; return (y - b) / a; } yPoint(x) { const { a, b } = this; return x * a + b; } } class AbstractSearchDomain { items = []; regExpOn = false; filtered = []; } class DefaultSearchDomain extends AbstractSearchDomain { constructor(items) { super(); this.items = items; this.filtered = [...this.items]; } search(filter) { if (filter?.length) { filter = filter.toLowerCase(); if (this.regExpOn) { const reg = new RegExp(filter, 'i'); this.filtered = this.items.filter(item => reg.test(item.text)); } else { this.filtered = this.items.filter(item => item.text.toLocaleLowerCase().includes(filter)); } } else { this.filtered = [...this.items]; } } } class Rectangle { width = 0; height = 0; start; end; constructor(start = new Point2D(), width = 0, height = 0) { Object.assign(this, { start, width, height }); } get left() { return this.start.x; } get bottom() { return this.start.y; } get top() { return this.start.y + this.height; } get right() { return this.start.x + this.width; } isPointIn(point = new Point2D()) { const { left, right, bottom, top } = this; if (point.x >= left && point.x <= right && point.y >= bottom && point.y <= top) { return true; } return false; } isRectanglesOverlapping(rectangle = new Rectangle()) { if (this.isPointIn(rectangle.start) || this.isPointIn(new Point2D(rectangle.right, rectangle.bottom)) || this.isPointIn(new Point2D(rectangle.right, rectangle.top)) || this.isPointIn(new Point2D(rectangle.left, rectangle.top)) || rectangle.isPointIn(this.start) || rectangle.isPointIn(new Point2D(this.right, this.bottom)) || rectangle.isPointIn(new Point2D(this.right, this.top)) || rectangle.isPointIn(new Point2D(this.left, this.top)) || Point2D.linesIntersect(this.start, new Point2D(this.right, this.top), rectangle.start, new Point2D(rectangle.right, rectangle.top))) { return true; } return false; } drawRectangle(ctx, stroke, strokeColor, fillColor) { const { start, width, height } = this; ctx.drawRect(start.x, start.y, width, height, stroke, strokeColor, fillColor); } } class Polygon { static random = (spaceWidth, spaceHeight, size = 20) => { const maxRadius = size; const points = []; const center = new Point2D(Math.random() * (spaceWidth - 2 * maxRadius) + maxRadius, Math.random() * (spaceHeight - 2 * maxRadius) + maxRadius); const nrOfPoints = Math.ceil(Math.random() * 5) + 5; for (let i = 0; i < nrOfPoints; i++) { const angle = (i / nrOfPoints) * Math.PI * 2; const radius = Math.random() * maxRadius; points.push(new Point2D(radius * Math.cos(angle) + center.x, radius * Math.sin(angle) + center.y)); } return new Polygon(points); }; points = []; constructor(points = []) { this.points = points; } getBondingRect() { const polygon = this.points; // detecting if in bounding box let minX = polygon[0].x; let maxX = polygon[0].x; let minY = polygon[0].y; let maxY = polygon[0].y; for (let i = 1; i < polygon.length; i++) { const q = polygon[i]; minX = Math.min(q.x, minX); maxX = Math.max(q.x, maxX); minY = Math.min(q.y, minY); maxY = Math.max(q.y, maxY); } return new Rectangle(new Point2D(minX, minY), maxX - minX, maxY - minY); } isPointIn(point) { const polygon = this.points; // detecting if in bounding box let minX = polygon[0].x; let maxX = polygon[0].x; let minY = polygon[0].y; let maxY = polygon[0].y; for (let i = 1; i < polygon.length; i++) { const q = polygon[i]; minX = Math.min(q.x, minX); maxX = Math.max(q.x, maxX); minY = Math.min(q.y, minY); maxY = Math.max(q.y, maxY); } if (point.x < minX || point.x > maxX || point.y < minY || point.y > maxY) { return false; } // https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html let inside = false; let j = polygon.length - 1; for (let i = 0; i < polygon.length; j = i++) { if (polygon[i].y > point.y !== polygon[j].y > point.y && point.x < ((polygon[j].x - polygon[i].x) * (point.y - polygon[i].y)) / (polygon[j].y - polygon[i].y) + polygon[i].x) { inside = !inside; } } return inside; } isIntersect(polygon = new Polygon()) { const { points } = this; const { points: pPoints } = polygon; for (let start = 0, end = points.length - 1; start < points.length - 1; end = start++) { const fp = points[start]; const lp = points[end]; for (let pStart = 0, pEnd = pPoints.length - 1; pStart < pPoints.length - 1; pEnd = pStart++) { const pfp = pPoints[pStart]; const plp = pPoints[pEnd]; if (Point2D.linesIntersect(fp, lp, pfp, plp)) { return true; } } } return false; } subtract(point) { return new Polygon(this.points.map((pt) => pt.subtract(point))); } multiply(scalar) { return new Polygon(this.points.map((point) => point.multiply(scalar))); } drawPolygon(ctx, stroke, color) { ctx.drawPolyline(this.points, stroke, color); } } /** * Generated bundle index. Do not edit. */ export { AbstractSearchDomain, ColorHSV, ColorRGB, Dates, DefaultSearchDomain, LinearFunction2D, Point2D, Point2DData, Point3D, Polygon, Rectangle, TextTransform }; //# sourceMappingURL=obliczeniowo-elementary-classes.mjs.map