├── .babelrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── build
├── geometric.js
├── geometric.min.js
└── geometric.zip
├── img
├── angle-thumb.png
├── centroid-thumb.png
├── geometric-logo.svg
├── length-thumb.png
└── point-on-with-line.png
├── index.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── angles
│ ├── angleReflect.js
│ ├── angleToDegrees.js
│ └── angleToRadians.js
├── lines
│ ├── lineAngle.js
│ ├── lineInterpolate.js
│ ├── lineLength.js
│ ├── lineMidpoint.js
│ ├── lineRotate.js
│ └── lineTranslate.js
├── points
│ ├── pointRotate.js
│ └── pointTranslate.js
├── polygons
│ ├── polygonArea.js
│ ├── polygonBounds.js
│ ├── polygonCentroid.js
│ ├── polygonHull.js
│ ├── polygonInterpolate.js
│ ├── polygonLength.js
│ ├── polygonMean.js
│ ├── polygonRandom.js
│ ├── polygonReflectX.js
│ ├── polygonReflectY.js
│ ├── polygonRegular.js
│ ├── polygonRotate.js
│ ├── polygonScale.js
│ ├── polygonScaleArea.js
│ ├── polygonScaleX.js
│ ├── polygonScaleY.js
│ ├── polygonTranslate.js
│ └── polygonWind.js
├── relationships
│ ├── lineIntersectsLine.js
│ ├── lineIntersectsPolygon.js
│ ├── pointInPolygon.js
│ ├── pointOnLine.js
│ ├── pointOnPolygon.js
│ ├── polygonInPolygon.js
│ └── polygonIntersectsPolygon.js
└── utils
│ ├── closePolygon.js
│ └── crossProduct.js
└── test
├── angleReflect-test.js
├── angleToDegrees-test.js
├── angleToRadians-test.js
├── lineAngle-test.js
├── lineInterpolate-test.js
├── lineIntersectsLine-test.js
├── lineIntersectsPolygon-test.js
├── lineLength-test.js
├── lineMidpoint-test.js
├── lineRotate-test.js
├── lineTranslate-test.js
├── pointInPolygon-test.js
├── pointOnLine-test.js
├── pointOnPolygon-test.js
├── pointRotate-test.js
├── pointTranslate-test.js
├── polygonArea-test.js
├── polygonBounds-test.js
├── polygonCentriod-test.js
├── polygonHull-test.js
├── polygonInPolygon-test.js
├── polygonInterpolate-test.js
├── polygonIntersectsPolygon-test.js
├── polygonLength-test.js
├── polygonMean-test.js
├── polygonRandom-test.js
├── polygonReflectX-test.js
├── polygonReflectY-test.js
├── polygonRegular-test.js
├── polygonRotate-test.js
├── polygonScale-test.js
├── polygonScaleArea-test.js
├── polygonScaleX-test.js
├── polygonScaleY-test.js
├── polygonTranslate-test.js
├── polygonWind-test.js
└── utils
└── roundArray.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/env", {"modules": false}]
4 | ]
5 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | .DS_Store
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | build/*.zip
2 | test/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - stable
5 |
6 | install:
7 | - npm install
8 |
9 | script:
10 | - npm test
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Harry Stevens
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Geometric.js
2 | A JavaScript library for doing geometry.
3 |
4 | [
](https://blocks.roadtolarissa.com/HarryStevens/5fe49df19892c04dfb9883c217571409)
5 | [
](https://blocks.roadtolarissa.com/HarryStevens/c4eddfb97535e8e01643325cb43175ff)
6 | [
](https://blocks.roadtolarissa.com/HarryStevens/37287b23b345f394f8276dc87a9c2bc6)
7 |
8 | ## Installation
9 |
10 | ### Web browser
11 | In vanilla, a `geometric` global is exported. You can use the latest version from unpkg.
12 | ```html
13 |
14 |
15 | ```
16 | If you'd rather host it yourself, download the latest release from the [`build` directory](https://github.com/HarryStevens/geometric/tree/master/build).
17 |
18 | ### npm
19 |
20 | ```bash
21 | npm i geometric -S
22 | ```
23 | ```js
24 | const geometric = require("geometric");
25 | ```
26 |
27 | ## API
28 |
29 | Geometric.js uses the geometric primitives points, lines, and polygons.
30 | * [Points](#points) are represented as arrays of two numbers, such as [0, 0].
31 | * [Lines](#lines) are represented as arrays of two points, such as [[0, 0], [1, 0]]. Because they have endpoints, these are technically [line segments](https://web.archive.org/web/20170829200252/https://www.mhschool.com/math/mathconnects/wa/assets/docs/394_397_wa_gr3_adllsn_onln.pdf), but Geometric.js refers to them as lines for simplicity's sake.
32 | * [Polygons](#polygons) are represented as arrays of vertices, each of which is a point, such as [[0, 0], [1, 0], [1, 1], [0, 1]]. Polygons can be closed – the first and last vertex are the same – or open.
33 | * There are also functions to [calculate relationships](#relationships) between these primitives.
34 |
35 | You will also encounter angles, areas, distances, and lengths.
36 | * [Angles](#angles) are represented as numbers, measured in degrees. Geometric.js also provides functions to convert angles from [degrees to radians](#angleToRadians) or [vice versa](#angleToDegrees).
37 | * Areas, distances, and lengths are represented as numbers, measured in pixels.
38 |
39 |
40 |
41 | ### Points
42 |
43 | # geometric.pointRotate(point, angle[, origin]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/points/pointRotate.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-pointrotate "Example")
44 |
45 | Returns the coordinates resulting from rotating a point about an origin by an angle in degrees. If origin is not specified, the origin defaults to [0, 0].
46 |
47 | # geometric.pointTranslate(point, angle, distance) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/points/pointTranslate.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-pointtranslate "Example")
48 |
49 | Returns the coordinates resulting from translating a point by an angle in degrees and a distance.
50 |
51 |
52 |
53 | ### Lines
54 |
55 | # geometric.lineAngle(line) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/lines/lineAngle.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-lineangle "Example")
56 |
57 | Returns the angle of a line, in degrees, with respect to the horizontal axis.
58 |
59 | # geometric.lineInterpolate(line[, clamp]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/lines/lineInterpolate.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-lineinterpolate "Example")
60 |
61 | Returns an interpolator function given a line [a, b]. The returned interpolator function takes a single argument t, where t is a number in [0, 1]; a value of 0 returns a, while a value of 1 returns b. Intermediate values interpolate from a to b along the line segment.
62 |
63 | By default, the interpolator will return points outside of the line segment if t is less than 0 or greater than 1. You can pass an optional boolean indicating whether to clamp the returned point to inside of the line segment, even if t is greater than 1 or less than 0.
64 |
65 | # geometric.lineLength(line) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/lines/lineLength.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-linelength "Example")
66 |
67 | Returns the length of a line.
68 |
69 | # geometric.lineMidpoint(line) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/lines/lineMidpoint.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-linemidpoint "Example")
70 |
71 | Returns the midpoint of a line.
72 |
73 | # geometric.lineRotate(line, angle[, origin]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/lines/lineRotate.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-linerotate "Example")
74 |
75 | Returns the coordinates resulting from rotating a line about an origin by an angle in degrees. If origin is not specified, the origin defaults to the midpoint of the line.
76 |
77 | # geometric.lineTranslate(line, angle, distance) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/lines/lineTranslate.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-linetranslate "Example")
78 |
79 | Returns the coordinates resulting from translating a line by an angle in degrees and a distance.
80 |
81 |
82 |
83 | ### Polygons
84 |
85 | # geometric.polygonArea(polygon[, signed]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonArea.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonarea "Example")
86 |
87 | Returns the area of a polygon. You can pass a boolean indicating whether the returned area is signed, which defaults to false.
88 |
89 | # geometric.polygonBounds(polygon) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonBounds.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonbounds "Example")
90 |
91 | Returns the bounds of a polygon, ignoring points with invalid values (null, undefined, NaN, Infinity). The returned bounds are represented as an array of two points, where the first point is the top-left corner and the second point is the bottom-right corner. For example:
92 |
93 | ```js
94 | const rectangle = [[0, 0], [0, 1], [1, 1], [1, 0]];
95 | const bounds = geometric.polygonBounds(rectangle); // [[0, 0], [1, 1]]
96 | ```
97 |
98 | Returns null if the polygon has fewer than three points.
99 |
100 | # geometric.polygonCentroid(polygon) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonCentroid.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygoncentroid-geometric-polygonmean "Example")
101 |
102 | Returns the weighted centroid of a polygon. Not to be [confused](https://github.com/Turfjs/turf/issues/334) with a [mean center](#polygonMean).
103 |
104 | # geometric.polygonHull(points) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonHull.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonhull "Example")
105 |
106 | Returns the [convex hull](https://en.wikipedia.org/wiki/Convex_hull), represented as a polygon, for an array of points. Returns null if the input array has fewer than three points. Uses [Andrew’s monotone chain algorithm](https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain#JavaScript).
107 |
108 | # geometric.polygonInterpolate(polygon]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonInterpolate.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygoninterpolate "Example")
109 |
110 | Returns an interpolator function given a polygon of vertices [a, ..., n]. The returned interpolator function takes a single argument t, where t is a number in [0, 1]; a value of 0 returns a, while a value of 1 returns n. Intermediate values interpolate from a to n along the polygon's perimeter.
111 |
112 | # geometric.polygonLength(polygon) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonLength.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonlength "Example")
113 |
114 | Returns the length of a polygon's perimeter.
115 |
116 | # geometric.polygonMean(polygon) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonMean.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygoncentroid-geometric-polygonmean "Example")
117 |
118 | Returns the arithmetic mean of the vertices of a polygon. Keeps duplicate vertices, resulting in different values for open and closed polygons. Not to be [confused](https://github.com/Turfjs/turf/issues/334) with a [centroid](#polygonCentroid).
119 |
120 | # geometric.polygonRandom([sides[, area[, centroid]]]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonRandom.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonrandom "Example")
121 |
122 | Returns the vertices of a random convex polygon of the specified number of sides, area, and centroid coordinates. If sides is not specified, defaults to 3. If area is not specified, defaults to 100. If centroid is not specified, defaults to [0, 0]. The returned polygon's winding order will be counter-clockwise. Based on an algorithm by Pavel Valtr and an [implementation by Maneesh Agrawala](https://observablehq.com/@magrawala/random-convex-polygon).
123 |
124 | # geometric.polygonReflectX(polygon[, reflectFactor]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonReflectX.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonreflectx "Example")
125 |
126 | Reflects a polygon over its vertical midline. Pass an optional reflectFactor between 0 and 1, where 1 indicates a full reflection, 0 leaves the polygon unchanged, and 0.5 collapses the polygon on its vertical midline.
127 |
128 | # geometric.polygonReflectY(polygon[, reflectFactor]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonReflectY.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonreflecty "Example")
129 |
130 | Reflects a polygon over its horizontal midline. Pass an optional reflectFactor between 0 and 1, where 1 indicates a full reflection, 0 leaves the polygon unchanged, and 0.5 collapses the polygon on its horizontal midline.
131 |
132 | # geometric.polygonRegular([sides[, area[, center]]]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonRegular.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonregular "Example")
133 |
134 | Returns the vertices of a regular polygon of the specified number of sides, area, and center coordinates. If sides is not specified, defaults to 3. If area is not specified, defaults to 100. If center is not specified, defaults to [0, 0]. The returned polygon's winding order will be counter-clockwise.
135 |
136 | # geometric.polygonRotate(polygon, angle[, origin]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonRotate.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonrotate "Example")
137 |
138 | Returns the vertices resulting from rotating a polygon about an origin by an angle in degrees. If origin is not specified, the origin defaults to [0, 0].
139 |
140 | # geometric.polygonScale(polygon, scaleFactor[, origin]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonScale.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonscale "Example")
141 |
142 | Returns the vertices resulting from scaling a polygon by a scaleFactor (where 1 is the polygon's current size) from an origin point. If origin is not specified, the origin defaults to the polygon's centroid.
143 |
144 | The returned polygon's area is equal to the input polygon's area multiplied by the square of the scaleFactor. To scale the polygon's area by the scaleFactor itself, see geometric.polygonScaleArea.
145 |
146 | # geometric.polygonScaleArea(polygon, scaleFactor[, origin]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonScaleArea.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonscalearea "Example")
147 |
148 | Returns the vertices resulting from scaling a polygon by a scaleFactor (where 1 is the polygon's current size) from an origin point. If origin is not specified, the origin defaults to the polygon's centroid.
149 |
150 | The returned polygon's area is equal to the input polygon's area multiplied by the scaleFactor. To scale the polygon's area by the square of the scaleFactor, see geometric.polygonScale.
151 |
152 | # geometric.polygonScaleX(polygon, scaleFactor[, origin]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonScaleX.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonscalex "Example")
153 |
154 | Returns the vertices resulting from scaling the horizontal coordinates of a polygon by a scaleFactor (where 1 is the polygon's current size) from an origin point. The vertical coordinates remain unchanged. If origin is not specified, the origin defaults to the polygon's centroid.
155 |
156 | The returned polygon's area is equal to the input polygon's area multiplied by the scaleFactor.
157 |
158 | # geometric.polygonScaleY(polygon, scaleFactor[, origin]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonScaleY.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonscaley "Example")
159 |
160 | Returns the vertices resulting from scaling the vertical coordinates of a polygon by a scaleFactor (where 1 is the polygon's current size) from an origin point. The horizontal coordinates remain unchanged. If origin is not specified, the origin defaults to the polygon's centroid.
161 |
162 | The returned polygon's area is equal to the input polygon's area multiplied by the scaleFactor.
163 |
164 | # geometric.polygonTranslate(polygon, angle, distance) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonTranslate.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygontranslate "Example")
165 |
166 | Returns the vertices resulting from translating a polygon by an angle in degrees and a distance.
167 |
168 | # geometric.polygonWind(polygon[, order]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/polygons/polygonWind.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonwind "Example")
169 |
170 | Returns a polygon in the specified winding order. If an order string is passed as either "cw" or "clockwise", returns a polygon with a clockwise winding order. Otherwise, returns a polygon with a counter-clockwise winding order. Returns null if the polygon has fewer than three points.
171 |
172 | On computer screens where the top-left corner is at [0, 0], a polygon with a negative signed area has a counter-clockwise winding order.
173 |
174 |
175 |
176 | ### Relationships
177 |
178 | # geometric.lineIntersectsLine(lineA, lineB) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/relationships/lineIntersectsLine.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-lineintersectsline "Example")
179 |
180 | Returns a boolean representing whether lineA intersects lineB.
181 |
182 | # geometric.lineIntersectsPolygon(line, polygon) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/relationships/lineIntersectsPolygon.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-lineintersectspolygon "Example")
183 |
184 | Returns a boolean representing whether a line intersects a polygon.
185 |
186 | # geometric.pointInPolygon(point, polygon) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/relationships/pointInPolygon.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-pointinpolygon "Example")
187 |
188 | Returns a boolean representing whether a point is inside of a polygon. Uses [ray casting](https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm).
189 |
190 | # geometric.pointOnPolygon(point, polygon[, epsilon]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/relationships/pointOnPolygon.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-pointonpolygon "Example")
191 |
192 | Returns a boolean representing whether a point is located on one of the edges of a polygon. An optional epsilon number, such as 1e-6, can be passed to reduce the precision with which the relationship is measured.
193 |
194 | # geometric.pointOnLine(point, line[, epsilon]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/relationships/pointOnLine.js#L17 "Source"), [Example](https://observablehq.com/@harrystevens/geometric-pointonline-geometric-pointwithline "Example")
195 |
196 | Returns a boolean representing whether a point is collinear with a line and is also located on the line segment. An optional epsilon number, such as 1e-6, can be passed to reduce the precision with which the relationship is measured. See also [pointWithLine](#pointWithLine).
197 |
198 | [
](https://observablehq.com/d/c463ce4b7cbcd048)
199 |
200 | # geometric.pointWithLine(point, line[, epsilon]) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/relationships/pointOnLine.js#L21 "Source"), [Example](https://observablehq.com/@harrystevens/geometric-pointonline-geometric-pointwithline "Example")
201 |
202 | Returns a boolean representing whether a point is collinear with a line. An optional epsilon number, such as 1e-6, can be passed to reduce the precision with which the relationship is measured. See also [pointOnLine](#pointOnLine).
203 |
204 | # geometric.pointLeftofLine(point, line) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/relationships/pointOnLine.js#L9 "Source"), [Example](https://observablehq.com/@harrystevens/geometric-pointleftofline-geometric-pointrightofline-g "Example")
205 |
206 | Returns a boolean representing whether a point is to the left of a line.
207 |
208 | # geometric.pointRightofLine(point, line) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/relationships/pointOnLine.js#L13 "Source"), [Example](https://observablehq.com/@harrystevens/geometric-pointleftofline-geometric-pointrightofline-g "Example")
209 |
210 | Returns a boolean representing whether a point is to the right of a line.
211 |
212 | # geometric.polygonInPolygon(polygonA, polygonB) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/relationships/polygonInPolygon.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygoninpolygon "Example")
213 |
214 | Returns a boolean representing whether polygonA is contained by polygonB.
215 |
216 | # geometric.polygonIntersectsPolygon(polygonA, polygonB) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/relationships/polygonIntersectsPolygon.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-polygonintersectspolygon "Example")
217 |
218 | Returns a boolean representing whether polygonA intersects but is not contained by polygonB.
219 |
220 |
221 |
222 | ### Angles
223 |
224 | # geometric.angleReflect(incidence, surface) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/angles/angleReflect.js "Source"), [Example](https://observablehq.com/@harrystevens/geometric-anglereflect "Example")
225 |
226 | Returns the angle of reflection given a starting angle, also known as the angle of incidence, and the angle of the surface off of which it is reflected.
227 |
228 | # geometric.angleToDegrees(angle) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/angles/angleToDegrees.js "Source")
229 |
230 | Returns the result of converting an angle in radians to the same angle in degrees.
231 |
232 | # geometric.angleToRadians(angle) · [Source](https://github.com/HarryStevens/geometric/blob/master/src/angles/angleToRadians.js "Source")
233 |
234 | Returns the result of converting an angle in degrees to the same angle in radians.
--------------------------------------------------------------------------------
/build/geometric.js:
--------------------------------------------------------------------------------
1 | // https://github.com/HarryStevens/geometric#readme Version 2.5.5. Copyright 2024 Harry Stevens.
2 | (function (global, factory) {
3 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
4 | typeof define === 'function' && define.amd ? define(['exports'], factory) :
5 | (factory((global.geometric = {})));
6 | }(this, (function (exports) { 'use strict';
7 |
8 | // Converts radians to degrees.
9 | function angleToDegrees(angle) {
10 | return angle * 180 / Math.PI;
11 | }
12 |
13 | function lineAngle(line) {
14 | return angleToDegrees(Math.atan2(line[1][1] - line[0][1], line[1][0] - line[0][0]));
15 | }
16 |
17 | function _slicedToArray(arr, i) {
18 | return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest();
19 | }
20 |
21 | function _toConsumableArray(arr) {
22 | return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
23 | }
24 |
25 | function _arrayWithoutHoles(arr) {
26 | if (Array.isArray(arr)) {
27 | for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
28 |
29 | return arr2;
30 | }
31 | }
32 |
33 | function _arrayWithHoles(arr) {
34 | if (Array.isArray(arr)) return arr;
35 | }
36 |
37 | function _iterableToArray(iter) {
38 | if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
39 | }
40 |
41 | function _iterableToArrayLimit(arr, i) {
42 | var _arr = [];
43 | var _n = true;
44 | var _d = false;
45 | var _e = undefined;
46 |
47 | try {
48 | for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
49 | _arr.push(_s.value);
50 |
51 | if (i && _arr.length === i) break;
52 | }
53 | } catch (err) {
54 | _d = true;
55 | _e = err;
56 | } finally {
57 | try {
58 | if (!_n && _i["return"] != null) _i["return"]();
59 | } finally {
60 | if (_d) throw _e;
61 | }
62 | }
63 |
64 | return _arr;
65 | }
66 |
67 | function _nonIterableSpread() {
68 | throw new TypeError("Invalid attempt to spread non-iterable instance");
69 | }
70 |
71 | function _nonIterableRest() {
72 | throw new TypeError("Invalid attempt to destructure non-iterable instance");
73 | }
74 |
75 | // Returns an interpolator function given a line [a, b].
76 | // The returned interpolator function takes a single argument t, where t is a number ranging from 0 to 1;
77 | // a value of 0 returns a, while a value of 1 returns b.
78 | // Intermediate values interpolate from start to end along the line segment.
79 | // By default, the returned interpolator will output points outside of the line segment if t is less than 0 or greater than 1.
80 | // You can pass an optional boolean indicating whether to the returned point to inside of the line segment,
81 | // even if t is greater than 1 or less then 0.
82 | function lineInterpolate(line) {
83 | var clamp = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
84 |
85 | var _line = _slicedToArray(line, 2),
86 | _line$ = _slicedToArray(_line[0], 2),
87 | x1 = _line$[0],
88 | y1 = _line$[1],
89 | _line$2 = _slicedToArray(_line[1], 2),
90 | x2 = _line$2[0],
91 | y2 = _line$2[1];
92 |
93 | var x = function x(v) {
94 | return (x2 - x1) * v + x1;
95 | };
96 |
97 | var y = function y(v) {
98 | return (y2 - y1) * v + y1;
99 | };
100 |
101 | return function (t) {
102 | var t0 = clamp ? t < 0 ? 0 : t > 1 ? 1 : t : t;
103 | return [x(t0), y(t0)];
104 | };
105 | }
106 |
107 | // Calculates the distance between the endpoints of a line segment.
108 | function lineLength(line) {
109 | return Math.sqrt(Math.pow(line[1][0] - line[0][0], 2) + Math.pow(line[1][1] - line[0][1], 2));
110 | }
111 |
112 | // Calculates the midpoint of a line segment.
113 | function lineMidpoint(line) {
114 | return [(line[0][0] + line[1][0]) / 2, (line[0][1] + line[1][1]) / 2];
115 | }
116 |
117 | // Converts degrees to radians.
118 | function angleToRadians(angle) {
119 | return angle / 180 * Math.PI;
120 | }
121 |
122 | function pointRotate(point, angle, origin) {
123 | var r = angleToRadians(angle || 0);
124 |
125 | if (!origin || origin[0] === 0 && origin[1] === 0) {
126 | return rotate(point, r);
127 | } else {
128 | // See: https://math.stackexchange.com/questions/1964905/rotation-around-non-zero-point
129 | var p0 = point.map(function (c, i) {
130 | return c - origin[i];
131 | });
132 | var rotated = rotate(p0, r);
133 | return rotated.map(function (c, i) {
134 | return c + origin[i];
135 | });
136 | }
137 | }
138 |
139 | function rotate(point, angle) {
140 | // See: https://en.wikipedia.org/wiki/Cartesian_coordinate_system#Rotation
141 | return [point[0] * Math.cos(angle) - point[1] * Math.sin(angle), point[0] * Math.sin(angle) + point[1] * Math.cos(angle)];
142 | }
143 |
144 | // If origin is not specified, the origin defaults to the midpoint of the line.
145 |
146 | function lineRotate(line, angle, origin) {
147 | return line.map(function (point) {
148 | return pointRotate(point, angle, origin || lineMidpoint(line));
149 | });
150 | }
151 |
152 | function pointTranslate(point) {
153 | var angle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
154 | var distance = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
155 | var r = angleToRadians(angle);
156 | return [point[0] + distance * Math.cos(r), point[1] + distance * Math.sin(r)];
157 | }
158 |
159 | function lineTranslate(line, angle, distance) {
160 | return line.map(function (point) {
161 | return pointTranslate(point, angle, distance);
162 | });
163 | }
164 |
165 | // Calculates the area of a polygon.
166 | function polygonArea(vertices) {
167 | var signed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
168 | var a = 0;
169 |
170 | for (var i = 0, l = vertices.length; i < l; i++) {
171 | var v0 = vertices[i],
172 | v1 = vertices[i === l - 1 ? 0 : i + 1];
173 | a += v0[0] * v1[1];
174 | a -= v1[0] * v0[1];
175 | }
176 |
177 | return signed ? a / 2 : Math.abs(a / 2);
178 | }
179 |
180 | // Calculates the bounds of a polygon.
181 | function polygonBounds(polygon) {
182 | if (polygon.length < 3) {
183 | return null;
184 | }
185 |
186 | var xMin = Infinity,
187 | xMax = -Infinity,
188 | yMin = Infinity,
189 | yMax = -Infinity,
190 | found = false;
191 |
192 | for (var i = 0, l = polygon.length; i < l; i++) {
193 | var p = polygon[i],
194 | x = p[0],
195 | y = p[1];
196 |
197 | if (x != null && isFinite(x) && y != null && isFinite(y)) {
198 | found = true;
199 | if (x < xMin) xMin = x;
200 | if (x > xMax) xMax = x;
201 | if (y < yMin) yMin = y;
202 | if (y > yMax) yMax = y;
203 | }
204 | }
205 |
206 | return found ? [[xMin, yMin], [xMax, yMax]] : null;
207 | }
208 |
209 | // Calculates the weighted centroid a polygon.
210 | function polygonCentroid(vertices) {
211 | var a = 0,
212 | x = 0,
213 | y = 0,
214 | l = vertices.length;
215 |
216 | for (var i = 0; i < l; i++) {
217 | var s = i === l - 1 ? 0 : i + 1,
218 | v0 = vertices[i],
219 | v1 = vertices[s],
220 | f = v0[0] * v1[1] - v1[0] * v0[1];
221 | a += f;
222 | x += (v0[0] + v1[0]) * f;
223 | y += (v0[1] + v1[1]) * f;
224 | }
225 |
226 | var d = a * 3;
227 | return [x / d, y / d];
228 | }
229 |
230 | // See https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain#JavaScript
231 | // and https://math.stackexchange.com/questions/274712/calculate-on-which-side-of-a-straight-line-is-a-given-point-located
232 | function cross(a, b, o) {
233 | return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
234 | }
235 |
236 | // See https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain#JavaScript
237 |
238 | function polygonHull(points) {
239 | if (points.length < 3) {
240 | return null;
241 | }
242 |
243 | var pointsCopy = points.slice().sort(function (a, b) {
244 | return a[0] === b[0] ? a[1] - b[1] : a[0] - b[0];
245 | });
246 | var lower = [];
247 |
248 | for (var i = 0; i < pointsCopy.length; i++) {
249 | while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], pointsCopy[i]) <= 0) {
250 | lower.pop();
251 | }
252 |
253 | lower.push(pointsCopy[i]);
254 | }
255 |
256 | var upper = [];
257 |
258 | for (var _i = pointsCopy.length - 1; _i >= 0; _i--) {
259 | while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], pointsCopy[_i]) <= 0) {
260 | upper.pop();
261 | }
262 |
263 | upper.push(pointsCopy[_i]);
264 | }
265 |
266 | upper.pop();
267 | lower.pop();
268 | return lower.concat(upper);
269 | }
270 |
271 | // Closes a polygon if it's not closed already. Does not modify input polygon.
272 | function close(polygon) {
273 | return isClosed(polygon) ? polygon : [].concat(_toConsumableArray(polygon), [polygon[0]]);
274 | } // Tests whether a polygon is closed
275 |
276 | function isClosed(polygon) {
277 | var first = polygon[0],
278 | last = polygon[polygon.length - 1];
279 | return first[0] === last[0] && first[1] === last[1];
280 | }
281 |
282 | // Calculates the length of a polygon's perimeter. See https://github.com/d3/d3-polygon/blob/master/src/length.js
283 | function polygonLength(vertices) {
284 | if (vertices.length === 0) {
285 | return 0;
286 | }
287 |
288 | var i = -1,
289 | n = vertices.length,
290 | b = vertices[n - 1],
291 | xa,
292 | ya,
293 | xb = b[0],
294 | yb = b[1],
295 | perimeter = 0;
296 |
297 | while (++i < n) {
298 | xa = xb;
299 | ya = yb;
300 | b = vertices[i];
301 | xb = b[0];
302 | yb = b[1];
303 | xa -= xb;
304 | ya -= yb;
305 | perimeter += Math.sqrt(xa * xa + ya * ya);
306 | }
307 |
308 | return perimeter;
309 | }
310 |
311 | function polygonInterpolate(polygon) {
312 | return function (t) {
313 | if (t <= 0) {
314 | return polygon[0];
315 | }
316 |
317 | var closed = close(polygon);
318 |
319 | if (t >= 1) {
320 | return closed[closed.length - 1];
321 | }
322 |
323 | var target = polygonLength(closed) * t;
324 | var point = [],
325 | track = 0;
326 |
327 | for (var i = 0; i < closed.length - 1; i++) {
328 | var side = [closed[i], closed[i + 1]],
329 | length = lineLength(side),
330 | angle = lineAngle(side),
331 | delta = target - (track += length);
332 |
333 | if (delta < 0) {
334 | point = pointTranslate(side[0], angle, length + delta);
335 | break;
336 | } else if (i === polygon.length - 2) {
337 | point = pointTranslate(side[0], angle, delta);
338 | }
339 | }
340 |
341 | return point;
342 | };
343 | }
344 |
345 | // Calculates the arithmetic mean of a polygon's vertices.
346 | function polygonMean(vertices) {
347 | var x = 0,
348 | y = 0,
349 | l = vertices.length;
350 |
351 | for (var i = 0; i < l; i++) {
352 | var v = vertices[i];
353 | x += v[0];
354 | y += v[1];
355 | }
356 |
357 | return [x / l, y / l];
358 | }
359 |
360 | // The returned polygon's area is equal to the input polygon's area multiplied by the scaleFactor.
361 | // The origin defaults to the polygon's centroid.
362 |
363 | function polygonScaleArea(polygon, scale, origin) {
364 | if (!origin) {
365 | origin = polygonCentroid(polygon);
366 | }
367 |
368 | var p = [];
369 |
370 | for (var i = 0, l = polygon.length; i < l; i++) {
371 | var v = polygon[i],
372 | d = lineLength([origin, v]),
373 | a = lineAngle([origin, v]);
374 | p[i] = pointTranslate(origin, a, d * Math.sqrt(scale));
375 | }
376 |
377 | return p;
378 | }
379 |
380 | function polygonTranslate(polygon, angle, distance) {
381 | var p = [];
382 |
383 | for (var i = 0, l = polygon.length; i < l; i++) {
384 | p[i] = pointTranslate(polygon[i], angle, distance);
385 | }
386 |
387 | return p;
388 | }
389 |
390 | // Based on an algorithm by Pavel Valtr and an implementation by Maneesh Agrawala: https://observablehq.com/@magrawala/random-convex-polygon
391 |
392 | function polygonRandom() {
393 | var sides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 3;
394 | var area = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100;
395 | var centroid = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0];
396 | var r = Math.sqrt(area / Math.PI),
397 | xs = Array.from({
398 | length: sides
399 | }, function () {
400 | return 2 * r * Math.random();
401 | }),
402 | ys = Array.from({
403 | length: sides
404 | }, function () {
405 | return 2 * r * Math.random();
406 | });
407 | xs.sort(function (a, b) {
408 | return a - b;
409 | });
410 | ys.sort(function (a, b) {
411 | return a - b;
412 | });
413 | var vecXS = chain(xs, xs[0], xs[xs.length - 1]),
414 | vecYS = chain(ys, ys[0], ys[ys.length - 1]);
415 | shuffle(vecYS); //Make polygon coordinates from the vecs by laying them out end to end
416 |
417 | var polygon = [],
418 | x = 0,
419 | y = 0; // Zip the vector arrays together
420 | // Then, sort the vectors by angle, in a counter clockwise fashion.
421 | // a and b are tuples representing vectors. Compute angle for each vector and compare them.
422 |
423 | var vecs = vecXS.map(function (d, i) {
424 | return [d, vecYS[i]];
425 | }).sort(function (a, b) {
426 | return Math.atan2(b[1], b[0]) - Math.atan2(a[1], a[0]);
427 | }).forEach(function (vec) {
428 | x += vec[0] * 1;
429 | y += vec[1] * 1;
430 | polygon.push([x, y]);
431 | }); // Scale and translate
432 |
433 | var c = polygonCentroid(polygon);
434 | return polygonTranslate(polygonScaleArea(polygon, area / polygonArea(polygon)), lineAngle([c, centroid]), lineLength([c, centroid]));
435 | }
436 |
437 | function chain(values, min, max) {
438 | var lastMin = min,
439 | lastMax = min;
440 | var output = [];
441 |
442 | for (var i = 1; i < values.length - 1; i++) {
443 | var val = values[i];
444 |
445 | if (Math.random() > 0.5) {
446 | output.push(val - lastMin);
447 | lastMin = val;
448 | } else {
449 | output.push(lastMax - val);
450 | lastMax = val;
451 | }
452 | }
453 |
454 | output.push(max - lastMin);
455 | output.push(lastMax - max);
456 | return output;
457 | }
458 |
459 | function shuffle(array) {
460 | for (var i = array.length - 1; i > 0; i--) {
461 | var j = Math.floor(Math.random() * (i + 1));
462 | var _ref = [array[j], array[i]];
463 | array[i] = _ref[0];
464 | array[j] = _ref[1];
465 | }
466 | }
467 |
468 | function polygonReflectX(polygon) {
469 | var reflectFactor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
470 |
471 | var _polygonBounds = polygonBounds(polygon),
472 | _polygonBounds2 = _slicedToArray(_polygonBounds, 2),
473 | _polygonBounds2$ = _slicedToArray(_polygonBounds2[0], 2),
474 | min = _polygonBounds2$[0],
475 | _ = _polygonBounds2$[1],
476 | _polygonBounds2$2 = _slicedToArray(_polygonBounds2[1], 2),
477 | max = _polygonBounds2$2[0],
478 | __ = _polygonBounds2$2[1];
479 |
480 | var p = [];
481 |
482 | for (var i = 0, l = polygon.length; i < l; i++) {
483 | var _polygon$i = _slicedToArray(polygon[i], 2),
484 | x = _polygon$i[0],
485 | y = _polygon$i[1];
486 |
487 | var r = [min + max - x, y];
488 |
489 | if (reflectFactor === 0) {
490 | p[i] = [x, y];
491 | } else if (reflectFactor === 1) {
492 | p[i] = r;
493 | } else {
494 | var t = lineInterpolate([[x, y], r]);
495 | p[i] = t(Math.max(Math.min(reflectFactor, 1), 0));
496 | }
497 | }
498 |
499 | return p;
500 | }
501 |
502 | function polygonReflectY(polygon) {
503 | var reflectFactor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
504 |
505 | var _polygonBounds = polygonBounds(polygon),
506 | _polygonBounds2 = _slicedToArray(_polygonBounds, 2),
507 | _polygonBounds2$ = _slicedToArray(_polygonBounds2[0], 2),
508 | _ = _polygonBounds2$[0],
509 | min = _polygonBounds2$[1],
510 | _polygonBounds2$2 = _slicedToArray(_polygonBounds2[1], 2),
511 | __ = _polygonBounds2$2[0],
512 | max = _polygonBounds2$2[1];
513 |
514 | var p = [];
515 |
516 | for (var i = 0, l = polygon.length; i < l; i++) {
517 | var _polygon$i = _slicedToArray(polygon[i], 2),
518 | x = _polygon$i[0],
519 | y = _polygon$i[1];
520 |
521 | var r = [x, min + max - y];
522 |
523 | if (reflectFactor === 0) {
524 | p[i] = [x, y];
525 | } else if (reflectFactor === 1) {
526 | p[i] = r;
527 | } else {
528 | var t = lineInterpolate([[x, y], r]);
529 | p[i] = t(Math.max(Math.min(reflectFactor, 1), 0));
530 | }
531 | }
532 |
533 | return p;
534 | }
535 |
536 | function polygonRegular() {
537 | var sides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 3;
538 | var area = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100;
539 | var center = arguments.length > 2 ? arguments[2] : undefined;
540 | var polygon = [],
541 | point = [0, 0],
542 | sum = [0, 0],
543 | angle = 0;
544 |
545 | for (var i = 0; i < sides; i++) {
546 | polygon[i] = point;
547 | sum[0] += point[0];
548 | sum[1] += point[1];
549 | point = pointTranslate(point, angle, Math.sqrt(4 * area * Math.tan(Math.PI / sides) / sides)); // https://web.archive.org/web/20180404142713/http://keisan.casio.com/exec/system/1355985985
550 |
551 | angle -= 360 / sides;
552 | }
553 |
554 | if (center) {
555 | var line = [[sum[0] / sides, sum[1] / sides], center];
556 | polygon = polygonTranslate(polygon, lineAngle(line), lineLength(line));
557 | }
558 |
559 | return polygon;
560 | }
561 |
562 | function polygonRotate(polygon, angle, origin) {
563 | var p = [];
564 |
565 | for (var i = 0, l = polygon.length; i < l; i++) {
566 | p[i] = pointRotate(polygon[i], angle, origin);
567 | }
568 |
569 | return p;
570 | }
571 |
572 | // The returned polygon's area is equal to the input polygon's area multiplied by the square of the scaleFactor.
573 | // The origin defaults to the polygon's centroid.
574 |
575 | function polygonScale(polygon, scale, origin) {
576 | if (!origin) {
577 | origin = polygonCentroid(polygon);
578 | }
579 |
580 | var p = [];
581 |
582 | for (var i = 0, l = polygon.length; i < l; i++) {
583 | var v = polygon[i],
584 | d = lineLength([origin, v]),
585 | a = lineAngle([origin, v]);
586 | p[i] = pointTranslate(origin, a, d * scale);
587 | }
588 |
589 | return p;
590 | }
591 |
592 | // The origin defaults to the polygon's centroid.
593 |
594 | function polygonScaleX(polygon, scale, origin) {
595 | if (!origin) {
596 | origin = polygonCentroid(polygon);
597 | }
598 |
599 | var p = [];
600 |
601 | for (var i = 0, l = polygon.length; i < l; i++) {
602 | var v = polygon[i],
603 | d = lineLength([origin, v]),
604 | a = lineAngle([origin, v]),
605 | t = pointTranslate(origin, a, d * scale);
606 | p[i] = [t[0], v[1]];
607 | }
608 |
609 | return p;
610 | }
611 |
612 | // The origin defaults to the polygon's centroid.
613 |
614 | function polygonScaleY(polygon, scale, origin) {
615 | if (!origin) {
616 | origin = polygonCentroid(polygon);
617 | }
618 |
619 | var p = [];
620 |
621 | for (var i = 0, l = polygon.length; i < l; i++) {
622 | var v = polygon[i],
623 | d = lineLength([origin, v]),
624 | a = lineAngle([origin, v]),
625 | t = pointTranslate(origin, a, d * scale);
626 | p[i] = [v[0], t[1]];
627 | }
628 |
629 | return p;
630 | }
631 |
632 | // If order is passed as a strings of "cw" or "clockwise", returns a polygon with a clockwise winding order.
633 | // Otherwise, returns a polygon with a counter-clockwise winding order.
634 |
635 | function polygonWind(polygon) {
636 | var order = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "ccw";
637 | if (polygon.length < 3) return null;
638 | var reversed = polygon.slice().reverse();
639 | var isClockwise = polygonArea(polygon, true) > 0;
640 |
641 | if (order === "cw" || order === "clockwise") {
642 | return isClockwise ? polygon : reversed;
643 | } else {
644 | return isClockwise ? reversed : polygon;
645 | }
646 | }
647 |
648 | function topPointFirst(line) {
649 | return line[1][1] > line[0][1] ? line : [line[1], line[0]];
650 | }
651 |
652 | function pointLeftofLine(point, line) {
653 | var t = topPointFirst(line);
654 | return cross(point, t[1], t[0]) < 0;
655 | }
656 | function pointRightofLine(point, line) {
657 | var t = topPointFirst(line);
658 | return cross(point, t[1], t[0]) > 0;
659 | }
660 | function pointOnLine(point, line) {
661 | var epsilon = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
662 | var l = lineLength(line);
663 | return pointWithLine(point, line, epsilon) && lineLength([line[0], point]) <= l && lineLength([line[1], point]) <= l;
664 | }
665 | function pointWithLine(point, line) {
666 | var epsilon = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
667 | return Math.abs(cross(point, line[0], line[1])) <= epsilon;
668 | }
669 |
670 | // Returns a boolean.
671 |
672 | function lineIntersectsLine(lineA, lineB) {
673 | var _lineA = _slicedToArray(lineA, 2),
674 | _lineA$ = _slicedToArray(_lineA[0], 2),
675 | a0x = _lineA$[0],
676 | a0y = _lineA$[1],
677 | _lineA$2 = _slicedToArray(_lineA[1], 2),
678 | a1x = _lineA$2[0],
679 | a1y = _lineA$2[1],
680 | _lineB = _slicedToArray(lineB, 2),
681 | _lineB$ = _slicedToArray(_lineB[0], 2),
682 | b0x = _lineB$[0],
683 | b0y = _lineB$[1],
684 | _lineB$2 = _slicedToArray(_lineB[1], 2),
685 | b1x = _lineB$2[0],
686 | b1y = _lineB$2[1]; // Test for shared points
687 |
688 |
689 | if (a0x === b0x && a0y === b0y) return true;
690 | if (a1x === b1x && a1y === b1y) return true; // Test for point on line
691 |
692 | if (pointOnLine(lineA[0], lineB) || pointOnLine(lineA[1], lineB)) return true;
693 | if (pointOnLine(lineB[0], lineA) || pointOnLine(lineB[1], lineA)) return true;
694 | var denom = (b1y - b0y) * (a1x - a0x) - (b1x - b0x) * (a1y - a0y);
695 | if (denom === 0) return false;
696 | var deltaY = a0y - b0y,
697 | deltaX = a0x - b0x,
698 | numer0 = (b1x - b0x) * deltaY - (b1y - b0y) * deltaX,
699 | numer1 = (a1x - a0x) * deltaY - (a1y - a0y) * deltaX,
700 | quotA = numer0 / denom,
701 | quotB = numer1 / denom;
702 | return quotA > 0 && quotA < 1 && quotB > 0 && quotB < 1;
703 | }
704 |
705 | // Returns a boolean.
706 |
707 | function lineIntersectsPolygon(line, polygon) {
708 | var intersects = false;
709 | var closed = close(polygon);
710 |
711 | for (var i = 0, l = closed.length - 1; i < l; i++) {
712 | var v0 = closed[i],
713 | v1 = closed[i + 1];
714 |
715 | if (lineIntersectsLine(line, [v0, v1]) || pointOnLine(v0, line) && pointOnLine(v1, line)) {
716 | intersects = true;
717 | break;
718 | }
719 | }
720 |
721 | return intersects;
722 | }
723 |
724 | // Determines whether a point is inside of a polygon, represented as an array of vertices.
725 | // From https://github.com/substack/point-in-polygon/blob/master/index.js,
726 | // based on the ray-casting algorithm from https://web.archive.org/web/20180115151705/https://wrf.ecse.rpi.edu//Research/Short_Notes/pnpoly.html
727 | // Wikipedia: https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm
728 | // Returns a boolean.
729 | function pointInPolygon(point, polygon) {
730 | var x = point[0],
731 | y = point[1],
732 | inside = false;
733 |
734 | for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
735 | var xi = polygon[i][0],
736 | yi = polygon[i][1],
737 | xj = polygon[j][0],
738 | yj = polygon[j][1];
739 |
740 | if (yi > y != yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
741 | inside = !inside;
742 | }
743 | }
744 |
745 | return inside;
746 | }
747 |
748 | // Returns a boolean.
749 |
750 | function pointOnPolygon(point, polygon) {
751 | var epsilon = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
752 | var on = false;
753 | var closed = close(polygon);
754 |
755 | for (var i = 0, l = closed.length - 1; i < l; i++) {
756 | if (pointOnLine(point, [closed[i], closed[i + 1]], epsilon)) {
757 | on = true;
758 | break;
759 | }
760 | }
761 |
762 | return on;
763 | }
764 |
765 | // Polygons are represented as an array of vertices, each of which is an array of two numbers,
766 | // where the first number represents its x-coordinate and the second its y-coordinate.
767 | // Returns a boolean.
768 |
769 | function polygonInPolygon(polygonA, polygonB) {
770 | var inside = true;
771 | var closed = close(polygonA);
772 |
773 | for (var i = 0, l = closed.length - 1; i < l; i++) {
774 | var v0 = closed[i]; // Points test
775 |
776 | if (!pointInPolygon(v0, polygonB)) {
777 | inside = false;
778 | break;
779 | } // Lines test
780 |
781 |
782 | if (lineIntersectsPolygon([v0, closed[i + 1]], polygonB)) {
783 | inside = false;
784 | break;
785 | }
786 | }
787 |
788 | return inside;
789 | }
790 |
791 | // Polygons are represented as an array of vertices, each of which is an array of two numbers,
792 | // where the first number represents its x-coordinate and the second its y-coordinate.
793 | // Returns a boolean.
794 |
795 | function polygonIntersectsPolygon(polygonA, polygonB) {
796 | var intersects = false,
797 | onCount = 0;
798 | var closed = close(polygonA);
799 |
800 | for (var i = 0, l = closed.length - 1; i < l; i++) {
801 | var v0 = closed[i],
802 | v1 = closed[i + 1];
803 |
804 | if (lineIntersectsPolygon([v0, v1], polygonB)) {
805 | intersects = true;
806 | break;
807 | }
808 |
809 | if (pointOnPolygon(v0, polygonB)) {
810 | ++onCount;
811 | }
812 |
813 | if (onCount === 2) {
814 | intersects = true;
815 | break;
816 | }
817 | }
818 |
819 | return intersects;
820 | }
821 |
822 | // Returns the angle of reflection given an angle of incidence and a surface angle.
823 | function angleReflect(incidenceAngle, surfaceAngle) {
824 | return (surfaceAngle * 2 - incidenceAngle % 360 + 360) % 360;
825 | }
826 |
827 | exports.lineAngle = lineAngle;
828 | exports.lineInterpolate = lineInterpolate;
829 | exports.lineLength = lineLength;
830 | exports.lineMidpoint = lineMidpoint;
831 | exports.lineRotate = lineRotate;
832 | exports.lineTranslate = lineTranslate;
833 | exports.pointRotate = pointRotate;
834 | exports.pointTranslate = pointTranslate;
835 | exports.polygonArea = polygonArea;
836 | exports.polygonBounds = polygonBounds;
837 | exports.polygonCentroid = polygonCentroid;
838 | exports.polygonHull = polygonHull;
839 | exports.polygonInterpolate = polygonInterpolate;
840 | exports.polygonLength = polygonLength;
841 | exports.polygonMean = polygonMean;
842 | exports.polygonRandom = polygonRandom;
843 | exports.polygonReflectX = polygonReflectX;
844 | exports.polygonReflectY = polygonReflectY;
845 | exports.polygonRegular = polygonRegular;
846 | exports.polygonRotate = polygonRotate;
847 | exports.polygonScale = polygonScale;
848 | exports.polygonScaleArea = polygonScaleArea;
849 | exports.polygonScaleX = polygonScaleX;
850 | exports.polygonScaleY = polygonScaleY;
851 | exports.polygonTranslate = polygonTranslate;
852 | exports.polygonWind = polygonWind;
853 | exports.lineIntersectsLine = lineIntersectsLine;
854 | exports.lineIntersectsPolygon = lineIntersectsPolygon;
855 | exports.pointInPolygon = pointInPolygon;
856 | exports.pointOnPolygon = pointOnPolygon;
857 | exports.pointLeftofLine = pointLeftofLine;
858 | exports.pointRightofLine = pointRightofLine;
859 | exports.pointOnLine = pointOnLine;
860 | exports.pointWithLine = pointWithLine;
861 | exports.polygonInPolygon = polygonInPolygon;
862 | exports.polygonIntersectsPolygon = polygonIntersectsPolygon;
863 | exports.angleReflect = angleReflect;
864 | exports.angleToDegrees = angleToDegrees;
865 | exports.angleToRadians = angleToRadians;
866 |
867 | Object.defineProperty(exports, '__esModule', { value: true });
868 |
869 | })));
870 |
--------------------------------------------------------------------------------
/build/geometric.min.js:
--------------------------------------------------------------------------------
1 | !function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(n.geometric={})}(this,function(n){"use strict";function t(n){return 180*n/Math.PI}function r(n){return t(Math.atan2(n[1][1]-n[0][1],n[1][0]-n[0][0]))}function e(n,t){return a(n)||l(n,t)||c()}function o(n){return i(n)||u(n)||f()}function i(n){if(Array.isArray(n)){for(var t=0,r=new Array(n.length);t1&&void 0!==arguments[1]&&arguments[1],r=e(n,2),o=e(r[0],2),i=o[0],a=o[1],u=e(r[1],2),l=u[0],f=u[1],c=function(n){return(l-i)*n+i},h=function(n){return(f-a)*n+a};return function(n){var r=t?n<0?0:n>1?1:n:n;return[c(r),h(r)]}}function g(n){return Math.sqrt(Math.pow(n[1][0]-n[0][0],2)+Math.pow(n[1][1]-n[0][1],2))}function v(n){return[(n[0][0]+n[1][0])/2,(n[0][1]+n[1][1])/2]}function p(n){return n/180*Math.PI}function s(n,t,r){var e=p(t||0);return!r||0===r[0]&&0===r[1]?y(n,e):y(n.map(function(n,t){return n-r[t]}),e).map(function(n,t){return n+r[t]})}function y(n,t){return[n[0]*Math.cos(t)-n[1]*Math.sin(t),n[0]*Math.sin(t)+n[1]*Math.cos(t)]}function d(n,t,r){return n.map(function(e){return s(e,t,r||v(n))})}function M(n){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,e=p(t);return[n[0]+r*Math.cos(e),n[1]+r*Math.sin(e)]}function m(n,t,r){return n.map(function(n){return M(n,t,r)})}function b(n){for(var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],r=0,e=0,o=n.length;er&&(r=f),co&&(o=c))}return i?[[t,e],[r,o]]:null}function A(n){for(var t=0,r=0,e=0,o=n.length,i=0;i=2&&w(r[r.length-2],r[r.length-1],t[e])<=0;)r.pop();r.push(t[e])}for(var o=[],i=t.length-1;i>=0;i--){for(;o.length>=2&&w(o[o.length-2],o[o.length-1],t[i])<=0;)o.pop();o.push(t[i])}return o.pop(),r.pop(),r.concat(o)}function R(n){return k(n)?n:[].concat(o(n),[n[0]])}function k(n){var t=n[0],r=n[n.length-1];return t[0]===r[0]&&t[1]===r[1]}function L(n){if(0===n.length)return 0;for(var t,r,e=-1,o=n.length,i=n[o-1],a=i[0],u=i[1],l=0;++e=1)return e[e.length-1];for(var o=L(e)*t,i=[],a=0,u=0;u0&&void 0!==arguments[0]?arguments[0]:3,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:100,e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[0,0],o=Math.sqrt(t/Math.PI),i=Array.from({length:n},function(){return 2*o*Math.random()}),a=Array.from({length:n},function(){return 2*o*Math.random()});i.sort(function(n,t){return n-t}),a.sort(function(n,t){return n-t});var u=O(i,i[0],i[i.length-1]),l=O(a,a[0],a[a.length-1]);E(l);var f=[],c=0,h=0,v=(u.map(function(n,t){return[n,l[t]]}).sort(function(n,t){return Math.atan2(t[1],t[0])-Math.atan2(n[1],n[0])}).forEach(function(n){c+=1*n[0],h+=1*n[1],f.push([c,h])}),A(f));return j(x(f,t/b(f)),r([v,e]),g([v,e]))}function O(n,t,r){for(var e=t,o=t,i=[],a=1;a.5?(i.push(u-e),e=u):(i.push(o-u),o=u)}return i.push(r-e),i.push(o-r),i}function E(n){for(var t=n.length-1;t>0;t--){var r=Math.floor(Math.random()*(t+1)),e=[n[r],n[t]];n[t]=e[0],n[r]=e[1]}}function F(n){for(var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,r=I(n),o=e(r,2),i=e(o[0],2),a=i[0],u=(i[1],e(o[1],2)),l=u[0],f=(u[1],[]),c=0,g=n.length;c1&&void 0!==arguments[1]?arguments[1]:1,r=I(n),o=e(r,2),i=e(o[0],2),a=(i[0],i[1]),u=e(o[1],2),l=(u[0],u[1]),f=[],c=0,g=n.length;c0&&void 0!==arguments[0]?arguments[0]:3,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:100,e=arguments.length>2?arguments[2]:void 0,o=[],i=[0,0],a=[0,0],u=0,l=0;l1&&void 0!==arguments[1]?arguments[1]:"ccw";if(n.length<3)return null;var r=n.slice().reverse(),e=b(n,!0)>0;return"cw"===t||"clockwise"===t?e?n:r:e?r:n}function H(n){return n[1][1]>n[0][1]?n:[n[1],n[0]]}function z(n,t){var r=H(t);return w(n,r[1],r[0])<0}function G(n,t){var r=H(t);return w(n,r[1],r[0])>0}function J(n,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,e=g(t);return K(n,t,r)&&g([t[0],n])<=e&&g([t[1],n])<=e}function K(n,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return Math.abs(w(n,t[0],t[1]))<=r}function N(n,t){var r=e(n,2),o=e(r[0],2),i=o[0],a=o[1],u=e(r[1],2),l=u[0],f=u[1],c=e(t,2),h=e(c[0],2),g=h[0],v=h[1],p=e(c[1],2),s=p[0],y=p[1];if(i===g&&a===v)return!0;if(l===s&&f===y)return!0;if(J(n[0],t)||J(n[1],t))return!0;if(J(t[0],n)||J(t[1],n))return!0;var d=(y-v)*(l-i)-(s-g)*(f-a);if(0===d)return!1;var M=a-v,m=i-g,b=(s-g)*M-(y-v)*m,I=(l-i)*M-(f-a)*m,A=b/d,w=I/d;return A>0&&A<1&&w>0&&w<1}function Q(n,t){for(var r=!1,e=R(t),o=0,i=e.length-1;oe!=c>e&&r<(f-u)*(e-l)/(c-l)+u&&(o=!o)}return o}function V(n,t){for(var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,e=!1,o=R(t),i=0,a=o.length-1;i
2 |
3 |
38 |
--------------------------------------------------------------------------------
/img/length-thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HarryStevens/geometric/5d5b15dc70cacaf7d43b825469b6bf67b3728d62/img/length-thumb.png
--------------------------------------------------------------------------------
/img/point-on-with-line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HarryStevens/geometric/5d5b15dc70cacaf7d43b825469b6bf67b3728d62/img/point-on-with-line.png
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | export { lineAngle } from "./src/lines/lineAngle";
2 | export { lineInterpolate } from "./src/lines/lineInterpolate";
3 | export { lineLength } from "./src/lines/lineLength";
4 | export { lineMidpoint } from "./src/lines/lineMidpoint";
5 | export { lineRotate } from "./src/lines/lineRotate";
6 | export { lineTranslate } from "./src/lines/lineTranslate";
7 |
8 | export { pointRotate } from "./src/points/pointRotate";
9 | export { pointTranslate } from "./src/points/pointTranslate";
10 |
11 | export { polygonArea } from "./src/polygons/polygonArea";
12 | export { polygonBounds } from "./src/polygons/polygonBounds";
13 | export { polygonCentroid } from "./src/polygons/polygonCentroid";
14 | export { polygonHull } from "./src/polygons/polygonHull";
15 | export { polygonInterpolate } from "./src/polygons/polygonInterpolate";
16 | export { polygonLength } from "./src/polygons/polygonLength";
17 | export { polygonMean } from "./src/polygons/polygonMean";
18 | export { polygonRandom } from "./src/polygons/polygonRandom";
19 | export { polygonReflectX } from "./src/polygons/polygonReflectX";
20 | export { polygonReflectY } from "./src/polygons/polygonReflectY";
21 | export { polygonRegular } from "./src/polygons/polygonRegular";
22 | export { polygonRotate } from "./src/polygons/polygonRotate";
23 | export { polygonScale } from "./src/polygons/polygonScale";
24 | export { polygonScaleArea } from "./src/polygons/polygonScaleArea";
25 | export { polygonScaleX } from "./src/polygons/polygonScaleX";
26 | export { polygonScaleY } from "./src/polygons/polygonScaleY";
27 | export { polygonTranslate } from "./src/polygons/polygonTranslate";
28 | export { polygonWind } from "./src/polygons/polygonWind";
29 |
30 | export { lineIntersectsLine } from "./src/relationships/lineIntersectsLine";
31 | export { lineIntersectsPolygon } from "./src/relationships/lineIntersectsPolygon";
32 | export { pointInPolygon } from "./src/relationships/pointInPolygon";
33 | export { pointOnPolygon } from "./src/relationships/pointOnPolygon";
34 | export { pointLeftofLine, pointRightofLine, pointOnLine, pointWithLine } from "./src/relationships/pointOnLine";
35 | export { polygonInPolygon } from "./src/relationships/polygonInPolygon";
36 | export { polygonIntersectsPolygon } from "./src/relationships/polygonIntersectsPolygon";
37 |
38 | export { angleReflect } from "./src/angles/angleReflect";
39 | export { angleToDegrees } from "./src/angles/angleToDegrees";
40 | export { angleToRadians } from "./src/angles/angleToRadians";
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "geometric",
3 | "version": "2.5.5",
4 | "description": "A JavaScript library with geometric functions.",
5 | "main": "build/geometric.js",
6 | "unpkg": "build/geometric.min.js",
7 | "jsdelivr": "build/geometric.min.js",
8 | "module": "index",
9 | "scripts": {
10 | "pretest": "rm -rf build && mkdir build && rollup -c --banner \"$(preamble)\"",
11 | "test": "tape 'test/**/*-test.js'",
12 | "prepublishOnly": "npm run test && uglifyjs build/geometric.js -c -m -o build/geometric.min.js",
13 | "postpublish": "zip -j build/geometric.zip -- LICENSE README.md build/geometric.js build/geometric.min.js"
14 | },
15 | "devDependencies": {
16 | "@babel/core": "^7.4.5",
17 | "@babel/preset-env": "^7.4.5",
18 | "babel": "^6.23.0",
19 | "package-preamble": "0.1",
20 | "rollup": "^0.62.0",
21 | "rollup-plugin-babel": "^4.3.2",
22 | "tape": "4",
23 | "uglify-js": "^2.8.11"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/HarryStevens/geometric.git"
28 | },
29 | "keywords": [
30 | "geometry"
31 | ],
32 | "author": {
33 | "name": "Harry Stevens",
34 | "email": "harryjosephstevens@gmail.com",
35 | "url": "http://harryjstevens.com/"
36 | },
37 | "license": "MIT",
38 | "bugs": {
39 | "url": "https://github.com/HarryStevens/geometric/issues"
40 | },
41 | "homepage": "https://github.com/HarryStevens/geometric#readme"
42 | }
43 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from "rollup-plugin-babel";
2 |
3 | export default {
4 | input: "index.js",
5 | output: {
6 | file: "build/geometric.js",
7 | format: "umd",
8 | name: "geometric"
9 | },
10 | plugins: [
11 | babel({
12 | exclude: "node_modules/**"
13 | })
14 | ]
15 | };
--------------------------------------------------------------------------------
/src/angles/angleReflect.js:
--------------------------------------------------------------------------------
1 | // Returns the angle of reflection given an angle of incidence and a surface angle.
2 | export function angleReflect(incidenceAngle, surfaceAngle) {
3 | return (surfaceAngle * 2 - incidenceAngle % 360 + 360) % 360;
4 | }
--------------------------------------------------------------------------------
/src/angles/angleToDegrees.js:
--------------------------------------------------------------------------------
1 | // Converts radians to degrees.
2 | export function angleToDegrees(angle){
3 | return angle * 180 / Math.PI;
4 | }
--------------------------------------------------------------------------------
/src/angles/angleToRadians.js:
--------------------------------------------------------------------------------
1 | // Converts degrees to radians.
2 | export function angleToRadians(angle){
3 | return angle / 180 * Math.PI;
4 | }
--------------------------------------------------------------------------------
/src/lines/lineAngle.js:
--------------------------------------------------------------------------------
1 | import { angleToDegrees } from "../angles/angleToDegrees";
2 |
3 | // Calculates the angle of a line, in degrees.
4 | export function lineAngle(line){
5 | return angleToDegrees(Math.atan2(line[1][1] - line[0][1], line[1][0] - line[0][0]));
6 | }
--------------------------------------------------------------------------------
/src/lines/lineInterpolate.js:
--------------------------------------------------------------------------------
1 | // Returns an interpolator function given a line [a, b].
2 | // The returned interpolator function takes a single argument t, where t is a number ranging from 0 to 1;
3 | // a value of 0 returns a, while a value of 1 returns b.
4 | // Intermediate values interpolate from start to end along the line segment.
5 | // By default, the returned interpolator will output points outside of the line segment if t is less than 0 or greater than 1.
6 | // You can pass an optional boolean indicating whether to the returned point to inside of the line segment,
7 | // even if t is greater than 1 or less then 0.
8 | export function lineInterpolate(line, clamp = false){
9 | const [[x1, y1], [x2, y2]] = line;
10 | const x = v => (x2 - x1) * v + x1;
11 | const y = v => (y2 - y1) * v + y1;
12 | return t => {
13 | const t0 = clamp ? t < 0 ? 0 : t > 1 ? 1 : t : t;
14 | return [ x(t0), y(t0) ];
15 | }
16 | }
--------------------------------------------------------------------------------
/src/lines/lineLength.js:
--------------------------------------------------------------------------------
1 | // Calculates the distance between the endpoints of a line segment.
2 | export function lineLength(line){
3 | return Math.sqrt(Math.pow(line[1][0] - line[0][0], 2) + Math.pow(line[1][1] - line[0][1], 2));
4 | }
--------------------------------------------------------------------------------
/src/lines/lineMidpoint.js:
--------------------------------------------------------------------------------
1 | // Calculates the midpoint of a line segment.
2 | export function lineMidpoint(line){
3 | return [(line[0][0] + line[1][0]) / 2, (line[0][1] + line[1][1]) / 2];
4 | }
--------------------------------------------------------------------------------
/src/lines/lineRotate.js:
--------------------------------------------------------------------------------
1 | import { lineMidpoint } from "./lineMidpoint";
2 | import { pointRotate } from "../points/pointRotate";
3 |
4 | // Returns the coordinates resulting from rotating a line about an origin by an angle in degrees.
5 | // If origin is not specified, the origin defaults to the midpoint of the line.
6 | export function lineRotate(line, angle, origin){
7 | return line.map(point => pointRotate(point, angle, origin || lineMidpoint(line)));
8 | }
--------------------------------------------------------------------------------
/src/lines/lineTranslate.js:
--------------------------------------------------------------------------------
1 | import { pointTranslate } from "../points/pointTranslate";
2 |
3 | // Returns the coordinates resulting from translating a line by an angle in degrees and a distance.
4 | export function lineTranslate(line, angle, distance){
5 | return line.map(point => pointTranslate(point, angle, distance));
6 | }
--------------------------------------------------------------------------------
/src/points/pointRotate.js:
--------------------------------------------------------------------------------
1 | import { angleToRadians } from "../angles/angleToRadians";
2 |
3 | // Rotates a point by an angle in degrees around an origin.
4 | export function pointRotate(point, angle, origin){
5 | const r = angleToRadians(angle || 0);
6 |
7 | if (!origin || (origin[0] === 0 && origin[1] === 0)){
8 | return rotate(point, r);
9 | }
10 | else {
11 | // See: https://math.stackexchange.com/questions/1964905/rotation-around-non-zero-point
12 | const p0 = point.map((c, i) => c - origin[i]);
13 | const rotated = rotate(p0, r);
14 | return rotated.map((c, i) => c + origin[i]);
15 | }
16 | }
17 |
18 | function rotate(point, angle){
19 | // See: https://en.wikipedia.org/wiki/Cartesian_coordinate_system#Rotation
20 | return [(point[0] * Math.cos(angle)) - point[1] * Math.sin(angle), (point[0] * Math.sin(angle)) + point[1] * Math.cos(angle)];
21 | }
--------------------------------------------------------------------------------
/src/points/pointTranslate.js:
--------------------------------------------------------------------------------
1 | import { angleToRadians } from "../angles/angleToRadians";
2 |
3 | // Translates a point by an angle in degrees and distance
4 | export function pointTranslate(point, angle = 0, distance = 0){
5 | const r = angleToRadians(angle);
6 | return [point[0] + distance * Math.cos(r), point[1] + distance * Math.sin(r)];
7 | }
--------------------------------------------------------------------------------
/src/polygons/polygonArea.js:
--------------------------------------------------------------------------------
1 | // Calculates the area of a polygon.
2 | export function polygonArea(vertices, signed = false){
3 | let a = 0;
4 |
5 | for (let i = 0, l = vertices.length; i < l; i++) {
6 | const v0 = vertices[i],
7 | v1 = vertices[i === l - 1 ? 0 : i + 1];
8 |
9 | a += v0[0] * v1[1];
10 | a -= v1[0] * v0[1];
11 | }
12 |
13 | return signed ? a / 2 : Math.abs(a / 2);
14 | }
--------------------------------------------------------------------------------
/src/polygons/polygonBounds.js:
--------------------------------------------------------------------------------
1 | // Calculates the bounds of a polygon.
2 | export function polygonBounds(polygon){
3 | if (polygon.length < 3){
4 | return null;
5 | }
6 |
7 | let xMin = Infinity,
8 | xMax = -Infinity,
9 | yMin = Infinity,
10 | yMax = -Infinity,
11 | found = false;
12 |
13 | for (let i = 0, l = polygon.length; i < l; i++){
14 | const p = polygon[i],
15 | x = p[0],
16 | y = p[1];
17 |
18 | if (x != null && isFinite(x) && y != null && isFinite(y)){
19 | found = true;
20 | if (x < xMin) xMin = x;
21 | if (x > xMax) xMax = x;
22 | if (y < yMin) yMin = y;
23 | if (y > yMax) yMax = y;
24 | }
25 | }
26 |
27 | return found ? [[xMin, yMin], [xMax, yMax]] : null;
28 | }
--------------------------------------------------------------------------------
/src/polygons/polygonCentroid.js:
--------------------------------------------------------------------------------
1 | // Calculates the weighted centroid a polygon.
2 | export function polygonCentroid(vertices){
3 | let a = 0, x = 0, y = 0, l = vertices.length;
4 |
5 | for (let i = 0; i < l; i++) {
6 | const s = i === l - 1 ? 0 : i + 1,
7 | v0 = vertices[i],
8 | v1 = vertices[s],
9 | f = (v0[0] * v1[1]) - (v1[0] * v0[1]);
10 |
11 | a += f;
12 | x += (v0[0] + v1[0]) * f;
13 | y += (v0[1] + v1[1]) * f;
14 | }
15 |
16 | const d = a * 3;
17 |
18 | return [x / d, y / d];
19 | }
--------------------------------------------------------------------------------
/src/polygons/polygonHull.js:
--------------------------------------------------------------------------------
1 | import { cross } from "../utils/crossProduct";
2 |
3 | // Caclulates the convex hull of a set of points.
4 | // See https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain#JavaScript
5 | export function polygonHull(points){
6 | if (points.length < 3) { return null; }
7 |
8 | const pointsCopy = points.slice().sort((a, b) => a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]);
9 |
10 | let lower = [];
11 | for (let i = 0; i < pointsCopy.length; i++) {
12 | while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], pointsCopy[i]) <= 0) {
13 | lower.pop();
14 | }
15 | lower.push(pointsCopy[i]);
16 | }
17 |
18 | let upper = [];
19 | for (let i = pointsCopy.length - 1; i >= 0; i--) {
20 | while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], pointsCopy[i]) <= 0) {
21 | upper.pop();
22 | }
23 | upper.push(pointsCopy[i]);
24 | }
25 |
26 | upper.pop();
27 | lower.pop();
28 |
29 | return lower.concat(upper);
30 | }
--------------------------------------------------------------------------------
/src/polygons/polygonInterpolate.js:
--------------------------------------------------------------------------------
1 | import { close } from "../utils/closePolygon";
2 | import { lineAngle } from "../lines/lineAngle";
3 | import { lineLength } from "../lines/lineLength";
4 | import { pointTranslate } from "../points/pointTranslate";
5 | import { polygonLength } from "./polygonLength";
6 |
7 | export function polygonInterpolate(polygon){
8 | return (t) => {
9 | if (t <= 0){
10 | return polygon[0];
11 | }
12 |
13 | const closed = close(polygon);
14 |
15 | if (t >= 1){
16 | return closed[closed.length - 1];
17 | }
18 |
19 | const target = polygonLength(closed) * t;
20 | let point = [], track = 0;
21 |
22 | for (let i = 0; i < closed.length - 1; i++){
23 | const side = [closed[i], closed[i + 1]],
24 | length = lineLength(side),
25 | angle = lineAngle(side),
26 | delta = target - (track += length);
27 |
28 | if (delta < 0){
29 | point = pointTranslate(side[0], angle, length + delta);
30 | break;
31 | }
32 |
33 | else if (i === polygon.length - 2){
34 | point = pointTranslate(side[0], angle, delta);
35 | }
36 | }
37 |
38 | return point;
39 | }
40 | }
--------------------------------------------------------------------------------
/src/polygons/polygonLength.js:
--------------------------------------------------------------------------------
1 | // Calculates the length of a polygon's perimeter. See https://github.com/d3/d3-polygon/blob/master/src/length.js
2 | export function polygonLength(vertices){
3 | if (vertices.length === 0) {
4 | return 0;
5 | }
6 |
7 | let i = -1,
8 | n = vertices.length,
9 | b = vertices[n - 1],
10 | xa,
11 | ya,
12 | xb = b[0],
13 | yb = b[1],
14 | perimeter = 0;
15 |
16 | while (++i < n) {
17 | xa = xb;
18 | ya = yb;
19 | b = vertices[i];
20 | xb = b[0];
21 | yb = b[1];
22 | xa -= xb;
23 | ya -= yb;
24 | perimeter += Math.sqrt(xa * xa + ya * ya);
25 | }
26 |
27 | return perimeter;
28 | }
--------------------------------------------------------------------------------
/src/polygons/polygonMean.js:
--------------------------------------------------------------------------------
1 | // Calculates the arithmetic mean of a polygon's vertices.
2 | export function polygonMean(vertices){
3 | let x = 0, y = 0, l = vertices.length;
4 |
5 | for (let i = 0; i < l; i++) {
6 | const v = vertices[i];
7 |
8 | x += v[0];
9 | y += v[1];
10 | }
11 |
12 | return [x / l, y / l];
13 | }
--------------------------------------------------------------------------------
/src/polygons/polygonRandom.js:
--------------------------------------------------------------------------------
1 | import { close } from "../utils/closePolygon";
2 | import { lineAngle } from "../lines/lineAngle";
3 | import { lineLength } from "../lines/lineLength";
4 | import { polygonArea } from "./polygonArea";
5 | import { polygonCentroid } from "./polygonCentroid";
6 | import { polygonScaleArea } from "./polygonScaleArea";
7 | import { polygonTranslate } from "./polygonTranslate";
8 |
9 | // Returns a random polygon according to the specific number of sides, area, and centroid.
10 | // Based on an algorithm by Pavel Valtr and an implementation by Maneesh Agrawala: https://observablehq.com/@magrawala/random-convex-polygon
11 | export function polygonRandom(sides = 3, area = 100, centroid = [0, 0]) {
12 | const r = Math.sqrt(area / Math.PI),
13 | xs = Array.from({ length: sides }, () => 2 * r * Math.random()),
14 | ys = Array.from({ length: sides }, () => 2 * r * Math.random());
15 |
16 | xs.sort((a, b) => a - b);
17 | ys.sort((a, b) => a - b);
18 |
19 | const vecXS = chain(xs, xs[0], xs[xs.length-1]),
20 | vecYS = chain(ys, ys[0], ys[ys.length-1]);
21 |
22 | shuffle(vecYS);
23 |
24 | //Make polygon coordinates from the vecs by laying them out end to end
25 | let polygon = [],
26 | x = 0, y = 0;
27 |
28 | // Zip the vector arrays together
29 | // Then, sort the vectors by angle, in a counter clockwise fashion.
30 | // a and b are tuples representing vectors. Compute angle for each vector and compare them.
31 | const vecs = vecXS
32 | .map((d, i) => [d, vecYS[i]])
33 | .sort((a, b) => Math.atan2(b[1], b[0]) - Math.atan2(a[1], a[0]))
34 | .forEach(vec => {
35 | x += vec[0] * 1;
36 | y += vec[1] * 1;
37 | polygon.push([x,y])
38 | });
39 |
40 | // Scale and translate
41 | const c = polygonCentroid(polygon);
42 |
43 | return polygonTranslate(
44 | polygonScaleArea(polygon, area / polygonArea(polygon)),
45 | lineAngle([c, centroid]),
46 | lineLength([c, centroid])
47 | );
48 | }
49 |
50 | function chain(values, min, max) {
51 | let lastMin = min, lastMax = min;
52 | const output = []
53 |
54 | for (let i = 1; i < values.length - 1; i++) {
55 | const val = values[i];
56 |
57 | if (Math.random() > 0.5) {
58 | output.push(val - lastMin);
59 | lastMin = val;
60 | } else {
61 | output.push(lastMax - val);
62 | lastMax = val;
63 | }
64 | }
65 |
66 | output.push(max - lastMin);
67 | output.push(lastMax - max);
68 |
69 | return output;
70 | }
71 |
72 | function shuffle(array) {
73 | for (let i = array.length - 1; i > 0; i--) {
74 | const j = Math.floor(Math.random() * (i + 1));
75 | [array[i], array[j]] = [array[j], array[i]];
76 | }
77 | }
--------------------------------------------------------------------------------
/src/polygons/polygonReflectX.js:
--------------------------------------------------------------------------------
1 | import { lineInterpolate } from "../lines/lineInterpolate";
2 | import { polygonBounds } from "./polygonBounds";
3 |
4 | export function polygonReflectX(polygon, reflectFactor = 1){
5 | const [[min, _], [max, __]] = polygonBounds(polygon);
6 | const p = [];
7 |
8 | for (let i = 0, l = polygon.length; i < l; i++){
9 | const [x, y] = polygon[i];
10 | const r = [min + max - x, y];
11 |
12 | if (reflectFactor === 0){
13 | p[i] = [x, y];
14 | }
15 | else if (reflectFactor === 1){
16 | p[i] = r;
17 | }
18 | else {
19 | const t = lineInterpolate([[x, y], r]);
20 | p[i] = t(Math.max(Math.min(reflectFactor, 1), 0));
21 | }
22 | }
23 |
24 | return p;
25 | }
--------------------------------------------------------------------------------
/src/polygons/polygonReflectY.js:
--------------------------------------------------------------------------------
1 | import { lineInterpolate } from "../lines/lineInterpolate";
2 | import { polygonBounds } from "./polygonBounds";
3 |
4 | export function polygonReflectY(polygon, reflectFactor = 1){
5 | const [[_, min], [__, max]] = polygonBounds(polygon);
6 | const p = [];
7 |
8 | for (let i = 0, l = polygon.length; i < l; i++){
9 | const [x, y] = polygon[i];
10 | const r = [x, min + max - y];
11 |
12 | if (reflectFactor === 0){
13 | p[i] = [x, y];
14 | }
15 | else if (reflectFactor === 1){
16 | p[i] = r;
17 | }
18 | else {
19 | const t = lineInterpolate([[x, y], r]);
20 | p[i] = t(Math.max(Math.min(reflectFactor, 1), 0));
21 | }
22 | }
23 |
24 | return p;
25 | }
--------------------------------------------------------------------------------
/src/polygons/polygonRegular.js:
--------------------------------------------------------------------------------
1 | import { lineAngle } from "../lines/lineAngle";
2 | import { lineLength } from "../lines/lineLength";
3 | import { pointTranslate } from "../points/pointTranslate";
4 | import { polygonTranslate } from "./polygonTranslate";
5 |
6 | // Returns the vertices of a regular polygon of the specified number of sides, area, and center coordinates.
7 | export function polygonRegular(sides = 3, area = 100, center){
8 | let polygon = [],
9 | point = [0, 0],
10 | sum = [0, 0],
11 | angle = 0;
12 |
13 | for (let i = 0; i < sides; i++){
14 | polygon[i] = point;
15 | sum[0] += point[0];
16 | sum[1] += point[1];
17 | point = pointTranslate(point, angle, Math.sqrt((4 * area) * Math.tan(Math.PI / sides) / sides)); // https://web.archive.org/web/20180404142713/http://keisan.casio.com/exec/system/1355985985
18 | angle -= 360 / sides;
19 | }
20 |
21 | if (center){
22 | const line = [[sum[0] / sides, sum[1] / sides], center]
23 | polygon = polygonTranslate(polygon, lineAngle(line), lineLength(line));
24 | }
25 |
26 | return polygon;
27 | }
--------------------------------------------------------------------------------
/src/polygons/polygonRotate.js:
--------------------------------------------------------------------------------
1 | import { pointRotate } from "../points/pointRotate";
2 |
3 | // Rotates a polygon by an angle in degrees around an origin.
4 | export function polygonRotate(polygon, angle, origin){
5 | let p = [];
6 |
7 | for (let i = 0, l = polygon.length; i < l; i++){
8 | p[i] = pointRotate(polygon[i], angle, origin);
9 | }
10 |
11 | return p;
12 | }
--------------------------------------------------------------------------------
/src/polygons/polygonScale.js:
--------------------------------------------------------------------------------
1 | import { lineAngle } from "../lines/lineAngle";
2 | import { lineLength } from "../lines/lineLength";
3 | import { pointTranslate } from "../points/pointTranslate";
4 | import { polygonCentroid } from "./polygonCentroid";
5 |
6 | // Scales a polygon by a scale factor (where 1 is the original size) from an origin point.
7 | // The returned polygon's area is equal to the input polygon's area multiplied by the square of the scaleFactor.
8 | // The origin defaults to the polygon's centroid.
9 | export function polygonScale(polygon, scale, origin){
10 | if (!origin){
11 | origin = polygonCentroid(polygon);
12 | }
13 |
14 | let p = [];
15 |
16 | for (let i = 0, l = polygon.length; i < l; i++){
17 | const v = polygon[i],
18 | d = lineLength([origin, v]),
19 | a = lineAngle([origin, v]);
20 |
21 | p[i] = pointTranslate(origin, a, d * scale);
22 | }
23 |
24 | return p;
25 | }
--------------------------------------------------------------------------------
/src/polygons/polygonScaleArea.js:
--------------------------------------------------------------------------------
1 | import { lineAngle } from "../lines/lineAngle";
2 | import { lineLength } from "../lines/lineLength";
3 | import { pointTranslate } from "../points/pointTranslate";
4 | import { polygonCentroid } from "./polygonCentroid";
5 |
6 | // Scales a polygon by a scale factor (where 1 is the original size) from an origin point.
7 | // The returned polygon's area is equal to the input polygon's area multiplied by the scaleFactor.
8 | // The origin defaults to the polygon's centroid.
9 | export function polygonScaleArea(polygon, scale, origin){
10 | if (!origin){
11 | origin = polygonCentroid(polygon);
12 | }
13 |
14 | let p = [];
15 |
16 | for (let i = 0, l = polygon.length; i < l; i++){
17 | const v = polygon[i],
18 | d = lineLength([origin, v]),
19 | a = lineAngle([origin, v]);
20 |
21 | p[i] = pointTranslate(origin, a, d * Math.sqrt(scale));
22 | }
23 |
24 | return p;
25 | }
--------------------------------------------------------------------------------
/src/polygons/polygonScaleX.js:
--------------------------------------------------------------------------------
1 | import { lineAngle } from "../lines/lineAngle";
2 | import { lineLength } from "../lines/lineLength";
3 | import { pointTranslate } from "../points/pointTranslate";
4 | import { polygonCentroid } from "./polygonCentroid";
5 |
6 | // Scales a polygon's x-coordinates by a scale factor (where 1 is the original size) from an origin point.
7 | // The origin defaults to the polygon's centroid.
8 | export function polygonScaleX(polygon, scale, origin){
9 | if (!origin){
10 | origin = polygonCentroid(polygon);
11 | }
12 |
13 | let p = [];
14 |
15 | for (let i = 0, l = polygon.length; i < l; i++){
16 | const v = polygon[i],
17 | d = lineLength([origin, v]),
18 | a = lineAngle([origin, v]),
19 | t = pointTranslate(origin, a, d * scale);
20 |
21 | p[i] = [t[0], v[1]];
22 | }
23 |
24 | return p;
25 | }
--------------------------------------------------------------------------------
/src/polygons/polygonScaleY.js:
--------------------------------------------------------------------------------
1 | import { lineAngle } from "../lines/lineAngle";
2 | import { lineLength } from "../lines/lineLength";
3 | import { pointTranslate } from "../points/pointTranslate";
4 | import { polygonCentroid } from "./polygonCentroid";
5 |
6 | // Scales a polygon's y-coordinates by a scale factor (where 1 is the original size) from an origin point.
7 | // The origin defaults to the polygon's centroid.
8 | export function polygonScaleY(polygon, scale, origin){
9 | if (!origin){
10 | origin = polygonCentroid(polygon);
11 | }
12 |
13 | let p = [];
14 |
15 | for (let i = 0, l = polygon.length; i < l; i++){
16 | const v = polygon[i],
17 | d = lineLength([origin, v]),
18 | a = lineAngle([origin, v]),
19 | t = pointTranslate(origin, a, d * scale);
20 |
21 | p[i] = [v[0], t[1]];
22 | }
23 |
24 | return p;
25 | }
--------------------------------------------------------------------------------
/src/polygons/polygonTranslate.js:
--------------------------------------------------------------------------------
1 | import { pointTranslate } from "../points/pointTranslate";
2 |
3 | // Translates a polygon by an angle in degrees and distance.
4 | export function polygonTranslate(polygon, angle, distance){
5 | let p = [];
6 |
7 | for (let i = 0, l = polygon.length; i < l; i++){
8 | p[i] = pointTranslate(polygon[i], angle, distance);
9 | }
10 |
11 | return p;
12 | }
--------------------------------------------------------------------------------
/src/polygons/polygonWind.js:
--------------------------------------------------------------------------------
1 | import { polygonArea } from "./polygonArea";
2 |
3 | // Returns a polygon in the specified winding order.
4 | // If order is passed as a strings of "cw" or "clockwise", returns a polygon with a clockwise winding order.
5 | // Otherwise, returns a polygon with a counter-clockwise winding order.
6 | export function polygonWind(polygon, order = "ccw") {
7 | if (polygon.length < 3) return null;
8 |
9 | const reversed = polygon.slice().reverse();
10 | const isClockwise = polygonArea(polygon, true) > 0;
11 |
12 | if (order === "cw" || order === "clockwise") {
13 | return isClockwise ? polygon : reversed;
14 | }
15 | else {
16 | return isClockwise ? reversed : polygon;
17 | }
18 | }
--------------------------------------------------------------------------------
/src/relationships/lineIntersectsLine.js:
--------------------------------------------------------------------------------
1 | import { pointOnLine } from "../relationships/pointOnLine";
2 |
3 | // Determines if lineA intersects lineB.
4 | // Returns a boolean.
5 | export function lineIntersectsLine(lineA, lineB) {
6 | const [[a0x, a0y], [a1x, a1y]] = lineA,
7 | [[b0x, b0y], [b1x, b1y]] = lineB;
8 |
9 | // Test for shared points
10 | if (a0x === b0x && a0y === b0y) return true;
11 | if (a1x === b1x && a1y === b1y) return true;
12 |
13 | // Test for point on line
14 | if (pointOnLine(lineA[0], lineB) || pointOnLine(lineA[1], lineB)) return true;
15 | if (pointOnLine(lineB[0], lineA) || pointOnLine(lineB[1], lineA)) return true;
16 |
17 | const denom = ((b1y - b0y) * (a1x - a0x)) - ((b1x - b0x) * (a1y - a0y));
18 |
19 | if (denom === 0) return false;
20 |
21 | const deltaY = a0y - b0y,
22 | deltaX = a0x - b0x,
23 | numer0 = ((b1x - b0x) * deltaY) - ((b1y - b0y) * deltaX),
24 | numer1 = ((a1x - a0x) * deltaY) - ((a1y - a0y) * deltaX),
25 | quotA = numer0 / denom,
26 | quotB = numer1 / denom;
27 |
28 | return quotA > 0 && quotA < 1 && quotB > 0 && quotB < 1;
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/src/relationships/lineIntersectsPolygon.js:
--------------------------------------------------------------------------------
1 | import { close } from "../utils/closePolygon";
2 | import { lineIntersectsLine } from "./lineIntersectsLine";
3 | import { pointOnLine } from "../relationships/pointOnLine";
4 |
5 | // Determines whether a line intersects a polygon.
6 | // Returns a boolean.
7 | export function lineIntersectsPolygon(line, polygon){
8 | let intersects = false;
9 | const closed = close(polygon);
10 |
11 | for (let i = 0, l = closed.length - 1; i < l; i++){
12 | const v0 = closed[i],
13 | v1 = closed[i + 1];
14 |
15 | if (lineIntersectsLine(line, [v0, v1]) || (pointOnLine(v0, line) && pointOnLine(v1, line))){
16 | intersects = true;
17 | break;
18 | }
19 | }
20 |
21 | return intersects;
22 | }
--------------------------------------------------------------------------------
/src/relationships/pointInPolygon.js:
--------------------------------------------------------------------------------
1 | // Determines whether a point is inside of a polygon, represented as an array of vertices.
2 | // From https://github.com/substack/point-in-polygon/blob/master/index.js,
3 | // based on the ray-casting algorithm from https://web.archive.org/web/20180115151705/https://wrf.ecse.rpi.edu//Research/Short_Notes/pnpoly.html
4 | // Wikipedia: https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm
5 | // Returns a boolean.
6 | export function pointInPolygon(point, polygon) {
7 | let x = point[0],
8 | y = point[1],
9 | inside = false;
10 |
11 | for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
12 | const xi = polygon[i][0], yi = polygon[i][1],
13 | xj = polygon[j][0], yj = polygon[j][1];
14 |
15 | if (((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) { inside = !inside; }
16 | }
17 |
18 | return inside;
19 | }
--------------------------------------------------------------------------------
/src/relationships/pointOnLine.js:
--------------------------------------------------------------------------------
1 | import { lineLength } from "../lines/lineLength";
2 | import { cross } from "../utils/crossProduct";
3 |
4 | // See https://math.stackexchange.com/questions/274712/calculate-on-which-side-of-a-straight-line-is-a-given-point-located
5 | function topPointFirst(line){
6 | return line[1][1] > line[0][1] ? line : [line[1], line[0]];
7 | }
8 |
9 | export function pointLeftofLine(point, line){
10 | const t = topPointFirst(line);
11 | return cross(point, t[1], t[0]) < 0;
12 | }
13 | export function pointRightofLine(point, line){
14 | const t = topPointFirst(line);
15 | return cross(point, t[1], t[0]) > 0;
16 | }
17 | export function pointOnLine(point, line, epsilon = 0){
18 | const l = lineLength(line);
19 | return pointWithLine(point, line, epsilon) && lineLength([line[0], point]) <= l && lineLength([line[1], point]) <= l;
20 | }
21 | export function pointWithLine(point, line, epsilon = 0){
22 | return Math.abs(cross(point, line[0], line[1])) <= epsilon;
23 | }
--------------------------------------------------------------------------------
/src/relationships/pointOnPolygon.js:
--------------------------------------------------------------------------------
1 | import { close } from "../utils/closePolygon";
2 | import { pointOnLine } from "./pointOnLine";
3 |
4 | // Determines whether a point is located on one of the edges of a polygon.
5 | // Returns a boolean.
6 | export function pointOnPolygon(point, polygon, epsilon = 0){
7 | let on = false;
8 | const closed = close(polygon);
9 |
10 | for (let i = 0, l = closed.length - 1; i < l; i++){
11 | if (pointOnLine(point, [closed[i], closed[i + 1]], epsilon)){
12 | on = true;
13 | break;
14 | }
15 | }
16 |
17 | return on;
18 | }
--------------------------------------------------------------------------------
/src/relationships/polygonInPolygon.js:
--------------------------------------------------------------------------------
1 | import { close } from "../utils/closePolygon";
2 | import { lineIntersectsPolygon } from "./lineIntersectsPolygon";
3 | import { pointInPolygon } from "./pointInPolygon";
4 |
5 | // Determines whether a polygon is contained by another polygon.
6 | // Polygons are represented as an array of vertices, each of which is an array of two numbers,
7 | // where the first number represents its x-coordinate and the second its y-coordinate.
8 | // Returns a boolean.
9 | export function polygonInPolygon(polygonA, polygonB){
10 | let inside = true;
11 | const closed = close(polygonA);
12 |
13 | for (let i = 0, l = closed.length - 1; i < l; i++){
14 | const v0 = closed[i];
15 |
16 | // Points test
17 | if (!pointInPolygon(v0, polygonB)){
18 | inside = false;
19 | break;
20 | }
21 |
22 | // Lines test
23 | if (lineIntersectsPolygon([v0, closed[i + 1]], polygonB)){
24 | inside = false;
25 | break;
26 | }
27 | }
28 |
29 | return inside;
30 | }
--------------------------------------------------------------------------------
/src/relationships/polygonIntersectsPolygon.js:
--------------------------------------------------------------------------------
1 | import { close } from "../utils/closePolygon";
2 | import { lineIntersectsPolygon } from "./lineIntersectsPolygon";
3 | import { pointOnPolygon } from "./pointOnPolygon";
4 |
5 | // Determines whether a polygon intersects but is not contained by another polygon.
6 | // Polygons are represented as an array of vertices, each of which is an array of two numbers,
7 | // where the first number represents its x-coordinate and the second its y-coordinate.
8 | // Returns a boolean.
9 | export function polygonIntersectsPolygon(polygonA, polygonB){
10 | let intersects = false,
11 | onCount = 0;
12 | const closed = close(polygonA);
13 |
14 | for (let i = 0, l = closed.length - 1; i < l; i++){
15 | const v0 = closed[i],
16 | v1 = closed[i + 1];
17 |
18 | if (lineIntersectsPolygon([v0, v1], polygonB)){
19 | intersects = true;
20 | break;
21 | }
22 |
23 | if (pointOnPolygon(v0, polygonB)){
24 | ++onCount;
25 | }
26 |
27 | if (onCount === 2){
28 | intersects = true;
29 | break;
30 | }
31 | }
32 |
33 | return intersects;
34 | }
--------------------------------------------------------------------------------
/src/utils/closePolygon.js:
--------------------------------------------------------------------------------
1 | // Closes a polygon if it's not closed already. Does not modify input polygon.
2 | export function close(polygon){
3 | return isClosed(polygon) ? polygon : [...polygon, polygon[0]];
4 | }
5 |
6 | // Tests whether a polygon is closed
7 | export function isClosed(polygon){
8 | const first = polygon[0],
9 | last = polygon[polygon.length - 1];
10 | return first[0] === last[0] && first[1] === last[1];
11 | }
--------------------------------------------------------------------------------
/src/utils/crossProduct.js:
--------------------------------------------------------------------------------
1 | // See https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain#JavaScript
2 | // and https://math.stackexchange.com/questions/274712/calculate-on-which-side-of-a-straight-line-is-a-given-point-located
3 | export function cross (a, b, o){
4 | return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
5 | }
--------------------------------------------------------------------------------
/test/angleReflect-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("angleReflect(incidenceAngle, surfaceAngle) returns the angle of reflection.", function(test) {
5 | test.equal(geometric.angleReflect(0, 90), 180);
6 | test.equal(geometric.angleReflect(0, 45), 90);
7 | test.equal(geometric.angleReflect(180, 90), 0);
8 | test.equal(geometric.angleReflect(180, 45), 270);
9 | test.equal(geometric.angleReflect(45, 0), 315);
10 | test.equal(geometric.angleReflect(45, 90), 135);
11 | test.equal(geometric.angleReflect(45, 180), 315);
12 | test.equal(geometric.angleReflect(45, 270), 135);
13 | test.equal(geometric.angleReflect(45, 360), 315);
14 | test.end();
15 | });
16 |
17 | tape("angleReflect(incidenceAngle, surfaceAngle) returns an angle in [0, 360).", function(test) {
18 | test.equal(geometric.angleReflect(361, 0), 359);
19 | test.equal(geometric.angleReflect(-720, 0), 0);
20 | test.end();
21 | });
22 |
23 |
--------------------------------------------------------------------------------
/test/angleToDegrees-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("angleToDegrees(angle) converts an angle from radians to degrees", function(test) {
5 | test.equal(geometric.angleToDegrees(Math.PI), 180);
6 | test.equal(geometric.angleToDegrees(Math.PI * 2), 360);
7 | test.equal(geometric.angleToDegrees(0), 0);
8 | test.end();
9 | });
--------------------------------------------------------------------------------
/test/angleToRadians-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("angleToRadians(angle) converts an angle from degrees to radians", function(test) {
5 | test.equal(geometric.angleToRadians(180).toFixed(3), (Math.PI).toFixed(3));
6 | test.equal(geometric.angleToRadians(360).toFixed(3), (Math.PI * 2).toFixed(3));
7 | test.equal(geometric.angleToRadians(0), 0);
8 | test.end();
9 | });
--------------------------------------------------------------------------------
/test/lineAngle-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("lineAngle(line) calculates the angle of a line, in degrees", function(test) {
5 | test.equal(geometric.lineAngle([[0, 0], [0, 1]]), 90);
6 | test.equal(geometric.lineAngle([[0, 0], [0, -1]]), -90);
7 | test.equal(geometric.lineAngle([[0, 0], [1, 0]]), 0);
8 | test.equal(geometric.lineAngle([[0, 0], [-1, 0]]), 180);
9 | test.equal(geometric.lineAngle([[0, 0], [1, 1]]), 45);
10 | test.equal(geometric.lineAngle([[0, 0], [-1, -1]]), -135);
11 | test.end();
12 | });
--------------------------------------------------------------------------------
/test/lineInterpolate-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape");
2 | const geometric = require("../");
3 | const round = require("./utils/roundArray");
4 |
5 |
6 | tape("lineInterpolate(line) returns points along a line", test => {
7 | const line = [[236, 0], [708, 190]];
8 | const interpolator = geometric.lineInterpolate(line);
9 | test.deepEqual(round(interpolator(0), 1), line[0]);
10 | test.deepEqual(round(interpolator(1), 1), line[1]);
11 | test.deepEqual(round(interpolator(.1), 1), [283.2, 19]);
12 | test.deepEqual(round(interpolator(.2), 1), [330.4, 38]);
13 | test.deepEqual(round(interpolator(.3), 1), [377.6, 57]);
14 | test.deepEqual(round(interpolator(.4), 1), [424.8, 76]);
15 | test.deepEqual(round(interpolator(.5), 1), [472, 95]);
16 | test.deepEqual(round(interpolator(.6), 1), [519.2, 114]);
17 | test.deepEqual(round(interpolator(.7), 1), [566.4, 133]);
18 | test.deepEqual(round(interpolator(.8), 1), [613.6, 152]);
19 | test.deepEqual(round(interpolator(.9), 1), [660.8, 171]);
20 | test.end();
21 | });
22 |
23 | tape("lineInterpolate(line) returns points outside the line segment if clamp is false", test => {
24 | const line = [[236, 0], [708, 190]];
25 | const interpolator = geometric.lineInterpolate(line);
26 | test.deepEqual(round(interpolator(-0.1), 1), [188.8, -19]);
27 | test.deepEqual(round(interpolator(1.1), 1), [755.2, 209]);
28 | test.end();
29 | });
30 |
31 | tape("lineInterpolate(line) returns points inside the line segment if clamp is true", test => {
32 | const line = [[236, 0], [708, 190]];
33 | const interpolator = geometric.lineInterpolate(line, true);
34 | test.deepEqual(interpolator(-0.1), line[0]);
35 | test.deepEqual(interpolator(1.1), line[1]);
36 | test.end();
37 | });
--------------------------------------------------------------------------------
/test/lineIntersectsLine-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("lineIntersectsLine(lineA, lineB) determines whether lineA intersects lineB", test => {
5 | const lineA = [[1, 4], [3, 4]],
6 | lineB = [[2, 1], [2, 7]],
7 | lineC = [[1, 8], [3, 8]],
8 | lineD = [[1, 8], [3, 8]],
9 | lineE = [[1, 9], [3, 9]],
10 | lineF = [[1, 2], [3, 4]],
11 | lineG = [[0, 1], [2, 3]];
12 |
13 | // 1.
14 | test.equal(geometric.lineIntersectsLine(lineA, lineB), true);
15 | // 2.
16 | test.equal(geometric.lineIntersectsLine(lineA, lineC), false);
17 | // 3.
18 | test.equal(geometric.lineIntersectsLine(lineB, lineC), false);
19 | // 4. Same lines
20 | test.equal(geometric.lineIntersectsLine(lineC, lineD), true);
21 | // 5. Parallel lines not crossing
22 | test.equal(geometric.lineIntersectsLine(lineE, lineD), false);
23 | // 6. Parallel lines overlapping
24 | test.equal(geometric.lineIntersectsLine(lineF, lineG), true);
25 | test.end();
26 | });
27 |
28 | // See https://github.com/HarryStevens/geometric/issues/10
29 | tape("lineIntersectsLine(lineA, lineB) returns true if lineA and lineB share an endpoint", test => {
30 | const line1 = [
31 | [50.054358, 8.693184],
32 | [50.055604, 8.685873]
33 | ];
34 | const line2 = [
35 | [50.054228, 8.69338],
36 | [50.054358, 8.693184]
37 | ];
38 |
39 | // 7.
40 | test.equal(geometric.lineIntersectsLine(line1, line2), true);
41 | // 8.
42 | test.equal(geometric.lineIntersectsLine(line2, line1), true);
43 | test.end();
44 | });
45 |
46 | // See https://github.com/HarryStevens/geometric/issues/10#issuecomment-880587209
47 | tape("lineIntersectsLine(lineA, lineB) returns true if one of lineA and lineB points is on the other line", test => {
48 | const line1 = [
49 | [0, 0],
50 | [1, 0]
51 | ];
52 | const line2 = [
53 | [0.5, -1],
54 | [0.5, 0]
55 | ];
56 |
57 | // 9.
58 | test.equal(geometric.lineIntersectsLine(line1, line2), true);
59 | // 10.
60 | test.equal(geometric.lineIntersectsLine(line2, line1), true);
61 | test.end();
62 | });
63 |
64 | // https://github.com/HarryStevens/geometric/pull/25
65 | tape("lineIntersectsLine(lineA, lineB) returns false if two segments are collinear but do not intersect", test => {
66 | const line1 = [[0, 0], [10, 0]];
67 | const line2 = [[0, 0], [11, 0]];
68 | const line3 = [[15, 0],[25, 0]];
69 |
70 | // 11.
71 | test.equal(geometric.lineIntersectsLine(line1, line3), false);
72 | // 12.
73 | test.equal(geometric.lineIntersectsLine(line2, line3), false);
74 | test.end();
75 | })
--------------------------------------------------------------------------------
/test/lineIntersectsPolygon-test.js:
--------------------------------------------------------------------------------
1 | var tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("lineIntersectsPolygon(line, polygon) determines whether a line intersects a polygon", function(test) {
5 | const polygon = [[5, 3], [10, 3], [10, 8], [5, 8]],
6 | lineA = [[4, 6], [8, 2]],
7 | lineB = [[4, 6], [11, 6]],
8 | lineC = [[4, 9], [11, 9]];
9 |
10 | test.equal(geometric.lineIntersectsPolygon(lineA, polygon), true);
11 | test.equal(geometric.lineIntersectsPolygon(lineB, polygon), true);
12 | test.equal(geometric.lineIntersectsPolygon(lineC, polygon), false);
13 | test.end();
14 | });
15 |
16 | tape("lineIntersectsPolygon(line, polygon) should return true even if the line is only collinear with one of the polygon's segments", function(test) {
17 | const polygon = [[388, 150], [458, 150], [458, 110], [478, 53], [486, 50], [488, 20], [490, 50], [498, 53], [518, 110], [518, 150], [588, 150], [588, 270], [388, 270]],
18 | line = [[98, 150], [878, 150]];
19 |
20 | test.equal(geometric.lineIntersectsPolygon(line, polygon), true);
21 | test.end();
22 | });
--------------------------------------------------------------------------------
/test/lineLength-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("lineLength(line) calculates the length of a line segment", function(test) {
5 | test.equal(geometric.lineLength([[0, 0], [0, 1]]), 1);
6 | test.equal(geometric.lineLength([[0, 0], [0, -1]]), 1);
7 | test.equal(geometric.lineLength([[1, 0], [1, 0]]), 0);
8 | test.equal(geometric.lineLength([[1, 0], [-1, 0]]), 2);
9 | test.end();
10 | });
--------------------------------------------------------------------------------
/test/lineMidpoint-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("lineMidpoint(line) calculates the midpoint of a line segment", function(test) {
5 | test.equal(geometric.lineMidpoint([[0, 0], [0, 1]])[0], 0);
6 | test.equal(geometric.lineMidpoint([[0, 0], [0, 1]])[1], .5);
7 | test.equal(geometric.lineMidpoint([[0, 0], [0, -1]])[0], 0);
8 | test.equal(geometric.lineMidpoint([[0, 0], [0, -1]])[1], -.5);
9 | test.equal(geometric.lineMidpoint([[1, 0], [1, 0]])[0], 1);
10 | test.equal(geometric.lineMidpoint([[1, 0], [1, 0]])[1], 0);
11 | test.equal(geometric.lineMidpoint([[1, 0], [-1, 0]])[0], 0);
12 | test.equal(geometric.lineMidpoint([[1, 0], [-1, 0]])[1], 0);
13 | test.end();
14 | });
--------------------------------------------------------------------------------
/test/lineRotate-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("lineRotate(line, angle, origin) rotates a line by an angle in degrees around an origin, where the origin defaults to the midpoint", test => {
5 | const line = [[-1, -1], [1, 1]];
6 | const midpoint = geometric.lineMidpoint(line);
7 | const interpolator = geometric.polygonInterpolate(line);
8 |
9 | test.deepEqual(geometric.lineRotate(line, 0), line);
10 | test.deepEqual(geometric.lineRotate(line, 0, midpoint), line);
11 |
12 | test.deepEqual(round(geometric.lineRotate(line, 90)), [[1, -1], [-1, 1]]);
13 | test.deepEqual(round(geometric.lineRotate(line, 90, midpoint)), [[1, -1], [-1, 1]]);
14 | test.deepEqual(round(geometric.lineRotate(line, 90, line[0])), [[-1, -1], [-3, 1]]);
15 | test.deepEqual(round(geometric.lineRotate(line, 90, line[1])), [[3, -1], [1, 1]]);
16 |
17 | test.deepEqual(round(geometric.lineRotate(line, 180)), [[1, 1], [-1, -1]]);
18 | test.deepEqual(round(geometric.lineRotate(line, 180, midpoint)), [[1, 1], [-1, -1]]);
19 | test.deepEqual(round(geometric.lineRotate(line, 180, line[0])), [[-1, -1], [-3, -3]]);
20 | test.deepEqual(round(geometric.lineRotate(line, 180, line[1])), [[3, 3], [1, 1]]);
21 |
22 | test.deepEqual(round(geometric.lineRotate(line, 270)), [[-1, 1], [1, -1]]);
23 | test.deepEqual(round(geometric.lineRotate(line, 270, midpoint)), [[-1, 1], [1, -1]]);
24 | test.deepEqual(round(geometric.lineRotate(line, 270, line[0])), [[-1, -1], [1, -3]]);
25 | test.deepEqual(round(geometric.lineRotate(line, 270, line[1])), [[-1, 3], [1, 1]]);
26 |
27 | test.deepEqual(round(geometric.lineRotate(line, 360)), line);
28 | test.deepEqual(round(geometric.lineRotate(line, 360, midpoint)), line);
29 | test.deepEqual(round(geometric.lineRotate(line, 360, line[0])), line);
30 | test.deepEqual(round(geometric.lineRotate(line, 360, line[1])), line);
31 |
32 | test.end();
33 | });
34 |
35 | function round(line){
36 | return line.map(d => d.map(Math.round));
37 | }
--------------------------------------------------------------------------------
/test/lineTranslate-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("lineTranslate(line, angle, distance) translates a line by an angle in degrees and a distance", test => {
5 | const line = [[-1, -1], [1, 1]];
6 |
7 | test.deepEqual(geometric.lineTranslate(line), line);
8 | test.deepEqual(geometric.lineTranslate(line, 0), line);
9 | test.deepEqual(geometric.lineTranslate(line, 0, 0), line);
10 |
11 | test.deepEqual(geometric.lineTranslate(line, 0, 1), [[0, -1], [2, 1]]);
12 | test.deepEqual(geometric.lineTranslate(line, 0, -1), [[-2, -1], [0, 1]]);
13 |
14 | test.deepEqual(round(geometric.lineTranslate(line, 90, 1)), [[-1, 0], [1, 2]]);
15 | test.deepEqual(round(geometric.lineTranslate(line, 90, -1)), [[-1, -2], [1, 0]]);
16 |
17 | test.deepEqual(round(geometric.lineTranslate(line, 180, 1)), [[-2, -1], [0, 1]]);
18 | test.deepEqual(round(geometric.lineTranslate(line, 180, -1)), [[0, -1], [2, 1]]);
19 |
20 | test.deepEqual(round(geometric.lineTranslate(line, 270, 1)), [[-1, -2], [1, 0]]);
21 | test.deepEqual(round(geometric.lineTranslate(line, 270, -1)), [[-1, 0], [1, 2]]);
22 |
23 | test.deepEqual(round(geometric.lineTranslate(line, 360, 1)), [[0, -1], [2, 1]]);
24 | test.deepEqual(round(geometric.lineTranslate(line, 360, -1)), [[-2, -1], [0, 1]]);
25 |
26 | test.end();
27 | });
28 |
29 | function round(line){
30 | return line.map(d => d.map(Math.round));
31 | }
--------------------------------------------------------------------------------
/test/pointInPolygon-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("pointInPolygon(point, vertices) determines whether a point is in a polygon", function(test) {
5 | const polygon = [[0, 0], [2, 0], [2, 2], [0, 2]];
6 | test.equal(geometric.pointInPolygon([1, 1], polygon), true);
7 | test.equal(geometric.pointInPolygon([3, 3], polygon), false);
8 | test.end();
9 | });
--------------------------------------------------------------------------------
/test/pointOnLine-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../"),
3 | line = [[1, 0], [1, 2]];
4 |
5 | tape("pointLeftofLine(point, line) determines whether a point is to the left of a line", function(test) {
6 | test.equal(geometric.pointLeftofLine([0, 1], line), true);
7 | test.equal(geometric.pointLeftofLine([1, 1], line), false);
8 | test.equal(geometric.pointLeftofLine([2, 1], line), false);
9 | test.equal(geometric.pointLeftofLine([0, 1], line.reverse()), true);
10 | test.equal(geometric.pointLeftofLine([1, 1], line.reverse()), false);
11 | test.equal(geometric.pointLeftofLine([2, 1], line.reverse()), false);
12 | test.end();
13 | });
14 |
15 | tape("pointRightofLine(point, line) determines whether a point is to the right of a line", function(test) {
16 | test.equal(geometric.pointRightofLine([0, 1], line), false);
17 | test.equal(geometric.pointRightofLine([1, 1], line), false);
18 | test.equal(geometric.pointRightofLine([2, 1], line), true);
19 | test.equal(geometric.pointRightofLine([0, 1], line.reverse()), false);
20 | test.equal(geometric.pointRightofLine([1, 1], line.reverse()), false);
21 | test.equal(geometric.pointRightofLine([2, 1], line.reverse()), true);
22 |
23 | test.end();
24 | });
25 |
26 | tape("pointOnLine(point, line) determines whether a point is collinear with a line and is also on the line segment", function(test) {
27 | test.equal(geometric.pointOnLine([0, 1], line), false);
28 | test.equal(geometric.pointOnLine([1, 1], line), true);
29 | test.equal(geometric.pointOnLine([2, 1], line), false);
30 |
31 | // The point cannot be located outside of the line segment
32 | test.equal(geometric.pointOnLine([1, 100], line), false);
33 |
34 | test.end();
35 | });
36 |
37 | tape("pointWithLine(point, line) determines whether a point is collinear with a line", function(test) {
38 | test.equal(geometric.pointWithLine([0, 1], line), false);
39 | test.equal(geometric.pointWithLine([1, 1], line), true);
40 | test.equal(geometric.pointWithLine([2, 1], line), false);
41 |
42 | // The point can be located outside of the line segment
43 | test.equal(geometric.pointWithLine([1, 100], line), true);
44 |
45 | test.end();
46 | });
47 |
48 | tape("pointWithLine takes an epsilon test", function(test){
49 | // See https://github.com/HarryStevens/geometric/issues/19
50 | const relationships = [{"point":[-50.94850158691406,-3.027209166609623],"line":[[-49.69850158691406,-3.0271999835968018],[-51.19850158687358,-3.027211003212187]]},{"point":[-46.8047,-12.1569],"line":[[-46.2164,-12.0707],[-47.700552783102914,-12.288163827814842]]},{"point":[-45.41063565015793,-6.862901151180267],"line":[[-44.59149932861328,-7.436500072479248],[-45.82020378894317,-6.576101705927197]]},{"point":[-46.22977195704452,-6.289302240145567],"line":[[-44.59149932861328,-7.436500072479248],[-45.82020378894317,-6.576101705927197]]},{"point":[-33.12336051464081,-15.46659255027771],"line":[[-33.942501068115234,-14.892999649047852],[-32.713790276416105,-15.753388973924736]]},{"point":[-32.30421998684139,-16.040185433528965],"line":[[-33.942501068115234,-14.892999649047852],[-32.713790276416105,-15.753388973924736]]},{"point":[-33.94243496656418,-14.89300149679184],"line":[[-33.12329864501953,-15.46660041809082],[-34.35200310534942,-14.60620205153877]]},{"point":[-34.76157127345077,-14.319402585757139],"line":[[-33.12329864501953,-15.46660041809082],[-34.35200310534942,-14.60620205153877]]},{"point":[-33.94245994091034,-14.892992734909058],"line":[[-34.761600494384766,-14.3193998336792],[-33.53288970268564,-15.179789158556083]]},{"point":[-33.12331941311092,-15.466585618160314],"line":[[-34.761600494384766,-14.3193998336792],[-33.53288970268564,-15.179789158556083]]},{"point":[-34.76163738965988,-14.31940072774887],"line":[[-33.942501068115234,-14.892999649047852],[-35.17120552844512,-14.0326012824958]]}];
51 |
52 | relationships.forEach(relationship => {
53 | test.equal(geometric.pointWithLine(relationship.point, relationship.line), false);
54 | test.equal(geometric.pointWithLine(relationship.point, relationship.line, 1e-6), true);
55 | });
56 |
57 | test.end();
58 | });
--------------------------------------------------------------------------------
/test/pointOnPolygon-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../"),
3 | polygon = [[10, 10], [50, 10], [50, 50], [10, 50]];
4 |
5 | tape("pointOnPolygon(point, polygon) determines whether a point is located on one of the edges of a polygon", function(test) {
6 | test.equal(geometric.pointOnPolygon([30, 10], polygon), true);
7 | test.equal(geometric.pointOnPolygon([50, 30], polygon), true);
8 | test.equal(geometric.pointOnPolygon([30, 50], polygon), true);
9 | test.equal(geometric.pointOnPolygon([10, 30], polygon), true);
10 | test.equal(geometric.pointOnPolygon([30, 30], polygon), false);
11 | test.equal(geometric.pointOnPolygon([30, 70], polygon), false);
12 | test.end();
13 | });
--------------------------------------------------------------------------------
/test/pointRotate-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("pointRotate(point, angle, origin) rotates a point by an angle in degrees around an origin", test => {
5 | test.equal(geometric.pointRotate([1, 1], 90).map(d => Math.round(d))[0], -1);
6 | test.equal(geometric.pointRotate([1, 1], 90)[1], 1);
7 | test.equal(geometric.pointRotate([1, 1], 180).map(d => Math.round(d))[0], -1);
8 | test.equal(geometric.pointRotate([1, 1], 180).map(d => Math.round(d))[1], -1);
9 | test.equal(geometric.pointRotate([1, 1], 180, [0, 0]).map(d => Math.round(d))[0], -1);
10 | test.equal(geometric.pointRotate([1, 1], 180, [0, 0]).map(d => Math.round(d))[1], -1);
11 | test.equal(geometric.pointRotate([1, 1], 90, [2, 2])[0], 3);
12 | test.equal(geometric.pointRotate([1, 1], 90, [2, 2])[1], 1);
13 | test.equal(geometric.pointRotate([1, 1], 180, [2, 2])[0], 3);
14 | test.equal(geometric.pointRotate([1, 1], 180, [2, 2])[1], 3);
15 | test.end();
16 | });
17 |
18 | tape("pointRotate(point, angle, origin) returns the same point if second two arguments are not passed", test => {
19 | test.deepEqual(geometric.pointRotate([1, 1]), [1, 1]);
20 |
21 | test.end();
22 | });
--------------------------------------------------------------------------------
/test/pointTranslate-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("pointTranslate(point, angle, distance) translates a point by an angle in degrees and distance", test => {
5 | test.equal(geometric.pointTranslate([0, 0], 0, 1)[0], 1);
6 | test.equal(geometric.pointTranslate([0, 0], 0, 1)[1], 0);
7 |
8 | test.equal(geometric.pointTranslate([0, 0], 90, 1).map(d => Math.round(d))[0], 0);
9 | test.equal(geometric.pointTranslate([0, 0], 90, 1).map(d => Math.round(d))[1], 1);
10 |
11 | test.equal(geometric.pointTranslate([0, 0], 180, 1).map(d => Math.round(d))[0], -1);
12 | test.equal(geometric.pointTranslate([0, 0], 180, 1).map(d => Math.round(d))[1], 0);
13 |
14 | test.equal(geometric.pointTranslate([0, 0], 270, 1).map(d => Math.round(d))[0], 0);
15 | test.equal(geometric.pointTranslate([0, 0], 270, 1).map(d => Math.round(d))[1], -1);
16 |
17 | test.equal(geometric.pointTranslate([0, 0], 360, 1).map(d => Math.round(d))[0], 1);
18 | test.equal(geometric.pointTranslate([0, 0], 360, 1).map(d => Math.round(d))[1], 0);
19 |
20 | test.end();
21 | });
22 |
23 | tape("pointTranslate returns input point if only one argument is passed", test => {
24 | const point = [Math.random(), Math.random()]
25 | test.deepEqual(geometric.pointTranslate(point), point);
26 |
27 | test.end();
28 | });
--------------------------------------------------------------------------------
/test/polygonArea-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonArea(polygon) calculates the area of a polygon", test => {
5 | test.equal(geometric.polygonArea([[0, 0], [1, 0], [1, 1], [0, 1]]), 1);
6 | test.equal(geometric.polygonArea([[0, 0], [2, 0], [2, 2], [0, 2]]), 4);
7 | test.equal(geometric.polygonArea([[0, 0], [3, 0], [3, 3], [0, 3]]), 9);
8 | test.equal(geometric.polygonArea([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), 1);
9 | test.equal(geometric.polygonArea([[0, 0], [2, 0], [2, 2], [0, 2], [0, 0]]), 4);
10 | test.equal(geometric.polygonArea([[0, 0], [3, 0], [3, 3], [0, 3], [0, 0]]), 9);
11 | test.end();
12 | });
13 |
14 | tape("If the polygon's winding order is counter-clockwise and signed is set to true, returns a negative area", test => {
15 | const p = [[119, 87], [61, 150], [131, 197], [206, 135]];
16 | test.equal(geometric.polygonArea(p, true), -8065);
17 | test.equal(geometric.polygonArea(p, false), 8065);
18 | test.equal(geometric.polygonArea(p), 8065);
19 | test.end();
20 | });
--------------------------------------------------------------------------------
/test/polygonBounds-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonBounds(polygon) returns null if the polygon has fewer than 3 points", function(test) {
5 | test.equal(geometric.polygonBounds([]), null);
6 | test.equal(geometric.polygonBounds([[0, 1]]), null);
7 | test.equal(geometric.polygonBounds([[0, 1], [1, 2]]), null);
8 | test.deepEqual(geometric.polygonBounds([[0, 1], [1, 2], [0, 3]]), [[0, 1], [1, 3]]);
9 | test.end();
10 | });
11 |
12 | tape("polygonBounds(polygon) calculates the bounds a polygon", function(test) {
13 | const polygon = [[110, 40], [210, 10], [310, 40], [360, 140], [310, 240], [210, 270], [110, 240], [60, 140]];
14 | const bounds = geometric.polygonBounds(polygon);
15 |
16 | test.equal(bounds[0][0], 60);
17 | test.equal(bounds[0][1], 10);
18 | test.equal(bounds[1][0], 360);
19 | test.equal(bounds[1][1], 270);
20 | test.end();
21 | });
22 |
23 | tape("polygonBounds(polygon) ignores null values", function(test) {
24 | test.deepEqual(geometric.polygonBounds([[null, 5], [0, 1], [1, 2], [0, 3]]), [[0, 1], [1, 3]]);
25 | test.deepEqual(geometric.polygonBounds([[undefined, 5], [0, 1], [1, 2], [0, 3]]), [[0, 1], [1, 3]]);
26 | test.deepEqual(geometric.polygonBounds([[NaN, 5], [0, 1], [1, 2], [0, 3]]), [[0, 1], [1, 3]]);
27 | test.deepEqual(geometric.polygonBounds([[5, Infinity], [0, 1], [1, 2], [0, 3]]), [[0, 1], [1, 3]]);
28 | test.end();
29 | });
--------------------------------------------------------------------------------
/test/polygonCentriod-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonCentroid(polygon) calculates the weighted centroid of a polygon", function(test) {
5 | const p0 = geometric.polygonCentroid([[0, 0], [1, 0], [1, 1], [0, 1]]);
6 | const p1 = geometric.polygonCentroid([[0, 0], [2, 0], [2, 2], [0, 2]]);
7 | const p2 = geometric.polygonCentroid([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]);
8 | const p3 = geometric.polygonCentroid([[0, 0], [2, 0], [2, 2], [0, 2], [0, 0]]);
9 | test.equal(p0[0], 0.5);
10 | test.equal(p0[1], 0.5);
11 | test.equal(p1[0], 1);
12 | test.equal(p1[1], 1);
13 | test.equal(p2[0], 0.5);
14 | test.equal(p2[1], 0.5);
15 | test.equal(p3[0], 1);
16 | test.equal(p3[1], 1);
17 | test.end();
18 | });
--------------------------------------------------------------------------------
/test/polygonHull-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonHull(points) returns null if there are fewer than 3 input points", function(test) {
5 | test.equal(geometric.polygonHull([]), null);
6 | test.equal(geometric.polygonHull([[0, 1]]), null);
7 | test.equal(geometric.polygonHull([[0, 1], [1, 2]]), null);
8 | test.end();
9 | });
10 |
11 | tape("polygonHull(points) calculates the convex hull of a set of points", function(test) {
12 | const vertices = [[0, 0], [0, 2], [2, 2], [2, 0], [1, 1]];
13 | const hull = geometric.polygonHull(vertices);
14 | test.equal(hull.length, 4);
15 | test.equal(hull[0][0], 0);
16 | test.equal(hull[0][1], 0);
17 | test.equal(hull[1][0], 2);
18 | test.equal(hull[1][1], 0);
19 | test.equal(hull[2][0], 2);
20 | test.equal(hull[2][1], 2);
21 | test.equal(hull[3][0], 0);
22 | test.equal(hull[3][1], 2);
23 | test.end();
24 | });
25 |
26 | tape("polygonHull(points) does not modify its input array", function(test) {
27 | const input = [[0, 1], [1, 2], [0, 3]];
28 | const clone = input.slice();
29 | const hull = geometric.polygonHull(input);
30 | test.deepEqual(input, clone);
31 | test.end();
32 | });
33 |
--------------------------------------------------------------------------------
/test/polygonInPolygon-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonInPolygon(polygonA, polygonB) determines whether a polygon is contained by another polygon", function(test) {
5 | const polygon = [[0, 0], [2, 0], [2, 2], [0, 2]];
6 | test.equal(geometric.polygonInPolygon([[.5, .5], [1.5, .5], [1.5, 1.5], [.5, 1.5]], polygon), true);
7 | test.equal(geometric.polygonInPolygon([[.5, .5], [2.5, .5], [1.5, 1.5], [.5, 1.5]], polygon), false);
8 | test.equal(geometric.polygonInPolygon([[3, 3], [3, 4], [4, 4]], polygon), false);
9 |
10 | test.end();
11 | });
12 |
13 | tape("polygonInPolygon(polygonA, polygonB) returns false if all of polygonA's vertices fall inside polygonB but one of its lines intersects polygonB", function(test) {
14 | const polygonA = [[435, 223], [503, 223], [524, 158], [469, 118], [414, 158]];
15 | const polygonB = [[388, 150], [458, 150], [458, 110], [478, 53], [486, 50], [488, 20], [490, 50], [498, 53], [518, 110], [518, 150], [588, 150], [588, 270], [388, 270], [388, 150]];
16 | test.equal(geometric.polygonInPolygon(polygonA, polygonB), false);
17 |
18 | test.end();
19 | });
20 |
21 | tape("If polygonA in polygonInPolygon is forced closed, polygonA will not be altered", test => {
22 | const openPolygon = [[0, 0], [1, 0], [1, 1]];
23 | const clonedBefore = openPolygon.slice();
24 | geometric.polygonInPolygon(openPolygon, [[10, 0], [11, 0], [11, 11]]);
25 | test.deepEqual(clonedBefore, openPolygon);
26 |
27 | test.end();
28 | });
--------------------------------------------------------------------------------
/test/polygonInterpolate-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonInterpolate(polygon) returns points along a polygon", test => {
5 | const precision = 5;
6 |
7 | for (let sides = 3; sides <= 12; sides++){
8 | for (let area = 10; area <= 100; area += 10){
9 | const polygon = geometric.polygonRegular(sides, area);
10 | const interpolator = geometric.polygonInterpolate(polygon);
11 | for (let t = 0; t <= 1; t += 1 / (sides * 2)){
12 | const [ix, iy] = interpolator(t);
13 | const i = +(t * sides).toFixed(1);
14 |
15 | let vx, vy;
16 | if (Number.isInteger(i)){
17 | [vx, vy] = polygon[i === sides ? 0 : i];
18 | }
19 |
20 | else {
21 | const line =[polygon[Math.floor(i)], polygon[Math.ceil(i)] || polygon[0]];
22 | const midpoint = geometric.lineMidpoint(line);
23 | [vx, vy] = midpoint;
24 | }
25 |
26 | test.equal(+ix.toFixed(precision), +vx.toFixed(precision))
27 | test.equal(+iy.toFixed(precision), +vy.toFixed(precision))
28 | }
29 | }
30 | }
31 |
32 | test.end();
33 | });
--------------------------------------------------------------------------------
/test/polygonIntersectsPolygon-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonIntersectsPolygon(polygonA, polygonB) determines whether two polygons intersect but neither contains the other", function(test) {
5 | const polygon = [[0, 0], [2, 0], [2, 2], [0, 2]];
6 | test.equal(geometric.polygonIntersectsPolygon([[.5, .5], [1.5, .5], [1.5, 1.5], [.5, 1.5]], polygon), false);
7 | test.equal(geometric.polygonIntersectsPolygon([[.5, .5], [2.5, .5], [2.5, 2.5], [.5, 2.5]], polygon), true);
8 | test.equal(geometric.polygonIntersectsPolygon([[3, 3], [3, 4], [4, 4]], polygon), false);
9 |
10 | // Test cases where lines overlap but no points are inside
11 | const polygonA = [[5, 3], [10, 3], [10, 8], [5, 8]],
12 | polygonB = [[4, 6], [8, 2], [11, 6]],
13 | polygonC = [[4, 6], [11, 6], [11, 9], [4, 9]];
14 | test.equal(geometric.polygonIntersectsPolygon(polygonA, polygonB), true);
15 | test.equal(geometric.polygonIntersectsPolygon(polygonB, polygonA), true);
16 | test.equal(geometric.polygonIntersectsPolygon(polygonA, polygonC), true);
17 | test.equal(geometric.polygonIntersectsPolygon(polygonC, polygonA), true);
18 |
19 | test.end();
20 | });
21 |
22 | tape("If polygonA in polygonIntersectsPolygon is forced closed, polygonA will not be altered", test => {
23 | const openPolygon = [[0, 0], [1, 0], [1, 1]],
24 | clonedBefore = openPolygon.slice();
25 | geometric.polygonIntersectsPolygon(openPolygon, [[10, 0], [11, 0], [11, 11]]);
26 | test.deepEqual(clonedBefore, openPolygon);
27 |
28 | test.end();
29 | });
30 |
31 | tape("polygonIntersectsPolygon(polygonA, polygonB) returns true if overlapping rectangles share two edges", test => {
32 | const a = [[562.6875, 304.4375], [601.09375, 304.4375], [601.09375, 322.9375], [562.6875, 322.9375]],
33 | b = [[562.6875, 298.4375], [601.09375, 298.4375], [601.09375, 316.9375], [562.6875, 316.9375]];
34 |
35 | test.equal(geometric.polygonIntersectsPolygon(a, b), true);
36 |
37 | test.end();
38 | });
39 |
40 | tape("polygonIntersectsPolygon(polygonA, polygonB) detects intersections in special cases", test => {
41 | // One of the polygons just has two points. See https://github.com/HarryStevens/geometric/issues/13
42 | const a = [[180, 140], [180, 205]],
43 | b = [[160, 180], [170, 181.72], [180, 187.64], [185, 193.41]];
44 |
45 | test.equal(geometric.polygonIntersectsPolygon(a, b), true);
46 |
47 | test.end();
48 | });
49 |
50 | // https://github.com/HarryStevens/geometric/issues/29
51 | tape("polygonIntersectsPolygon(polygonA, polygonB) returns false in open U configuration", test => {
52 | const a = [[3, 3], [3, 4], [4, 4], [4, 3]],
53 | b = [[1, 1], [1, 4], [2, 4], [2, 2], [5, 2], [5, 4], [6, 4], [6, 1]];
54 |
55 | test.equal(geometric.polygonIntersectsPolygon(a, b), false);
56 |
57 | test.end();
58 | });
--------------------------------------------------------------------------------
/test/polygonLength-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonLength(polygon) calculates the length of a polygon's perimeter", function(test) {
5 | test.equal(geometric.polygonLength([[0, 0], [1, 0], [1, 1], [0, 1]]), 4);
6 | test.equal(geometric.polygonLength([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]), 4);
7 | test.end();
8 | });
9 |
10 | tape("polygonLength(polygon) returns 0 if the polygon has 0 or 1 points", function(test) {
11 | test.equal(geometric.polygonLength([]), 0);
12 | test.equal(geometric.polygonLength([[0, 0]]), 0);
13 | test.end();
14 | });
--------------------------------------------------------------------------------
/test/polygonMean-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonMean(p) calculates the arithmetic mean of a polygon's vertices", function(test) {
5 | const mc0 = geometric.polygonMean([[0, 0], [1, 0], [1, 1], [0, 1]]);
6 | const mc1 = geometric.polygonMean([[0, 0], [2, 0], [2, 2], [0, 2]]);
7 | test.equal(mc0[0], 0.5);
8 | test.equal(mc0[1], 0.5);
9 | test.equal(mc1[0], 1);
10 | test.equal(mc1[1], 1);
11 | test.end();
12 | });
--------------------------------------------------------------------------------
/test/polygonRandom-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonRandom() returns a random polygon with the expected number of vertices, area, and centroid", (test) => {
5 | for (let sides = 3; sides <= 12; sides++){
6 | for (let x = 0; x <= 5; x++){
7 | for (let y = 0; y <= 5; y++){
8 | for (let area = 10; area <= 50; area += 10){
9 | const polygon = geometric.polygonRandom(sides, area, [x, y]);
10 |
11 | test.equal(polygon.length, sides);
12 | test.equal(Math.round(geometric.polygonArea(polygon)), area)
13 |
14 | const [cx, cy] = geometric.polygonCentroid(polygon);
15 | test.equal(Math.round(cx), x);
16 | test.equal(Math.round(cy), y);
17 | }
18 | }
19 | }
20 | }
21 |
22 | test.end();
23 | });
--------------------------------------------------------------------------------
/test/polygonReflectX-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonReflectX(p) reflects a polygon horizontally", (test) => {
5 | const polygon = [[506,200],[546,200],[546,160],[566,103],[574,100],[576,70],[578,100],[586,103],[606,160],[606,200],[676,200],[646,320],[476,320]];
6 |
7 | test.deepEqual(geometric.polygonReflectX(polygon, 0), polygon);
8 | test.deepEqual(geometric.polygonReflectX(polygon, 1), [[646,200],[606,200],[606,160],[586,103],[578,100],[576,70],[574,100],[566,103],[546,160],[546,200],[476,200],[506,320],[676,320]]);
9 | test.deepEqual(geometric.polygonReflectX(polygon, 0.5), [[576,200],[576,200],[576,160],[576,103],[576,100],[576,70],[576,100],[576,103],[576,160],[576,200],[576,200],[576,320],[576,320]]);
10 | test.deepEqual(geometric.polygonReflectX(polygon, 0.25), [[541,200],[561,200],[561,160],[571,103],[575,100],[576,70],[577,100],[581,103],[591,160],[591,200],[626,200],[611,320],[526,320]]);
11 | test.deepEqual(geometric.polygonReflectX(polygon, 0.75), [[611,200],[591,200],[591,160],[581,103],[577,100],[576,70],[575,100],[571,103],[561,160],[561,200],[526,200],[541,320],[626,320]]);
12 |
13 | test.deepEqual(geometric.polygonReflectX(polygon, -1), polygon);
14 | test.deepEqual(geometric.polygonReflectX(polygon, 2), [[646,200],[606,200],[606,160],[586,103],[578,100],[576,70],[574,100],[566,103],[546,160],[546,200],[476,200],[506,320],[676,320]]);
15 |
16 | test.end();
17 | });
--------------------------------------------------------------------------------
/test/polygonReflectY-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonReflectY(p) reflects a polygon vertically", (test) => {
5 | const polygon = [[476,200],[546,200],[546,160],[566,103],[574,100],[576,70],[578,100],[586,103],[606,160],[606,200],[676,200],[676,320],[476,320]];
6 |
7 | test.deepEqual(geometric.polygonReflectY(polygon, 0), polygon);
8 | test.deepEqual(geometric.polygonReflectY(polygon, 1), [[476,190],[546,190],[546,230],[566,287],[574,290],[576,320],[578,290],[586,287],[606,230],[606,190],[676,190],[676,70],[476,70]]);
9 | test.deepEqual(geometric.polygonReflectY(polygon, 0.5), [[476,195],[546,195],[546,195],[566,195],[574,195],[576,195],[578,195],[586,195],[606,195],[606,195],[676,195],[676,195],[476,195]]);
10 | test.deepEqual(geometric.polygonReflectY(polygon, 0.25), [[476,197.5],[546,197.5],[546,177.5],[566,149],[574,147.5],[576,132.5],[578,147.5],[586,149],[606,177.5],[606,197.5],[676,197.5],[676,257.5],[476,257.5]]);
11 | test.deepEqual(geometric.polygonReflectY(polygon, 0.75), [[476,192.5],[546,192.5],[546,212.5],[566,241],[574,242.5],[576,257.5],[578,242.5],[586,241],[606,212.5],[606,192.5],[676,192.5],[676,132.5],[476,132.5]]);
12 |
13 | test.deepEqual(geometric.polygonReflectY(polygon, -1), polygon);
14 | test.deepEqual(geometric.polygonReflectY(polygon, 2), [[476,190],[546,190],[546,230],[566,287],[574,290],[576,320],[578,290],[586,287],[606,230],[606,190],[676,190],[676,70],[476,70]]);
15 |
16 | test.end();
17 | });
--------------------------------------------------------------------------------
/test/polygonRegular-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonRegular() returns the expected default", function(test) {
5 | const p = geometric.polygonRegular();
6 |
7 | test.deepEqual(p, [[0, 0], [15.196713713031848, 0], [7.598356856515927, -13.160740129524923]]);
8 | test.end();
9 | });
10 |
11 | tape("polygonRegular() returns some regular polygons", function(test){
12 | const p3 = geometric.polygonRegular(3, 1000, [100, 100]);
13 | test.deepEqual(p3, [[75.97188585865246, 113.87263816762606], [124.02811414134754, 113.87263816762606], [100, 72.2547236647479]]);
14 |
15 | const p4 = geometric.polygonRegular(4, 1000, [100, 100]);
16 | test.deepEqual(p4, [[84.18861169915812, 115.81138830084188], [115.81138830084191, 115.81138830084188], [115.81138830084191, 84.18861169915809], [84.18861169915812, 84.18861169915809]]);
17 |
18 | const p100 = geometric.polygonRegular(100, 200000, [5000, 2000]);
19 | test.deepEqual(p100, [[4992.0720411147195, 2252.271742189941], [5007.927958885281, 2252.271742189941], [5023.752588624062, 2251.276140875513], [5039.48347774707, 2249.2888674301994], [5055.058543621695, 2246.3177647143884], [5070.416318578501, 2242.374558313132], [5085.4961924960335, 2237.474810260617], [5100.23865200128, 2231.637857623924], [5114.585515341761, 2224.88673618846], [5128.48016200233, 2217.248089546237], [5141.867756160478, 2208.752063945787], [5154.695463098275, 2199.432189318689], [5166.912657716856, 2189.3252469522504], [5178.471124330569, 2178.471124330569], [5189.32524695225, 2166.912657716856], [5199.432189318689, 2154.6954630982746], [5208.752063945787, 2141.867756160478], [5217.248089546238, 2128.4801620023295], [5224.88673618846, 2114.58551534176], [5231.637857623924, 2100.2386520012797], [5237.474810260617, 2085.496192496034], [5242.374558313132, 2070.416318578501], [5246.317764714388, 2055.0585436216948], [5249.288867430199, 2039.4834777470699], [5251.276140875513, 2023.7525886240624], [5252.2717421899415, 2007.927958885281], [5252.2717421899415, 1992.0720411147195], [5251.276140875513, 1976.2474113759383], [5249.288867430199, 1960.5165222529308], [5246.317764714388, 1944.9414563783057], [5242.374558313132, 1929.5836814214995], [5237.474810260617, 1914.503807503967], [5231.637857623924, 1899.7613479987212], [5224.88673618846, 1885.4144846582403], [5217.248089546238, 1871.519837997671], [5208.752063945787, 1858.1322438395223], [5199.432189318689, 1845.3045369017261], [5189.32524695225, 1833.0873422831446], [5178.471124330569, 1821.5288756694315], [5166.912657716856, 1810.6747530477503], [5154.695463098275, 1800.5678106813118], [5141.867756160478, 1791.2479360542136], [5128.48016200233, 1782.751910453763], [5114.585515341761, 1775.1132638115403], [5100.23865200128, 1768.3621423760765], [5085.496192496034, 1762.5251897393834], [5070.416318578501, 1757.6254416868683], [5055.058543621695, 1753.682235285612], [5039.4834777470705, 1750.711132569801], [5023.752588624063, 1748.7238591244868], [5007.927958885281, 1747.728257810059], [4992.0720411147195, 1747.728257810059], [4976.247411375939, 1748.7238591244866], [4960.516522252931, 1750.7111325698006], [4944.941456378306, 1753.6822352856116], [4929.5836814215, 1757.6254416868678], [4914.503807503967, 1762.525189739383], [4899.761347998721, 1768.362142376076], [4885.41448465824, 1775.1132638115396], [4871.519837997671, 1782.7519104537623], [4858.132243839523, 1791.2479360542127], [4845.304536901726, 1800.567810681311], [4833.087342283145, 1810.6747530477492], [4821.528875669432, 1821.5288756694304], [4810.6747530477505, 1833.0873422831432], [4800.567810681312, 1845.304536901725], [4791.247936054214, 1858.132243839521], [4782.751910453763, 1871.5198379976696], [4775.1132638115405, 1885.414484658239], [4768.362142376076, 1899.7613479987197], [4762.525189739384, 1914.5038075039654], [4757.625441686868, 1929.583681421498], [4753.682235285612, 1944.941456378304], [4750.711132569801, 1960.5165222529292], [4748.723859124487, 1976.2474113759367], [4747.7282578100585, 1992.0720411147179], [4747.7282578100585, 2007.9279588852794], [4748.723859124486, 2023.7525886240608], [4750.711132569801, 2039.4834777470683], [4753.682235285612, 2055.0585436216934], [4757.625441686868, 2070.4163185784996], [4762.525189739383, 2085.496192496032], [4768.362142376076, 2100.238652001278], [4775.11326381154, 2114.5855153417588], [4782.751910453762, 2128.480162002328], [4791.247936054213, 2141.867756160477], [4800.567810681311, 2154.6954630982727], [4810.67475304775, 2166.9126577168545], [4821.528875669431, 2178.4711243305674], [4833.087342283144, 2189.3252469522486], [4845.304536901725, 2199.432189318687], [4858.132243839521, 2208.7520639457853], [4871.51983799767, 2217.248089546236], [4885.414484658239, 2224.8867361884586], [4899.76134799872, 2231.637857623922], [4914.503807503966, 2237.474810260615], [4929.583681421499, 2242.3745583131304], [4944.941456378305, 2246.317764714386], [4960.5165222529295, 2249.288867430197], [4976.247411375937, 2251.2761408755114]]);
20 |
21 | test.end();
22 | });
--------------------------------------------------------------------------------
/test/polygonRotate-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonRotate(polygon, angle, origin) rotates a polygon by an angle in degrees around an origin", function(test) {
5 | const myPolygon = [[100, 100], [150, 100], [125, 125], [150, 150], [100, 150]];
6 | const myOrigin = [200, 200];
7 |
8 | const by45 = geometric.polygonRotate(myPolygon, 45, myOrigin);
9 |
10 | test.equal(by45[0][0], 200);
11 | test.equal(Math.round(by45[0][1]), 59);
12 |
13 | test.equal(Math.round(by45[1][0]), 235);
14 | test.equal(Math.round(by45[1][1]), 94);
15 |
16 | test.equal(by45[2][0], 200);
17 | test.equal(Math.round(by45[2][1]), 94);
18 |
19 | test.equal(by45[3][0], 200);
20 | test.equal(Math.round(by45[3][1]), 129);
21 |
22 | test.equal(Math.round(by45[4][0]), 165);
23 | test.equal(Math.round(by45[4][1]), 94);
24 |
25 | test.end();
26 | });
--------------------------------------------------------------------------------
/test/polygonScale-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonScale(polygon, scaleFactor, origin) scales a polygon by a scale factor from an origin point.", function(test) {
5 | const polygon = [[738.5, 168.5], [838.5, 138.5], [938.5, 168.5], [988.5, 268.5], [938.5, 368.5], [838.5, 398.5], [738.5, 368.5], [688.5, 268.5]];
6 | const polygonDoubled = geometric.polygonScale(polygon, 2);
7 |
8 | test.equal(polygonDoubled[0][0], 638.5);
9 | test.equal(Math.round(polygonDoubled[0][1]), 68);
10 |
11 | test.equal(polygonDoubled[1][0], 838.5);
12 | test.equal(polygonDoubled[1][1], 8.5);
13 |
14 | test.equal(polygonDoubled[2][0], 1038.5);
15 | test.equal(polygonDoubled[2][1], 68.5);
16 |
17 | test.equal(polygonDoubled[3][0], 1138.5);
18 | test.equal(polygonDoubled[3][1], 268.5);
19 |
20 | test.equal(polygonDoubled[4][0], 1038.5);
21 | test.equal(polygonDoubled[4][1], 468.5);
22 |
23 | test.equal(polygonDoubled[5][0], 838.5);
24 | test.equal(polygonDoubled[5][1], 528.5);
25 |
26 | test.equal(polygonDoubled[6][0], 638.5);
27 | test.equal(polygonDoubled[6][1], 468.5);
28 |
29 | test.equal(polygonDoubled[7][0], 538.5);
30 | test.equal(Math.round(polygonDoubled[7][1]), 269);
31 |
32 | test.end();
33 | });
--------------------------------------------------------------------------------
/test/polygonScaleArea-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonScaleArea(polygon, scaleFactor, origin) scales a polygon by a scale factor from an origin point. The returned polygon's area is equal to the input polygon's area multiplied by the scale factor.", function(test) {
5 | const polygon = [[738.5, 168.5], [838.5, 138.5], [938.5, 168.5], [988.5, 268.5], [938.5, 368.5], [838.5, 398.5], [738.5, 368.5], [688.5, 268.5]];
6 | const a0 = geometric.polygonArea(polygon);
7 |
8 | for (let scale = 0; scale <= 10; scale += 0.01) {
9 | const scaled = geometric.polygonScaleArea(polygon, scale);
10 | const a1 = geometric.polygonArea(scaled);
11 |
12 | test.equal(+(a0 * scale).toFixed(5), +a1.toFixed(5))
13 | }
14 |
15 | test.end();
16 | });
--------------------------------------------------------------------------------
/test/polygonScaleX-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonScaleX(polygon, scaleFactor, origin) scales a polygon's x-coordinates by a scale factor from an origin point.", function(test) {
5 | const polygon = [[738.5, 168.5], [838.5, 138.5], [938.5, 168.5], [988.5, 268.5], [938.5, 368.5], [838.5, 398.5], [738.5, 368.5], [688.5, 268.5]];
6 | const polygonDoubled = geometric.polygonScaleX(polygon, 2);
7 |
8 | test.equal(polygonDoubled[0][0], 638.5);
9 | test.equal(polygonDoubled[0][1], polygon[0][1]);
10 |
11 | test.equal(polygonDoubled[1][0], 838.5);
12 | test.equal(polygonDoubled[1][1], polygon[1][1]);
13 |
14 | test.equal(polygonDoubled[2][0], 1038.5);
15 | test.equal(polygonDoubled[2][1], polygon[2][1]);
16 |
17 | test.equal(polygonDoubled[3][0], 1138.5);
18 | test.equal(polygonDoubled[3][1], polygon[3][1]);
19 |
20 | test.equal(polygonDoubled[4][0], 1038.5);
21 | test.equal(polygonDoubled[4][1], polygon[4][1]);
22 |
23 | test.equal(polygonDoubled[5][0], 838.5);
24 | test.equal(polygonDoubled[5][1], polygon[5][1]);
25 |
26 | test.equal(polygonDoubled[6][0], 638.5);
27 | test.equal(polygonDoubled[6][1], polygon[6][1]);
28 |
29 | test.equal(polygonDoubled[7][0], 538.5);
30 | test.equal(polygonDoubled[7][1], polygon[7][1]);
31 |
32 | test.end();
33 | });
--------------------------------------------------------------------------------
/test/polygonScaleY-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonScaleY(polygon, scaleFactor, origin) scales a polygon's y-coordinates by a scale factor from an origin point.", function(test) {
5 | const polygon = [[738.5, 168.5], [838.5, 138.5], [938.5, 168.5], [988.5, 268.5], [938.5, 368.5], [838.5, 398.5], [738.5, 368.5], [688.5, 268.5]];
6 | const polygonDoubled = geometric.polygonScaleY(polygon, 2);
7 |
8 | test.equal(polygonDoubled[0][0], polygon[0][0]);
9 | test.equal(Math.round(polygonDoubled[0][1]), 68);
10 |
11 | test.equal(polygonDoubled[1][0], polygon[1][0]);
12 | test.equal(polygonDoubled[1][1], 8.5);
13 |
14 | test.equal(polygonDoubled[2][0], polygon[2][0]);
15 | test.equal(polygonDoubled[2][1], 68.5);
16 |
17 | test.equal(polygonDoubled[3][0], polygon[3][0]);
18 | test.equal(polygonDoubled[3][1], 268.5);
19 |
20 | test.equal(polygonDoubled[4][0], polygon[4][0]);
21 | test.equal(polygonDoubled[4][1], 468.5);
22 |
23 | test.equal(polygonDoubled[5][0], polygon[5][0]);
24 | test.equal(polygonDoubled[5][1], 528.5);
25 |
26 | test.equal(polygonDoubled[6][0], polygon[6][0]);
27 | test.equal(polygonDoubled[6][1], 468.5);
28 |
29 | test.equal(polygonDoubled[7][0], polygon[7][0]);
30 | test.equal(Math.round(polygonDoubled[7][1]), 269);
31 |
32 | test.end();
33 | });
--------------------------------------------------------------------------------
/test/polygonTranslate-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonTranslate(polygon, angle, distance) translates a polygon by an angle in degrees and distance.", function(test) {
5 | const myPolygon = [[100, 100], [150, 100], [125, 125], [150, 150], [100, 150]];
6 |
7 | const newPolygon = geometric.polygonTranslate(myPolygon, 90, 100);
8 |
9 | test.equal(newPolygon[0][0], 100);
10 | test.equal(newPolygon[0][1], 200);
11 |
12 | test.equal(newPolygon[1][0], 150);
13 | test.equal(newPolygon[1][1], 200);
14 |
15 | test.equal(newPolygon[2][0], 125);
16 | test.equal(newPolygon[2][1], 225);
17 |
18 | test.equal(newPolygon[3][0], 150);
19 | test.equal(newPolygon[3][1], 250);
20 |
21 | test.equal(newPolygon[4][0], 100);
22 | test.equal(newPolygon[4][1], 250);
23 |
24 | test.end();
25 | });
--------------------------------------------------------------------------------
/test/polygonWind-test.js:
--------------------------------------------------------------------------------
1 | const tape = require("tape"),
2 | geometric = require("../");
3 |
4 | tape("polygonWind(polygon) sets the winding order of a polygon", test => {
5 | const polygon = [
6 | [58, 58],
7 | [123, 27],
8 | [233, 86],
9 | [244, 159],
10 | [192, 250],
11 | [110, 278],
12 | [60, 237],
13 | [30, 159],
14 | [19, 101]
15 | ];
16 | const isClockwise = geometric.polygonArea(polygon, true) > 0;
17 |
18 | const reversed = polygon.slice().reverse(),
19 | wound = geometric.polygonWind(polygon),
20 | ccw = geometric.polygonWind(polygon, "ccw"),
21 | cw = geometric.polygonWind(polygon, "cw"),
22 | clockwise = geometric.polygonWind(polygon, "clockwise");
23 |
24 | test.equal(isClockwise, true);
25 | test.deepEqual(wound, reversed);
26 | test.deepEqual(ccw, reversed);
27 | test.deepEqual(cw, polygon);
28 | test.deepEqual(clockwise, polygon);
29 |
30 | test.end();
31 | });
32 |
33 | tape("polygonWind(polygon) returns null if the polygon has fewer than three points", test => {
34 | test.equal(geometric.polygonWind([]), null);
35 | test.equal(geometric.polygonWind([[0, 1]]), null);
36 | test.equal(geometric.polygonWind([[0, 1], [1, 2]]), null);
37 |
38 | test.end();
39 | });
--------------------------------------------------------------------------------
/test/utils/roundArray.js:
--------------------------------------------------------------------------------
1 | module.exports = function roundArray(array, precision){
2 | return array.map(n => +n.toFixed(precision));
3 | }
--------------------------------------------------------------------------------