diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 631b6dc..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = { - env: { - browser: true, - commonjs: true, - es6: true, - node: true, - mocha: true - }, - plugins: [ - 'mocha' - ], - extends: [ - 'standard', - 'plugin:mocha/recommended' - ], - globals: { - Atomics: 'readonly', - SharedArrayBuffer: 'readonly' - }, - parserOptions: { - ecmaVersion: 2018 - }, - rules: { - } -} diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml deleted file mode 100644 index 55f434e..0000000 --- a/.github/workflows/nodejs.yml +++ /dev/null @@ -1,28 +0,0 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Tests - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [16.x, 18.x] - - steps: - - uses: actions/checkout@v3 - - name: Using Node.js v${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4640904 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1 @@ +# TODO diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3a4cacc --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,22 @@ +name: Tests +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + - name: Install dependencies + run: npm ci --no-audit + - name: Run Jest tests + run: npm test diff --git a/.gitignore b/.gitignore index 25c8fdb..b07ff2a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -package-lock.json \ No newline at end of file +package-lock.json +coverage diff --git a/README.md b/README.md index 942f227..b04dec8 100644 --- a/README.md +++ b/README.md @@ -1,603 +1,729 @@ -[![Version](http://img.shields.io/npm/v/@pangenerator/utils.svg)](https://www.npmjs.org/package/@pangenerator/utils) [![Tests](https://img.shields.io/github/workflow/status/panGenerator/utils/Tests)](https://github.com/panGenerator/utils) -![Dependencies](https://img.shields.io/david/panGenerator/utils) ![Dev Dependencies](https://img.shields.io/david/dev/panGenerator/utils) - - - -## utils -Various functions used in javascript tools - - -* [utils](#module_utils) - * _static_ - * [.map(value, low1, high1, low2, high2)](#module_utils.map) ⇒ Number - * [.clamp(value, min, max)](#module_utils.clamp) ⇒ Number - * [.norm(value, start, stop)](#module_utils.norm) ⇒ Number - * [.random([low], high)](#module_utils.random) ⇒ Number - * [.randomDir()](#module_utils.randomDir) ⇒ Number - * [.lerp(start, stop, amt)](#module_utils.lerp) ⇒ Number - * [.lerp3(A, B, amt)](#module_utils.lerp3) ⇒ Point - * [.lerpedPoints(A, B, count)](#module_utils.lerpedPoints) ⇒ Array.<Point> - * [.square(a)](#module_utils.square) ⇒ Number - * [.dist(A, B)](#module_utils.dist) ⇒ Number - * [.degrees(radians)](#module_utils.degrees) ⇒ Number - * [.radians(degrees)](#module_utils.radians) ⇒ Number - * [.intersection(c1, c2)](#module_utils.intersection) ⇒ Array \| Boolean - * [.randomName(N)](#module_utils.randomName) ⇒ String - * [.timestampName()](#module_utils.timestampName) ⇒ String - * [.randomIndex(N)](#module_utils.randomIndex) ⇒ Number - * [.copyArray(source)](#module_utils.copyArray) ⇒ Array - * [.shuffleArray(source)](#module_utils.shuffleArray) ⇒ Array - * [.filterUnique(source)](#module_utils.filterUnique) ⇒ Array - * [.lerpColor(a, b, amt)](#module_utils.lerpColor) ⇒ String - * [.precision(value, precision)](#module_utils.precision) ⇒ Number - * [.loadJSON(address, callback)](#module_utils.loadJSON) - * [.removeDiacritics(str)](#module_utils.removeDiacritics) ⇒ String - * [.removeNonAlphaNumeric(str)](#module_utils.removeNonAlphaNumeric) ⇒ String - * [.splitChunks(str, n, discard)](#module_utils.splitChunks) ⇒ Array - * [.getQuarter(d)](#module_utils.getQuarter) ⇒ Array - * [.quarterExtent(quarter, year)](#module_utils.quarterExtent) ⇒ Array - * [.datesBetween(start, end)](#module_utils.datesBetween) ⇒ Array - * [.downloadDataUri(options)](#module_utils.downloadDataUri) - * [.polarToCartesian(r, angle)](#module_utils.polarToCartesian) ⇒ Point - * [.cartesianToPolar(x, y)](#module_utils.cartesianToPolar) ⇒ Object - * [.pageOffset(elem)](#module_utils.pageOffset) ⇒ Object - * [.fuzzySearch(list, searchValue)](#module_utils.fuzzySearch) ⇒ Array - * [.dist2(A, B)](#module_utils.dist2) ⇒ Number - * [.distToSegment2(A, S, E)](#module_utils.distToSegment2) ⇒ Number - * [.distToSegment(A, S, E)](#module_utils.distToSegment) ⇒ Number - * [.sepCase(str)](#module_utils.sepCase) ⇒ string - * [.snakeCase(str)](#module_utils.snakeCase) ⇒ string - * [.kebabCase(str)](#module_utils.kebabCase) ⇒ string - * [.camelCase(str)](#module_utils.camelCase) ⇒ string - * [.contains(elem, arr)](#module_utils.contains) ⇒ boolean - * [.getCSS(parentElement)](#module_utils.getCSS) ⇒ string - * [.appendCSS(cssText, element)](#module_utils.appendCSS) - * [.getSVGString(svgNode)](#module_utils.getSVGString) ⇒ string - * [.svgStringToImage(svgString, width, height, format, transparent, callback)](#module_utils.svgStringToImage) - * [.svgToUri(svgNode)](#module_utils.svgToUri) ⇒ string - * [.shallowCopyExcluding(obj, prop)](#module_utils.shallowCopyExcluding) ⇒ Object - * _inner_ - * [~Point](#module_utils..Point) : Object - * [~Circle](#module_utils..Circle) : Object - - -* * * - - - -### utils.map(value, low1, high1, low2, high2) ⇒ Number -Map a number from one range to another - -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - Mapped number +[![Version](http://img.shields.io/npm/v/@pangenerator/utils.svg)](https://www.npmjs.org/package/@pangenerator/utils) +[![Tests](https://img.shields.io/github/actions/workflow/status/panGenerator/utils/tests.yml)](https://github.com/panGenerator/utils) + +## Modules + +
+
PID
+

PID controller

+
+
+ +## Constants + +
+
copyArrayArray
+

Copy array

+
+
shuffleArrayArray
+

Shuffle array

+
+
filterUniqueArray
+

Filter array unique

+
+
fuzzySearchArray
+

Fuzzy search element in list

+
+
containsboolean
+

Check if array contains

+
+
lerpColorString
+

Linear color interpolation

+
+
getQuarterArray
+

Get quarter from date

+
+
quarterExtentArray
+

Get quarter extent

+
+
datesBetweenArray
+

Get all dates between two dates

+
+
lerp3Point
+

Linear interpolation in 3D

+
+
lerpStopsArray.<Point>
+

Linear interpolation in 3D array

+
+
distNumber
+

Distance between two points (2D and 3D)

+
+
intersectLinesPoint | Boolean
+

Find intersection point between two lines

+
+
intersectCirclesArray | Boolean
+

Find intersection points between two circles

+
+
polarToCartesianPoint
+

Convert coordinates from polar to cartesian

+
+
cartesianToPolarObject
+

Convert coordinates from cartesian to polar

+
+
dist2Number
+

Distance between two points (2D and 3D) squared

+
+
distToSegment2Number
+

Distance between point and segment squared

+
+
distToSegmentNumber
+

Distance between point and segment

+
+
mapNumber
+

Map a number from one range to another

+
+
clampNumber
+

Clamp a number to range

+
+
normNumber
+

Normalize a number

+
+
lerpNumber
+

Linear interpolation

+
+
squareNumber
+

Square

+
+
degreesNumber
+

Convert angle in radians to degrees

+
+
radiansNumber
+

Convert angle in degrees to radians

+
+
precisionNumber
+

Round number to precision

+
+
shallowCopyExcludingObject
+

Copy object excluding property

+
+
randomNumber
+

Generate random number from range

+
+
randomDirNumber
+

Generate random direction (-1 or 1)

+
+
randomIndexNumber
+

Generate random index

+
+
randomNameString
+

Generate random name

+
+
timestampNameString
+

Generate timestamp name

+
+
removeDiacriticsString
+

Remove polish diacritics

+
+
removeNonAlphaNumericString
+

Remove all non alphanumeric characters

+
+
splitChunksArray
+

Split string to N sized chunks

+
+
sepCasestring
+

Convert string to custom separator case

+
+
snakeCasestring
+

Convert string to snake case

+
+
kebabCasestring
+

Convert string to kebab case

+
+
camelCasestring
+

Convert string to camel case

+
+
+ +## Typedefs + +
+
Point : Object
+
+
Circle : Object
+
+
+ + + +## PID +PID controller + + +* [PID](#module_PID) + * [.set(P, I, D)](#module_PID+set) + * [.update(current, target)](#module_PID+update) ⇒ number + + +* * * + + + +### piD.set(P, I, D) +Set PID gains + +**Kind**: instance method of [PID](#module_PID) +**Params** + +- P number = 0 - Proportional Gain +- I number = 0 - Integral Gain +- D number = 0 - Derivative Gain + + +* * * + + + +### piD.update(current, target) ⇒ number +Update PID controller + +**Kind**: instance method of [PID](#module_PID) +**Returns**: number - Output value **Params** -- value Number - Number to map -- low1 Number - Source range lower bound -- high1 Number - Source range upper bound -- low2 Number - Target range lower bound -- high2 Number - Target range upper bound +- current number - Current value +- target number - Target value * * * - + -### utils.clamp(value, min, max) ⇒ Number -Clamp a number to range +## copyArray ⇒ Array +Copy array -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - Clamped number +**Kind**: global constant +**Returns**: Array - copy of the array **Params** -- value Number - Number to clamp -- min Number - Range lower bound -- max Number - Range upper bound +- source Array - source array * * * - + -### utils.norm(value, start, stop) ⇒ Number -Normalize a number +## shuffleArray ⇒ Array +Shuffle array -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - normalized number (0.0 - 1.0) +**Kind**: global constant +**Returns**: Array - shuffled array copy **Params** -- value Number - value to normalize -- start Number - Source range lower bound -- stop Number - Source range upper bound +- source Array - source array * * * - + -### utils.random([low], high) ⇒ Number -Generate random number from range +## filterUnique ⇒ Array +Filter array unique -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - Random number +**Kind**: global constant +**Returns**: Array - array with unique elements only **Params** -- [low] Number - Range lower bound -- high Number - Range upper bound +- source Array - source array * * * - + -### utils.randomDir() ⇒ Number -Generate random direction (-1 or 1) +## fuzzySearch ⇒ Array +Fuzzy search element in list + +**Kind**: global constant +**Returns**: Array - elements matching search value +**Params** + +- list Array - Array of terms +- searchValue String - search value to find -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - Random direction * * * - + -### utils.lerp(start, stop, amt) ⇒ Number -Linear interpolation +## contains ⇒ boolean +Check if array contains -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - Interpolated value +**Kind**: global constant +**Returns**: boolean - - true when element is in array **Params** -- start Number - First value -- stop Number - Second value -- amt Number - amount to interpolate +- elem any - element to find in array +- arr Array - array to look in * * * - + -### utils.lerp3(A, B, amt) ⇒ Point -Linear interpolation in 3D +## lerpColor ⇒ String +Linear color interpolation -**Kind**: static method of [utils](#module_utils) -**Returns**: Point - Interpolated point +**Kind**: global constant +**Returns**: String - Interpolated color **Params** -- A Point - First point -- B Point - Second point +- a String - First color +- b String - Second color - amt Number - amount to interpolate * * * - + -### utils.lerpedPoints(A, B, count) ⇒ Array.<Point> -Linear interpolation in 3D array +## getQuarter ⇒ Array +Get quarter from date -**Kind**: static method of [utils](#module_utils) -**Returns**: Array.<Point> - Interpolated points +**Kind**: global constant +**Returns**: Array - year and quarter (1-4) **Params** -- A Point - First point -- B Point - Second point -- count Number - Point count +- d Date - Date to get quarter from * * * - + -### utils.square(a) ⇒ Number -Square +## quarterExtent ⇒ Array +Get quarter extent -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - squared number +**Kind**: global constant +**Returns**: Array - start and end date of quarter **Params** -- a Number - Number to square +- quarter Number - quarter (1-4) +- year Number - full year * * * - + -### utils.dist(A, B) ⇒ Number -Distance between two points (2D and 3D) +## datesBetween ⇒ Array +Get all dates between two dates -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - distance between the points +**Kind**: global constant +**Returns**: Array - all dates between start and end **Params** -- A Point - First point -- B Point - Second point +- start Date - start date +- end Date - end date * * * - + -### utils.degrees(radians) ⇒ Number -Convert angle in radians to degrees +## lerp3 ⇒ [Point](#Point) +Linear interpolation in 3D -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - angle in degrees +**Kind**: global constant +**Returns**: [Point](#Point) - Interpolated point **Params** -- radians Number - angle in radians +- A [Point](#Point) - First point +- B [Point](#Point) - Second point +- amt Number - amount to interpolate * * * - + -### utils.radians(degrees) ⇒ Number -Convert angle in degrees to radians +## lerpStops ⇒ [Array.<Point>](#Point) +Linear interpolation in 3D array -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - angle in radians +**Kind**: global constant +**Returns**: [Array.<Point>](#Point) - Interpolated points **Params** -- degrees Number - angle in degrees +- A [Point](#Point) - First point +- B [Point](#Point) - Second point +- count Number - Point count * * * - + -### utils.intersection(c1, c2) ⇒ Array \| Boolean -Find intersection points between two circles +## dist ⇒ Number +Distance between two points (2D and 3D) -**Kind**: static method of [utils](#module_utils) -**Returns**: Array \| Boolean - intersection or false (if no intersection) +**Kind**: global constant +**Returns**: Number - distance between the points **Params** -- c1 Circle - first circle -- c2 Circle - second circle +- A [Point](#Point) - First point +- B [Point](#Point) - Second point * * * - + -### utils.randomName(N) ⇒ String -Generate random name +## intersectLines ⇒ [Point](#Point) \| Boolean +Find intersection point between two lines -**Kind**: static method of [utils](#module_utils) -**Returns**: String - random name +**Kind**: global constant +**Returns**: [Point](#Point) \| Boolean - intersection or false (if no intersection) **Params** -- N Number - length of the name +- p1 [Point](#Point) - first point of first line +- p2 [Point](#Point) - second point of first line +- p3 [Point](#Point) - first point of second line +- p4 [Point](#Point) - second point of second line * * * - + -### utils.timestampName() ⇒ String -Generate timestamp name +## intersectCircles ⇒ Array \| Boolean +Find intersection points between two circles + +**Kind**: global constant +**Returns**: Array \| Boolean - intersection or false (if no intersection) +**Params** + +- c1 [Circle](#Circle) - first circle +- c2 [Circle](#Circle) - second circle -**Kind**: static method of [utils](#module_utils) -**Returns**: String - timestamp name * * * - + -### utils.randomIndex(N) ⇒ Number -Generate random name +## polarToCartesian ⇒ [Point](#Point) +Convert coordinates from polar to cartesian -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - random index +**Kind**: global constant +**Returns**: [Point](#Point) - cartesian coordinates **Params** -- N Number - max index +- r Number - radius +- angle Number - angle * * * - + -### utils.copyArray(source) ⇒ Array -Copy array +## cartesianToPolar ⇒ Object +Convert coordinates from cartesian to polar -**Kind**: static method of [utils](#module_utils) -**Returns**: Array - array copy +**Kind**: global constant +**Returns**: Object - polar coordinates **Params** -- source Array - source array +- P [Point](#Point) - cartesian coordinates * * * - + -### utils.shuffleArray(source) ⇒ Array -Shuffle array +## dist2 ⇒ Number +Distance between two points (2D and 3D) squared -**Kind**: static method of [utils](#module_utils) -**Returns**: Array - shuffled array copy +**Kind**: global constant +**Returns**: Number - squared distance between the points **Params** -- source Array - source array +- A [Point](#Point) - First point +- B [Point](#Point) - Second point * * * - + -### utils.filterUnique(source) ⇒ Array -Filter array unique +## distToSegment2 ⇒ Number +Distance between point and segment squared -**Kind**: static method of [utils](#module_utils) -**Returns**: Array - array with unique elements only +**Kind**: global constant +**Returns**: Number - squared distance between the point and the segment **Params** -- source Array - source array +- A [Point](#Point) - First point +- S [Point](#Point) - Segment start +- E [Point](#Point) - Segment end * * * - + -### utils.lerpColor(a, b, amt) ⇒ String -Linear color interpolation +## distToSegment ⇒ Number +Distance between point and segment -**Kind**: static method of [utils](#module_utils) -**Returns**: String - Interpolated color +**Kind**: global constant +**Returns**: Number - distance between the point and the segment **Params** -- a String - First color -- b String - Second color -- amt Number - amount to interpolate +- A [Point](#Point) - First point +- S [Point](#Point) - Segment start +- E [Point](#Point) - Segment end * * * - + -### utils.precision(value, precision) ⇒ Number -Round number to precision +## map ⇒ Number +Map a number from one range to another -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - rounded number +**Kind**: global constant +**Returns**: Number - Mapped number **Params** -- value Number - value to round -- precision Number - decimal places +- value Number - Number to map +- low1 Number - Source range lower bound +- high1 Number - Source range upper bound +- low2 Number - Target range lower bound +- high2 Number - Target range upper bound * * * - + -### utils.loadJSON(address, callback) -Load JSON +## clamp ⇒ Number +Clamp a number to range -**Kind**: static method of [utils](#module_utils) +**Kind**: global constant +**Returns**: Number - Clamped number **Params** -- address String - address of JSON to load -- callback function - function to call on result +- value Number - Number to clamp +- min Number - Range lower bound +- max Number - Range upper bound * * * - + -### utils.removeDiacritics(str) ⇒ String -Remove polish diacritics +## norm ⇒ Number +Normalize a number -**Kind**: static method of [utils](#module_utils) -**Returns**: String - string without diacritics +**Kind**: global constant +**Returns**: Number - normalized number (0.0 - 1.0) **Params** -- str String - string with diacritics +- value Number - value to normalize +- start Number - Source range lower bound +- stop Number - Source range upper bound * * * - + -### utils.removeNonAlphaNumeric(str) ⇒ String -Remove all non alphanumeric characters +## lerp ⇒ Number +Linear interpolation -**Kind**: static method of [utils](#module_utils) -**Returns**: String - string without non alphanumeric characters +**Kind**: global constant +**Returns**: Number - Interpolated value **Params** -- str String - string with non alphanumeric characters +- start Number - First value +- stop Number - Second value +- amt Number - amount to interpolate * * * - + -### utils.splitChunks(str, n, discard) ⇒ Array -Split string to N sized chunks +## square ⇒ Number +Square -**Kind**: static method of [utils](#module_utils) -**Returns**: Array - array of string chunks +**Kind**: global constant +**Returns**: Number - squared number **Params** -- str String - string to split -- n Number - chunk length -- discard Boolean - discard chunks shorter than N +- a Number - Number to square * * * - + -### utils.getQuarter(d) ⇒ Array -Get quarter from date +## degrees ⇒ Number +Convert angle in radians to degrees -**Kind**: static method of [utils](#module_utils) -**Returns**: Array - year and quarter (1-4) +**Kind**: global constant +**Returns**: Number - angle in degrees **Params** -- d Date - Date to get quarter from +- radians Number - angle in radians * * * - + -### utils.quarterExtent(quarter, year) ⇒ Array -Get quarter extent +## radians ⇒ Number +Convert angle in degrees to radians -**Kind**: static method of [utils](#module_utils) -**Returns**: Array - start and end date of quarter +**Kind**: global constant +**Returns**: Number - angle in radians **Params** -- quarter Number - quarter (1-4) -- year Number - full year +- degrees Number - angle in degrees * * * - + -### utils.datesBetween(start, end) ⇒ Array -Get all dates between two dates +## precision ⇒ Number +Round number to precision -**Kind**: static method of [utils](#module_utils) -**Returns**: Array - all dates between start and end +**Kind**: global constant +**Returns**: Number - rounded number **Params** -- start Date - start date -- end Date - end date +- value Number - value to round +- precision Number - decimal places * * * - + -### utils.downloadDataUri(options) -Download file from base64 data uri +## shallowCopyExcluding ⇒ Object +Copy object excluding property -**Kind**: static method of [utils](#module_utils) +**Kind**: global constant +**Returns**: Object - - copied object **Params** -- options Object - options for the downloaded file - - .data String - contents of the file - - .filename String - name of the file +- obj Object - Object to copy +- prop string - property name * * * - + -### utils.polarToCartesian(r, angle) ⇒ Point -Convert coordinates from polar to cartesian +## random ⇒ Number +Generate random number from range -**Kind**: static method of [utils](#module_utils) -**Returns**: Point - cartesian coordinates +**Kind**: global constant +**Returns**: Number - Random number **Params** -- r Number - radius -- angle Number - angle +- [low] Number - Range lower bound +- high Number - Range upper bound * * * - + -### utils.cartesianToPolar(x, y) ⇒ Object -Convert coordinates from cartesian to polar - -**Kind**: static method of [utils](#module_utils) -**Returns**: Object - polar coordinates -**Params** - -- x Number - x coordinate -- y Number - y coordinate +## randomDir ⇒ Number +Generate random direction (-1 or 1) +**Kind**: global constant +**Returns**: Number - Random direction * * * - + -### utils.pageOffset(elem) ⇒ Object -Get element page offset +## randomIndex ⇒ Number +Generate random index -**Kind**: static method of [utils](#module_utils) -**Returns**: Object - top and left page offset +**Kind**: global constant +**Returns**: Number - random index **Params** -- elem Object - HTML element +- N Number - max index * * * - + -### utils.fuzzySearch(list, searchValue) ⇒ Array -Fuzzy search element in list +## randomName ⇒ String +Generate random name -**Kind**: static method of [utils](#module_utils) -**Returns**: Array - elements matching search value +**Kind**: global constant +**Returns**: String - random name **Params** -- list Array - Array of terms -- searchValue String - search value to find +- N Number - length of the name * * * - + -### utils.dist2(A, B) ⇒ Number -Distance between two points (2D and 3D) squared +## timestampName ⇒ String +Generate timestamp name -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - squared distance between the points +**Kind**: global constant +**Returns**: String - timestamp name + +* * * + + + +## removeDiacritics ⇒ String +Remove polish diacritics + +**Kind**: global constant +**Returns**: String - string without diacritics **Params** -- A Point - First point -- B Point - Second point +- str String - string with diacritics * * * - + -### utils.distToSegment2(A, S, E) ⇒ Number -Distance between point and segment squared +## removeNonAlphaNumeric ⇒ String +Remove all non alphanumeric characters -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - squared distance between the point and the segment +**Kind**: global constant +**Returns**: String - string without non alphanumeric characters **Params** -- A Point - First point -- S Point - Segment start -- E Point - Segment end +- str String - string with non alphanumeric characters * * * - + -### utils.distToSegment(A, S, E) ⇒ Number -Distance between point and segment +## splitChunks ⇒ Array +Split string to N sized chunks -**Kind**: static method of [utils](#module_utils) -**Returns**: Number - distance between the point and the segment +**Kind**: global constant +**Returns**: Array - array of string chunks **Params** -- A Point - First point -- S Point - Segment start -- E Point - Segment end +- str String - string to split +- n Number - chunk length +- discard Boolean - discard chunks shorter than N * * * - + -### utils.sepCase(str) ⇒ string +## sepCase ⇒ string Convert string to custom separator case -**Kind**: static method of [utils](#module_utils) +**Kind**: global constant **Returns**: string - custom cased string **Params** @@ -606,12 +732,12 @@ Convert string to custom separator case * * * - + -### utils.snakeCase(str) ⇒ string +## snakeCase ⇒ string Convert string to snake case -**Kind**: static method of [utils](#module_utils) +**Kind**: global constant **Returns**: string - snake cased string **Params** @@ -620,12 +746,12 @@ Convert string to snake case * * * - + -### utils.kebabCase(str) ⇒ string +## kebabCase ⇒ string Convert string to kebab case -**Kind**: static method of [utils](#module_utils) +**Kind**: global constant **Returns**: string - kebab cased string **Params** @@ -634,12 +760,12 @@ Convert string to kebab case * * * - + -### utils.camelCase(str) ⇒ string +## camelCase ⇒ string Convert string to camel case -**Kind**: static method of [utils](#module_utils) +**Kind**: global constant **Returns**: string - camel cased string **Params** @@ -648,114 +774,10 @@ Convert string to camel case * * * - - -### utils.contains(elem, arr) ⇒ boolean -Check if array contains - -**Kind**: static method of [utils](#module_utils) -**Returns**: boolean - - true when element is in array -**Params** - -- elem any - element to find in array -- arr Array - array to look in - - -* * * - - - -### utils.getCSS(parentElement) ⇒ string -Get CSS Styles from element - -**Kind**: static method of [utils](#module_utils) -**Returns**: string - - extracted CSS -**Params** - -- parentElement HTMLElement - Element to get styles from - - -* * * - - - -### utils.appendCSS(cssText, element) -Append CSS to element - -**Kind**: static method of [utils](#module_utils) -**Params** - -- cssText string - CSS text to append -- element HTMLElement - element to append CSS to - - -* * * - - - -### utils.getSVGString(svgNode) ⇒ string -Get SVG string from node - -**Kind**: static method of [utils](#module_utils) -**Returns**: string - - svg as string -**Params** - -- svgNode HTMLElement - svg node to get text from - - -* * * - - - -### utils.svgStringToImage(svgString, width, height, format, transparent, callback) -Convert SVG string to image and call the callback - -**Kind**: static method of [utils](#module_utils) -**Params** - -- svgString string - SVG string to convert -- width Number - width of output image -- height Number - height of output image -- format string - format of output image -- transparent boolean - transparency flag -- callback function - function to call when ready - - -* * * - - - -### utils.svgToUri(svgNode) ⇒ string -Convert SVG to data uri - -**Kind**: static method of [utils](#module_utils) -**Returns**: string - - uri data scheme string -**Params** - -- svgNode HTMLElement - SVG element to get uri from - - -* * * - - - -### utils.shallowCopyExcluding(obj, prop) ⇒ Object -Copy object excluding property - -**Kind**: static method of [utils](#module_utils) -**Returns**: Object - - copied object -**Params** - -- obj Object - Object to copy -- prop string - property name - - -* * * - - + -### utils~Point : Object -**Kind**: inner typedef of [utils](#module_utils) +## Point : Object +**Kind**: global typedef **Properties** - x Number - x coordinate @@ -765,10 +787,10 @@ Copy object excluding property * * * - + -### utils~Circle : Object -**Kind**: inner typedef of [utils](#module_utils) +## Circle : Object +**Kind**: global typedef **Properties** - x Number - x coordinate of the center point @@ -779,4 +801,4 @@ Copy object excluding property * * * -Copyright © 2023 panGenerator +[panGenerator](https://pangenerator.com) 2024 diff --git a/jsdoc.conf b/jsdoc.conf new file mode 100644 index 0000000..15d1b97 --- /dev/null +++ b/jsdoc.conf @@ -0,0 +1,5 @@ +{ + "source": { + "includePattern": ".+\\.(js(doc|x)?|mjs)$" + } +} diff --git a/package.json b/package.json index 84f7d31..1c3b09a 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,13 @@ { "name": "@pangenerator/utils", "version": "2.8.7", - "description": "A collection of snippets for creative coding", + "description": "A collection of functions and classes for creative coding and interactive projects", "main": "utils.js", "scripts": { "rollup": "rollup --config src/rollup.config.mjs", - "docs": "jsdoc2md --template src/README.hbs --files src/utils.js --separators --param-list-format list --property-list-format list --helper src/year.js> README.md", + "docs": "jsdoc2md -c jsdoc.conf --template src/README.hbs --files src/**/*.mjs --separators --param-list-format list --property-list-format list --helper src/year.js> README.md", "build": "npm run rollup && npm run docs", - "pretest": "npm run build", - "test": "mocha" + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --collectCoverage" }, "engines": { "npm": ">=7.0.0 <=20.0.0", @@ -32,16 +31,22 @@ }, "license": "MIT", "devDependencies": { - "@rollup/plugin-terser": "^0.4.3", - "eslint": "^7.0.0", - "eslint-config-standard": "^14.1.1", - "eslint-plugin-import": "^2.20.2", - "eslint-plugin-mocha": "^7.0.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-standard": "^4.0.1", - "jsdoc-to-markdown": "^6.0.1", - "mocha": "^7.1.2", - "rollup": "^3.25.1" + "@rollup/plugin-terser": "^0.4.4", + "jest": "^29.7.0", + "jsdoc-to-markdown": "^8.0.3", + "rollup": "^4.20.0" + }, + "jest": { + "testMatch": [ + "**/?(*.)test.?js" + ], + "transform": {} + }, + "prettier": { + "bracketSameLine": true, + "printWidth": 80, + "semi": false, + "singleQuote": true, + "tabWidth": 2 } } diff --git a/src/README.hbs b/src/README.hbs index 407ed92..b4c7889 100644 --- a/src/README.hbs +++ b/src/README.hbs @@ -1,6 +1,6 @@ -[![Version](http://img.shields.io/npm/v/@pangenerator/utils.svg)](https://www.npmjs.org/package/@pangenerator/utils) [![Tests](https://img.shields.io/github/workflow/status/panGenerator/utils/Tests)](https://github.com/panGenerator/utils) -![Dependencies](https://img.shields.io/david/panGenerator/utils) ![Dev Dependencies](https://img.shields.io/david/dev/panGenerator/utils) +[![Version](http://img.shields.io/npm/v/@pangenerator/utils.svg)](https://www.npmjs.org/package/@pangenerator/utils) +[![Tests](https://img.shields.io/github/actions/workflow/status/panGenerator/utils/tests.yml)](https://github.com/panGenerator/utils) {{>main}} -Copyright © {{year}} panGenerator +[panGenerator](https://pangenerator.com) {{year}} diff --git a/src/TweakpaneSettings.js b/src/TweakpaneSettings.js deleted file mode 100644 index 8c71007..0000000 --- a/src/TweakpaneSettings.js +++ /dev/null @@ -1,202 +0,0 @@ -import { snakeCase, shallowCopyExcluding } from "./utils.js"; - -export default class TweakpaneSettings { - constructor(ctrl, controllables, settingsName = null) { - this.settingsName = settingsName ?? ctrl.title + "-settings"; - this.ctrl = ctrl; - this.presets = controllables[0].settings.presets; - if (this.presets) { - Object.keys(this.presets).forEach((p) => { - this.presets[p] = JSON.parse(this.presets[p]); - }); - } else { - this.presets = {}; - } - controllables.forEach((g, i) => { - const folder = g.settings.name - ? ctrl.addFolder({ title: g.settings.name }) - : ctrl; - - if ("controls" in g.settings) { - Object.keys(g.settings.controls).forEach((s) => { - const options = shallowCopyExcluding(g.settings.controls[s], "val"); - options.presetKey = g.settings.name - ? snakeCase(g.settings.name) + "_$" + s - : null; - options.label = options.label ? options.label : s; - - // shortcut props - Object.defineProperty(g, "$" + s, { - get: () => { - return g.settings.controls[s].val; - }, - set: (v) => { - g.settings.controls[s].val = v; - }, - }); - - const input = folder.addInput(g, "$" + s, options); //g.settings.controls[s].val - - if (options.callback) { - input.on("change", (ev) => { - options.callback(ev); - }); - } - }); - } - - if ("buttons" in g.settings) { - Object.keys(g.settings.buttons).forEach((b) => { - folder - .addButton({ title: g.settings.buttons[b].label }) - .on("click", () => { - g.settings.buttons[b].callback(); - }); - }); - } - - if ("buttons_grid" in g.settings) { - g.settings.buttons_grid.grids.forEach((bg) => { - folder - .addBlade({ - view: "buttongrid", - size: bg.size, - label: bg.label, - cells: bg.cells, - }) - .on("click", (ev) => { - bg.callbacks[ev.index[1]][ev.index[0]](); - }); - }); - } - - if ("monitors" in g.settings) { - Object.keys(g.settings.monitors).forEach((m) => { - const options = g.settings.monitors_options[m]; - folder.addMonitor(g.settings.monitors, m, options); - - // shortcut props - Object.defineProperty(g, "$" + m, { - get: () => { - return g.settings.monitors[m]; - }, - set: (v) => { - g.settings.monitors[m] = v; - }, - }); - }); - } - }); - - const presets = ctrl.addFolder({ title: "Presets", expanded: false }); - - if (Object.keys(this.presets).length > 0) { - presets - .addBlade({ - view: "list", - label: "preset", - options: Object.keys(this.presets).map((p) => { - return { text: p, value: p }; - }), - value: Object.keys(this.presets)[0], - }) - .on("change", (ev) => { - this.loadSettings(this.presets[ev.value]); - }); - - presets.addSeparator(); - } - - presets.addButton({ title: "Store settings" }).on("click", () => { - console.log("save settings"); - const preset = ctrl.exportPreset(); - localStorage.setItem(settingsName, JSON.stringify(preset)); - console.log(preset); - console.log("json:\n", JSON.stringify(preset)); - }); - - presets.addButton({ title: "Restore settings" }).on("click", () => { - this.loadSettings(); - }); - - presets.addButton({ title: "Download settings" }).on("click", () => { - console.log("download settings"); - - const fileName = - settingsName + - "_" + - new Date().toLocaleString().replace(/[^0-9]+/g, "-") + - ".json"; - const url = - "data:text/json;charset=utf-8," + - encodeURIComponent(JSON.stringify(ctrl.exportPreset(), null, 2)); - console.log(JSON.stringify(ctrl.exportPreset())); - const link = document.createElement("a"); - link.download = fileName; - link.href = url; - link.click(); - link.remove(); - }); - - presets.addButton({ title: "Upload settings" }).on("click", () => { - console.log("upload settings"); - const input = document.createElement("input"); - input.setAttribute("type", "file"); - input.setAttribute("accept", "application/json"); - input.style.opacity = "0"; - input.style.position = "fixed"; - document.body.appendChild(input); - input.addEventListener( - "input", - (ev) => { - if (input.files && input.files[0]) { - const file = input.files[0]; - let reader = new FileReader(); - reader.readAsText(file); - - reader.onload = () => { - console.log("settings loaded..."); - console.log(reader.result); - ctrl.importPreset(JSON.parse(reader.result)); - }; - - reader.onerror = () => { - console.log("error loading file!"); - console.log(reader.error); - }; - } - - document.body.removeChild(input); - }, - { once: true } - ); - input.click(); - }); - - presets.addButton({ title: "Default settings" }).on("click", () => { - if (this.presets.default) { - this.loadSettings(this.presets.default); - } - }); - } - - loadSettings(settings = null) { - console.log("restore settings"); - console.log("presets", this.presets); - if (settings) { - this.ctrl.importPreset(settings); - console.log("loaded settings:", settings); - } else { - if (this.presets.default) { - this.ctrl.importPreset(this.presets.default); - console.log("loaded default settings:", this.presets.default); - } else { - const localPreset = localStorage.getItem(this.settingsName); - if (localPreset) { - this.ctrl.importPreset(JSON.parse(localPreset)); - console.log("loaded settings from local storage:", localPreset); - } - } - } - } -} diff --git a/src/banner.mjs b/src/banner.mjs index dee54b0..68c8416 100644 --- a/src/banner.mjs +++ b/src/banner.mjs @@ -1,11 +1,11 @@ //const pkg = require('../package.json') -import pkg from '../package.json' assert { type: 'json' }; -const year = new Date().getFullYear() +import pkg from "../package.json" with { "type": "json" } +const year = new Date().getFullYear(); export default (pluginFilename) => { return `/*! - * @license ${pkg.name} v${pkg.version}, Copyright © ${year} ${pkg.author} + * @license ${pkg.name} v${pkg.version}, ${pkg.author} ${year} * Released under ${pkg.license} license * ${pkg.homepage} - */` -} + */`; +}; diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 8ce1e01..0000000 --- a/src/main.js +++ /dev/null @@ -1,4 +0,0 @@ -import * as utils from './utils.js' -import TweakpaneSettings from './TweakpaneSettings.js' - -export default { TweakpaneSettings, ...utils } diff --git a/src/modules/arrays.mjs b/src/modules/arrays.mjs new file mode 100644 index 0000000..5f03f2a --- /dev/null +++ b/src/modules/arrays.mjs @@ -0,0 +1,63 @@ +/** + * Array functions + */ + +/** + * Copy array + * @param {Array} source - source array + * @returns {Array} copy of the array + */ +export const copyArray = (source) => { + const array = Array(source.length) + for (let i = 0; i < source.length; i++) { + array[i] = source[i] + } + return array +} + +/** + * Shuffle array + * @param {Array} source - source array + * @returns {Array} shuffled array copy + */ +export const shuffleArray = (source) => { + const array = copyArray(source) + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[array[i], array[j]] = [array[j], array[i]] + } + return array +} + +/** + * Filter array unique + * @param {Array} source - source array + * @returns {Array} array with unique elements only + */ +export const filterUnique = (source) => { + return [...new Set(source)] +} + +/** + * Fuzzy search element in list + * @param {Array} list - Array of terms + * @param {String} searchValue - search value to find + * @returns {Array} elements matching search value + */ +export const fuzzySearch = (list, searchValue) => { + const buf = '.*' + searchValue.replace(/(.)/g, '$1.*').toLowerCase() + var reg = new RegExp(buf) + return list.filter((e) => { + return reg.test(e.toLowerCase()) + }) +} + +/** + * Check if array contains + * @param {any} elem - element to find in array + * @param {Array} arr - array to look in + * @returns {boolean} - true when element is in array + */ +export const contains = (elem, arr) => { + return arr.indexOf(elem) !== -1 +} diff --git a/src/modules/browser.js b/src/modules/browser.js new file mode 100644 index 0000000..1b66a52 --- /dev/null +++ b/src/modules/browser.js @@ -0,0 +1,198 @@ +/** + * Browser functions + */ + +/** + * Load JSON + * @param {String} address - address of JSON to load + * @param {Function} callback - function to call on result + */ +export const loadJSON = (address, callback) => { + const xObj = new XMLHttpRequest() + xObj.overrideMimeType('application/json') + xObj.open('GET', address, true) + xObj.onreadystatechange = () => { + if (xObj.readyState === 4 && xObj.status === 200) { + callback(JSON.parse(xObj.responseText)) + } + } + xObj.send(null) +} + +/** + * Download file from base64 data uri + * @param {Object} options - options for the downloaded file + * @param {String} options.data - contents of the file + * @param {String} options.filename - name of the file + */ +export const downloadDataUri = (options) => { + var element = document.createElement('a') + element.setAttribute('href', options.data) + element.setAttribute('download', options.filename) + element.style.display = 'none' + document.body.appendChild(element) + element.click() + document.body.removeChild(element) +} + +/** + * Get element page offset + * @param {Object} elem - HTML element + * @returns {Object} top and left page offset + */ +export const pageOffset = (elem) => { + const rect = elem.getBoundingClientRect() + const win = elem.ownerDocument.defaultView + return { + top: rect.top + win.pageYOffset, + left: rect.left + win.pageXOffset, + } +} + +/** + * Get CSS Styles from element + * @param {HTMLElement} parentElement - Element to get styles from + * @returns {string} - extracted CSS + */ +export const getCSS = (parentElement) => { + const selectorTextArr = [] + + // Add Parent element Id and Classes to the list + selectorTextArr.push('#' + parentElement.id) + for (let c = 0; c < parentElement.classList.length; c++) { + if (!contains('.' + parentElement.classList[c], selectorTextArr)) { + selectorTextArr.push('.' + parentElement.classList[c]) + } + } + + // Add Children element Ids and Classes to the list + const nodes = parentElement.getElementsByTagName('*') + for (let i = 0; i < nodes.length; i++) { + const id = nodes[i].id + if (!contains('#' + id, selectorTextArr)) { + selectorTextArr.push('#' + id) + } + + const classes = nodes[i].classList + for (let c = 0; c < classes.length; c++) { + if (!contains('.' + classes[c], selectorTextArr)) { + selectorTextArr.push('.' + classes[c]) + } + } + } + // Extract CSS Rules + let extractedCSSText = '' + for (let i = 0; i < document.styleSheets.length; i++) { + var s = document.styleSheets[i] + try { + if (!s.cssRules) continue + } catch (e) { + if (e.name !== 'SecurityError') throw e // for Firefox + continue + } + + var cssRules = s.cssRules + for (var r = 0; r < cssRules.length; r++) { + if (contains(cssRules[r].selectorText, selectorTextArr)) { + extractedCSSText += cssRules[r].cssText + } + } + } + return extractedCSSText +} + +/** + * Append CSS to element + * @param {string} cssText - CSS text to append + * @param {HTMLElement} element - element to append CSS to + */ +export const appendCSS = (cssText, element) => { + var styleElement = document.createElement('style') + styleElement.setAttribute('type', 'text/css') + styleElement.innerHTML = cssText + var refNode = element.hasChildNodes() ? element.children[0] : null + element.insertBefore(styleElement, refNode) +} + +/** + * Get SVG string from node + * @param {HTMLElement} svgNode - svg node to get text from + * @returns {string} - svg as string + */ +export const getSVGString = (svgNode) => { + svgNode.setAttribute('xlink', 'http://www.w3.org/1999/xlink') + var cssStyleText = getCSS(svgNode) + appendCSS(cssStyleText, svgNode) + + var serializer = new XMLSerializer() + var svgString = serializer.serializeToString(svgNode) + svgString = svgString.replace(/(\w+)?:?xlink=/g, 'xmlns:xlink=') // Fix root xlink without namespace + svgString = svgString.replace(/NS\d+:href/g, 'xlink:href') // Safari NS namespace fix + + return svgString +} + +/** + * Convert SVG string to image and call the callback + * @param {string} svgString - SVG string to convert + * @param {Number} width - width of output image + * @param {Number} height - height of output image + * @param {string} format - format of output image + * @param {boolean} transparent - transparency flag + * @param {Function} callback - function to call when ready + */ +export const svgStringToImage = ( + svgString, + width, + height, + format, + transparent, + callback +) => { + format = format || 'png' + + var imgsrc = + 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgString))) // Convert SVG string to data URL + + var canvas = document.createElement('canvas') + var context = canvas.getContext('2d') + + canvas.width = width + canvas.height = height + + var image = new Image() + image.onload = () => { + context.clearRect(0, 0, width, height) + if (!transparent) { + context.beginPath() + context.fillStyle = '#fff' + context.fillRect(0, 0, canvas.width, canvas.height) + } + context.drawImage(image, 0, 0, width, height) + if (callback) callback(canvas.toDataURL()) + } + image.src = imgsrc +} + +/** + * Convert SVG to data uri + * @param {HTMLElement} svgNode - SVG element to get uri from + * @returns {string} - uri data scheme string + */ +export const svgToUri = (svgNode) => { + const serializer = new XMLSerializer() + let source = serializer.serializeToString(svgNode) + if (!source.match(/^]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) { + source = source.replace(/^]+"http:\/\/www\.w3\.org\/1999\/xlink"/)) { + source = source.replace( + /^ { + var ah = parseInt(a.replace(/#/g, ''), 16) + var ar = ah >> 16 + var ag = (ah >> 8) & 0xff + var ab = ah & 0xff + var bh = parseInt(b.replace(/#/g, ''), 16) + var br = bh >> 16 + var bg = (bh >> 8) & 0xff + var bb = bh & 0xff + var rr = ar + amount * (br - ar) + var rg = ag + amount * (bg - ag) + var rb = ab + amount * (bb - ab) + return ( + '#' + (((1 << 24) + (rr << 16) + (rg << 8) + rb) | 0).toString(16).slice(1) + ) +} diff --git a/src/modules/dates.mjs b/src/modules/dates.mjs new file mode 100644 index 0000000..bbd19be --- /dev/null +++ b/src/modules/dates.mjs @@ -0,0 +1,46 @@ +/** + * Date functions + */ +/** + * Get quarter from date + * @param {Date} d - Date to get quarter from + * @returns {Array} year and quarter (1-4) + */ +export const getQuarter = (d) => { + d = d || new Date() + return [d.getFullYear(), Math.floor(d.getMonth() / 3) + 1] +} // getQuarter().join('Q') + +/** + * Get quarter extent + * @param {Number} quarter - quarter (1-4) + * @param {Number} year - full year + * @returns {Array} start and end date of quarter + */ +export const quarterExtent = (quarter, year) => { + return [ + new Date( + `${year}-${((quarter - 1) * 3 + 1).toString().padStart(2, '0')}-01` + ), + new Date( + `${year}-${(quarter * 3).toString().padStart(2, '0')}-${ + quarter === 1 || quarter === 4 ? 31 : 30 + }` + ), + ] +} + +/** + * Get all dates between two dates + * @param {Date} start - start date + * @param {Date} end - end date + * @returns {Array} all dates between start and end + */ +export const datesBetween = (start, end) => { + const output = [] + + for (let date = start; date <= end; date.setDate(date.getDate() + 1)) { + output.push(new Date(date)) + } + return output +} diff --git a/src/modules/geometry.mjs b/src/modules/geometry.mjs new file mode 100644 index 0000000..99926f8 --- /dev/null +++ b/src/modules/geometry.mjs @@ -0,0 +1,206 @@ +/** + * Geometry functions + */ + +import { lerp, square } from './maths' +/** + * @typedef Point + * @type {Object} + * @property {Number} x - x coordinate + * @property {Number} y - y coordinate + * @property {Number} [z] - z coordinate + */ + +/** + * @typedef Circle + * @type {Object} + * @property {Number} x - x coordinate of the center point + * @property {Number} y - y coordinate of the center point + * @property {Number} r - radius + */ + +/** + * Linear interpolation in 3D + * @param {Point} A - First point + * @param {Point} B - Second point + * @param {Number} amt - amount to interpolate + * @returns {Point} Interpolated point + */ +export const lerp3 = (A, B, amt) => { + return { + x: lerp(A.x, B.x, amt), + y: lerp(A.y, B.y, amt), + z: lerp(A.z, B.z, amt), + } +} + +/** + * Linear interpolation in 3D array + * @param {Point} A - First point + * @param {Point} B - Second point + * @param {Number} count - Point count + * @returns {Point[]} Interpolated points + */ +export const lerpStops = (A, B, count) => { + const points = [] + const step = 1 / (count + 1) + for (let i = 0; i < count; i++) { + points.push(lerp3(A, B, step + step * i)) + } + return points +} + +/** + * Distance between two points (2D and 3D) + * @param {Point} A - First point + * @param {Point} B - Second point + * @returns {Number} distance between the points + */ +export const dist = (A, B) => { + return Math.sqrt(dist2(A, B)) +} + +/** + * Find intersection point between two lines + * @param {Point} p1 - first point of first line + * @param {Point} p2 - second point of first line + * @param {Point} p3 - first point of second line + * @param {Point} p4 - second point of second line + * @returns {(Point|Boolean)} intersection or false (if no intersection) + */ +export const intersectLines = (p1, p2, p3, p4) => { + if ((p1.x === p2.x && p1.y === p2.y) || (p3.x === p4.x && p3.y === p4.y)) { + return false + } + const denominator = + (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y) + if (denominator === 0) { + return false + } + let ua = + ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / + denominator + let ub = + ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / + denominator + if (ua < 0 || ua > 1 || ub < 0 || ub > 1) { + return false + } + let x = p1.x + ua * (p2.x - p1.x) + let y = p1.y + ua * (p2.y - p1.y) + return { x, y } +} + +/** + * Find intersection points between two circles + * @param {Circle} c1 - first circle + * @param {Circle} c2 - second circle + * @returns {(Array|Boolean)} intersection or false (if no intersection) + */ +export const intersectCircles = (c1, c2) => { + const dx = c2.x - c1.x + const dy = c2.y - c1.y + const d = Math.sqrt(dy * dy + dx * dx) + + if (d > c1.r + c2.r) { + return false + } + if (d < Math.abs(c1.r - c2.r)) { + return false + } + const a = (c1.r * c1.r - c2.r * c2.r + d * d) / (2.0 * d) + + const xc = c1.x + (dx * a) / d + const yc = c1.y + (dy * a) / d + + const h = Math.sqrt(c1.r * c1.r - a * a) + + const rx = -dy * (h / d) + const ry = dx * (h / d) + + return [ + { x: xc + rx, y: yc + ry }, + { x: xc - rx, y: yc - ry }, + ] +} + +/** + * Convert coordinates from polar to cartesian + * @param {Number} r - radius + * @param {Number} angle - angle + * @returns {Point} cartesian coordinates + */ +export const polarToCartesian = (r, angle) => { + return { + x: r * Math.cos(angle), + y: r * Math.sin(angle), + } +} + +/** + * Convert coordinates from cartesian to polar + * @param {Point} P - cartesian coordinates + * @returns {Object} polar coordinates + */ +export const cartesianToPolar = (P) => { + let angle = Math.atan2(P.y, P.x) + if (angle < 0) { + while (angle < 0) { + angle += Math.PI * 2.0 + } + } + // if (angle >= Math.PI * 2.0) { + // while (angle >= Math.PI) { + // angle -= Math.PI * 2.0 + // } + // } + return { + r: Math.sqrt(P.x * P.x + P.y * P.y), + angle, + } +} + +/** + * Distance between two points (2D and 3D) squared + * @param {Point} A - First point + * @param {Point} B - Second point + * @returns {Number} squared distance between the points + */ +export const dist2 = (A, B) => { + return ( + square(B.x - A.x) + + square(B.y - A.y) + + (A.z !== undefined && B.z !== undefined ? square(B.z - A.z) : 0) + ) +} + +/** + * Distance between point and segment squared + * @param {Point} A - First point + * @param {Point} S - Segment start + * @param {Point} E - Segment end + * @returns {Number} squared distance between the point and the segment + */ +export const distToSegment2 = (A, S, E) => { + const l2 = dist2(S, E) + + if (l2 === 0) return dist2(A, S) + + const t = ((A.x - S.x) * (E.x - S.x) + (A.y - S.y) * (E.y - S.y)) / l2 + + if (t < 0) return dist2(A, S) + if (t > 1) return dist2(A, E) + + return dist2(A, { x: S.x + t * (E.x - S.x), y: S.y + t * (E.y - S.y) }) +} + +/** + * Distance between point and segment + * @param {Point} A - First point + * @param {Point} S - Segment start + * @param {Point} E - Segment end + * @returns {Number} distance between the point and the segment + */ +export const distToSegment = (A, S, E) => { + return Math.sqrt(distToSegment2(A, S, E)) +} diff --git a/src/modules/maths.mjs b/src/modules/maths.mjs new file mode 100644 index 0000000..a7af56c --- /dev/null +++ b/src/modules/maths.mjs @@ -0,0 +1,86 @@ +/** + * Math functions + */ + +/** + * Map a number from one range to another + * @param {Number} value - Number to map + * @param {Number} low1 - Source range lower bound + * @param {Number} high1 - Source range upper bound + * @param {Number} low2 - Target range lower bound + * @param {Number} high2 - Target range upper bound + * @returns {Number} Mapped number + */ +export const map = (value, low1, high1, low2, high2) => { + return low2 + ((high2 - low2) * (value - low1)) / (high1 - low1) +} + +/** + * Clamp a number to range + * @param {Number} value - Number to clamp + * @param {Number} min - Range lower bound + * @param {Number} max - Range upper bound + * @returns {Number} Clamped number + */ +export const clamp = (val, min, max) => { + return val > max ? max : val < min ? min : val +} + +/** + * Normalize a number + * @param {Number} value - value to normalize + * @param {Number} start - Source range lower bound + * @param {Number} stop - Source range upper bound + * @returns {Number} normalized number (0.0 - 1.0) + */ +export const norm = (value, start, stop) => { + return (value - start) / (stop - start) +} + +/** + * Linear interpolation + * @param {Number} start - First value + * @param {Number} stop - Second value + * @param {Number} amt - amount to interpolate + * @returns {Number} Interpolated value + */ +export const lerp = (start, stop, amt) => { + return start + (stop - start) * amt +} + +/** + * Square + * @param {Number} a - Number to square + * @returns {Number} squared number + */ +export const square = (a) => { + return a * a +} + +/** + * Convert angle in radians to degrees + * @param {Number} radians - angle in radians + * @returns {Number} angle in degrees + */ +export const degrees = (radians) => { + return (radians * 180.0) / Math.PI +} + +/** + * Convert angle in degrees to radians + * @param {Number} degrees - angle in degrees + * @returns {Number} angle in radians + */ +export const radians = (degrees) => { + return (degrees * Math.PI) / 180.0 +} + +/** + * Round number to precision + * @param {Number} value - value to round + * @param {Number} precision - decimal places + * @returns {Number} rounded number + */ +export const precision = (value, precision) => { + return Math.round(value * Math.pow(10, precision)) / Math.pow(10, precision) +} diff --git a/src/modules/objects.mjs b/src/modules/objects.mjs new file mode 100644 index 0000000..5db1016 --- /dev/null +++ b/src/modules/objects.mjs @@ -0,0 +1,15 @@ +/** + * Object functions + */ + +/** + * Copy object excluding property + * @param {Object} obj - Object to copy + * @param {string} prop - property name + * @returns {Object} - copied object + */ + +export const shallowCopyExcluding = (obj, prop) => { + const { [prop]: _, ...copy } = obj + return copy +} diff --git a/src/modules/pid.mjs b/src/modules/pid.mjs new file mode 100644 index 0000000..1b4fff5 --- /dev/null +++ b/src/modules/pid.mjs @@ -0,0 +1,39 @@ +/** + * PID controller + * @module PID + * @alias pid + */ +export default class PID { + constructor(P = 0, I = 0, D = 0) { + this.set(P, I, D) + this.ep = 0 // error proportional + this.ei = 0 // error integral + this.ed = 0 // error derivative + } + + /** + * Set PID gains + * @param {number} P - Proportional Gain + * @param {number} I - Integral Gain + * @param {number} D - Derivative Gain + */ + set(P = 0, I = 0, D = 0) { + this.Kp = P // Proportional Gain + this.Ki = I // Integral Gain + this.Kd = D // Derivative Gain + } + + /** + * Update PID controller + * @param {number} current - Current value + * @param {number} target - Target value + * @returns {number} Output value + */ + update(current, target) { + const error = target - current + this.ei += error + this.ed = error - this.ep + this.ep = error + return this.Kp * this.ep + this.Ki * this.ei + this.Kd * this.ed + } +} diff --git a/src/modules/random.mjs b/src/modules/random.mjs new file mode 100644 index 0000000..91a7bc8 --- /dev/null +++ b/src/modules/random.mjs @@ -0,0 +1,43 @@ +/** + * Random functions + */ + +/** + * Generate random number from range + * @param {Number} [low] - Range lower bound + * @param {Number} high - Range upper bound + * @returns {Number} Random number + */ +export const random = (low, high) => { + if (high === undefined) { + high = low + low = 0 + } + return low + Math.random() * (high - low) +} + +/** + * Generate random direction (-1 or 1) + * @returns {Number} Random direction + */ +export const randomDir = () => { + return Math.random() > 0.5 ? 1 : -1 +} + +/** + * Generate random index + * @param {Number} N - max index + * @returns {Number} random index + */ +export const randomIndex = (N) => { + return Math.floor(Math.random() * N) +} + +/** + * Generate random name + * @param {Number} N - length of the name + * @returns {String} random name + */ +export const randomName = (N) => { + return (Math.random().toString(36) + '00000000000000000').slice(2, N + 2) +} diff --git a/src/modules/strings.mjs b/src/modules/strings.mjs new file mode 100644 index 0000000..576b5db --- /dev/null +++ b/src/modules/strings.mjs @@ -0,0 +1,120 @@ +/** + * String functions + */ + +/** + * Generate timestamp name + * @returns {String} timestamp name + */ +export const timestampName = () => { + var tzoffset = new Date().getTimezoneOffset() * 60000 + let date = new Date(Date.now() - tzoffset) + .toISOString() + .replace(/z|t/gi, ' ') + .trim() + .replace(/:/gi, '-') + date = date.substring(0, date.indexOf('.')) + return date +} +const table = { + Ą: 'A', + Ć: 'C', + Ę: 'E', + Ł: 'L', + Ń: 'N', + Ó: 'O', + Ś: 'S', + Ź: 'Z', + Ż: 'Z', + ą: 'a', + ć: 'c', + ę: 'e', + ł: 'l', + ń: 'n', + ó: 'o', + ś: 's', + ź: 'z', + ż: 'z', +} + +/** + * Remove polish diacritics + * @param {String} str - string with diacritics + * @returns {String} string without diacritics + */ +export const removeDiacritics = (str) => { + return str.replace(/([ĄĆĘŁŃÓŚŹŻąćęłńóśźż])/g, function (l) { + return table[l] + }) +} + +/** + * Remove all non alphanumeric characters + * @param {String} str - string with non alphanumeric characters + * @returns {String} string without non alphanumeric characters + */ +export const removeNonAlphaNumeric = (str) => str.replace(/[^A-Za-z0-9]/g, '') + +/** + * Split string to N sized chunks + * @param {String} str - string to split + * @param {Number} n - chunk length + * @param {Boolean} discard - discard chunks shorter than N + * @returns {Array} array of string chunks + */ +export const splitChunks = (str, n, discard) => { + const chunks = str.split(new RegExp('(.{' + n.toString() + '})')) + return discard + ? chunks.filter((x) => x.length === n) + : chunks.filter((x) => x.length > 0) +} + +/** + * Convert string to custom separator case + * @param {string} str - string to convert + * @returns {string} custom cased string + */ +export const sepCase = (str, sep = '-') => { + const text = removeDiacritics(str) + // text = removeNonAlphaNumeric(text) + return text + .replace(/[A-Z]/g, (letter, index) => { + const lcLet = letter.toLowerCase() + return index ? sep + lcLet : lcLet + }) + .replace(/([-_ ]){1,}/g, sep) +} + +/** + * Convert string to snake case + * @param {string} str - string to convert + * @returns {string} snake cased string + */ +export const snakeCase = (str) => { + return sepCase(str, '_') +} + +/** + * Convert string to kebab case + * @param {string} str - string to convert + * @returns {string} kebab cased string + */ +export const kebabCase = (str) => { + return sepCase(str, '-') +} + +/** + * Convert string to camel case + * @param {string} str - string to convert + * @returns {string} camel cased string + */ +export const camelCase = (str) => { + const text = removeDiacritics(str) + // text = removeNonAlphaNumeric(text) + return (text.slice(0, 1).toLowerCase() + text.slice(1)) + .replace(/([-_ ]){1,}/g, ' ') + .split(/[-_ ]/) + .reduce((cur, acc) => { + return cur + acc[0].toUpperCase() + acc.substring(1) + }) +} diff --git a/src/rollup.config.mjs b/src/rollup.config.mjs index 3091d8a..263b1ca 100644 --- a/src/rollup.config.mjs +++ b/src/rollup.config.mjs @@ -2,12 +2,16 @@ import terser from '@rollup/plugin-terser' import banner from './banner.mjs' export default { - input: 'src/main.js', + input: 'src/utils.mjs', output: { banner, name: 'utils', file: 'utils.js', - format: 'umd' + format: 'umd', }, - plugins: [terser()] -} \ No newline at end of file + plugins: [ + terser({ + mangle: false, + }), + ], +} diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 6090d7f..0000000 --- a/src/utils.js +++ /dev/null @@ -1,849 +0,0 @@ -/** - * Various functions used in javascript tools - * @module utils - */ - -/** - * @typedef Point - * @type {Object} - * @property {Number} x - x coordinate - * @property {Number} y - y coordinate - * @property {Number} [z] - z coordinate - */ - -/** - * @typedef Circle - * @type {Object} - * @property {Number} x - x coordinate of the center point - * @property {Number} y - y coordinate of the center point - * @property {Number} r - radius - */ - -/** - * Map a number from one range to another - * @alias module:utils.map - * @param {Number} value - Number to map - * @param {Number} low1 - Source range lower bound - * @param {Number} high1 - Source range upper bound - * @param {Number} low2 - Target range lower bound - * @param {Number} high2 - Target range upper bound - * @returns {Number} Mapped number - */ -const map = (value, low1, high1, low2, high2) => { - return low2 + ((high2 - low2) * (value - low1)) / (high1 - low1); -}; - -/** - * Clamp a number to range - * @alias module:utils.clamp - * @param {Number} value - Number to clamp - * @param {Number} min - Range lower bound - * @param {Number} max - Range upper bound - * @returns {Number} Clamped number - */ -const clamp = (val, min, max) => { - return val > max ? max : val < min ? min : val; -}; - -/** - * Normalize a number - * @alias module:utils.norm - * @param {Number} value - value to normalize - * @param {Number} start - Source range lower bound - * @param {Number} stop - Source range upper bound - * @returns {Number} normalized number (0.0 - 1.0) - */ -const norm = (value, start, stop) => { - return (value - start) / (stop - start); -}; - -/** - * Generate random number from range - * @alias module:utils.random - * @param {Number} [low] - Range lower bound - * @param {Number} high - Range upper bound - * @returns {Number} Random number - */ -const random = (low, high) => { - if (high === undefined) { - high = low; - low = 0; - } - return low + Math.random() * (high - low); -}; - -/** - * Generate random direction (-1 or 1) - * @alias module:utils.randomDir - * @returns {Number} Random direction - */ -const randomDir = () => { - return Math.random() > 0.5 ? 1 : -1; -}; - -/** - * Linear interpolation - * @alias module:utils.lerp - * @param {Number} start - First value - * @param {Number} stop - Second value - * @param {Number} amt - amount to interpolate - * @returns {Number} Interpolated value - */ -const lerp = (start, stop, amt) => { - return start + (stop - start) * amt; -}; - -/** - * Linear interpolation in 3D - * @alias module:utils.lerp3 - * @param {Point} A - First point - * @param {Point} B - Second point - * @param {Number} amt - amount to interpolate - * @returns {Point} Interpolated point - */ -const lerp3 = (A, B, amt) => { - return { - x: lerp(A.x, B.x, amt), - y: lerp(A.y, B.y, amt), - z: lerp(A.z, B.z, amt), - }; -}; - -/** - * Linear interpolation in 3D array - * @alias module:utils.lerpedPoints - * @param {Point} A - First point - * @param {Point} B - Second point - * @param {Number} count - Point count - * @returns {Point[]} Interpolated points - */ -const lerpedPoints = (A, B, count) => { - const points = []; - const step = 1 / (count + 1); - for (let i = 0; i < count; i++) { - points.push(lerp3(A, B, step + step * i)); - } - return points; -}; - -/** - * Square - * @alias module:utils.square - * @param {Number} a - Number to square - * @returns {Number} squared number - */ -const square = (a) => { - return a * a; -}; - -/** - * Distance between two points (2D and 3D) - * @alias module:utils.dist - * @param {Point} A - First point - * @param {Point} B - Second point - * @returns {Number} distance between the points - */ -const dist = (A, B) => { - return Math.sqrt(dist2(A, B)); -}; - -/** - * Convert angle in radians to degrees - * @alias module:utils.degrees - * @param {Number} radians - angle in radians - * @returns {Number} angle in degrees - */ -const degrees = (radians) => { - return (radians * 180.0) / Math.PI; -}; - -/** - * Convert angle in degrees to radians - * @alias module:utils.radians - * @param {Number} degrees - angle in degrees - * @returns {Number} angle in radians - */ -const radians = (degrees) => { - return (degrees * Math.PI) / 180.0; -}; - -/** - * Find intersection points between two circles - * @alias module:utils.intersection - * @param {Circle} c1 - first circle - * @param {Circle} c2 - second circle - * @returns {(Array|Boolean)} intersection or false (if no intersection) - */ -const intersection = (c1, c2) => { - const dx = c2.x - c1.x; - const dy = c2.y - c1.y; - const d = Math.sqrt(dy * dy + dx * dx); - - if (d > c1.r + c2.r) { - return false; - } - if (d < Math.abs(c1.r - c2.r)) { - return false; - } - const a = (c1.r * c1.r - c2.r * c2.r + d * d) / (2.0 * d); - - const xc = c1.x + (dx * a) / d; - const yc = c1.y + (dy * a) / d; - - const h = Math.sqrt(c1.r * c1.r - a * a); - - const rx = -dy * (h / d); - const ry = dx * (h / d); - - return [ - { x: xc + rx, y: yc + ry }, - { x: xc - rx, y: yc - ry }, - ]; -}; - -/** - * Generate random name - * @alias module:utils.randomName - * @param {Number} N - length of the name - * @returns {String} random name - */ -const randomName = (N) => { - return (Math.random().toString(36) + "00000000000000000").slice(2, N + 2); -}; - -/** - * Generate timestamp name - * @alias module:utils.timestampName - * @returns {String} timestamp name - */ -const timestampName = () => { - var tzoffset = new Date().getTimezoneOffset() * 60000; - let date = new Date(Date.now() - tzoffset) - .toISOString() - .replace(/z|t/gi, " ") - .trim() - .replace(/:/gi, "-"); - date = date.substring(0, date.indexOf(".")); - return date; -}; - -/** - * Generate random name - * @alias module:utils.randomIndex - * @param {Number} N - max index - * @returns {Number} random index - */ -const randomIndex = (N) => { - return Math.floor(Math.random() * N); -}; - -/** - * Copy array - * @alias module:utils.copyArray - * @param {Array} source - source array - * @returns {Array} array copy - */ -const copyArray = (source) => { - const array = Array(source.length); - for (let i = 0; i < source.length; i++) { - array[i] = source[i]; - } - return array; -}; - -/** - * Shuffle array - * @alias module:utils.shuffleArray - * @param {Array} source - source array - * @returns {Array} shuffled array copy - */ -const shuffleArray = (source) => { - const array = copyArray(source); - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; -}; - -/** - * Filter array unique - * @alias module:utils.filterUnique - * @param {Array} source - source array - * @returns {Array} array with unique elements only - */ -const filterUnique = (source) => { - return [...new Set(source)]; -}; - -/** - * Linear color interpolation - * @alias module:utils.lerpColor - * @param {String} a - First color - * @param {String} b - Second color - * @param {Number} amt - amount to interpolate - * @returns {String} Interpolated color - */ -const lerpColor = (a, b, amount) => { - var ah = parseInt(a.replace(/#/g, ""), 16); - var ar = ah >> 16; - var ag = (ah >> 8) & 0xff; - var ab = ah & 0xff; - var bh = parseInt(b.replace(/#/g, ""), 16); - var br = bh >> 16; - var bg = (bh >> 8) & 0xff; - var bb = bh & 0xff; - var rr = ar + amount * (br - ar); - var rg = ag + amount * (bg - ag); - var rb = ab + amount * (bb - ab); - return ( - "#" + (((1 << 24) + (rr << 16) + (rg << 8) + rb) | 0).toString(16).slice(1) - ); -}; - -/** - * Round number to precision - * @alias module:utils.precision - * @param {Number} value - value to round - * @param {Number} precision - decimal places - * @returns {Number} rounded number - */ -const precision = (value, precision) => { - return Math.round(value * Math.pow(10, precision)) / Math.pow(10, precision); -}; - -/** - * Load JSON - * @alias module:utils.loadJSON - * @param {String} address - address of JSON to load - * @param {Function} callback - function to call on result - */ -const loadJSON = (address, callback) => { - const xObj = new XMLHttpRequest(); - xObj.overrideMimeType("application/json"); - xObj.open("GET", address, true); - xObj.onreadystatechange = () => { - if (xObj.readyState === 4 && xObj.status === 200) { - callback(JSON.parse(xObj.responseText)); - } - }; - xObj.send(null); -}; - -const table = { - Ą: "A", - Ć: "C", - Ę: "E", - Ł: "L", - Ń: "N", - Ó: "O", - Ś: "S", - Ź: "Z", - Ż: "Z", - ą: "a", - ć: "c", - ę: "e", - ł: "l", - ń: "n", - ó: "o", - ś: "s", - ź: "z", - ż: "z", -}; - -/** - * Remove polish diacritics - * @alias module:utils.removeDiacritics - * @param {String} str - string with diacritics - * @returns {String} string without diacritics - */ -const removeDiacritics = (str) => - str.replace(/([ĄĆĘŁŃÓŚŹŻąćęłńóśźż])/g, function (l) { - return table[l]; - }); - -/** - * Remove all non alphanumeric characters - * @alias module:utils.removeNonAlphaNumeric - * @param {String} str - string with non alphanumeric characters - * @returns {String} string without non alphanumeric characters - */ -const removeNonAlphaNumeric = (str) => str.replace(/[^A-Za-z0-9]/g, ""); - -/** - * Split string to N sized chunks - * @alias module:utils.splitChunks - * @param {String} str - string to split - * @param {Number} n - chunk length - * @param {Boolean} discard - discard chunks shorter than N - * @returns {Array} array of string chunks - */ -const splitChunks = (str, n, discard) => { - const chunks = str.split(new RegExp("(.{" + n.toString() + "})")); - return discard - ? chunks.filter((x) => x.length === n) - : chunks.filter((x) => x.length > 0); -}; - -/** - * Get quarter from date - * @alias module:utils.getQuarter - * @param {Date} d - Date to get quarter from - * @returns {Array} year and quarter (1-4) - */ -const getQuarter = (d) => { - d = d || new Date(); - return [d.getFullYear(), Math.floor(d.getMonth() / 3) + 1]; -}; // getQuarter().join('Q') - -/** - * Get quarter extent - * @alias module:utils.quarterExtent - * @param {Number} quarter - quarter (1-4) - * @param {Number} year - full year - * @returns {Array} start and end date of quarter - */ -const quarterExtent = (quarter, year) => { - return [ - new Date( - `${year}-${((quarter - 1) * 3 + 1).toString().padStart(2, "0")}-01` - ), - new Date( - `${year}-${(quarter * 3).toString().padStart(2, "0")}-${ - quarter === 1 || quarter === 4 ? 31 : 30 - }` - ), - ]; -}; - -/** - * Get all dates between two dates - * @alias module:utils.datesBetween - * @param {Date} start - start date - * @param {Date} end - end date - * @returns {Array} all dates between start and end - */ -const datesBetween = (start, end) => { - const output = []; - - for (let date = start; date <= end; date.setDate(date.getDate() + 1)) { - output.push(new Date(date)); - } - return output; -}; - -/** - * Download file from base64 data uri - * @alias module:utils.downloadDataUri - * @param {Object} options - options for the downloaded file - * @param {String} options.data - contents of the file - * @param {String} options.filename - name of the file - */ -const downloadDataUri = (options) => { - var element = document.createElement("a"); - element.setAttribute("href", options.data); - element.setAttribute("download", options.filename); - element.style.display = "none"; - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); -}; - -/** - * Convert coordinates from polar to cartesian - * @alias module:utils.polarToCartesian - * @param {Number} r - radius - * @param {Number} angle - angle - * @returns {Point} cartesian coordinates - */ -const polarToCartesian = (r, angle) => { - return { - x: r * Math.cos(angle), - y: r * Math.sin(angle), - }; -}; - -/** - * Convert coordinates from cartesian to polar - * @alias module:utils.cartesianToPolar - * @param {Number} x - x coordinate - * @param {Number} y - y coordinate - * @returns {Object} polar coordinates - */ -const cartesianToPolar = (x, y) => { - let angle = Math.atan2(y, x); - if (angle < 0) { - while (angle < 0) { - angle += Math.PI * 2.0; - } - } - if (angle >= Math.PI * 2.0) { - while (angle >= Math.PI) { - angle -= Math.PI * 2.0; - } - } - return { - r: Math.sqrt(x * x + y * y), - angle, - }; -}; - -/** - * Get element page offset - * @alias module:utils.pageOffset - * @param {Object} elem - HTML element - * @returns {Object} top and left page offset - */ -const pageOffset = (elem) => { - const rect = elem.getBoundingClientRect(); - const win = elem.ownerDocument.defaultView; - return { - top: rect.top + win.pageYOffset, - left: rect.left + win.pageXOffset, - }; -}; - -/** - * Fuzzy search element in list - * @alias module:utils.fuzzySearch - * @param {Array} list - Array of terms - * @param {String} searchValue - search value to find - * @returns {Array} elements matching search value - */ -const fuzzySearch = (list, searchValue) => { - const buf = ".*" + searchValue.replace(/(.)/g, "$1.*").toLowerCase(); - var reg = new RegExp(buf); - return list.filter((e) => { - return reg.test(e.toLowerCase()); - }); -}; - -/** - * Distance between two points (2D and 3D) squared - * @alias module:utils.dist2 - * @param {Point} A - First point - * @param {Point} B - Second point - * @returns {Number} squared distance between the points - */ -const dist2 = (A, B) => { - return ( - square(B.x - A.x) + - square(B.y - A.y) + - (A.z !== undefined && B.z !== undefined ? square(B.z - A.z) : 0) - ); -}; - -/** - * Distance between point and segment squared - * @alias module:utils.distToSegment2 - * @param {Point} A - First point - * @param {Point} S - Segment start - * @param {Point} E - Segment end - * @returns {Number} squared distance between the point and the segment - */ -const distToSegment2 = (A, S, E) => { - const l2 = dist2(S, E); - - if (l2 === 0) return dist2(A, S); - - const t = ((A.x - S.x) * (E.x - S.x) + (A.y - S.y) * (E.y - S.y)) / l2; - - if (t < 0) return dist2(A, S); - if (t > 1) return dist2(A, E); - - return dist2(A, { x: S.x + t * (E.x - S.x), y: S.y + t * (E.y - S.y) }); -}; - -/** - * Distance between point and segment - * @alias module:utils.distToSegment - * @param {Point} A - First point - * @param {Point} S - Segment start - * @param {Point} E - Segment end - * @returns {Number} distance between the point and the segment - */ -const distToSegment = (A, S, E) => { - return Math.sqrt(distToSegment2(A, S, E)); -}; - -/** - * Convert string to custom separator case - * @alias module:utils.sepCase - * @param {string} str - string to convert - * @returns {string} custom cased string - */ -const sepCase = (str, sep = "-") => { - const text = removeDiacritics(str); - // text = removeNonAlphaNumeric(text) - return text - .replace(/[A-Z]/g, (letter, index) => { - const lcLet = letter.toLowerCase(); - return index ? sep + lcLet : lcLet; - }) - .replace(/([-_ ]){1,}/g, sep); -}; - -/** - * Convert string to snake case - * @alias module:utils.snakeCase - * @param {string} str - string to convert - * @returns {string} snake cased string - */ -const snakeCase = (str) => { - return sepCase(str, "_"); -}; - -/** - * Convert string to kebab case - * @alias module:utils.kebabCase - * @param {string} str - string to convert - * @returns {string} kebab cased string - */ -const kebabCase = (str) => { - return sepCase(str, "-"); -}; - -/** - * Convert string to camel case - * @alias module:utils.camelCase - * @param {string} str - string to convert - * @returns {string} camel cased string - */ -const camelCase = (str) => { - const text = removeDiacritics(str); - // text = removeNonAlphaNumeric(text) - return (text.slice(0, 1).toLowerCase() + text.slice(1)) - .replace(/([-_ ]){1,}/g, " ") - .split(/[-_ ]/) - .reduce((cur, acc) => { - return cur + acc[0].toUpperCase() + acc.substring(1); - }); -}; - -/** - * Check if array contains - * @alias module:utils.contains - * @param {any} elem - element to find in array - * @param {Array} arr - array to look in - * @returns {boolean} - true when element is in array - */ -const contains = (elem, arr) => { - return arr.indexOf(elem) !== -1; -}; - -/** - * Get CSS Styles from element - * @alias module:utils.getCSS - * @param {HTMLElement} parentElement - Element to get styles from - * @returns {string} - extracted CSS - */ -const getCSS = (parentElement) => { - const selectorTextArr = []; - - // Add Parent element Id and Classes to the list - selectorTextArr.push("#" + parentElement.id); - for (let c = 0; c < parentElement.classList.length; c++) { - if (!contains("." + parentElement.classList[c], selectorTextArr)) { - selectorTextArr.push("." + parentElement.classList[c]); - } - } - - // Add Children element Ids and Classes to the list - const nodes = parentElement.getElementsByTagName("*"); - for (let i = 0; i < nodes.length; i++) { - const id = nodes[i].id; - if (!contains("#" + id, selectorTextArr)) { - selectorTextArr.push("#" + id); - } - - const classes = nodes[i].classList; - for (let c = 0; c < classes.length; c++) { - if (!contains("." + classes[c], selectorTextArr)) { - selectorTextArr.push("." + classes[c]); - } - } - } - // Extract CSS Rules - let extractedCSSText = ""; - for (let i = 0; i < document.styleSheets.length; i++) { - var s = document.styleSheets[i]; - try { - if (!s.cssRules) continue; - } catch (e) { - if (e.name !== "SecurityError") throw e; // for Firefox - continue; - } - - var cssRules = s.cssRules; - for (var r = 0; r < cssRules.length; r++) { - if (contains(cssRules[r].selectorText, selectorTextArr)) { - extractedCSSText += cssRules[r].cssText; - } - } - } - return extractedCSSText; -}; - -/** - * Append CSS to element - * @alias module:utils.appendCSS - * @param {string} cssText - CSS text to append - * @param {HTMLElement} element - element to append CSS to - */ -const appendCSS = (cssText, element) => { - var styleElement = document.createElement("style"); - styleElement.setAttribute("type", "text/css"); - styleElement.innerHTML = cssText; - var refNode = element.hasChildNodes() ? element.children[0] : null; - element.insertBefore(styleElement, refNode); -}; - -/** - * Get SVG string from node - * @alias module:utils.getSVGString - * @param {HTMLElement} svgNode - svg node to get text from - * @returns {string} - svg as string - */ -const getSVGString = (svgNode) => { - svgNode.setAttribute("xlink", "http://www.w3.org/1999/xlink"); - var cssStyleText = getCSS(svgNode); - appendCSS(cssStyleText, svgNode); - - var serializer = new XMLSerializer(); - var svgString = serializer.serializeToString(svgNode); - svgString = svgString.replace(/(\w+)?:?xlink=/g, "xmlns:xlink="); // Fix root xlink without namespace - svgString = svgString.replace(/NS\d+:href/g, "xlink:href"); // Safari NS namespace fix - - return svgString; -}; - -/** - * Convert SVG string to image and call the callback - * @alias module:utils.svgStringToImage - * @param {string} svgString - SVG string to convert - * @param {Number} width - width of output image - * @param {Number} height - height of output image - * @param {string} format - format of output image - * @param {boolean} transparent - transparency flag - * @param {Function} callback - function to call when ready - */ -const svgStringToImage = ( - svgString, - width, - height, - format, - transparent, - callback -) => { - format = format || "png"; - - var imgsrc = - "data:image/svg+xml;base64," + - btoa(unescape(encodeURIComponent(svgString))); // Convert SVG string to data URL - - var canvas = document.createElement("canvas"); - var context = canvas.getContext("2d"); - - canvas.width = width; - canvas.height = height; - - var image = new Image(); - image.onload = () => { - context.clearRect(0, 0, width, height); - if (!transparent) { - context.beginPath(); - context.fillStyle = "#fff"; - context.fillRect(0, 0, canvas.width, canvas.height); - } - context.drawImage(image, 0, 0, width, height); - if (callback) callback(canvas.toDataURL()); - }; - image.src = imgsrc; -}; - -/** - * Convert SVG to data uri - * @alias module:utils.svgToUri - * @param {HTMLElement} svgNode - SVG element to get uri from - * @returns {string} - uri data scheme string - */ -const svgToUri = (svgNode) => { - const serializer = new XMLSerializer(); - let source = serializer.serializeToString(svgNode); - if (!source.match(/^]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) { - source = source.replace(/^]+"http:\/\/www\.w3\.org\/1999\/xlink"/)) { - source = source.replace( - /^ { - const { [prop]: _, ...copy } = obj; - return copy; -}; - -export { - map, - clamp, - random, - randomDir, - lerp, - lerp3, - lerpedPoints, - square, - dist, - norm, - degrees, - radians, - intersection, - randomName, - timestampName, - randomIndex, - copyArray, - shuffleArray, - filterUnique, - lerpColor, - precision, - loadJSON, - removeDiacritics, - removeNonAlphaNumeric, - splitChunks, - getQuarter, - quarterExtent, - datesBetween, - downloadDataUri, - polarToCartesian, - cartesianToPolar, - pageOffset, - fuzzySearch, - dist2, - distToSegment2, - distToSegment, - sepCase, - snakeCase, - kebabCase, - camelCase, - contains, - getCSS, - appendCSS, - getSVGString, - svgStringToImage, - svgToUri, - shallowCopyExcluding, -}; diff --git a/src/utils.mjs b/src/utils.mjs new file mode 100644 index 0000000..540f62e --- /dev/null +++ b/src/utils.mjs @@ -0,0 +1,108 @@ +/** + * Various functions used in javascript tools + */ +import { + map, + clamp, + norm, + lerp, + square, + degrees, + radians, + precision, +} from './modules/maths' +import { + lerp3, + intersectCircles, + intersectLines, + lerpStops, + polarToCartesian, + cartesianToPolar, + dist, + dist2, + distToSegment, + distToSegment2, +} from './modules/geometry' +import { random, randomDir, randomName, randomIndex } from './modules/random' +import { + timestampName, + removeDiacritics, + removeNonAlphaNumeric, + splitChunks, + sepCase, + snakeCase, + kebabCase, + camelCase, +} from './modules/strings' +import { + copyArray, + shuffleArray, + filterUnique, + fuzzySearch, + contains, +} from './modules/arrays' +import { lerpColor } from './modules/colors.mjs' +import { getQuarter, quarterExtent, datesBetween } from './modules/dates' +import { + loadJSON, + downloadDataUri, + pageOffset, + getCSS, + appendCSS, + getSVGString, + svgStringToImage, + svgToUri, +} from './modules/browser' +import { shallowCopyExcluding } from './modules/objects' +import PID from './modules/pid' +export { + map, + clamp, + random, + randomDir, + lerp, + lerp3, + lerpStops, + square, + dist, + norm, + degrees, + radians, + intersectCircles, + intersectLines, + randomName, + timestampName, + randomIndex, + copyArray, + shuffleArray, + filterUnique, + lerpColor, + precision, + loadJSON, + removeDiacritics, + removeNonAlphaNumeric, + splitChunks, + getQuarter, + quarterExtent, + datesBetween, + downloadDataUri, + polarToCartesian, + cartesianToPolar, + pageOffset, + fuzzySearch, + dist2, + distToSegment, + distToSegment2, + sepCase, + snakeCase, + kebabCase, + camelCase, + contains, + getCSS, + appendCSS, + getSVGString, + svgStringToImage, + svgToUri, + shallowCopyExcluding, + PID, +} diff --git a/test/test.js b/test/test.js deleted file mode 100644 index 3f63b4d..0000000 --- a/test/test.js +++ /dev/null @@ -1,522 +0,0 @@ -const { - map, - clamp, - random, - randomDir, - lerp, - lerp3, - lerpedPoints, - square, - dist, - norm, - degrees, - radians, - intersection, - randomName, - timestampName, - randomIndex, - copyArray, - shuffleArray, - filterUnique, - lerpColor, - precision, - removeDiacritics, - removeNonAlphaNumeric, - splitChunks, - getQuarter, - quarterExtent, - datesBetween, - polarToCartesian, - cartesianToPolar, - fuzzySearch, - dist2, - distToSegment2, - distToSegment, - sepCase, - snakeCase, - kebabCase, - camelCase, - shallowCopyExcluding, -} = require("../utils"); -const assert = require("assert"); - -describe("Utils", function () { - describe("#map()", function () { - it("should map the value from source range to provided range", function () { - assert.equal(map(0.5, 0, 2, 100, 200), 125); - }); - }); - describe("#clamp()", function () { - it("should return min if number lower than range", function () { - assert.equal(clamp(0, 1, 10), 1); - }); - it("should return max if number higher than range", function () { - assert.equal(clamp(100, 1, 10), 10); - }); - it("should return the number if in range", function () { - assert.equal(clamp(5, 1, 10), 5); - }); - }); - describe("#norm()", function () { - it("should return 0 when number equals min", function () { - assert.equal(norm(2, 2, 10), 0); - }); - it("should return 1 when number equals max", function () { - assert.equal(norm(10, 2, 10), 1); - }); - it("should return normalized number from provided range", function () { - assert.equal(norm(5, 0, 10), 0.5); - }); - }); - describe("#random()", function () { - it("should generate random number from provided range", function () { - const min = 5; - const max = 10; - const r = random(min, max); - assert(r >= min && r < max); - }); - it("should generate random number from 0 to provided max", function () { - const max = 10; - const r = random(max); - assert(r >= 0 && r < max); - }); - }); - describe("#randomDir()", function () { - it("should generate random direction", function () { - const r = randomDir(); - assert(r === -1 || r === 1); - }); - }); - describe("#lerp()", function () { - it("should interpolate between two numbers", function () { - assert.equal(lerp(0, 100, 0.5), 50); - }); - }); - describe("#lerp3()", function () { - it("should interpolate between two points in 3D", function () { - assert.deepEqual( - lerp3({ x: 0, y: 100, z: 200 }, { x: 100, y: 50, z: 100 }, 0.5), - { x: 50, y: 75, z: 150 } - ); - }); - }); - describe("#lerpedPoints()", function () { - it("should return n points between supplied points", function () { - assert.deepEqual( - lerpedPoints({ x: 0, y: 100, z: 200 }, { x: 300, y: 400, z: 500 }, 2), - [ - { x: 100, y: 200, z: 300 }, - { x: 200, y: 300, z: 400 }, - ] - ); - }); - }); - describe("#square()", function () { - it("should return provided number squared", function () { - assert.equal(square(2), 4); - }); - }); - describe("#dist()", function () { - it("should return 0 if the points are the same", function () { - assert.equal(dist({ x: 10, y: 100 }, { x: 10, y: 100 }), 0); - }); - it("should return distance between two points in 2D", function () { - assert.equal( - dist({ x: 0, y: 100 }, { x: 100, y: 500 }), - 412.31056256176606 - ); - }); - it("should return distance between two points in 3D", function () { - assert.equal( - dist({ x: 0, y: 100, z: 200 }, { x: 100, y: 500, z: 300 }), - 424.26406871192853 - ); - }); - }); - describe("#degrees()", function () { - it("should return angle in degrees", function () { - assert.equal(degrees(Math.PI / 2.0), 90); - }); - }); - describe("#radians()", function () { - it("should return angle in radians", function () { - assert.equal(radians(180), Math.PI); - }); - }); - describe("#intersection()", function () { - it("should return false if one circle is contained in the other", function () { - const c1 = { x: 100, y: 100, r: 50 }; - const c2 = { x: 100, y: 100, r: 60 }; - assert.equal(intersection(c1, c2), false); - }); - it("should return false if the circles are not intersecting", function () { - const c1 = { x: 300, y: 100, r: 50 }; - const c2 = { x: 100, y: 100, r: 60 }; - assert.equal(intersection(c1, c2), false); - }); - it("should return an array of points of intersection", function () { - const c1 = { x: 300, y: 100, r: 120 }; - const c2 = { x: 100, y: 100, r: 100 }; - assert.deepEqual(intersection(c1, c2), [ - { x: 189, y: 54.40394753928801 }, - { x: 189, y: 145.59605246071197 }, - ]); - }); - }); - describe("#randomName()", function () { - it("should return string", function () { - assert.equal(typeof randomName(3), "string"); - }); - it("should return string with provided length", function () { - const length = 8; - const name = randomName(length); - assert.equal(name.length, length); - }); - }); - describe("#timestampName()", function () { - it("should return string", function () { - assert.equal(typeof timestampName(), "string"); - }); - it("should return 19 characters", function () { - const name = timestampName(); - assert.equal(name.length, 19); - }); - }); - describe("#randomIndex()", function () { - it("should return a random number between 0 and N", function () { - const r = randomIndex(10); - assert(r >= 0 && r < 10); - }); - it("should return an integer", function () { - const r = randomIndex(5); - assert.equal(r % 1, 0); - }); - }); - describe("#copyArray()", function () { - it("should return copy of the array ", function () { - assert.deepEqual(copyArray([1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]); - }); - }); - describe("#shuffleArray()", function () { - it("should return an array with the same length", function () { - assert.equal(shuffleArray([1, 2, 3]).length, 3); - }); - }); - describe("#filterUnique()", function () { - it("should return an array type", function () { - assert.equal(filterUnique([1, 2, 3]).constructor, Array); - }); - it("should return the source array when no duplicates are present", function () { - assert.deepEqual(filterUnique([1, 2, 3]), [1, 2, 3]); - }); - it("should return an array with unique elements only", function () { - assert.deepEqual(filterUnique([1, 2, 3, 3, 2, 1, 2]), [1, 2, 3]); - }); - }); - describe("#lerpColor()", function () { - it("should return lerped color", function () { - assert.equal(lerpColor("#ff0000", "#00ff00", 0.4), "#996600"); - }); - it("should return a string", function () { - assert.equal(typeof lerpColor("#ff0000", "#00ff00", 0.9), "string"); - }); - it("should return hex color", function () { - assert.equal(lerpColor("#ff0000", "#00ffff", 0.4).length, 7); - assert.equal(lerpColor("#ff0000", "#ffff00", 0.4).charAt(0), "#"); - }); - }); - describe("#precision", function () { - it("should return a number with specified digits after decimal point", function () { - assert.equal(precision(10.13432234324324, 2), 10.13); - }); - }); - describe("#removeDiacritics", function () { - it("should return a string without diacritics", function () { - assert.equal( - removeDiacritics("ĄĆĘŁŃÓŚŹŻąćęłńóśźż"), - "ACELNOSZZacelnoszz" - ); - }); - }); - - describe("#removeNonAlphaNumeric", function () { - it("should return a string without any alpha numeric characters", function () { - assert.equal( - removeNonAlphaNumeric( - "a!b@c#d$e%f^g&h*i(j)k_l-m=n+1234567890 ,.<>/?'|\"\\[]{}~`£§" - ), - "abcdefghijklmn1234567890" - ); - }); - }); - - describe("#splitChunks", function () { - it("should return an array of chunks", function () { - assert.deepEqual(splitChunks("1234567890", 4), ["1234", "5678", "90"]); - }); - it("should return an array of chunks of length N", function () { - assert.deepEqual(splitChunks("1234567890", 4, true), ["1234", "5678"]); - }); - }); - describe("#getQuarter", function () { - it("should return an array with proper year and quarter", function () { - assert.deepEqual(getQuarter(new Date(2020, 0, 10)), [2020, 1]); - assert.deepEqual(getQuarter(new Date(2019, 1, 11)), [2019, 1]); - assert.deepEqual(getQuarter(new Date(2018, 2, 12)), [2018, 1]); - assert.deepEqual(getQuarter(new Date(2017, 3, 13)), [2017, 2]); - assert.deepEqual(getQuarter(new Date(2016, 4, 14)), [2016, 2]); - assert.deepEqual(getQuarter(new Date(2015, 5, 15)), [2015, 2]); - assert.deepEqual(getQuarter(new Date(2014, 6, 16)), [2014, 3]); - assert.deepEqual(getQuarter(new Date(2013, 7, 17)), [2013, 3]); - assert.deepEqual(getQuarter(new Date(2012, 8, 18)), [2012, 3]); - assert.deepEqual(getQuarter(new Date(2011, 9, 19)), [2011, 4]); - assert.deepEqual(getQuarter(new Date(2010, 10, 20)), [2010, 4]); - assert.deepEqual(getQuarter(new Date(2009, 11, 21)), [2009, 4]); - }); - }); - - describe("#quarterExtent", function () { - it("should return an array with proper start and end dates of quarter", function () { - assert.deepEqual(quarterExtent(1, 2019), [ - new Date("2019-01-01"), - new Date("2019-03-31"), - ]); - assert.deepEqual(quarterExtent(2, 2020), [ - new Date("2020-04-01"), - new Date("2020-06-30"), - ]); - assert.deepEqual(quarterExtent(3, 2021), [ - new Date("2021-07-01"), - new Date("2021-09-30"), - ]); - assert.deepEqual(quarterExtent(4, 2022), [ - new Date("2022-10-01"), - new Date("2022-12-31"), - ]); - }); - }); - describe("#datesBetween", function () { - it("should return array of dates between start and end date", function () { - assert.deepEqual( - datesBetween(new Date("2019-01-01"), new Date("2019-01-03")), - [new Date("2019-01-01"), new Date("2019-01-02"), new Date("2019-01-03")] - ); - }); - it("should return one date when start and end date are the same", function () { - assert.deepEqual( - datesBetween(new Date("2019-01-01"), new Date("2019-01-01")), - [new Date("2019-01-01")] - ); - }); - }); - describe("#polarToCartesian", function () { - it("should convert radius and angle to proper x and y coordinates", function () { - assert.deepEqual(polarToCartesian(1, (0.0 * Math.PI) / 4.0), { - x: 1, - y: 0, - }); - assert.deepEqual(polarToCartesian(1, (1.0 * Math.PI) / 4.0), { - x: 0.7071067811865476, - y: 0.7071067811865475, - }); - assert.deepEqual(polarToCartesian(1, (2.0 * Math.PI) / 4.0), { - x: 6.123233995736766e-17, - y: 1, - }); - assert.deepEqual(polarToCartesian(1, (3.0 * Math.PI) / 4.0), { - x: -0.7071067811865475, - y: 0.7071067811865476, - }); - assert.deepEqual(polarToCartesian(1, (4.0 * Math.PI) / 4.0), { - x: -1, - y: 1.2246467991473532e-16, - }); - assert.deepEqual(polarToCartesian(1, (5.0 * Math.PI) / 4.0), { - x: -0.7071067811865477, - y: -0.7071067811865475, - }); - assert.deepEqual(polarToCartesian(1, (6.0 * Math.PI) / 4.0), { - x: -1.8369701987210297e-16, - y: -1, - }); - assert.deepEqual(polarToCartesian(1, (7.0 * Math.PI) / 4.0), { - x: 0.7071067811865474, - y: -0.7071067811865477, - }); - assert.deepEqual(polarToCartesian(1, (8.0 * Math.PI) / 4.0), { - x: 1, - y: -2.4492935982947064e-16, - }); - }); - }); - describe("#cartesianToPolar", function () { - it("should convert x and y coordinates to proper radius and angle", function () { - assert.deepEqual(cartesianToPolar(1, 0), { - r: 1, - angle: (0.0 * Math.PI) / 4.0, - }); - assert.deepEqual( - cartesianToPolar(0.7071067811865476, 0.7071067811865475), - { r: 1, angle: (1.0 * Math.PI) / 4.0 } - ); - assert.deepEqual(cartesianToPolar(0, 1), { - r: 1, - angle: (2.0 * Math.PI) / 4.0, - }); - assert.deepEqual( - cartesianToPolar(-0.7071067811865475, 0.7071067811865476), - { r: 1, angle: (3.0 * Math.PI) / 4.0 } - ); - assert.deepEqual(cartesianToPolar(-1, 0), { - r: 1, - angle: (4.0 * Math.PI) / 4.0, - }); - assert.deepEqual( - cartesianToPolar(-0.7071067811865477, -0.7071067811865475), - { r: 1, angle: (5.0 * Math.PI) / 4.0 } - ); - assert.deepEqual(cartesianToPolar(0, -1), { - r: 1, - angle: (6.0 * Math.PI) / 4.0, - }); - assert.deepEqual( - cartesianToPolar(0.7071067811865474, -0.7071067811865477), - { r: 1, angle: (7.0 * Math.PI) / 4.0 } - ); - assert.deepEqual(cartesianToPolar(1, 0), { - r: 1, - angle: (0.0 * Math.PI) / 4.0, - }); - }); - }); - describe("#fuzzzySearch", function () { - it("should find elements matching search value", function () { - const list = [ - "Mickiewicz", - "Słowacki", - "Lechoń", - "Wierzyński", - "Krasiński", - "Tuwim", - "Sienkiewicz", - ]; - assert.deepEqual(fuzzySearch(list, "s"), [ - "Słowacki", - "Wierzyński", - "Krasiński", - "Sienkiewicz", - ]); - assert.deepEqual(fuzzySearch(list, "sie"), ["Sienkiewicz"]); - assert.deepEqual(fuzzySearch(list, "wu"), []); - assert.deepEqual(fuzzySearch(list, "lEcHoŃ"), ["Lechoń"]); - assert.deepEqual(fuzzySearch(list, "ski"), [ - "Słowacki", - "Wierzyński", - "Krasiński", - "Sienkiewicz", - ]); - assert.deepEqual(fuzzySearch(list, "icz"), ["Mickiewicz", "Sienkiewicz"]); - }); - }); - - describe("#dist2()", function () { - it("should return 0 if the points are the same", function () { - assert.equal(dist2({ x: 10, y: 100 }, { x: 10, y: 100 }), 0); - }); - it("should return squared distance between two points in 2D", function () { - assert.equal(dist2({ x: 0, y: 100 }, { x: 100, y: 500 }), 170000); - }); - it("should return squared distance between two points in 3D", function () { - assert.equal( - dist2({ x: 0, y: 100, z: 200 }, { x: 100, y: 500, z: 300 }), - 180000 - ); - }); - }); - - describe("#distToSegment()", function () { - it("should return 0 if the point is on segment", function () { - assert.equal( - distToSegment({ x: 50, y: 100 }, { x: 0, y: 100 }, { x: 100, y: 100 }), - 0 - ); - }); - it("should return distance between point and segment", function () { - assert.equal( - distToSegment({ x: 0, y: 100 }, { x: 100, y: 500 }, { x: 10, y: 100 }), - 10 - ); - }); - }); - - describe("#distToSegment2()", function () { - it("should return 0 if the point is on segment", function () { - assert.equal( - distToSegment2({ x: 50, y: 100 }, { x: 0, y: 100 }, { x: 100, y: 100 }), - 0 - ); - }); - it("should return squared distance between point and segment", function () { - assert.equal( - distToSegment2({ x: 0, y: 100 }, { x: 100, y: 500 }, { x: 10, y: 100 }), - 100 - ); - }); - }); - - describe("#sepCase()", function () { - it("should return a camel cased string as custom case", function () { - assert.equal(sepCase("SomeCamelString", "*"), "some*camel*string"); - }); - it("should return a kebab cased string as custom case", function () { - assert.equal(sepCase("some-kebab-string", "|"), "some|kebab|string"); - }); - it("should return string with polish diacritics as custom case", function () { - assert.equal(sepCase("zażółć gęślą jaźń", "$"), "zazolc$gesla$jazn"); - }); - }); - - describe("#snakeCase()", function () { - it("should return a camel cased string as snake case", function () { - assert.equal(snakeCase("SomeCamelString"), "some_camel_string"); - }); - it("should return a kebab cased string as snake case", function () { - assert.equal(snakeCase("some-kebab-string"), "some_kebab_string"); - }); - it("should return string with polish diacritics as snake case", function () { - assert.equal(snakeCase("zażółć gęślą jaźń"), "zazolc_gesla_jazn"); - }); - }); - - describe("#kebabCase()", function () { - it("should return a camel cased string as kebab case", function () { - assert.equal(kebabCase("SomeCamelString"), "some-camel-string"); - }); - it("should return a snake cased string as kebab case", function () { - assert.equal(kebabCase("some_snake_string"), "some-snake-string"); - }); - it("should return string with polish diacritics as kebab case", function () { - assert.equal(kebabCase("zażółć gęślą jaźń"), "zazolc-gesla-jazn"); - }); - }); - - describe("#camelCase()", function () { - it("should return a camel cased string as camel case", function () { - assert.equal(camelCase("some-kebab-string"), "someKebabString"); - }); - it("should return a snake cased string as camel case", function () { - assert.equal(camelCase("some_snake_string"), "someSnakeString"); - }); - it("should return string with polish diacritics as camel case", function () { - assert.equal(camelCase("zażółć gęślą jaźń"), "zazolcGeslaJazn"); - }); - }); - - describe("#shallowCopyExcluding()", function () { - it("should return a copy of object without specified property", function () { - assert.deepEqual(shallowCopyExcluding({ a: "one", b: "two" }, "a"), { - b: "two", - }); - }); - }); -}); diff --git a/tests/arrays.test.mjs b/tests/arrays.test.mjs new file mode 100644 index 0000000..4fa9139 --- /dev/null +++ b/tests/arrays.test.mjs @@ -0,0 +1,66 @@ +import * as arrays from '../src/modules/arrays' +describe('Arrays', () => { + describe('copyArray()', function () { + it('should return copy of the array ', function () { + expect(arrays.copyArray([1, 2, 3, 4, 5])).toStrictEqual([1, 2, 3, 4, 5]) + }) + }) + describe('shuffleArray()', function () { + it('should return an array with the same length', function () { + expect(arrays.shuffleArray([1, 2, 3]).length).toBe(3) + }) + }) + describe('filterUnique()', function () { + it('should return an array type', function () { + expect(arrays.filterUnique([1, 2, 3]).constructor).toBe(Array) + }) + it('should return the source array when no duplicates are present', function () { + expect(arrays.filterUnique([1, 2, 3])).toStrictEqual([1, 2, 3]) + }) + it('should return an array with unique elements only', function () { + expect(arrays.filterUnique([1, 2, 3, 3, 2, 1, 2])).toStrictEqual([ + 1, 2, 3, + ]) + }) + }) + describe('fuzzzySearch()', function () { + it('should find elements matching search value', function () { + const list = [ + 'Mickiewicz', + 'Słowacki', + 'Lechoń', + 'Wierzyński', + 'Krasiński', + 'Tuwim', + 'Sienkiewicz', + ] + expect(arrays.fuzzySearch(list, 's')).toStrictEqual([ + 'Słowacki', + 'Wierzyński', + 'Krasiński', + 'Sienkiewicz', + ]) + expect(arrays.fuzzySearch(list, 'sie')).toStrictEqual(['Sienkiewicz']) + expect(arrays.fuzzySearch(list, 'wu')).toStrictEqual([]) + expect(arrays.fuzzySearch(list, 'lEcHoŃ')).toStrictEqual(['Lechoń']) + expect(arrays.fuzzySearch(list, 'ski')).toStrictEqual([ + 'Słowacki', + 'Wierzyński', + 'Krasiński', + 'Sienkiewicz', + ]) + expect(arrays.fuzzySearch(list, 'icz')).toStrictEqual([ + 'Mickiewicz', + 'Sienkiewicz', + ]) + }) + }) + describe('contains()', function () { + it('return false if array does not contain element', function () { + expect(arrays.contains(4, [1, 2, 3])).toBe(false) + }) + it('return true if array contains element', function () { + expect(arrays.contains(2, [1, 2, 3])).toBe(true) + }) + }) +}) diff --git a/tests/colors.test.mjs b/tests/colors.test.mjs new file mode 100644 index 0000000..ac6f897 --- /dev/null +++ b/tests/colors.test.mjs @@ -0,0 +1,15 @@ +import * as colors from '../src/modules/colors' +describe('Colors', () => { + describe('lerpColor()', function () { + it('should return lerped color', function () { + expect(colors.lerpColor('#ff0000', '#00ff00', 0.4)).toBe('#996600') + }) + it('should return a string', function () { + expect(typeof colors.lerpColor('#ff0000', '#00ff00', 0.9)).toBe('string') + }) + it('should return hex color', function () { + expect(colors.lerpColor('#ff0000', '#00ffff', 0.4).length).toBe(7) + expect(colors.lerpColor('#ff0000', '#ffff00', 0.4).charAt(0)).toBe('#') + }) + }) +}) diff --git a/tests/dates.test.mjs b/tests/dates.test.mjs new file mode 100644 index 0000000..f3625ec --- /dev/null +++ b/tests/dates.test.mjs @@ -0,0 +1,59 @@ +import * as date from '../src/modules/dates' +describe('Dates', () => { + describe('getQuarter()', function () { + it('should return an array with proper year and quarter', function () { + expect(date.getQuarter(new Date(2020, 0, 10))).toStrictEqual([2020, 1]) + expect(date.getQuarter(new Date(2019, 1, 11))).toStrictEqual([2019, 1]) + expect(date.getQuarter(new Date(2018, 2, 12))).toStrictEqual([2018, 1]) + expect(date.getQuarter(new Date(2017, 3, 13))).toStrictEqual([2017, 2]) + expect(date.getQuarter(new Date(2016, 4, 14))).toStrictEqual([2016, 2]) + expect(date.getQuarter(new Date(2015, 5, 15))).toStrictEqual([2015, 2]) + expect(date.getQuarter(new Date(2014, 6, 16))).toStrictEqual([2014, 3]) + expect(date.getQuarter(new Date(2013, 7, 17))).toStrictEqual([2013, 3]) + expect(date.getQuarter(new Date(2012, 8, 18))).toStrictEqual([2012, 3]) + expect(date.getQuarter(new Date(2011, 9, 19))).toStrictEqual([2011, 4]) + expect(date.getQuarter(new Date(2010, 10, 20))).toStrictEqual([2010, 4]) + expect(date.getQuarter(new Date(2009, 11, 21))).toStrictEqual([2009, 4]) + expect(date.getQuarter()).toStrictEqual([ + new Date().getFullYear(), + Math.floor(new Date().getMonth() / 3) + 1, + ]) + }) + }) + describe('quarterExtent()', function () { + it('should return an array with proper start and end dates of quarter', function () { + expect(date.quarterExtent(1, 2019)).toStrictEqual([ + new Date('2019-01-01'), + new Date('2019-03-31'), + ]) + expect(date.quarterExtent(2, 2020)).toStrictEqual([ + new Date('2020-04-01'), + new Date('2020-06-30'), + ]) + expect(date.quarterExtent(3, 2021)).toStrictEqual([ + new Date('2021-07-01'), + new Date('2021-09-30'), + ]) + expect(date.quarterExtent(4, 2022)).toStrictEqual([ + new Date('2022-10-01'), + new Date('2022-12-31'), + ]) + }) + }) + describe('#datesBetween', function () { + it('should return array of dates between start and end date', function () { + expect( + date.datesBetween(new Date('2019-01-01'), new Date('2019-01-03')) + ).toStrictEqual([ + new Date('2019-01-01'), + new Date('2019-01-02'), + new Date('2019-01-03'), + ]) + }) + it('should return one date when start and end date are the same', function () { + expect( + date.datesBetween(new Date('2019-01-01'), new Date('2019-01-01')) + ).toStrictEqual([new Date('2019-01-01')]) + }) + }) +}) diff --git a/tests/geometry.test.mjs b/tests/geometry.test.mjs new file mode 100644 index 0000000..d630e92 --- /dev/null +++ b/tests/geometry.test.mjs @@ -0,0 +1,287 @@ +import * as geometry from '../src/modules/geometry' +describe('Geometry', () => { + describe('lerp3()', function () { + it('should interpolate between two points in 3D', function () { + expect( + geometry.lerp3({ x: 0, y: 100, z: 200 }, { x: 100, y: 50, z: 100 }, 0.5) + ).toStrictEqual({ x: 50, y: 75, z: 150 }) + }) + }) + describe('lerpStops()', function () { + it('should return n points between supplied points', function () { + expect( + geometry.lerpStops( + { x: 0, y: 100, z: 200 }, + { x: 300, y: 400, z: 500 }, + 2 + ) + ).toStrictEqual([ + { x: 100, y: 200, z: 300 }, + { x: 200, y: 300, z: 400 }, + ]) + }) + }) + describe('dist()', function () { + it('should return 0 if the points are the same', function () { + expect(geometry.dist({ x: 10, y: 100 }, { x: 10, y: 100 })).toBe(0) + }) + it('should return distance between two points in 2D', function () { + expect(geometry.dist({ x: 0, y: 100 }, { x: 100, y: 500 })).toBe( + 412.31056256176606 + ) + }) + it('should return distance between two points in 3D', function () { + expect( + geometry.dist({ x: 0, y: 100, z: 200 }, { x: 100, y: 500, z: 300 }) + ).toBe(424.26406871192853) + }) + }) + describe('intersectCircles())', function () { + it('should return false if one circle is contained in the other', function () { + const c1 = { x: 100, y: 100, r: 50 } + const c2 = { x: 100, y: 100, r: 60 } + expect(geometry.intersectCircles(c1, c2)).toBe(false) + }) + it('should return false if the circles are not intersecting', function () { + const c1 = { x: 300, y: 100, r: 50 } + const c2 = { x: 100, y: 100, r: 60 } + expect(geometry.intersectCircles(c1, c2)).toBe(false) + }) + it('should return an array of points of intersection', function () { + const c1 = { x: 300, y: 100, r: 120 } + const c2 = { x: 100, y: 100, r: 100 } + expect(geometry.intersectCircles(c1, c2)).toStrictEqual([ + { x: 189, y: 54.40394753928801 }, + { x: 189, y: 145.59605246071197 }, + ]) + }) + }) + describe('intersectLines()', function () { + it('should return false if one of the lines is a point', function () { + expect( + geometry.intersectLines( + { x: 0, y: 0 }, + { x: 0, y: 0 }, + { x: 100, y: 0 }, + { x: 100, y: 100 } + ) + ).toBe(false) + expect( + geometry.intersectLines( + { x: 100, y: 0 }, + { x: 0, y: 100 }, + { x: 100, y: 100 }, + { x: 100, y: 100 } + ) + ).toBe(false) + }) + it("should return false if the lines don't intersect", function () { + expect( + geometry.intersectLines( + { x: 0, y: 0 }, + { x: 100, y: 0 }, + { x: 0, y: 100 }, + { x: 100, y: 100 } + ) + ).toBe(false) + }) + it('should return false if the lines are parallel', function () { + expect( + geometry.intersectLines( + { x: 0, y: 100 }, + { x: 100, y: 0 }, + { x: 100, y: 200 }, + { x: 200, y: 120 } + ) + ).toBe(false) + }) + + it('should return a point if the lines intersect', function () { + expect( + geometry.intersectLines( + { x: 100, y: 0 }, + { x: 100, y: 200 }, + { x: 0, y: 100 }, + { x: 200, y: 100 } + ) + ).toStrictEqual({ x: 100, y: 100 }) + }) + }) + describe('polarToCartesian()', function () { + it('should convert radius and angle to proper x and y coordinates', function () { + expect(geometry.polarToCartesian(1, (0.0 * Math.PI) / 4.0)).toStrictEqual( + { + x: 1, + y: 0, + } + ) + expect(geometry.polarToCartesian(1, (1.0 * Math.PI) / 4.0)).toStrictEqual( + { + x: 0.7071067811865476, + y: 0.7071067811865475, + } + ) + expect(geometry.polarToCartesian(1, (2.0 * Math.PI) / 4.0)).toStrictEqual( + { + x: 6.123233995736766e-17, + y: 1, + } + ) + expect(geometry.polarToCartesian(1, (3.0 * Math.PI) / 4.0)).toStrictEqual( + { + x: -0.7071067811865475, + y: 0.7071067811865476, + } + ) + expect(geometry.polarToCartesian(1, (4.0 * Math.PI) / 4.0)).toStrictEqual( + { + x: -1, + y: 1.2246467991473532e-16, + } + ) + expect(geometry.polarToCartesian(1, (5.0 * Math.PI) / 4.0)).toStrictEqual( + { + x: -0.7071067811865477, + y: -0.7071067811865475, + } + ) + expect(geometry.polarToCartesian(1, (6.0 * Math.PI) / 4.0)).toStrictEqual( + { + x: -1.8369701987210297e-16, + y: -1, + } + ) + expect(geometry.polarToCartesian(1, (7.0 * Math.PI) / 4.0)).toStrictEqual( + { + x: 0.7071067811865474, + y: -0.7071067811865477, + } + ) + expect(geometry.polarToCartesian(1, (8.0 * Math.PI) / 4.0)).toStrictEqual( + { + x: 1, + y: -2.4492935982947064e-16, + } + ) + }) + }) + describe('cartesianToPolar', function () { + it('should convert x and y coordinates to proper radius and angle', function () { + expect(geometry.cartesianToPolar({ x: 1, y: 0 })).toStrictEqual({ + r: 1, + angle: (0.0 * Math.PI) / 4.0, + }) + expect( + geometry.cartesianToPolar({ + x: 0.7071067811865476, + y: 0.7071067811865475, + }) + ).toStrictEqual({ r: 1, angle: (1.0 * Math.PI) / 4.0 }) + expect(geometry.cartesianToPolar({ x: 0, y: 1 })).toStrictEqual({ + r: 1, + angle: (2.0 * Math.PI) / 4.0, + }) + expect( + geometry.cartesianToPolar({ + x: -0.7071067811865475, + y: 0.7071067811865476, + }) + ).toStrictEqual({ r: 1, angle: (3.0 * Math.PI) / 4.0 }) + expect(geometry.cartesianToPolar({ x: -1, y: 0 })).toStrictEqual({ + r: 1, + angle: (4.0 * Math.PI) / 4.0, + }) + expect( + geometry.cartesianToPolar({ + x: -0.7071067811865477, + y: -0.7071067811865475, + }) + ).toStrictEqual({ r: 1, angle: (5.0 * Math.PI) / 4.0 }) + expect(geometry.cartesianToPolar({ x: 0, y: -1 })).toStrictEqual({ + r: 1, + angle: (6.0 * Math.PI) / 4.0, + }) + expect( + geometry.cartesianToPolar({ + x: 0.7071067811865474, + y: -0.7071067811865477, + }) + ).toStrictEqual({ r: 1, angle: (7.0 * Math.PI) / 4.0 }) + expect(geometry.cartesianToPolar({ x: 1, y: 0 })).toStrictEqual({ + r: 1, + angle: (0.0 * Math.PI) / 4.0, + }) + }) + }) + describe('dist2()', function () { + it('should return 0 if the points are the same', function () { + expect(geometry.dist2({ x: 10, y: 100 }, { x: 10, y: 100 })).toBe(0) + }) + it('should return squared distance between two points in 2D', function () { + expect(geometry.dist2({ x: 0, y: 100 }, { x: 100, y: 500 })).toBe(170000) + }) + it('should return squared distance between two points in 3D', function () { + expect( + geometry.dist2({ x: 0, y: 100, z: 200 }, { x: 100, y: 500, z: 300 }) + ).toBe(180000) + }) + }) + describe('distToSegment()', function () { + it('should return 0 if the point is on segment', function () { + expect( + geometry.distToSegment( + { x: 50, y: 100 }, + { x: 0, y: 100 }, + { x: 100, y: 100 } + ) + ).toBe(0) + }) + it('should return distance between point and segment', function () { + expect( + geometry.distToSegment( + { x: 0, y: 100 }, + { x: 100, y: 500 }, + { x: 10, y: 100 } + ) + ).toBe(10) + }) + }) + describe('distToSegment2()', function () { + it('should return distance to point i segment start and end are the same point', function () { + expect( + geometry.distToSegment2( + { x: 50, y: 100 }, + { x: 0, y: 100 }, + { x: 0, y: 100 } + ) + ).toBe(2500) + }) + it('should return 0 if the point is on segment', function () { + expect( + geometry.distToSegment2( + { x: 50, y: 100 }, + { x: 0, y: 100 }, + { x: 100, y: 100 } + ) + ).toBe(0) + }) + it('should return distance to start when point is before segment', function () { + expect( + geometry.distToSegment2( + { x: 0, y: 0 }, + { x: 10, y: 0 }, + { x: 100, y: 0 } + ) + ).toBe(100) + }) + it('should return squared distance between point and segment', function () { + expect( + geometry.distToSegment2( + { x: 0, y: 100 }, + { x: 100, y: 500 }, + { x: 10, y: 100 } + ) + ).toBe(100) + }) + }) +}) diff --git a/tests/maths.test.mjs b/tests/maths.test.mjs new file mode 100644 index 0000000..30d52da --- /dev/null +++ b/tests/maths.test.mjs @@ -0,0 +1,59 @@ +import * as maths from '../src/modules/maths' +describe('Maths', () => { + describe('map()', () => { + it('should map the value from source range to provided range', () => { + expect(maths.map(0.5, 0, 2, 100, 200)).toBe(125) + }) + }) + describe('clamp()', function () { + it('should return min if number lower than range', function () { + expect(maths.clamp(0, 1, 10)).toBe(1) + }) + + it('should return max if number higher than range', function () { + expect(maths.clamp(100, 1, 10)).toBe(10) + }) + + it('should return the number if in range', function () { + expect(maths.clamp(5, 1, 10)).toBe(5) + }) + }) + describe('norm()', function () { + it('should return 0 when number equals min', function () { + expect(maths.norm(2, 2, 10)).toBe(0) + }) + + it('should return 1 when number equals max', function () { + expect(maths.norm(10, 2, 10)).toBe(1) + }) + + it('should return normalized number from provided range', function () { + expect(maths.norm(5, 0, 10)).toBe(0.5) + }) + }) + describe('lerp()', function () { + it('should interpolate between two numbers', function () { + expect(maths.lerp(0, 100, 0.5)).toBe(50) + }) + }) + describe('square()', function () { + it('should return provided number squared', function () { + expect(maths.square(2)).toBe(4) + }) + }) + describe('degrees()', function () { + it('should return angle in degrees', function () { + expect(maths.degrees(Math.PI / 2.0)).toBe(90) + }) + }) + describe('radians()', function () { + it('should return angle in radians', function () { + expect(maths.radians(180)).toBe(Math.PI) + }) + }) + describe('precision()', function () { + it('should return a number with specified digits after decimal point', function () { + expect(maths.precision(10.13432234324324, 2)).toBe(10.13) + }) + }) +}) diff --git a/tests/objects.test.mjs b/tests/objects.test.mjs new file mode 100644 index 0000000..e184c87 --- /dev/null +++ b/tests/objects.test.mjs @@ -0,0 +1,12 @@ +import * as objects from '../src/modules/objects' +describe('Objects', () => { + describe('shallowCopyExcluding()', function () { + it('should return a copy of object without specified property', function () { + expect( + objects.shallowCopyExcluding({ a: 'one', b: 'two' }, 'a') + ).toStrictEqual({ + b: 'two', + }) + }) + }) +}) diff --git a/tests/random.test.mjs b/tests/random.test.mjs new file mode 100644 index 0000000..c1d3c99 --- /dev/null +++ b/tests/random.test.mjs @@ -0,0 +1,50 @@ +import * as random from '../src/modules/random' +describe('Random', () => { + describe('random()', function () { + it('should generate random number from provided range', function () { + const min = 5 + const max = 10 + const r = random.random(min, max) + expect(r >= min && r < max).toBe(true) + }) + it('should generate random number from 0 to provided max', function () { + const max = 10 + const r = random.random(max) + expect(r >= 0 && r < max).toBe(true) + }) + }) + describe('randomDir()', function () { + it('should generate random direction as a number', function () { + const r = random.randomDir() + expect(typeof r).toBe('number') + }) + it('should generate random direction with absolute value of 1', function () { + const r = random.randomDir() + expect(Math.abs(r)).toBe(1) + }) + it('should generate random direction as -1 or 1', function () { + const r = random.randomDir() + expect([-1, 1].includes(r)).toBe(true) + }) + }) + describe('randomName()', function () { + it('should return string', function () { + expect(typeof random.randomName(3)).toBe('string') + }) + it('should return string with provided length', function () { + const length = 8 + const name = random.randomName(length) + expect(name.length).toBe(length) + }) + }) + describe('randomIndex()', function () { + it('should return a random number between 0 and N', function () { + const r = random.randomIndex(10) + expect(r >= 0 && r < 10).toBe(true) + }) + it('should return an integer', function () { + const r = random.randomIndex(5) + expect(r % 1).toBe(0) + }) + }) +}) diff --git a/tests/strings.test.mjs b/tests/strings.test.mjs new file mode 100644 index 0000000..6f6f079 --- /dev/null +++ b/tests/strings.test.mjs @@ -0,0 +1,96 @@ +import * as strings from '../src/modules/strings' +describe('Strings', () => { + describe('timestampName()', function () { + it('should return string', function () { + expect(typeof strings.timestampName()).toBe('string') + }) + it('should return 19 characters', function () { + const name = strings.timestampName() + expect(name.length).toBe(19) + }) + }) + describe('removeDiacritics()', function () { + it('should return a string without diacritics', function () { + expect(strings.removeDiacritics('ĄĆĘŁŃÓŚŹŻąćęłńóśźż')).toBe( + 'ACELNOSZZacelnoszz' + ) + }) + }) + describe('removeNonAlphaNumeric()', function () { + it('should return a string without any alpha numeric characters', function () { + expect( + strings.removeNonAlphaNumeric( + 'a!b@c#d$e%f^g&h*i(j)k_l-m=n+1234567890 ,.<>/?\'|"\\[]{}~`£§' + ) + ).toBe('abcdefghijklmn1234567890') + }) + }) + + describe('splitChunks()', function () { + it('should return an array of chunks', function () { + expect(strings.splitChunks('1234567890', 4)).toStrictEqual([ + '1234', + '5678', + '90', + ]) + }) + it('should return an array of chunks of length N', function () { + expect(strings.splitChunks('1234567890', 4, true)).toStrictEqual([ + '1234', + '5678', + ]) + }) + }) + describe('sepCase()', function () { + it("should return a camel cased string as custom case with - as a separater when you don't provide a separator", function () { + expect(strings.sepCase('SomeCamelString')).toBe('some-camel-string') + }) + + it('should return a camel cased string as custom case', function () { + expect(strings.sepCase('SomeCamelString', '*')).toBe('some*camel*string') + }) + it('should return a kebab cased string as custom case', function () { + expect(strings.sepCase('some-kebab-string', '|')).toBe( + 'some|kebab|string' + ) + }) + it('should return string with polish diacritics as custom case', function () { + expect(strings.sepCase('zażółć gęślą jaźń', '$')).toBe( + 'zazolc$gesla$jazn' + ) + }) + }) + describe('snakeCase()', function () { + it('should return a camel cased string as snake case', function () { + expect(strings.snakeCase('SomeCamelString')).toBe('some_camel_string') + }) + it('should return a kebab cased string as snake case', function () { + expect(strings.snakeCase('some-kebab-string')).toBe('some_kebab_string') + }) + it('should return string with polish diacritics as snake case', function () { + expect(strings.snakeCase('zażółć gęślą jaźń')).toBe('zazolc_gesla_jazn') + }) + }) + describe('kebabCase()', function () { + it('should return a camel cased string as kebab case', function () { + expect(strings.kebabCase('SomeCamelString')).toBe('some-camel-string') + }) + it('should return a snake cased string as kebab case', function () { + expect(strings.kebabCase('some_snake_string')).toBe('some-snake-string') + }) + it('should return string with polish diacritics as kebab case', function () { + expect(strings.kebabCase('zażółć gęślą jaźń')).toBe('zazolc-gesla-jazn') + }) + }) + describe('camelCase()', function () { + it('should return a camel cased string as camel case', function () { + expect(strings.camelCase('some-kebab-string')).toBe('someKebabString') + }) + it('should return a snake cased string as camel case', function () { + expect(strings.camelCase('some_snake_string')).toBe('someSnakeString') + }) + it('should return string with polish diacritics as camel case', function () { + expect(strings.camelCase('zażółć gęślą jaźń')).toBe('zazolcGeslaJazn') + }) + }) +}) diff --git a/utils.js b/utils.js index 4fc032c..1d5b6c8 100644 --- a/utils.js +++ b/utils.js @@ -1,6 +1,6 @@ /*! - * @license @pangenerator/utils v2.8.6, Copyright © 2023 panGenerator + * @license @pangenerator/utils v2.8.7, panGenerator 2024 * Released under MIT license * https://github.com/panGenerator/utils#readme */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).utils=t()}(this,(function(){"use strict";const e=(e,t,s)=>e+(t-e)*s,t=(t,s,n)=>({x:e(t.x,s.x,n),y:e(t.y,s.y,n),z:e(t.z,s.z,n)}),s=e=>e*e,n=e=>{const t=Array(e.length);for(let s=0;se.replace(/([ĄĆĘŁŃÓŚŹŻąćęłńóśźż])/g,(function(e){return o[e]})),a=(e,t)=>s(t.x-e.x)+s(t.y-e.y)+(void 0!==e.z&&void 0!==t.z?s(t.z-e.z):0),l=(e,t,s)=>{const n=a(t,s);if(0===n)return a(e,t);const o=((e.x-t.x)*(s.x-t.x)+(e.y-t.y)*(s.y-t.y))/n;return a(e,o<0?t:o>1?s:{x:t.x+o*(s.x-t.x),y:t.y+o*(s.y-t.y)})},i=(e,t="-")=>r(e).replace(/[A-Z]/g,((e,s)=>{const n=e.toLowerCase();return s?t+n:n})).replace(/([-_ ]){1,}/g,t),c=e=>i(e,"_"),d=(e,t)=>-1!==t.indexOf(e),g=e=>{const t=[];t.push("#"+e.id);for(let s=0;s{var s=document.createElement("style");s.setAttribute("type","text/css"),s.innerHTML=e;var n=t.hasChildNodes()?t.children[0]:null;t.insertBefore(s,n)},u=(e,t)=>{const{[t]:s,...n}=e;return n};var h=Object.freeze({__proto__:null,appendCSS:p,camelCase:e=>{const t=r(e);return(t.slice(0,1).toLowerCase()+t.slice(1)).replace(/([-_ ]){1,}/g," ").split(/[-_ ]/).reduce(((e,t)=>e+t[0].toUpperCase()+t.substring(1)))},cartesianToPolar:(e,t)=>{let s=Math.atan2(t,e);if(s<0)for(;s<0;)s+=2*Math.PI;if(s>=2*Math.PI)for(;s>=Math.PI;)s-=2*Math.PI;return{r:Math.sqrt(e*e+t*t),angle:s}},clamp:(e,t,s)=>e>s?s:e{const s=[];for(let n=e;n<=t;n.setDate(n.getDate()+1))s.push(new Date(n));return s},degrees:e=>180*e/Math.PI,dist:(e,t)=>Math.sqrt(a(e,t)),dist2:a,distToSegment:(e,t,s)=>Math.sqrt(l(e,t,s)),distToSegment2:l,downloadDataUri:e=>{var t=document.createElement("a");t.setAttribute("href",e.data),t.setAttribute("download",e.filename),t.style.display="none",document.body.appendChild(t),t.click(),document.body.removeChild(t)},filterUnique:e=>[...new Set(e)],fuzzySearch:(e,t)=>{const s=".*"+t.replace(/(.)/g,"$1.*").toLowerCase();var n=new RegExp(s);return e.filter((e=>n.test(e.toLowerCase())))},getCSS:g,getQuarter:e=>[(e=e||new Date).getFullYear(),Math.floor(e.getMonth()/3)+1],getSVGString:e=>{e.setAttribute("xlink","http://www.w3.org/1999/xlink");var t=g(e);p(t,e);var s=(new XMLSerializer).serializeToString(e);return s=(s=s.replace(/(\w+)?:?xlink=/g,"xmlns:xlink=")).replace(/NS\d+:href/g,"xlink:href")},intersection:(e,t)=>{const s=t.x-e.x,n=t.y-e.y,o=Math.sqrt(n*n+s*s);if(o>e.r+t.r)return!1;if(oi(e,"-"),lerp:e,lerp3:t,lerpColor:(e,t,s)=>{var n=parseInt(e.replace(/#/g,""),16),o=n>>16,r=n>>8&255,a=255&n,l=parseInt(t.replace(/#/g,""),16);return"#"+((1<<24)+(o+s*((l>>16)-o)<<16)+(r+s*((l>>8&255)-r)<<8)+(a+s*((255&l)-a))|0).toString(16).slice(1)},lerpedPoints:(e,s,n)=>{const o=[],r=1/(n+1);for(let a=0;a{const s=new XMLHttpRequest;s.overrideMimeType("application/json"),s.open("GET",e,!0),s.onreadystatechange=()=>{4===s.readyState&&200===s.status&&t(JSON.parse(s.responseText))},s.send(null)},map:(e,t,s,n,o)=>n+(o-n)*(e-t)/(s-t),norm:(e,t,s)=>(e-t)/(s-t),pageOffset:e=>{const t=e.getBoundingClientRect(),s=e.ownerDocument.defaultView;return{top:t.top+s.pageYOffset,left:t.left+s.pageXOffset}},polarToCartesian:(e,t)=>({x:e*Math.cos(t),y:e*Math.sin(t)}),precision:(e,t)=>Math.round(e*Math.pow(10,t))/Math.pow(10,t),quarterExtent:(e,t)=>[new Date(`${t}-${(3*(e-1)+1).toString().padStart(2,"0")}-01`),new Date(`${t}-${(3*e).toString().padStart(2,"0")}-${1===e||4===e?31:30}`)],radians:e=>e*Math.PI/180,random:(e,t)=>(void 0===t&&(t=e,e=0),e+Math.random()*(t-e)),randomDir:()=>Math.random()>.5?1:-1,randomIndex:e=>Math.floor(Math.random()*e),randomName:e=>(Math.random().toString(36)+"00000000000000000").slice(2,e+2),removeDiacritics:r,removeNonAlphaNumeric:e=>e.replace(/[^A-Za-z0-9]/g,""),sepCase:i,shallowCopyExcluding:u,shuffleArray:e=>{const t=n(e);for(let e=t.length-1;e>0;e--){const s=Math.floor(Math.random()*(e+1));[t[e],t[s]]=[t[s],t[e]]}return t},snakeCase:c,splitChunks:(e,t,s)=>{const n=e.split(new RegExp("(.{"+t.toString()+"})"));return s?n.filter((e=>e.length===t)):n.filter((e=>e.length>0))},square:s,svgStringToImage:(e,t,s,n,o,r)=>{var a="data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(e))),l=document.createElement("canvas"),i=l.getContext("2d");l.width=t,l.height=s;var c=new Image;c.onload=()=>{i.clearRect(0,0,t,s),o||(i.beginPath(),i.fillStyle="#fff",i.fillRect(0,0,l.width,l.height)),i.drawImage(c,0,0,t,s),r&&r(l.toDataURL())},c.src=a},svgToUri:e=>{let t=(new XMLSerializer).serializeToString(e);return t.match(/^]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)||(t=t.replace(/^]+"http:\/\/www\.w3\.org\/1999\/xlink"/)||(t=t.replace(/^{var e=6e4*(new Date).getTimezoneOffset();let t=new Date(Date.now()-e).toISOString().replace(/z|t/gi," ").trim().replace(/:/gi,"-");return t=t.substring(0,t.indexOf(".")),t}});return{TweakpaneSettings:class{constructor(e,t,s=null){this.settingsName=s??e.title+"-settings",this.ctrl=e,this.presets=t[0].settings.presets,this.presets?Object.keys(this.presets).forEach((e=>{this.presets[e]=JSON.parse(this.presets[e])})):this.presets={},t.forEach(((t,s)=>{const n=t.settings.name?e.addFolder({title:t.settings.name}):e;"controls"in t.settings&&Object.keys(t.settings.controls).forEach((e=>{const s=u(t.settings.controls[e],"val");s.presetKey=t.settings.name?c(t.settings.name)+"_$"+e:null,s.label=s.label?s.label:e,Object.defineProperty(t,"$"+e,{get:()=>t.settings.controls[e].val,set:s=>{t.settings.controls[e].val=s}});const o=n.addInput(t,"$"+e,s);s.callback&&o.on("change",(e=>{s.callback(e)}))})),"buttons"in t.settings&&Object.keys(t.settings.buttons).forEach((e=>{n.addButton({title:t.settings.buttons[e].label}).on("click",(()=>{t.settings.buttons[e].callback()}))})),"buttons_grid"in t.settings&&t.settings.buttons_grid.grids.forEach((e=>{n.addBlade({view:"buttongrid",size:e.size,label:e.label,cells:e.cells}).on("click",(t=>{e.callbacks[t.index[1]][t.index[0]]()}))})),"monitors"in t.settings&&Object.keys(t.settings.monitors).forEach((e=>{const s=t.settings.monitors_options[e];n.addMonitor(t.settings.monitors,e,s),Object.defineProperty(t,"$"+e,{get:()=>t.settings.monitors[e],set:s=>{t.settings.monitors[e]=s}})}))}));const n=e.addFolder({title:"Presets",expanded:!1});Object.keys(this.presets).length>0&&(n.addBlade({view:"list",label:"preset",options:Object.keys(this.presets).map((e=>({text:e,value:e}))),value:Object.keys(this.presets)[0]}).on("change",(e=>{this.loadSettings(this.presets[e.value])})),n.addSeparator()),n.addButton({title:"Store settings"}).on("click",(()=>{console.log("save settings");const t=e.exportPreset();localStorage.setItem(s,JSON.stringify(t)),console.log(t),console.log("json:\n",JSON.stringify(t))})),n.addButton({title:"Restore settings"}).on("click",(()=>{this.loadSettings()})),n.addButton({title:"Download settings"}).on("click",(()=>{console.log("download settings");const t=s+"_"+(new Date).toLocaleString().replace(/[^0-9]+/g,"-")+".json",n="data:text/json;charset=utf-8,"+encodeURIComponent(JSON.stringify(e.exportPreset(),null,2));console.log(JSON.stringify(e.exportPreset()));const o=document.createElement("a");o.download=t,o.href=n,o.click(),o.remove()})),n.addButton({title:"Upload settings"}).on("click",(()=>{console.log("upload settings");const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","application/json"),t.style.opacity="0",t.style.position="fixed",document.body.appendChild(t),t.addEventListener("input",(s=>{if(t.files&&t.files[0]){const s=t.files[0];let n=new FileReader;n.readAsText(s),n.onload=()=>{console.log("settings loaded..."),console.log(n.result),e.importPreset(JSON.parse(n.result))},n.onerror=()=>{console.log("error loading file!"),console.log(n.error)}}document.body.removeChild(t)}),{once:!0}),t.click()})),n.addButton({title:"Default settings"}).on("click",(()=>{this.presets.default&&this.loadSettings(this.presets.default)}))}loadSettings(e=null){if(console.log("restore settings"),console.log("presets",this.presets),e)this.ctrl.importPreset(e),console.log("loaded settings:",e);else if(this.presets.default)this.ctrl.importPreset(this.presets.default),console.log("loaded default settings:",this.presets.default);else{const e=localStorage.getItem(this.settingsName);e&&(this.ctrl.importPreset(JSON.parse(e)),console.log("loaded settings from local storage:",e))}}},...h}})); +!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?factory(exports):"function"==typeof define&&define.amd?define(["exports"],factory):factory((global="undefined"!=typeof globalThis?globalThis:global||self).utils={})}(this,(function(exports){"use strict";const lerp=(start,stop,amt)=>start+(stop-start)*amt,square=a=>a*a,lerp3=(A,B,amt)=>({x:lerp(A.x,B.x,amt),y:lerp(A.y,B.y,amt),z:lerp(A.z,B.z,amt)}),dist2=(A,B)=>square(B.x-A.x)+square(B.y-A.y)+(void 0!==A.z&&void 0!==B.z?square(B.z-A.z):0),distToSegment2=(A,S,E)=>{const l2=dist2(S,E);if(0===l2)return dist2(A,S);const t=((A.x-S.x)*(E.x-S.x)+(A.y-S.y)*(E.y-S.y))/l2;return dist2(A,t<0?S:t>1?E:{x:S.x+t*(E.x-S.x),y:S.y+t*(E.y-S.y)})},table={"Ą":"A","Ć":"C","Ę":"E","Ł":"L","Ń":"N","Ó":"O","Ś":"S","Ź":"Z","Ż":"Z","ą":"a","ć":"c","ę":"e","ł":"l","ń":"n","ó":"o","ś":"s","ź":"z","ż":"z"},removeDiacritics=str=>str.replace(/([ĄĆĘŁŃÓŚŹŻąćęłńóśźż])/g,(function(l){return table[l]})),sepCase=(str,sep="-")=>removeDiacritics(str).replace(/[A-Z]/g,((letter,index)=>{const lcLet=letter.toLowerCase();return index?sep+lcLet:lcLet})).replace(/([-_ ]){1,}/g,sep),copyArray=source=>{const array=Array(source.length);for(let i=0;i{const selectorTextArr=[];selectorTextArr.push("#"+parentElement.id);for(let c=0;c{var styleElement=document.createElement("style");styleElement.setAttribute("type","text/css"),styleElement.innerHTML=cssText;var refNode=element.hasChildNodes()?element.children[0]:null;element.insertBefore(styleElement,refNode)};exports.PID=class{constructor(P=0,I=0,D=0){this.set(P,I,D),this.ep=0,this.ei=0,this.ed=0}set(P=0,I=0,D=0){this.Kp=P,this.Ki=I,this.Kd=D}update(current,target){const error=target-current;return this.ei+=error,this.ed=error-this.ep,this.ep=error,this.Kp*this.ep+this.Ki*this.ei+this.Kd*this.ed}},exports.appendCSS=appendCSS,exports.camelCase=str=>{const text=removeDiacritics(str);return(text.slice(0,1).toLowerCase()+text.slice(1)).replace(/([-_ ]){1,}/g," ").split(/[-_ ]/).reduce(((cur,acc)=>cur+acc[0].toUpperCase()+acc.substring(1)))},exports.cartesianToPolar=P=>{let angle=Math.atan2(P.y,P.x);if(angle<0)for(;angle<0;)angle+=2*Math.PI;return{r:Math.sqrt(P.x*P.x+P.y*P.y),angle:angle}},exports.clamp=(val,min,max)=>val>max?max:val-1!==arr.indexOf(elem),exports.copyArray=copyArray,exports.datesBetween=(start,end)=>{const output=[];for(let date=start;date<=end;date.setDate(date.getDate()+1))output.push(new Date(date));return output},exports.degrees=radians=>180*radians/Math.PI,exports.dist=(A,B)=>Math.sqrt(dist2(A,B)),exports.dist2=dist2,exports.distToSegment=(A,S,E)=>Math.sqrt(distToSegment2(A,S,E)),exports.distToSegment2=distToSegment2,exports.downloadDataUri=options=>{var element=document.createElement("a");element.setAttribute("href",options.data),element.setAttribute("download",options.filename),element.style.display="none",document.body.appendChild(element),element.click(),document.body.removeChild(element)},exports.filterUnique=source=>[...new Set(source)],exports.fuzzySearch=(list,searchValue)=>{const buf=".*"+searchValue.replace(/(.)/g,"$1.*").toLowerCase();var reg=new RegExp(buf);return list.filter((e=>reg.test(e.toLowerCase())))},exports.getCSS=getCSS,exports.getQuarter=d=>[(d=d||new Date).getFullYear(),Math.floor(d.getMonth()/3)+1],exports.getSVGString=svgNode=>{svgNode.setAttribute("xlink","http://www.w3.org/1999/xlink");var cssStyleText=getCSS(svgNode);appendCSS(cssStyleText,svgNode);var svgString=(new XMLSerializer).serializeToString(svgNode);return svgString=(svgString=svgString.replace(/(\w+)?:?xlink=/g,"xmlns:xlink=")).replace(/NS\d+:href/g,"xlink:href")},exports.intersectCircles=(c1,c2)=>{const dx=c2.x-c1.x,dy=c2.y-c1.y,d=Math.sqrt(dy*dy+dx*dx);if(d>c1.r+c2.r)return!1;if(d{if(p1.x===p2.x&&p1.y===p2.y||p3.x===p4.x&&p3.y===p4.y)return!1;const denominator=(p4.y-p3.y)*(p2.x-p1.x)-(p4.x-p3.x)*(p2.y-p1.y);if(0===denominator)return!1;let ua=((p4.x-p3.x)*(p1.y-p3.y)-(p4.y-p3.y)*(p1.x-p3.x))/denominator,ub=((p2.x-p1.x)*(p1.y-p3.y)-(p2.y-p1.y)*(p1.x-p3.x))/denominator;return!(ua<0||ua>1||ub<0||ub>1)&&{x:p1.x+ua*(p2.x-p1.x),y:p1.y+ua*(p2.y-p1.y)}},exports.kebabCase=str=>sepCase(str,"-"),exports.lerp=lerp,exports.lerp3=lerp3,exports.lerpColor=(a,b,amount)=>{var ah=parseInt(a.replace(/#/g,""),16),ar=ah>>16,ag=ah>>8&255,ab=255&ah,bh=parseInt(b.replace(/#/g,""),16);return"#"+((1<<24)+(ar+amount*((bh>>16)-ar)<<16)+(ag+amount*((bh>>8&255)-ag)<<8)+(ab+amount*((255&bh)-ab))|0).toString(16).slice(1)},exports.lerpStops=(A,B,count)=>{const points=[],step=1/(count+1);for(let i=0;i{const xObj=new XMLHttpRequest;xObj.overrideMimeType("application/json"),xObj.open("GET",address,!0),xObj.onreadystatechange=()=>{4===xObj.readyState&&200===xObj.status&&callback(JSON.parse(xObj.responseText))},xObj.send(null)},exports.map=(value,low1,high1,low2,high2)=>low2+(high2-low2)*(value-low1)/(high1-low1),exports.norm=(value,start,stop)=>(value-start)/(stop-start),exports.pageOffset=elem=>{const rect=elem.getBoundingClientRect(),win=elem.ownerDocument.defaultView;return{top:rect.top+win.pageYOffset,left:rect.left+win.pageXOffset}},exports.polarToCartesian=(r,angle)=>({x:r*Math.cos(angle),y:r*Math.sin(angle)}),exports.precision=(value,precision)=>Math.round(value*Math.pow(10,precision))/Math.pow(10,precision),exports.quarterExtent=(quarter,year)=>[new Date(`${year}-${(3*(quarter-1)+1).toString().padStart(2,"0")}-01`),new Date(`${year}-${(3*quarter).toString().padStart(2,"0")}-${1===quarter||4===quarter?31:30}`)],exports.radians=degrees=>degrees*Math.PI/180,exports.random=(low,high)=>(void 0===high&&(high=low,low=0),low+Math.random()*(high-low)),exports.randomDir=()=>Math.random()>.5?1:-1,exports.randomIndex=N=>Math.floor(Math.random()*N),exports.randomName=N=>(Math.random().toString(36)+"00000000000000000").slice(2,N+2),exports.removeDiacritics=removeDiacritics,exports.removeNonAlphaNumeric=str=>str.replace(/[^A-Za-z0-9]/g,""),exports.sepCase=sepCase,exports.shallowCopyExcluding=(obj,prop)=>{const{[prop]:_,...copy}=obj;return copy},exports.shuffleArray=source=>{const array=copyArray(source);for(let i=array.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[array[i],array[j]]=[array[j],array[i]]}return array},exports.snakeCase=str=>sepCase(str,"_"),exports.splitChunks=(str,n,discard)=>{const chunks=str.split(new RegExp("(.{"+n.toString()+"})"));return discard?chunks.filter((x=>x.length===n)):chunks.filter((x=>x.length>0))},exports.square=square,exports.svgStringToImage=(svgString,width,height,format,transparent,callback)=>{var imgsrc="data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svgString))),canvas=document.createElement("canvas"),context=canvas.getContext("2d");canvas.width=width,canvas.height=height;var image=new Image;image.onload=()=>{context.clearRect(0,0,width,height),transparent||(context.beginPath(),context.fillStyle="#fff",context.fillRect(0,0,canvas.width,canvas.height)),context.drawImage(image,0,0,width,height),callback&&callback(canvas.toDataURL())},image.src=imgsrc},exports.svgToUri=svgNode=>{let source=(new XMLSerializer).serializeToString(svgNode);return source.match(/^]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)||(source=source.replace(/^]+"http:\/\/www\.w3\.org\/1999\/xlink"/)||(source=source.replace(/^{var tzoffset=6e4*(new Date).getTimezoneOffset();let date=new Date(Date.now()-tzoffset).toISOString().replace(/z|t/gi," ").trim().replace(/:/gi,"-");return date=date.substring(0,date.indexOf(".")),date}}));