├── .gitignore
├── .husky
└── pre-commit
├── .lintstagedrc.json
├── .npmignore
├── .prettierrc.json
├── API.md
├── LICENSE.md
├── README.md
├── about.html
├── css
├── example.css
├── hyperbolic-canvas.css
├── normalize.css
└── skeleton.css
├── dist
└── hyperbolic_canvas.js
├── documentation.html
├── example.html
├── examples.html
├── images
├── background.png
└── favicon.png
├── index.html
├── index.js
├── jasmine
├── MIT.LICENSE
├── SpecRunner.html
├── lib
│ └── jasmine-2.4.1+
│ │ ├── boot.js
│ │ ├── console.js
│ │ ├── jasmine-html.js
│ │ ├── jasmine.css
│ │ ├── jasmine.js
│ │ └── jasmine_favicon.png
└── spec
│ ├── AngleSpec.js
│ ├── CanvasSpec.js
│ ├── CircleSpec.js
│ ├── HyperbolicCanvasSpec.js
│ ├── JasmineSpec.js
│ ├── LineSpec.js
│ ├── PointSpec.js
│ ├── PolygonSpec.js
│ └── SpecHelper.js
├── package.json
├── scripts
├── comets.js
├── concentric-circles.js
├── hand-drawn-polygon.js
├── hexagons.js
├── laser-spaceship.js
├── mouse-interaction.js
└── web.js
├── src
├── angle.js
├── canvas.js
├── circle.js
├── hyperbolic_canvas.js
├── line.js
├── point.js
└── polygon.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | jasmine/lib/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | # stash unstaged and untracked files, except for dist/
5 | git add dist/
6 | git stash --include-untracked --keep-index
7 | # recompile dist/
8 | yarn compile
9 | # restore unstaged and untracked files
10 | git stash pop --quiet
11 | # add recompiled dist/ to staging
12 | git add dist/
13 |
14 | # format staged files wih prettier via lint-staged
15 | yarn run lint-staged
16 |
17 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "*.{js,ts,json,md}": ["prettier --write"]
3 | }
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | css/
2 | dist/
3 | images/
4 | jasmine/
5 | scripts/
6 | *.html
7 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "bracketSpacing": true,
6 | "plugins": ["@trivago/prettier-plugin-sort-imports"]
7 | }
8 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | ## Exposed Variables and Constants
4 |
5 | An object containing all Canvas objects is exposed through the `HyperbolicCanvas` namespace.
6 |
7 | ```javascript
8 | HyperbolicCanvas.canvases;
9 | ```
10 |
11 | Approximations of `Infinity` and `0` are defined for use in internal comparisons:
12 |
13 | ```javascript
14 | HyperbolicCanvas.INFINITY;
15 | HyperbolicCanvas.ZERO;
16 | ```
17 |
18 | The constant [Tau][manifesto] is defined on the Math object as `2 * Math.PI`:
19 |
20 | ```javascript
21 | Math.TAU;
22 | // 6.283185307179586
23 | // you're welcome
24 | ```
25 |
26 | [manifesto]: http://tauday.com/tau-manifesto
27 |
28 | ## Geometric Object Classes and Their Functions
29 |
30 | The hyperbolic canvas makes use of several geometric object classes, defined relative to the Euclidean plane.
31 |
32 | When instantiating objects, it is **not recommended to call the constructor directly**. Instead, use the provided factory methods.
33 |
34 | ### Angle
35 |
36 | A non-function object which contains convenience functions related to angles.
37 |
38 | Functions:
39 |
40 | ```javascript
41 | Angle.normalize(angle);
42 | // return the equivalent angle a where 0 < a < Tau
43 |
44 | Angle.fromDegrees(degrees);
45 | Angle.toDegrees(radians);
46 | // convert between primary- and secondary-school mathematics
47 |
48 | Angle.opposite(angle);
49 |
50 | Angle.toSlope(angle);
51 | Angle.fromSlope(slope);
52 | // convert between angle and slope of Line
53 |
54 | Angle.random(quadrant);
55 | // return a random angle, optionally within a given quadrant [1 - 4]
56 | ```
57 |
58 | ### Point
59 |
60 | A representation of a point on the Canvas, where the center is defined as (0, 0) and the radius is defined as 1, and the y axis is not inverted.
61 |
62 | Constants:
63 |
64 | ```javascript
65 | Point.ORIGIN;
66 | Point.CENTER;
67 | // the point at the center of the canvas, (0,0)
68 | ```
69 |
70 | Factory methods:
71 |
72 | ```javascript
73 | Point.givenCoordinates(x, y);
74 | // generate a point given x and y coordinates, relative to the center of the unit circle
75 |
76 | Point.givenEuclideanPolarCoordinates(radius, angle);
77 | // generate a point given polar coodinates, relative to the center of the unit circle
78 |
79 | Point.givenHyperbolicPolarCoordinates(radius, angle);
80 | // generate a point given polar coodinates, relative to the center of the unit circle, where the given distance is hyperbolic
81 |
82 | Point.givenIdealAngle(angle);
83 | // generate an ideal point at the given angle, relative to the unit circle
84 |
85 | Point.euclideanBetween(somePoint, someOtherPoint);
86 | // generate the point between two other Points, in a Euclidean sense
87 |
88 | Point.hyperbolicBetween(somePoint, someOtherPoint);
89 | // generate the point between tow other Points, in a hyperbolic sense
90 | // will return false if either Point is not on the hyperbolic plane
91 | ```
92 |
93 | Instance functions:
94 |
95 | ```javascript
96 | Point.prototype.equals(otherPoint);
97 | // determine whether x and y properties of the point match those of another point
98 |
99 | Point.prototype.getAngle();
100 | // calculate the angle at which the point is located relative to the unit circle
101 |
102 | Point.prototype.getDirection();
103 | // if this Point was calculated as a result of hyperbolicDistantPoint, return the angle of continued travel along the same geodesic, otherwise return the result of getAngle
104 |
105 | Point.prototype.getEuclideanRadius();
106 | // calculate the Euclidean distance of the point from the center of the canvas
107 |
108 | Point.prototype.getHyperbolicRadius();
109 | // calculate the hyperbolic distance of the point from the center of the canvas
110 |
111 | Point.prototype.getX();
112 |
113 | Point.prototype.getY();
114 |
115 | Point.prototype.euclideanAngleTo(otherPoint);
116 | Point.prototype.euclideanAngleFrom(otherPoint);
117 | // calculate the angle towards or from another Point, along a Euclidean geodesic
118 |
119 | Point.prototype.hyperbolicAngleTo(otherPoint);
120 | Point.prototype.hypebrolicAngleFrom(otherPoint);
121 | // calculate the angle towards or from another Point, along a hyperbolic geodesic
122 |
123 | Point.prototype.euclideanDistanceTo(otherPoint);
124 | Point.prototype.hyperbolicDistanceTo(ohterPoint);
125 | // calculate the Euclidean or hyperbolic distance to another Point
126 |
127 | Point.prototype.euclideanDistantPoint(distance, direction);
128 | // calculate the point's relative point a given Euclidean distance away at a given angle, along a Euclidean geodesic
129 |
130 | Point.prototype.hyperbolicDistantPoint(distance, direction);
131 | // calculate the point's relative point a given hyperbolic distance away at a given angle, along a hyperbolic geodesic
132 | // the returned distant point has an additional property "direction" which indicates the angle one would be facing, having traveled from the point to the distant point
133 | // if this function is called without a "direction" argument, the point is checked for a "direction" attribute
134 | // if neither a "direction" argument nor attribute exists, the point's angle() is used
135 |
136 | Point.prototype.isIdeal();
137 | // determine whether the point lies on the boundary of the unit circle
138 |
139 | Point.prototype.isOnPlane();
140 | // determine whether the point lies within the bounds of the unit circle
141 |
142 | Point.prototype.opposite();
143 | // return the Point rotated PI radians about the origin
144 | ```
145 |
146 | ### Line
147 |
148 | The relationship between two Points. Contains various functions which act on either the Euclidean or the hyperbolic plane. Can represent a line, line segment, or ray.
149 |
150 | Constants:
151 |
152 | ```javascript
153 | Line.X_AXIS;
154 |
155 | Line.Y_AXIS;
156 | ```
157 |
158 | Factory methods:
159 |
160 | ```javascript
161 | Line.givenPointSlope(point, slope);
162 | // generate a line given a point and a slope
163 |
164 | Line.givenTwoPoints(somePoint, someOtherPoint);
165 | // generate a line through two Points
166 |
167 | Line.givenAnglesOfIdealPoints(someAngle, someOtherAngle);
168 | // generate a line through two ideal Points at given angles
169 | ```
170 |
171 | Class functions:
172 |
173 | ```javascript
174 | Line.euclideanIntersect(someLine, someOtherLine);
175 | // calculate the point of intersection of two Euclidean lines
176 |
177 | Line.hyperbolicIntersect(someLine, someOtherLine);
178 | // calculate the point of intersection of two hyperbolic lines
179 | ```
180 |
181 | Instance functions:
182 |
183 | ```javascript
184 | Line.prototype.getHyperbolicGeodesic();
185 | // returns the circle whose arc matches the hyperbolic geodesic through the line's points
186 |
187 | Line.prototype.euclideanIncludesPoint(point);
188 | // determine whether a point lies on the Euclidean line
189 |
190 | Line.prototype.equals(otherLine);
191 | // determine whether the line's slope matches that of another line, and the line contains a point of another line
192 |
193 | Line.prototype.hyperbolicEquals(otherLine);
194 | // determine whether the Line shares a hyperbolic geodesic with given other Line
195 |
196 | Line.prototype.xAtY(y);
197 | // return the x coordinate of the point on the Euclidean line at a given y coordinate
198 |
199 | Line.prototype.yAtX(x);
200 | // return the y coordinate of the point on the Euclidean line at a given x coordinate
201 |
202 | Line.prototype.euclideanPerpindicularBisector();
203 | // return the line which is the perpindicular bisector of the Euclidean line segment
204 |
205 | Line.prototype.euclideanPerpindicularSlope();
206 | // return the opposite reciprocal of the slope of the Euclidean line
207 |
208 | Line.prototype.getEuclideanMidpoint();
209 | // return the point between the Euclidean line segment's two endpoints
210 |
211 | Line.prototype.getEuclideanLength();
212 | // calculate the length of the Euclidean line segment
213 |
214 | Line.prototype.hyperbolicDistance();
215 | // calculate the length of the hyperbolic line segment
216 |
217 | Line.prototype.getEuclideanUnitCircleIntersects();
218 | // calculate the Euclidean line's points of intersection with the unit circle
219 | ```
220 |
221 | ### Circle
222 |
223 | A Euclidean center Point and a Euclidean radius; potentially also a hyperbolic center Point and a hyperbolic radius.
224 |
225 | Constants:
226 |
227 | ```javascript
228 | Circle.UNIT;
229 | // the unit circle; center (0,0), Euclidean radius 1, hyperbolic radius Infinity
230 | ```
231 |
232 | Factory methods:
233 |
234 | ```javascript
235 | Circle.givenEuclideanCenterRadius(center, radius);
236 | // generate a circle with a given center point and Euclidean radius
237 |
238 | Circle.givenHyperbolicCenterRadius(center, radius);
239 | // generate a circle with a given center point and hyperbolic radius
240 |
241 | Circle.givenTwoPoints(somePoint, someOtherPoint);
242 | // generate a circle given two diametrically opposed points
243 |
244 | Circle.givenThreePoints(somePoint, someOtherPoint, someOtherOtherPoint);
245 | // generate a circle given three points on its edge
246 | ```
247 |
248 | Class functions:
249 |
250 | ```javascript
251 | Circle.intersect(someCircle, someOtherCircle);
252 | // calculate the points of intersection between two circles
253 | ```
254 |
255 | Instance functions:
256 |
257 | ```javascript
258 | Circle.prototype.equals(otherCircle);
259 | // determine whether the circle's center and radius match those of another circle
260 |
261 | Circle.prototype.getEuclideanArea();
262 | Circle.prototype.getHyperbolicArea();
263 | Circle.prototype.getEuclideanCenter();
264 | Circle.prototype.getHyperbolicCenter();
265 | Circle.prototype.getEuclideanCircumference();
266 | Circle.prototype.getHyperbolicCircumference();
267 | Circle.prototype.getEuclideanDiameter();
268 | Circle.prototype.getHyperbolicDiameter();
269 | // return the Euclidean or hyperbolic property of the circle
270 |
271 | Circle.prototype.containsPoint(point);
272 | // determine whether the circle contains the given point within its bounds
273 |
274 | Circle.prototype.includesPoint(point);
275 | // determine whether the given point lies on the edge of the circle
276 |
277 | Circle.prototype.euclideanAngleAt(point);
278 | Circle.prototype.hyperbolicAngleAt(point);
279 | // calculate the angle of a point relative to the circle's center, in a Euclidean or hyperbolic context
280 |
281 | Circle.prototype.euclideanPointAt(angle);
282 | Circle.prototype.hyperbolicPointAt(angle);
283 | // calculate the point on a circle at a given angle relative to its center, in a Euclidean or hyperbolic context
284 |
285 | Circle.prototype.pointsAtX(x);
286 | Circle.prototype.pointsAtY(y);
287 | // return the point or points on the edge of the circle with the given x or y coordinate
288 |
289 | Circle.prototype.xAtY(y);
290 | Circle.prototype.yAtX(x);
291 | // calculate the x or y coordinate of the points on the edge of the circle with a given y or x coordinate, respectively
292 |
293 | Circle.prototype.euclideanTangentAtAngle(angle);
294 | // calculate the tangent line to the circle at a given angle
295 |
296 | Circle.prototype.euclideanTangentAtPoint(point);
297 | // calculate the line which passes through a given point and is perpindicular to the line through the point and the circle's center
298 |
299 | Circle.prototype.getUnitCircleIntersects();
300 | // calculate the circle's points of intersection with the unit circle
301 | ```
302 |
303 | ### Polygon
304 |
305 | An ordered collection of Points.
306 |
307 | Factory methods:
308 |
309 | ```javascript
310 | Polygon.givenVertices(vertices);
311 | // generate a polygon from a given ordered array of Point objects
312 |
313 | Polygon.givenAnglesOfIdealVertices(angles);
314 | // generate an ideal polygon with vertices at the given angles, relative to the unit circle
315 |
316 | Polygon.givenEuclideanNCenterRadius(n, center, radius);
317 | // generate a regular polygon with n sides, where each vertex is radius Euclidean distance from the center Point
318 |
319 | Polygon.givenHyperbolicNCenterRadius(n, center, radius);
320 | // generate a regular polygon with n sides, where each vertex is radius hyperbolic distance from the center Point
321 | ```
322 |
323 |
328 |
329 | Instance functions:
330 |
331 | ```javascript
332 | Polygon.prototype.getLines();
333 | // return the lines between the polygon's vertices
334 |
335 | Polygon.prototype.getVertices();
336 | // return the polygon's vertices
337 | ```
338 |
339 | ## The Canvas Class and Its Functions
340 |
341 | The canvas class is used to draw hyperbolic lines and shapes.
342 |
343 | Instance functions:
344 |
345 | ```javascript
346 | Canvas.prototype.getUnderlayElement();
347 | // return the div behind the canvas element, which is used to visually delineate
348 | // the hyperbolic plane
349 |
350 | Canvas.prototype.getContainerElement();
351 | // return the element which contains all Hyperbolic Canvas elements
352 |
353 | Canvas.prototype.getCanvasElement();
354 | // return the HTML canvas element
355 |
356 | Canvas.prototype.getBackdropElement();
357 | // return the div which is the direct parent of the canvas element
358 |
359 | Canvas.prototype.getContext();
360 | // return the CanvasRenderingContext2D of the underlying canvas
361 |
362 | Canvas.prototype.getRadius();
363 | // return the radius of the HTML canvas
364 |
365 | Canvas.prototype.getDiameter();
366 | // return the diameter of the HTML canvas
367 |
368 | Canvas.prototype.setContextProperties(properties);
369 | Canvas.prototype.setContextProperty(property, value);
370 | // set the properties of the 2d context of the underlying HTML canvas
371 | // lineDash is also supported
372 |
373 | Canvas.prototype.at(coordinates);
374 | // generate a Point given an array of coordinates [x, y] relative to the HTML canvas
375 |
376 | Canvas.prototype.at(point);
377 | // generate an array of coordinates [x, y] relative to the HTML canvas given a Point
378 |
379 | Canvas.prototype.clear();
380 | // clear the canvas
381 |
382 | Canvas.prototype.fill(path);
383 | Canvas.prototype.stroke(path);
384 | Canvas.prototype.fillAndStroke(path);
385 | // call fill() and/or stroke() on the context of the underlying canvas
386 | // optionally with a Path2D
387 |
388 | Canvas.prototype.pathForReferenceAngles(n, rotation, options);
389 | // generate path for lines on the canvas betwen n radial slices, offset by rotation
390 |
391 | Canvas.prototype.pathForReferenceGrid(n, options);
392 | // generate path for a grid on the canvas with n divisions of each axis
393 |
394 | Canvas.prototype.pathForReferenceRings(n, d, options);
395 | // generate path for n rings on the canvas with increasing radius in increments of r
396 |
397 | Canvas.prototype.pathForEuclidean(object, options);
398 | Canvas.prototype.pathForHyperbolic(object, options);
399 | // generate Euclidean or hyperbolic path for a given object
400 | ```
401 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Nick Barry
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hyperbolic Canvas
2 |
3 | A Javascript implementation of the [Poincaré disk model](https://en.wikipedia.org/wiki/Poincar%C3%A9_disk_model) of the hyperbolic plane, on an HTML canvas.
4 |
5 | Usage examples can be found on the [project site](https://ItsNickBarry.github.io/hyperbolic-canvas).
6 |
7 | ## Installation
8 |
9 | ### Via NPM
10 |
11 | ```
12 | npm install --save hyperbolic-canvas
13 | ```
14 |
15 | ### In-Browser
16 |
17 | ```bash
18 | yarn compile`
19 | ```
20 |
21 | ```html
22 |
23 | ```
24 |
25 | ## Usage
26 |
27 | Pass a unique selector of a div element, to the function `HyperbolicCanvas.create`. Nonzero width and height styling must be specified. Absolute px values in a 1:1 ratio are recommended:
28 |
29 | ```html
30 |
31 | ```
32 |
33 | ```javascript
34 | let canvas = HyperbolicCanvas.create('#hyperbolic-canvas');
35 | ```
36 |
37 | ### API
38 |
39 | See `API.md` for a list of functions and their descriptions.
40 |
41 | ## Scope
42 |
43 | This library prioritizes the visualization of hyperbolic geometry over precise mathematical calculation. Due to the less-than-infinite precision of floating-point numbers, and because certain trigonometric functions are [ill-conditioned](https://en.wikipedia.org/wiki/Condition_number), these goals are often at odds.
44 |
45 | ### Accuracy Thresholds
46 |
47 | The arbitrary constants `HyperbolicCanvas.INFINITY` and `HyperbolicCanvas.ZERO` have been defined for use in internal comparisons in place of `Infinity` and `0`, respectively. Their values may be overridden, but increased accuracy will tend to lead to more unpredictable behavior.
48 |
49 | ### Jasmine Specs
50 |
51 | This library uses [Jasmine specs][jasmine] to validate the code and prevent regressions.
52 |
53 | The specs have been written to use random input values. While this approach is unconventional, it provides more confidence than would an attempt to test an effectively infinite number of edge cases. Some specs do occasionally fail; the frequency at which this occurs is determined by the accuracy of the constants `HyperbolicCanvas.INFINITY` and `HyperbolicCanvas.ZERO`.
54 |
55 | The Jasmine library itself has been modified to run each spec multiple times, and a random number seed is used so that errors may be reproduced. The seed and the spec run count can be set in the options menu on the [SpecRunner][jasmine] page.
56 |
57 | [jasmine]: https://ItsNickBarry.github.io/hyperbolic-canvas/jasmine/SpecRunner.html
58 |
59 | ### Browser Support
60 |
61 | Certain browsers do not provide support for the hyperbolic trigonometric functions. Polyfills are available.
62 |
63 | ## Development
64 |
65 | Install dependencies via Yarn:
66 |
67 | ```bash
68 | yarn install
69 | ```
70 |
71 | Setup Husky to format code on commit:
72 |
73 | ```bash
74 | yarn prepare
75 | ```
76 |
--------------------------------------------------------------------------------
/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | Hyperbolic Canvas
8 |
12 |
13 |
14 |
16 |
17 |
18 |
20 |
25 |
26 |
28 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
45 |
48 |
51 |
54 |
55 |
56 |
57 |
58 |
59 |
About
60 |
61 | Read about hyperbolic geometry on
62 | Wikipedia .
65 |
66 |
67 | Hyperbolic Canvas is a Javscript library which uses the Poincaré
68 | disk model of the hyperbolic plane to map hyperbolic space onto an
69 | HTML canvas.
70 |
71 |
72 | View the source code on
73 | GitHub .
76 |
77 |
78 | Automated tests running on a modified version of
79 | Jasmine can be found
80 | here .
81 |
82 |
83 | Released under the
84 | MIT License .
88 |
89 |
90 |
91 |
92 |
93 |
95 |
96 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/css/example.css:
--------------------------------------------------------------------------------
1 | /*
2 | A hyperbolic-canvas div with a percentage-based size must have a parent with non-zero size.
3 | Margin is removed for simplicity.
4 | */
5 | html,
6 | body {
7 | height: 100%;
8 | margin: 0;
9 | background-color: #2c001e;
10 | }
11 |
12 | /*
13 | Each hyperbolic-canvas div must be of non-zero size.
14 | A size with a 1:1 ratio is recommended.
15 | In this case, it expands to fill the page.
16 | */
17 | div#hyperbolic-canvas {
18 | height: 100%;
19 | width: 100%;
20 | }
21 |
22 | /*
23 | A backdrop div is automatically appended into each hyperbolic-canvas div.
24 | It is automatically scaled to fill its parent, and cropped to a 1:1 ratio.
25 | It is not recommended to apply styling beyond background color.
26 | */
27 | div#hyperbolic-canvas div.backdrop {
28 | background-color: #2c001e;
29 | }
30 |
31 | /*
32 | An HTML canvas is automatically appended into each backdrop div.
33 | It is automatically scaled to fit its parent.
34 | It is not recommended to apply styling beyond background color.
35 | */
36 | div#hyperbolic-canvas div.backdrop div.underlay {
37 | background-color: #d6d3cf;
38 | }
39 |
--------------------------------------------------------------------------------
/css/hyperbolic-canvas.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-image: url(../images/background.png);
3 | background-attachment: fixed;
4 | }
5 |
6 | nav {
7 | margin: 13px 0 0 0;
8 | }
9 |
10 | a {
11 | text-decoration: none;
12 | }
13 |
14 | a:hover {
15 | text-decoration: underline;
16 | }
17 |
18 | p > a:hover {
19 | text-decoration: none;
20 | }
21 |
22 | .nav-button a {
23 | /*font-family: Raleway;*/
24 | display: block;
25 | text-align: center;
26 | padding: 10px 0px;
27 | }
28 |
29 | div#hyperbolic-canvas.background-canvas {
30 | display: block;
31 | position: fixed;
32 | z-index: -1;
33 | left: 0px;
34 | top: 0px;
35 | height: 10000%;
36 | width: 200%;
37 | }
38 |
39 | div#hyperbolic-canvas.background-canvas div.backdrop {
40 | transform: translate(calc(-50%), calc(-25%));
41 | }
42 |
43 | div.blockquote {
44 | background-color: white;
45 | padding: 20px;
46 | margin-top: 15%;
47 | }
48 |
49 | div.container {
50 | margin-top: 50px;
51 | margin-bottom: 50px;
52 | }
53 |
54 | div.content {
55 | background-color: white;
56 | padding: 13px;
57 | margin-top: 50px;
58 | }
59 |
60 | div.example:hover {
61 | background: #fbece7;
62 | }
63 |
64 | div.example {
65 | padding: 3px;
66 | cursor: pointer;
67 | }
68 |
69 | div.nav-button {
70 | background-color: white;
71 | height: 100%;
72 | }
73 |
74 | /*
75 | Each hyperbolic-canvas div must be of non-zero size.
76 | A size with a 1:1 ratio is recommended.
77 | In this case, it expands to fill its container.
78 | */
79 | div#hyperbolic-canvas {
80 | height: 100%;
81 | width: 100%;
82 | }
83 |
84 | /*
85 | A backdrop div is automatically appended into each hyperbolic-canvas div.
86 | It is automatically scaled to fill its parent, and cropped to a 1:1 ratio.
87 | It is not recommended to apply styling beyond background color.
88 | */
89 | div#hyperbolic-canvas div.backdrop {
90 | background-color: #2c001e;
91 | }
92 |
93 | /*
94 | An HTML canvas is automatically appended into each backdrop div.
95 | It is automatically scaled to fit its parent.
96 | It is not recommended to apply styling beyond background color.
97 | */
98 | div#hyperbolic-canvas div.backdrop div.underlay {
99 | background-color: #d6d3cf;
100 | }
101 |
--------------------------------------------------------------------------------
/css/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 |
3 | /**
4 | * 1. Set default font family to sans-serif.
5 | * 2. Prevent iOS text size adjust after orientation change, without disabling
6 | * user zoom.
7 | */
8 |
9 | html {
10 | font-family: sans-serif; /* 1 */
11 | -ms-text-size-adjust: 100%; /* 2 */
12 | -webkit-text-size-adjust: 100%; /* 2 */
13 | }
14 |
15 | /**
16 | * Remove default margin.
17 | */
18 |
19 | body {
20 | margin: 0;
21 | }
22 |
23 | /* HTML5 display definitions
24 | ========================================================================== */
25 |
26 | /**
27 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
29 | * and Firefox.
30 | * Correct `block` display not defined for `main` in IE 11.
31 | */
32 |
33 | article,
34 | aside,
35 | details,
36 | figcaption,
37 | figure,
38 | footer,
39 | header,
40 | hgroup,
41 | main,
42 | menu,
43 | nav,
44 | section,
45 | summary {
46 | display: block;
47 | }
48 |
49 | /**
50 | * 1. Correct `inline-block` display not defined in IE 8/9.
51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
52 | */
53 |
54 | audio,
55 | canvas,
56 | progress,
57 | video {
58 | display: inline-block; /* 1 */
59 | vertical-align: baseline; /* 2 */
60 | }
61 |
62 | /**
63 | * Prevent modern browsers from displaying `audio` without controls.
64 | * Remove excess height in iOS 5 devices.
65 | */
66 |
67 | audio:not([controls]) {
68 | display: none;
69 | height: 0;
70 | }
71 |
72 | /**
73 | * Address `[hidden]` styling not present in IE 8/9/10.
74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
75 | */
76 |
77 | [hidden],
78 | template {
79 | display: none;
80 | }
81 |
82 | /* Links
83 | ========================================================================== */
84 |
85 | /**
86 | * Remove the gray background color from active links in IE 10.
87 | */
88 |
89 | a {
90 | background-color: transparent;
91 | }
92 |
93 | /**
94 | * Improve readability when focused and also mouse hovered in all browsers.
95 | */
96 |
97 | a:active,
98 | a:hover {
99 | outline: 0;
100 | }
101 |
102 | /* Text-level semantics
103 | ========================================================================== */
104 |
105 | /**
106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
107 | */
108 |
109 | abbr[title] {
110 | border-bottom: 1px dotted;
111 | }
112 |
113 | /**
114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
115 | */
116 |
117 | b,
118 | strong {
119 | font-weight: bold;
120 | }
121 |
122 | /**
123 | * Address styling not present in Safari and Chrome.
124 | */
125 |
126 | dfn {
127 | font-style: italic;
128 | }
129 |
130 | /**
131 | * Address variable `h1` font-size and margin within `section` and `article`
132 | * contexts in Firefox 4+, Safari, and Chrome.
133 | */
134 |
135 | h1 {
136 | font-size: 2em;
137 | margin: 0.67em 0;
138 | }
139 |
140 | /**
141 | * Address styling not present in IE 8/9.
142 | */
143 |
144 | mark {
145 | background: #ff0;
146 | color: #000;
147 | }
148 |
149 | /**
150 | * Address inconsistent and variable font size in all browsers.
151 | */
152 |
153 | small {
154 | font-size: 80%;
155 | }
156 |
157 | /**
158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
159 | */
160 |
161 | sub,
162 | sup {
163 | font-size: 75%;
164 | line-height: 0;
165 | position: relative;
166 | vertical-align: baseline;
167 | }
168 |
169 | sup {
170 | top: -0.5em;
171 | }
172 |
173 | sub {
174 | bottom: -0.25em;
175 | }
176 |
177 | /* Embedded content
178 | ========================================================================== */
179 |
180 | /**
181 | * Remove border when inside `a` element in IE 8/9/10.
182 | */
183 |
184 | img {
185 | border: 0;
186 | }
187 |
188 | /**
189 | * Correct overflow not hidden in IE 9/10/11.
190 | */
191 |
192 | svg:not(:root) {
193 | overflow: hidden;
194 | }
195 |
196 | /* Grouping content
197 | ========================================================================== */
198 |
199 | /**
200 | * Address margin not present in IE 8/9 and Safari.
201 | */
202 |
203 | figure {
204 | margin: 1em 40px;
205 | }
206 |
207 | /**
208 | * Address differences between Firefox and other browsers.
209 | */
210 |
211 | hr {
212 | -moz-box-sizing: content-box;
213 | box-sizing: content-box;
214 | height: 0;
215 | }
216 |
217 | /**
218 | * Contain overflow in all browsers.
219 | */
220 |
221 | pre {
222 | overflow: auto;
223 | }
224 |
225 | /**
226 | * Address odd `em`-unit font size rendering in all browsers.
227 | */
228 |
229 | code,
230 | kbd,
231 | pre,
232 | samp {
233 | font-family: monospace, monospace;
234 | font-size: 1em;
235 | }
236 |
237 | /* Forms
238 | ========================================================================== */
239 |
240 | /**
241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
242 | * styling of `select`, unless a `border` property is set.
243 | */
244 |
245 | /**
246 | * 1. Correct color not being inherited.
247 | * Known issue: affects color of disabled elements.
248 | * 2. Correct font properties not being inherited.
249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
250 | */
251 |
252 | button,
253 | input,
254 | optgroup,
255 | select,
256 | textarea {
257 | color: inherit; /* 1 */
258 | font: inherit; /* 2 */
259 | margin: 0; /* 3 */
260 | }
261 |
262 | /**
263 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
264 | */
265 |
266 | button {
267 | overflow: visible;
268 | }
269 |
270 | /**
271 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
272 | * All other form control elements do not inherit `text-transform` values.
273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
274 | * Correct `select` style inheritance in Firefox.
275 | */
276 |
277 | button,
278 | select {
279 | text-transform: none;
280 | }
281 |
282 | /**
283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
284 | * and `video` controls.
285 | * 2. Correct inability to style clickable `input` types in iOS.
286 | * 3. Improve usability and consistency of cursor style between image-type
287 | * `input` and others.
288 | */
289 |
290 | button,
291 | html input[type="button"], /* 1 */
292 | input[type="reset"],
293 | input[type="submit"] {
294 | -webkit-appearance: button; /* 2 */
295 | cursor: pointer; /* 3 */
296 | }
297 |
298 | /**
299 | * Re-set default cursor for disabled elements.
300 | */
301 |
302 | button[disabled],
303 | html input[disabled] {
304 | cursor: default;
305 | }
306 |
307 | /**
308 | * Remove inner padding and border in Firefox 4+.
309 | */
310 |
311 | button::-moz-focus-inner,
312 | input::-moz-focus-inner {
313 | border: 0;
314 | padding: 0;
315 | }
316 |
317 | /**
318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
319 | * the UA stylesheet.
320 | */
321 |
322 | input {
323 | line-height: normal;
324 | }
325 |
326 | /**
327 | * It's recommended that you don't attempt to style these elements.
328 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
329 | *
330 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
331 | * 2. Remove excess padding in IE 8/9/10.
332 | */
333 |
334 | input[type='checkbox'],
335 | input[type='radio'] {
336 | box-sizing: border-box; /* 1 */
337 | padding: 0; /* 2 */
338 | }
339 |
340 | /**
341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
342 | * `font-size` values of the `input`, it causes the cursor style of the
343 | * decrement button to change from `default` to `text`.
344 | */
345 |
346 | input[type='number']::-webkit-inner-spin-button,
347 | input[type='number']::-webkit-outer-spin-button {
348 | height: auto;
349 | }
350 |
351 | /**
352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
354 | * (include `-moz` to future-proof).
355 | */
356 |
357 | input[type='search'] {
358 | -webkit-appearance: textfield; /* 1 */
359 | -moz-box-sizing: content-box;
360 | -webkit-box-sizing: content-box; /* 2 */
361 | box-sizing: content-box;
362 | }
363 |
364 | /**
365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
366 | * Safari (but not Chrome) clips the cancel button when the search input has
367 | * padding (and `textfield` appearance).
368 | */
369 |
370 | input[type='search']::-webkit-search-cancel-button,
371 | input[type='search']::-webkit-search-decoration {
372 | -webkit-appearance: none;
373 | }
374 |
375 | /**
376 | * Define consistent border, margin, and padding.
377 | */
378 |
379 | fieldset {
380 | border: 1px solid #c0c0c0;
381 | margin: 0 2px;
382 | padding: 0.35em 0.625em 0.75em;
383 | }
384 |
385 | /**
386 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
388 | */
389 |
390 | legend {
391 | border: 0; /* 1 */
392 | padding: 0; /* 2 */
393 | }
394 |
395 | /**
396 | * Remove default vertical scrollbar in IE 8/9/10/11.
397 | */
398 |
399 | textarea {
400 | overflow: auto;
401 | }
402 |
403 | /**
404 | * Don't inherit the `font-weight` (applied by a rule above).
405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
406 | */
407 |
408 | optgroup {
409 | font-weight: bold;
410 | }
411 |
412 | /* Tables
413 | ========================================================================== */
414 |
415 | /**
416 | * Remove most spacing between table cells.
417 | */
418 |
419 | table {
420 | border-collapse: collapse;
421 | border-spacing: 0;
422 | }
423 |
424 | td,
425 | th {
426 | padding: 0;
427 | }
428 |
--------------------------------------------------------------------------------
/css/skeleton.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Skeleton V2.0.4
3 | * Copyright 2014, Dave Gamache
4 | * www.getskeleton.com
5 | * Free to use under the MIT license.
6 | * http://www.opensource.org/licenses/mit-license.php
7 | * 12/29/2014
8 | */
9 |
10 | /* Table of contents
11 | ––––––––––––––––––––––––––––––––––––––––––––––––––
12 | - Grid
13 | - Base Styles
14 | - Typography
15 | - Links
16 | - Buttons
17 | - Forms
18 | - Lists
19 | - Code
20 | - Tables
21 | - Spacing
22 | - Utilities
23 | - Clearing
24 | - Media Queries
25 | */
26 |
27 | /* Grid
28 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
29 | .container {
30 | position: relative;
31 | width: 100%;
32 | max-width: 960px;
33 | margin: 0 auto;
34 | padding: 0 20px;
35 | box-sizing: border-box;
36 | }
37 | .column,
38 | .columns {
39 | width: 100%;
40 | float: left;
41 | box-sizing: border-box;
42 | }
43 |
44 | /* For devices larger than 400px */
45 | @media (min-width: 400px) {
46 | .container {
47 | width: 85%;
48 | padding: 0;
49 | }
50 | }
51 |
52 | /* For devices larger than 550px */
53 | @media (min-width: 550px) {
54 | .container {
55 | width: 80%;
56 | }
57 | .column,
58 | .columns {
59 | margin-left: 4%;
60 | }
61 | .column:first-child,
62 | .columns:first-child {
63 | margin-left: 0;
64 | }
65 |
66 | .one.column,
67 | .one.columns {
68 | width: 4.66666666667%;
69 | }
70 | .two.columns {
71 | width: 13.3333333333%;
72 | }
73 | .three.columns {
74 | width: 22%;
75 | }
76 | .four.columns {
77 | width: 30.6666666667%;
78 | }
79 | .five.columns {
80 | width: 39.3333333333%;
81 | }
82 | .six.columns {
83 | width: 48%;
84 | }
85 | .seven.columns {
86 | width: 56.6666666667%;
87 | }
88 | .eight.columns {
89 | width: 65.3333333333%;
90 | }
91 | .nine.columns {
92 | width: 74%;
93 | }
94 | .ten.columns {
95 | width: 82.6666666667%;
96 | }
97 | .eleven.columns {
98 | width: 91.3333333333%;
99 | }
100 | .twelve.columns {
101 | width: 100%;
102 | margin-left: 0;
103 | }
104 |
105 | .one-third.column {
106 | width: 30.6666666667%;
107 | }
108 | .two-thirds.column {
109 | width: 65.3333333333%;
110 | }
111 |
112 | .one-half.column {
113 | width: 48%;
114 | }
115 |
116 | /* Offsets */
117 | .offset-by-one.column,
118 | .offset-by-one.columns {
119 | margin-left: 8.66666666667%;
120 | }
121 | .offset-by-two.column,
122 | .offset-by-two.columns {
123 | margin-left: 17.3333333333%;
124 | }
125 | .offset-by-three.column,
126 | .offset-by-three.columns {
127 | margin-left: 26%;
128 | }
129 | .offset-by-four.column,
130 | .offset-by-four.columns {
131 | margin-left: 34.6666666667%;
132 | }
133 | .offset-by-five.column,
134 | .offset-by-five.columns {
135 | margin-left: 43.3333333333%;
136 | }
137 | .offset-by-six.column,
138 | .offset-by-six.columns {
139 | margin-left: 52%;
140 | }
141 | .offset-by-seven.column,
142 | .offset-by-seven.columns {
143 | margin-left: 60.6666666667%;
144 | }
145 | .offset-by-eight.column,
146 | .offset-by-eight.columns {
147 | margin-left: 69.3333333333%;
148 | }
149 | .offset-by-nine.column,
150 | .offset-by-nine.columns {
151 | margin-left: 78%;
152 | }
153 | .offset-by-ten.column,
154 | .offset-by-ten.columns {
155 | margin-left: 86.6666666667%;
156 | }
157 | .offset-by-eleven.column,
158 | .offset-by-eleven.columns {
159 | margin-left: 95.3333333333%;
160 | }
161 |
162 | .offset-by-one-third.column,
163 | .offset-by-one-third.columns {
164 | margin-left: 34.6666666667%;
165 | }
166 | .offset-by-two-thirds.column,
167 | .offset-by-two-thirds.columns {
168 | margin-left: 69.3333333333%;
169 | }
170 |
171 | .offset-by-one-half.column,
172 | .offset-by-one-half.columns {
173 | margin-left: 52%;
174 | }
175 | }
176 |
177 | /* Base Styles
178 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
179 | /* NOTE
180 | html is set to 62.5% so that all the REM measurements throughout Skeleton
181 | are based on 10px sizing. So basically 1.5rem = 15px :) */
182 | html {
183 | font-size: 62.5%;
184 | }
185 | body {
186 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
187 | line-height: 1.6;
188 | font-weight: 400;
189 | font-family: 'Raleway', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
190 | sans-serif;
191 | color: #222;
192 | }
193 |
194 | /* Typography
195 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
196 | h1,
197 | h2,
198 | h3,
199 | h4,
200 | h5,
201 | h6 {
202 | margin-top: 0;
203 | margin-bottom: 2rem;
204 | font-weight: 300;
205 | }
206 | h1 {
207 | font-size: 4rem;
208 | line-height: 1.2;
209 | letter-spacing: -0.1rem;
210 | }
211 | h2 {
212 | font-size: 3.6rem;
213 | line-height: 1.25;
214 | letter-spacing: -0.1rem;
215 | }
216 | h3 {
217 | font-size: 3rem;
218 | line-height: 1.3;
219 | letter-spacing: -0.1rem;
220 | }
221 | h4 {
222 | font-size: 2.4rem;
223 | line-height: 1.35;
224 | letter-spacing: -0.08rem;
225 | }
226 | h5 {
227 | font-size: 1.8rem;
228 | line-height: 1.5;
229 | letter-spacing: -0.05rem;
230 | }
231 | h6 {
232 | font-size: 1.5rem;
233 | line-height: 1.6;
234 | letter-spacing: 0;
235 | }
236 |
237 | /* Larger than phablet */
238 | @media (min-width: 550px) {
239 | h1 {
240 | font-size: 5rem;
241 | }
242 | h2 {
243 | font-size: 4.2rem;
244 | }
245 | h3 {
246 | font-size: 3.6rem;
247 | }
248 | h4 {
249 | font-size: 3rem;
250 | }
251 | h5 {
252 | font-size: 2.4rem;
253 | }
254 | h6 {
255 | font-size: 1.5rem;
256 | }
257 | }
258 |
259 | p {
260 | margin-top: 0;
261 | }
262 |
263 | /* Links
264 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
265 | a {
266 | color: #1eaedb;
267 | }
268 | a:hover {
269 | color: #0fa0ce;
270 | }
271 |
272 | /* Buttons
273 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
274 | .button,
275 | button,
276 | input[type='submit'],
277 | input[type='reset'],
278 | input[type='button'] {
279 | display: inline-block;
280 | height: 38px;
281 | padding: 0 30px;
282 | color: #555;
283 | text-align: center;
284 | font-size: 11px;
285 | font-weight: 600;
286 | line-height: 38px;
287 | letter-spacing: 0.1rem;
288 | text-transform: uppercase;
289 | text-decoration: none;
290 | white-space: nowrap;
291 | background-color: transparent;
292 | border-radius: 4px;
293 | border: 1px solid #bbb;
294 | cursor: pointer;
295 | box-sizing: border-box;
296 | }
297 | .button:hover,
298 | button:hover,
299 | input[type='submit']:hover,
300 | input[type='reset']:hover,
301 | input[type='button']:hover,
302 | .button:focus,
303 | button:focus,
304 | input[type='submit']:focus,
305 | input[type='reset']:focus,
306 | input[type='button']:focus {
307 | color: #333;
308 | border-color: #888;
309 | outline: 0;
310 | }
311 | .button.button-primary,
312 | button.button-primary,
313 | input[type='submit'].button-primary,
314 | input[type='reset'].button-primary,
315 | input[type='button'].button-primary {
316 | color: #fff;
317 | background-color: #33c3f0;
318 | border-color: #33c3f0;
319 | }
320 | .button.button-primary:hover,
321 | button.button-primary:hover,
322 | input[type='submit'].button-primary:hover,
323 | input[type='reset'].button-primary:hover,
324 | input[type='button'].button-primary:hover,
325 | .button.button-primary:focus,
326 | button.button-primary:focus,
327 | input[type='submit'].button-primary:focus,
328 | input[type='reset'].button-primary:focus,
329 | input[type='button'].button-primary:focus {
330 | color: #fff;
331 | background-color: #1eaedb;
332 | border-color: #1eaedb;
333 | }
334 |
335 | /* Forms
336 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
337 | input[type='email'],
338 | input[type='number'],
339 | input[type='search'],
340 | input[type='text'],
341 | input[type='tel'],
342 | input[type='url'],
343 | input[type='password'],
344 | textarea,
345 | select {
346 | height: 38px;
347 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
348 | background-color: #fff;
349 | border: 1px solid #d1d1d1;
350 | border-radius: 4px;
351 | box-shadow: none;
352 | box-sizing: border-box;
353 | }
354 | /* Removes awkward default styles on some inputs for iOS */
355 | input[type='email'],
356 | input[type='number'],
357 | input[type='search'],
358 | input[type='text'],
359 | input[type='tel'],
360 | input[type='url'],
361 | input[type='password'],
362 | textarea {
363 | -webkit-appearance: none;
364 | -moz-appearance: none;
365 | appearance: none;
366 | }
367 | textarea {
368 | min-height: 65px;
369 | padding-top: 6px;
370 | padding-bottom: 6px;
371 | }
372 | input[type='email']:focus,
373 | input[type='number']:focus,
374 | input[type='search']:focus,
375 | input[type='text']:focus,
376 | input[type='tel']:focus,
377 | input[type='url']:focus,
378 | input[type='password']:focus,
379 | textarea:focus,
380 | select:focus {
381 | border: 1px solid #33c3f0;
382 | outline: 0;
383 | }
384 | label,
385 | legend {
386 | display: block;
387 | margin-bottom: 0.5rem;
388 | font-weight: 600;
389 | }
390 | fieldset {
391 | padding: 0;
392 | border-width: 0;
393 | }
394 | input[type='checkbox'],
395 | input[type='radio'] {
396 | display: inline;
397 | }
398 | label > .label-body {
399 | display: inline-block;
400 | margin-left: 0.5rem;
401 | font-weight: normal;
402 | }
403 |
404 | /* Lists
405 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
406 | ul {
407 | list-style: circle inside;
408 | }
409 | ol {
410 | list-style: decimal inside;
411 | }
412 | ol,
413 | ul {
414 | padding-left: 0;
415 | margin-top: 0;
416 | }
417 | ul ul,
418 | ul ol,
419 | ol ol,
420 | ol ul {
421 | margin: 1.5rem 0 1.5rem 3rem;
422 | font-size: 90%;
423 | }
424 | li {
425 | margin-bottom: 1rem;
426 | }
427 |
428 | /* Code
429 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
430 | code {
431 | padding: 0.2rem 0.5rem;
432 | margin: 0 0.2rem;
433 | font-size: 90%;
434 | white-space: nowrap;
435 | background: #f1f1f1;
436 | border: 1px solid #e1e1e1;
437 | border-radius: 4px;
438 | }
439 | pre > code {
440 | display: block;
441 | padding: 1rem 1.5rem;
442 | white-space: pre;
443 | }
444 |
445 | /* Tables
446 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
447 | th,
448 | td {
449 | padding: 12px 15px;
450 | text-align: left;
451 | border-bottom: 1px solid #e1e1e1;
452 | }
453 | th:first-child,
454 | td:first-child {
455 | padding-left: 0;
456 | }
457 | th:last-child,
458 | td:last-child {
459 | padding-right: 0;
460 | }
461 |
462 | /* Spacing
463 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
464 | button,
465 | .button {
466 | margin-bottom: 1rem;
467 | }
468 | input,
469 | textarea,
470 | select,
471 | fieldset {
472 | margin-bottom: 1.5rem;
473 | }
474 | pre,
475 | blockquote,
476 | dl,
477 | figure,
478 | table,
479 | p,
480 | ul,
481 | ol,
482 | form {
483 | margin-bottom: 2.5rem;
484 | }
485 |
486 | /* Utilities
487 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
488 | .u-full-width {
489 | width: 100%;
490 | box-sizing: border-box;
491 | }
492 | .u-max-full-width {
493 | max-width: 100%;
494 | box-sizing: border-box;
495 | }
496 | .u-pull-right {
497 | float: right;
498 | }
499 | .u-pull-left {
500 | float: left;
501 | }
502 |
503 | /* Misc
504 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
505 | hr {
506 | margin-top: 3rem;
507 | margin-bottom: 3.5rem;
508 | border-width: 0;
509 | border-top: 1px solid #e1e1e1;
510 | }
511 |
512 | /* Clearing
513 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
514 |
515 | /* Self Clearing Goodness */
516 | .container:after,
517 | .row:after,
518 | .u-cf {
519 | content: '';
520 | display: table;
521 | clear: both;
522 | }
523 |
524 | /* Media Queries
525 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
526 | /*
527 | Note: The best way to structure the use of media queries is to create the queries
528 | near the relevant code. For example, if you wanted to change the styles for buttons
529 | on small devices, paste the mobile query code up in the buttons section and style it
530 | there.
531 | */
532 |
533 | /* Larger than mobile */
534 | @media (min-width: 400px) {
535 | }
536 |
537 | /* Larger than phablet (also point when grid becomes active) */
538 | @media (min-width: 550px) {
539 | }
540 |
541 | /* Larger than tablet */
542 | @media (min-width: 750px) {
543 | }
544 |
545 | /* Larger than desktop */
546 | @media (min-width: 1000px) {
547 | }
548 |
549 | /* Larger than Desktop HD */
550 | @media (min-width: 1200px) {
551 | }
552 |
--------------------------------------------------------------------------------
/documentation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | Hyperbolic Canvas
8 |
12 |
13 |
14 |
16 |
17 |
18 |
20 |
25 |
26 |
28 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
45 |
48 |
51 |
54 |
55 |
56 |
57 |
58 |
59 |
Documentation
60 |
61 | The readme is transcluded here using the
62 | marked library.
63 |
64 |
65 | View the original readme on
66 | GitHub .
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
79 |
80 |
81 |
91 |
92 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hyperbolic Canvas Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/examples.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | Hyperbolic Canvas
8 |
12 |
13 |
14 |
16 |
17 |
18 |
20 |
25 |
26 |
28 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
45 |
48 |
51 |
54 |
55 |
56 |
57 |
58 |
59 |
Examples
60 |
72 |
84 |
93 |
105 |
114 |
115 |
Bad Examples
116 |
122 |
131 |
132 |
133 |
134 |
135 |
137 |
138 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/images/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItsNickBarry/hyperbolic-canvas/0a3fa5c4501e7f60e7048a6599983f8a310a51c7/images/background.png
--------------------------------------------------------------------------------
/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItsNickBarry/hyperbolic-canvas/0a3fa5c4501e7f60e7048a6599983f8a310a51c7/images/favicon.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | Hyperbolic Canvas
8 |
12 |
13 |
14 |
16 |
17 |
18 |
20 |
25 |
26 |
28 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
45 |
48 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Please enable Javascript in order to view this site.
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | For God's sake, please give it up. Fear it no less than the sensual
66 | passion, because it, too, may take up all your time and deprive you
67 | of your health, peace of mind, and happiness in life.
68 |
69 |
70 | Farkas Bolyai, to his son
71 | János Bolyai , on hyperbolic geometry
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
83 |
87 |
88 |
89 |
90 |
105 |
106 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/hyperbolic_canvas.js');
2 |
--------------------------------------------------------------------------------
/jasmine/MIT.LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2008-2014 Pivotal Labs
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/jasmine/SpecRunner.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Hyperbolic Canvas - Jasmine Spec Runner v2.4.1+
6 |
7 |
12 |
13 |
14 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
Hyperbolic Canvas Specifications
104 |
back
105 |
106 |
107 |
108 | This version of Jasmine is modified to run each spec multiple times.
109 |
110 |
111 | Uses
112 | seedrandom.js
113 | by David Bau.
114 |
115 |
116 |
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/jasmine/lib/jasmine-2.4.1+/boot.js:
--------------------------------------------------------------------------------
1 | /**
2 | Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js` and `jasmine_html.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project.
3 |
4 | If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms.
5 |
6 | The location of `boot.js` can be specified and/or overridden in `jasmine.yml`.
7 |
8 | [jasmine-gem]: http://github.com/pivotal/jasmine-gem
9 | */
10 |
11 | (function() {
12 |
13 | /**
14 | * ## Require & Instantiate
15 | *
16 | * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
17 | */
18 | window.jasmine = jasmineRequire.core(jasmineRequire);
19 |
20 | /**
21 | * Seed the random number generator, setup helper functions, and attach variables to Jasmine
22 | */
23 | var getQueryVariable = function (variable) {
24 | // https://css-tricks.com/snippets/javascript/get-url-variables/
25 | var query = window.location.search.substring(1);
26 | var vars = query.split("&");
27 | for (var i=0;i 0) {
78 | printNewline();
79 |
80 | var specCounts = specCount + ' ' + plural('spec', specCount) + ', ' +
81 | failureCount + ' ' + plural('failure', failureCount);
82 |
83 | if (pendingCount) {
84 | specCounts += ', ' + pendingCount + ' pending ' + plural('spec', pendingCount);
85 | }
86 |
87 | print(specCounts);
88 | } else {
89 | print('No specs found');
90 | }
91 |
92 | printNewline();
93 | var seconds = timer.elapsed() / 1000;
94 | print('Finished in ' + seconds + ' ' + plural('second', seconds));
95 | printNewline();
96 |
97 | for(i = 0; i < failedSuites.length; i++) {
98 | suiteFailureDetails(failedSuites[i]);
99 | }
100 |
101 | onComplete(failureCount === 0);
102 | };
103 |
104 | this.specDone = function(result) {
105 | specCount++;
106 |
107 | if (result.status == 'pending') {
108 | pendingCount++;
109 | print(colored('yellow', '*'));
110 | return;
111 | }
112 |
113 | if (result.status == 'passed') {
114 | print(colored('green', '.'));
115 | return;
116 | }
117 |
118 | if (result.status == 'failed') {
119 | failureCount++;
120 | failedSpecs.push(result);
121 | print(colored('red', 'F'));
122 | }
123 | };
124 |
125 | this.suiteDone = function(result) {
126 | if (result.failedExpectations && result.failedExpectations.length > 0) {
127 | failureCount++;
128 | failedSuites.push(result);
129 | }
130 | };
131 |
132 | return this;
133 |
134 | function printNewline() {
135 | print('\n');
136 | }
137 |
138 | function colored(color, str) {
139 | return showColors ? (ansi[color] + str + ansi.none) : str;
140 | }
141 |
142 | function plural(str, count) {
143 | return count == 1 ? str : str + 's';
144 | }
145 |
146 | function repeat(thing, times) {
147 | var arr = [];
148 | for (var i = 0; i < times; i++) {
149 | arr.push(thing);
150 | }
151 | return arr;
152 | }
153 |
154 | function indent(str, spaces) {
155 | var lines = (str || '').split('\n');
156 | var newArr = [];
157 | for (var i = 0; i < lines.length; i++) {
158 | newArr.push(repeat(' ', spaces).join('') + lines[i]);
159 | }
160 | return newArr.join('\n');
161 | }
162 |
163 | function specFailureDetails(result) {
164 | printNewline();
165 | print(result.fullName);
166 |
167 | for (var i = 0; i < result.failedExpectations.length; i++) {
168 | var failedExpectation = result.failedExpectations[i];
169 | printNewline();
170 | print(indent(failedExpectation.message, 2));
171 | print(indent(failedExpectation.stack, 2));
172 | }
173 |
174 | printNewline();
175 | }
176 |
177 | function suiteFailureDetails(result) {
178 | for (var i = 0; i < result.failedExpectations.length; i++) {
179 | printNewline();
180 | print(colored('red', 'An error was thrown in an afterAll'));
181 | printNewline();
182 | print(colored('red', 'AfterAll ' + result.failedExpectations[i].message));
183 |
184 | }
185 | printNewline();
186 | }
187 | }
188 |
189 | return ConsoleReporter;
190 | };
191 |
--------------------------------------------------------------------------------
/jasmine/lib/jasmine-2.4.1+/jasmine.css:
--------------------------------------------------------------------------------
1 | body { overflow-y: scroll; }
2 |
3 | .jasmine_html-reporter { background-color: #eee; padding: 5px; margin: -8px; font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333; }
4 | .jasmine_html-reporter a { text-decoration: none; }
5 | .jasmine_html-reporter a:hover { text-decoration: underline; }
6 | .jasmine_html-reporter p, .jasmine_html-reporter h1, .jasmine_html-reporter h2, .jasmine_html-reporter h3, .jasmine_html-reporter h4, .jasmine_html-reporter h5, .jasmine_html-reporter h6 { margin: 0; line-height: 14px; }
7 | .jasmine_html-reporter .jasmine-banner, .jasmine_html-reporter .jasmine-symbol-summary, .jasmine_html-reporter .jasmine-summary, .jasmine_html-reporter .jasmine-result-message, .jasmine_html-reporter .jasmine-spec .jasmine-description, .jasmine_html-reporter .jasmine-spec-detail .jasmine-description, .jasmine_html-reporter .jasmine-alert .jasmine-bar, .jasmine_html-reporter .jasmine-stack-trace { padding-left: 9px; padding-right: 9px; }
8 | .jasmine_html-reporter .jasmine-banner { position: relative; }
9 | .jasmine_html-reporter .jasmine-banner .jasmine-title { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAZCAMAAACGusnyAAACdlBMVEX/////AP+AgICqVaqAQICZM5mAVYCSSZKAQICOOY6ATYCLRouAQICJO4mSSYCIRIiPQICHPIeOR4CGQ4aMQICGPYaLRoCFQ4WKQICPPYWJRYCOQoSJQICNPoSIRICMQoSHQICHRICKQoOHQICKPoOJO4OJQYOMQICMQ4CIQYKLQICIPoKLQ4CKQICNPoKJQISMQ4KJQoSLQYKJQISLQ4KIQoSKQYKIQICIQISMQoSKQYKLQIOLQoOJQYGLQIOKQIOMQoGKQYOLQYGKQIOLQoGJQYOJQIOKQYGJQIOKQoGKQIGLQIKLQ4KKQoGLQYKJQIGKQYKJQIGKQIKJQoGKQYKLQIGKQYKLQIOJQoKKQoOJQYKKQIOJQoKKQoOKQIOLQoKKQYOLQYKJQIOKQoKKQYKKQoKJQYOKQYKLQIOKQoKLQYOKQYKLQIOJQoGKQYKJQYGJQoGKQYKLQoGLQYGKQoGJQYKKQYGJQIKKQoGJQYKLQIKKQYGLQYKKQYGKQYGKQYKJQYOKQoKJQYOKQYKLQYOLQYOKQYKLQYOKQoKKQYKKQYOKQYOJQYKKQYKLQYKKQIKKQoKKQYKKQYKKQoKJQIKKQYKLQYKKQYKKQIKKQYKKQYKKQYKKQIKKQYKJQYGLQYGKQYKKQYKKQYGKQIKKQYGKQYOJQoKKQYOLQYKKQYOKQoKKQYKKQoKKQYKKQYKJQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKJQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKmIDpEAAAA0XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAiIyQlJycoKissLS4wMTQ1Njc4OTo7PDw+P0BCQ0RISUpLTE1OUFNUVVdYWFlaW15fYGFiY2ZnaGlqa2xtb3BxcnN0dnh5ent8fX5/gIGChIWIioyNjo+QkZOUlZaYmZqbnJ2eoKGio6WmqKmsra6vsLGztre4ubq7vL2+wMHDxMjJysvNzs/Q0dLU1tfY2dvc3t/g4eLj5ebn6Onq6+zt7u/w8vP09fb3+Pn6+/z9/vkVQXAAAAMaSURBVHhe5dXxV1N1GMfxz2ABbDgIAm5VDJOyVDIJLUMaVpBWUZUaGbmqoGpZRSiGiRWp6KoZ5AB0ZY50RImZQIlahKkMYXv/R90dBvET/rJfOr3Ouc8v99zPec59zvf56j+vYKlViSf7250X4Mr3O29Tgq08BdGB4DhcekEJ5YkQKFsgWZdtj9JpV+I8xPjLFqkrsEIqO8PHSpis36jWazcqjEsfJjkvRssVU37SdIOu4XCf5vEJPsnwJpnRNU9JmxhMk8l1gehIrq7hTFjzOD+Vf88629qKMJVNltInFeRexRQyJlNeqd1iGDlSzrIUIyXbyFfm3RYprcQRe7lqtWyGYbfc6dT0R2vmdOOkX3u55C1rP37ftiH+tDby4r/RBT0w8TyEkr+epB9XgPDmSYYWbrhCuFYaIyw3fDQAXTnSkh+ANofiHmWf9l+FY1I90FdQTetstO00o23novzVsJ7uB3/C5TkbjRwZ5JerwV4iRWq9HFbFMaK/d0TYqayRiQPuIxxS3Bu8JWU90/60tKi7vkhaznez0a/TbVOKj5CaOZh6fWG6/Lyv9B/ZLR1gw/S/fpbeVD3MCW1li6SvWDOn65tr99/uvWtBS0XDm4s1t+sOHpG0kpBKx/l77wOSnxLpcx6TXmXLTPQOKYOf9Q1dfr8/SJ2mFdCvl1Yl93DiHUZvXeLJbGSzYu5gVJ2slbSakOR8dxCq5adQ2oFLqsE9Ex3L4qQO0eOPeU5x56bypXp4onSEb5OkICX6lDat55TeoztNKQcJaakrz9KCb95oD69IKq+yKW4XPjknaS52V0TZqE2cTtXjcHSCRmUO88e+85hj3EP74i9p8pylw7lxgMDyyl6OV7ZejnjNMfatu87LxRbH0IS35gt2a4ZjmGpVBdKK3Wr6INk8jWWSGqbA55CKgjBRC6E9w78ydTg3ABS3AFV1QN0Y4Aa2pgEjWnQURj9L0ayK6R2ysEqxHUKzYnLvvyU+i9KM2JHJzE4vyZOyDcOwOsySajeLPc8sNvPJkFlyJd20wpqAzZeAfZ3oWybxd+P/3j+SG3uSBdf2VQAAAABJRU5ErkJggg==') no-repeat; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICB3aWR0aD0iNjgxLjk2MjUyIgogICBoZWlnaHQ9IjE4Ny41IgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhOCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczYiPjxjbGlwUGF0aAogICAgICAgaWQ9ImNsaXBQYXRoMTgiPjxwYXRoCiAgICAgICAgIGQ9Ik0gMCwxNTAwIDAsMCBsIDU0NTUuNzQsMCAwLDE1MDAgTCAwLDE1MDAgeiIKICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgaWQ9InBhdGgyMCIgLz48L2NsaXBQYXRoPjwvZGVmcz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMjUsMCwwLC0xLjI1LDAsMTg3LjUpIgogICAgIGlkPSJnMTAiPjxnCiAgICAgICB0cmFuc2Zvcm09InNjYWxlKDAuMSwwLjEpIgogICAgICAgaWQ9ImcxMiI+PGcKICAgICAgICAgaWQ9ImcxNCI+PGcKICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxOCkiCiAgICAgICAgICAgaWQ9ImcxNiI+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTU0NCw1OTkuNDM0IGMgMC45MiwtNDAuMzUyIDI1LjY4LC04MS42MDIgNzEuNTMsLTgxLjYwMiAyNy41MSwwIDQ3LjY4LDEyLjgzMiA2MS40NCwzNS43NTQgMTIuODMsMjIuOTMgMTIuODMsNTYuODUyIDEyLjgzLDgyLjUyNyBsIDAsMzI5LjE4NCAtNzEuNTIsMCAwLDEwNC41NDMgMjY2LjgzLDAgMCwtMTA0LjU0MyAtNzAuNiwwIDAsLTM0NC43NyBjIDAsLTU4LjY5MSAtMy42OCwtMTA0LjUzMSAtNDQuOTMsLTE1Mi4yMTggLTM2LjY4LC00Mi4xOCAtOTYuMjgsLTY2LjAyIC0xNTMuMTQsLTY2LjAyIC0xMTcuMzcsMCAtMjA3LjI0LDc3Ljk0MSAtMjAyLjY0LDE5Ny4xNDUgbCAxMzAuMiwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMjIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDIzMDEuNCw2NjIuNjk1IGMgMCw4MC43MDMgLTY2Ljk0LDE0NS44MTMgLTE0Ny42MywxNDUuODEzIC04My40NCwwIC0xNDcuNjMsLTY4Ljc4MSAtMTQ3LjYzLC0xNTEuMzAxIDAsLTc5Ljc4NSA2Ni45NCwtMTQ1LjgwMSAxNDUuOCwtMTQ1LjgwMSA4NC4zNSwwIDE0OS40Niw2Ny44NTIgMTQ5LjQ2LDE1MS4yODkgeiBtIC0xLjgzLC0xODEuNTQ3IGMgLTM1Ljc3LC01NC4wOTcgLTkzLjUzLC03OC44NTkgLTE1Ny43MiwtNzguODU5IC0xNDAuMywwIC0yNTEuMjQsMTE2LjQ0OSAtMjUxLjI0LDI1NC45MTggMCwxNDIuMTI5IDExMy43LDI2MC40MSAyNTYuNzQsMjYwLjQxIDYzLjI3LDAgMTE4LjI5LC0yOS4zMzYgMTUyLjIyLC04Mi41MjMgbCAwLDY5LjY4NyAxNzUuMTQsMCAwLC0xMDQuNTI3IC02MS40NCwwIDAsLTI4MC41OTggNjEuNDQsMCAwLC0xMDQuNTI3IC0xNzUuMTQsMCAwLDY2LjAxOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAyNjIyLjMzLDU1Ny4yNTggYyAzLjY3LC00NC4wMTYgMzMuMDEsLTczLjM0OCA3OC44NiwtNzMuMzQ4IDMzLjkzLDAgNjYuOTMsMjMuODI0IDY2LjkzLDYwLjUwNCAwLDQ4LjYwNiAtNDUuODQsNTYuODU2IC04My40NCw2Ni45NDEgLTg1LjI4LDIyLjAwNCAtMTc4LjgxLDQ4LjYwNiAtMTc4LjgxLDE1NS44NzkgMCw5My41MzYgNzguODYsMTQ3LjYzMyAxNjUuOTgsMTQ3LjYzMyA0NCwwIDgzLjQzLC05LjE3NiAxMTAuOTQsLTQ0LjAwOCBsIDAsMzMuOTIyIDgyLjUzLDAgMCwtMTMyLjk2NSAtMTA4LjIxLDAgYyAtMS44MywzNC44NTYgLTI4LjQyLDU3Ljc3NCAtNjMuMjYsNTcuNzc0IC0zMC4yNiwwIC02Mi4zNSwtMTcuNDIyIC02Mi4zNSwtNTEuMzQ4IDAsLTQ1Ljg0NyA0NC45MywtNTUuOTMgODAuNjksLTY0LjE4IDg4LjAyLC0yMC4xNzUgMTgyLjQ3LC00Ny42OTUgMTgyLjQ3LC0xNTcuNzM0IDAsLTk5LjAyNyAtODMuNDQsLTE1NC4wMzkgLTE3NS4xMywtMTU0LjAzOSAtNDkuNTMsMCAtOTQuNDYsMTUuNTgyIC0xMjYuNTUsNTMuMTggbCAwLC00MC4zNCAtODUuMjcsMCAwLDE0Mi4xMjkgMTE0LjYyLDAiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGgyNiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMjk4OC4xOCw4MDAuMjU0IC02My4yNiwwIDAsMTA0LjUyNyAxNjUuMDUsMCAwLC03My4zNTUgYyAzMS4xOCw1MS4zNDcgNzguODYsODUuMjc3IDE0MS4yMSw4NS4yNzcgNjcuODUsMCAxMjQuNzEsLTQxLjI1OCAxNTIuMjEsLTEwMi42OTkgMjYuNiw2Mi4zNTEgOTIuNjIsMTAyLjY5OSAxNjAuNDcsMTAyLjY5OSA1My4xOSwwIDEwNS40NiwtMjIgMTQxLjIxLC02Mi4zNTEgMzguNTIsLTQ0LjkzOCAzOC41MiwtOTMuNTMyIDM4LjUyLC0xNDkuNDU3IGwgMCwtMTg1LjIzOSA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40MiwwIDAsMTA0LjUyNyA2My4yOCwwIDAsMTU3LjcxNSBjIDAsMzIuMTAyIDAsNjAuNTI3IC0xNC42Nyw4OC45NTcgLTE4LjM0LDI2LjU4MiAtNDguNjEsNDAuMzQ0IC03OS43Nyw0MC4zNDQgLTMwLjI2LDAgLTYzLjI4LC0xMi44NDQgLTgyLjUzLC0zNi42NzIgLTIyLjkzLC0yOS4zNTUgLTIyLjkzLC01Ni44NjMgLTIyLjkzLC05Mi42MjkgbCAwLC0xNTcuNzE1IDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM4LjQxLDAgMCwxMDQuNTI3IDYzLjI4LDAgMCwxNTAuMzgzIGMgMCwyOS4zNDggMCw2Ni4wMjMgLTE0LjY3LDkxLjY5OSAtMTUuNTksMjkuMzM2IC00Ny42OSw0NC45MzQgLTgwLjcsNDQuOTM0IC0zMS4xOCwwIC01Ny43NywtMTEuMDA4IC03Ny45NCwtMzUuNzc0IC0yNC43NywtMzAuMjUzIC0yNi42LC02Mi4zNDMgLTI2LjYsLTk5Ljk0MSBsIDAsLTE1MS4zMDEgNjMuMjcsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNiwwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAzOTk4LjY2LDk1MS41NDcgLTExMS44NywwIDAsMTE4LjI5MyAxMTEuODcsMCAwLC0xMTguMjkzIHogbSAwLC00MzEuODkxIDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM5LjMzLDAgMCwxMDQuNTI3IDY0LjE5LDAgMCwyODAuNTk4IC02My4yNywwIDAsMTA0LjUyNyAxNzUuMTQsMCAwLC0zODUuMTI1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDQxNTkuMTIsODAwLjI1NCAtNjMuMjcsMCAwLDEwNC41MjcgMTc1LjE0LDAgMCwtNjkuNjg3IGMgMjkuMzUsNTQuMTAxIDg0LjM2LDgwLjY5OSAxNDQuODcsODAuNjk5IDUzLjE5LDAgMTA1LjQ1LC0yMi4wMTYgMTQxLjIyLC02MC41MjcgNDAuMzQsLTQ0LjkzNCA0MS4yNiwtODguMDMyIDQxLjI2LC0xNDMuOTU3IGwgMCwtMTkxLjY1MyA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40LDAgMCwxMDQuNTI3IDYzLjI2LDAgMCwxNTguNjM3IGMgMCwzMC4yNjIgMCw2MS40MzQgLTE5LjI2LDg4LjAzNSAtMjAuMTcsMjYuNTgyIC01My4xOCwzOS40MTQgLTg2LjE5LDM5LjQxNCAtMzMuOTMsMCAtNjguNzcsLTEzLjc1IC04OC45NCwtNDEuMjUgLTIxLjA5LC0yNy41IC0yMS4wOSwtNjkuNjg3IC0yMS4wOSwtMTAyLjcwNyBsIDAsLTE0Mi4xMjkgNjMuMjYsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNywwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDMyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA1MDgyLjQ4LDcwMy45NjUgYyAtMTkuMjQsNzAuNjA1IC04MS42LDExNS41NDcgLTE1NC4wNCwxMTUuNTQ3IC02Ni4wNCwwIC0xMjkuMywtNTEuMzQ4IC0xNDMuMDUsLTExNS41NDcgbCAyOTcuMDksMCB6IG0gODUuMjcsLTE0NC44ODMgYyAtMzguNTEsLTkzLjUyMyAtMTI5LjI3LC0xNTYuNzkzIC0yMzEuMDUsLTE1Ni43OTMgLTE0My4wNywwIC0yNTcuNjgsMTExLjg3MSAtMjU3LjY4LDI1NS44MzYgMCwxNDQuODgzIDEwOS4xMiwyNjEuMzI4IDI1NC45MSwyNjEuMzI4IDY3Ljg3LDAgMTM1LjcyLC0zMC4yNTggMTgzLjM5LC03OC44NjMgNDguNjIsLTUxLjM0NCA2OC43OSwtMTEzLjY5NSA2OC43OSwtMTgzLjM4MyBsIC0zLjY3LC0zOS40MzQgLTM5Ni4xMywwIGMgMTQuNjcsLTY3Ljg2MyA3Ny4wMywtMTE3LjM2MyAxNDYuNzIsLTExNy4zNjMgNDguNTksMCA5MC43NiwxOC4zMjggMTE4LjI4LDU4LjY3MiBsIDExNi40NCwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDY5MC44OTUsODUwLjcwMyA5MC43NSwwIDIyLjU0MywzMS4wMzUgMCwyNDMuMTIyIC0xMzUuODI5LDAgMCwtMjQzLjE0MSAyMi41MzYsLTMxLjAxNiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDM2IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA2MzIuMzk1LDc0Mi4yNTggMjguMDM5LDg2LjMwNCAtMjIuNTUxLDMxLjA0IC0yMzEuMjIzLDc1LjEyOCAtNDEuOTc2LC0xMjkuMTgzIDIzMS4yNTcsLTc1LjEzNyAzNi40NTQsMTEuODQ4IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzgiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDcxNy40NDksNjUzLjEwNSAtNzMuNDEsNTMuMzYgLTM2LjQ4OCwtMTEuODc1IC0xNDIuOTAzLC0xOTYuNjkyIDEwOS44ODMsLTc5LjgyOCAxNDIuOTE4LDE5Ni43MDMgMCwzOC4zMzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0MCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gODI4LjUyLDcwNi40NjUgLTczLjQyNiwtNTMuMzQgMC4wMTEsLTM4LjM1OSBMIDg5OC4wMDQsNDE4LjA3IDEwMDcuOSw0OTcuODk4IDg2NC45NzMsNjk0LjYwOSA4MjguNTIsNzA2LjQ2NSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA4MTIuMDg2LDgyOC41ODYgMjguMDU1LC04Ni4zMiAzNi40ODQsLTExLjgzNiAyMzEuMjI1LDc1LjExNyAtNDEuOTcsMTI5LjE4MyAtMjMxLjIzOSwtNzUuMTQgLTIyLjU1NSwtMzEuMDA0IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDczNi4zMDEsMTMzNS44OCBjIC0zMjMuMDQ3LDAgLTU4NS44NzUsLTI2Mi43OCAtNTg1Ljg3NSwtNTg1Ljc4MiAwLC0zMjMuMTE4IDI2Mi44MjgsLTU4NS45NzcgNTg1Ljg3NSwtNTg1Ljk3NyAzMjMuMDE5LDAgNTg1LjgwOSwyNjIuODU5IDU4NS44MDksNTg1Ljk3NyAwLDMyMy4wMDIgLTI2Mi43OSw1ODUuNzgyIC01ODUuODA5LDU4NS43ODIgbCAwLDAgeiBtIDAsLTExOC42MSBjIDI1Ny45NzIsMCA0NjcuMTg5LC0yMDkuMTMgNDY3LjE4OSwtNDY3LjE3MiAwLC0yNTguMTI5IC0yMDkuMjE3LC00NjcuMzQ4IC00NjcuMTg5LC00NjcuMzQ4IC0yNTguMDc0LDAgLTQ2Ny4yNTQsMjA5LjIxOSAtNDY3LjI1NCw0NjcuMzQ4IDAsMjU4LjA0MiAyMDkuMTgsNDY3LjE3MiA0NjcuMjU0LDQ2Ny4xNzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0NiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTA5MS4xMyw2MTkuODgzIC0xNzUuNzcxLDU3LjEyMSAxMS42MjksMzUuODA4IDE3NS43NjIsLTU3LjEyMSAtMTEuNjIsLTM1LjgwOCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQ4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA4NjYuOTU3LDkwMi4wNzQgODM2LjUsOTI0LjE5OSA5NDUuMTIxLDEwNzMuNzMgOTc1LjU4NiwxMDUxLjYxIDg2Ni45NTcsOTAyLjA3NCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDUwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA2MDcuNDY1LDkwMy40NDUgNDk4Ljg1NSwxMDUyLjk3IDUyOS4zMiwxMDc1LjEgNjM3LjkzLDkyNS41NjYgNjA3LjQ2NSw5MDMuNDQ1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDM4MC42ODgsNjIyLjEyOSAtMTEuNjI2LDM1LjgwMSAxNzUuNzU4LDU3LjA5IDExLjYyMSwtMzUuODAxIC0xNzUuNzUzLC01Ny4wOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA3MTYuMjg5LDM3Ni41OSAzNy42NDA2LDAgMCwxODQuODE2IC0zNy42NDA2LDAgMCwtMTg0LjgxNiB6IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTYiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=') no-repeat, none; -moz-background-size: 100%; -o-background-size: 100%; -webkit-background-size: 100%; background-size: 100%; display: block; float: left; width: 90px; height: 25px; }
10 | .jasmine_html-reporter .jasmine-banner .jasmine-version { margin-left: 14px; position: relative; top: 6px; }
11 | .jasmine_html-reporter #jasmine_content { position: fixed; right: 100%; }
12 | .jasmine_html-reporter .jasmine-version { color: #aaa; }
13 | .jasmine_html-reporter .jasmine-banner { margin-top: 14px; }
14 | .jasmine_html-reporter .jasmine-duration { color: #fff; float: right; line-height: 28px; padding-right: 9px; }
15 | .jasmine_html-reporter .jasmine-symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; }
16 | .jasmine_html-reporter .jasmine-symbol-summary li { display: inline-block; height: 10px; width: 14px; font-size: 16px; }
17 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed { font-size: 14px; }
18 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed:before { color: #007069; content: "\02022"; }
19 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed { line-height: 9px; }
20 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed:before { color: #ca3a11; content: "\d7"; font-weight: bold; margin-left: -1px; }
21 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-disabled { font-size: 14px; }
22 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-disabled:before { color: #bababa; content: "\02022"; }
23 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending { line-height: 17px; }
24 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending:before { color: #ba9d37; content: "*"; }
25 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty { font-size: 14px; }
26 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty:before { color: #ba9d37; content: "\02022"; }
27 | .jasmine_html-reporter .jasmine-run-options { float: right; margin-right: 5px; border: 1px solid #8a4182; color: #8a4182; position: relative; line-height: 20px; }
28 | .jasmine_html-reporter .jasmine-run-options .jasmine-trigger { cursor: pointer; padding: 8px 16px; }
29 | .jasmine_html-reporter .jasmine-run-options .jasmine-payload { position: absolute; display: none; right: -1px; border: 1px solid #8a4182; background-color: #eee; white-space: nowrap; padding: 4px 8px; }
30 | .jasmine_html-reporter .jasmine-run-options .jasmine-payload.jasmine-open { display: block; }
31 | .jasmine_html-reporter .jasmine-bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
32 | .jasmine_html-reporter .jasmine-bar.jasmine-failed { background-color: #ca3a11; }
33 | .jasmine_html-reporter .jasmine-bar.jasmine-passed { background-color: #007069; }
34 | .jasmine_html-reporter .jasmine-bar.jasmine-skipped { background-color: #bababa; }
35 | .jasmine_html-reporter .jasmine-bar.jasmine-errored { background-color: #ca3a11; }
36 | .jasmine_html-reporter .jasmine-bar.jasmine-menu { background-color: #fff; color: #aaa; }
37 | .jasmine_html-reporter .jasmine-bar.jasmine-menu a { color: #333; }
38 | .jasmine_html-reporter .jasmine-bar a { color: white; }
39 | .jasmine_html-reporter.jasmine-spec-list .jasmine-bar.jasmine-menu.jasmine-failure-list, .jasmine_html-reporter.jasmine-spec-list .jasmine-results .jasmine-failures { display: none; }
40 | .jasmine_html-reporter.jasmine-failure-list .jasmine-bar.jasmine-menu.jasmine-spec-list, .jasmine_html-reporter.jasmine-failure-list .jasmine-summary { display: none; }
41 | .jasmine_html-reporter .jasmine-results { margin-top: 14px; }
42 | .jasmine_html-reporter .jasmine-summary { margin-top: 14px; }
43 | .jasmine_html-reporter .jasmine-summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; }
44 | .jasmine_html-reporter .jasmine-summary ul.jasmine-suite { margin-top: 7px; margin-bottom: 7px; }
45 | .jasmine_html-reporter .jasmine-summary li.jasmine-passed a { color: #007069; }
46 | .jasmine_html-reporter .jasmine-summary li.jasmine-failed a { color: #ca3a11; }
47 | .jasmine_html-reporter .jasmine-summary li.jasmine-empty a { color: #ba9d37; }
48 | .jasmine_html-reporter .jasmine-summary li.jasmine-pending a { color: #ba9d37; }
49 | .jasmine_html-reporter .jasmine-summary li.jasmine-disabled a { color: #bababa; }
50 | .jasmine_html-reporter .jasmine-description + .jasmine-suite { margin-top: 0; }
51 | .jasmine_html-reporter .jasmine-suite { margin-top: 14px; }
52 | .jasmine_html-reporter .jasmine-suite a { color: #333; }
53 | .jasmine_html-reporter .jasmine-failures .jasmine-spec-detail { margin-bottom: 28px; }
54 | .jasmine_html-reporter .jasmine-failures .jasmine-spec-detail .jasmine-description { background-color: #ca3a11; }
55 | .jasmine_html-reporter .jasmine-failures .jasmine-spec-detail .jasmine-description a { color: white; }
56 | .jasmine_html-reporter .jasmine-result-message { padding-top: 14px; color: #333; white-space: pre; }
57 | .jasmine_html-reporter .jasmine-result-message span.jasmine-result { display: block; }
58 | .jasmine_html-reporter .jasmine-stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666; border: 1px solid #ddd; background: white; white-space: pre; }
59 |
--------------------------------------------------------------------------------
/jasmine/lib/jasmine-2.4.1+/jasmine_favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ItsNickBarry/hyperbolic-canvas/0a3fa5c4501e7f60e7048a6599983f8a310a51c7/jasmine/lib/jasmine-2.4.1+/jasmine_favicon.png
--------------------------------------------------------------------------------
/jasmine/spec/AngleSpec.js:
--------------------------------------------------------------------------------
1 | describe('Angle', function () {
2 | var Angle = HyperbolicCanvas.Angle;
3 |
4 | it('converts from degrees to radians', function () {
5 | var degrees = Math.random() * 360;
6 | var radians = Angle.fromDegrees(degrees);
7 | expect(radians).toBeARealNumber();
8 | expect(radians).toBe(Angle.normalize(radians));
9 | expect(radians / Math.TAU).toApproximate(degrees / 360);
10 | });
11 |
12 | it('finds the angle of a slope', function () {
13 | var slope = (Math.random() - 0.5) * 100;
14 | expect(Angle.fromSlope(slope)).toApproximate(Math.atan(slope));
15 | });
16 |
17 | it('normalizes angles to within 0 and TAU', function () {
18 | var angle = Angle.normalize((Math.random() - 0.5) * 100);
19 | expect(angle).toBeGreaterThan(0);
20 | expect(angle).toBeLessThan(Math.TAU);
21 | });
22 |
23 | it('generates a random angle in a given quadrant', function () {
24 | [1, 2, 3, 4].forEach(function (q) {
25 | var angle = Angle.random(q);
26 | expect(angle).toBeGreaterThan((Math.PI / 2) * (q - 1));
27 | expect(angle).toBeLessThan((Math.PI / 2) * q);
28 | });
29 | });
30 |
31 | describe('generated at random', function () {
32 | var angle;
33 | beforeEach(function () {
34 | angle = Angle.random();
35 | });
36 |
37 | it('is between 0 and tau', function () {
38 | expect(angle).toBeARealNumber();
39 | expect(angle).toBeGreaterThan(0);
40 | expect(angle).toBeLessThan(Math.TAU);
41 | });
42 |
43 | it('converts to degrees', function () {
44 | expect(Angle.toDegrees(angle)).toApproximate((angle * 360) / Math.TAU);
45 | });
46 |
47 | it('has opposite angle', function () {
48 | var opposite = Angle.opposite(angle);
49 | expect(opposite).toApproximate(
50 | angle < Math.PI ? angle + Math.PI : angle - Math.PI,
51 | );
52 | expect(opposite).toBe(Angle.normalize(opposite));
53 | });
54 |
55 | it('has equivalent slope', function () {
56 | var slope = Angle.toSlope(angle);
57 | expect(slope).toBeARealNumber();
58 | expect(slope).toApproximate(Math.tan(angle));
59 | });
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/jasmine/spec/CanvasSpec.js:
--------------------------------------------------------------------------------
1 | describe('Canvas', function () {
2 | var Canvas = HyperbolicCanvas.Canvas;
3 | var canvas;
4 |
5 | beforeEach(function () {
6 | canvas = new Canvas({ el: document.createElement('div') });
7 | // fake the size
8 | var radius = 100;
9 | canvas._radius = radius;
10 | canvas._diameter = radius * 2;
11 | });
12 |
13 | describe('container element', function () {
14 | var el;
15 | beforeEach(function () {
16 | el = canvas.getContainerElement();
17 | });
18 |
19 | it(
20 | 'is defined',
21 | function () {
22 | expect(el).toBeA(HTMLElement);
23 | },
24 | true,
25 | );
26 |
27 | it(
28 | 'has one child div element',
29 | function () {
30 | expect(el.children.length).toBe(1);
31 | expect(el.firstChild).toBeA(HTMLDivElement);
32 | },
33 | true,
34 | );
35 | });
36 |
37 | describe('backdrop element', function () {
38 | var el;
39 | beforeEach(function () {
40 | el = canvas.getBackdropElement();
41 | });
42 |
43 | it(
44 | 'is div element',
45 | function () {
46 | expect(el).toBeA(HTMLDivElement);
47 | },
48 | true,
49 | );
50 |
51 | it(
52 | 'has one child div element',
53 | function () {
54 | expect(el.children.length).toBe(1);
55 | expect(el.firstChild).toBeA(HTMLDivElement);
56 | },
57 | true,
58 | );
59 |
60 | it(
61 | 'has "backdrop" class',
62 | function () {
63 | expect(el.className).toBe('backdrop');
64 | },
65 | true,
66 | );
67 | });
68 |
69 | describe('underlay element', function () {
70 | var el;
71 | beforeEach(function () {
72 | el = canvas.getUnderlayElement();
73 | });
74 |
75 | it(
76 | 'is div element',
77 | function () {
78 | expect(el).toBeA(HTMLDivElement);
79 | },
80 | true,
81 | );
82 |
83 | it(
84 | 'has "underlay" class',
85 | function () {
86 | expect(el.className).toBe('underlay');
87 | },
88 | true,
89 | );
90 |
91 | it(
92 | 'has border-radius style',
93 | function () {
94 | expect(el.style['border-radius']).not.toBe('');
95 | },
96 | true,
97 | );
98 |
99 | it(
100 | 'has one child canvas element',
101 | function () {
102 | expect(el.children.length).toBe(1);
103 | expect(el.firstChild).toBeA(HTMLCanvasElement);
104 | },
105 | true,
106 | );
107 | });
108 |
109 | describe('canvas element', function () {
110 | var el;
111 | beforeEach(function () {
112 | el = canvas.getCanvasElement();
113 | });
114 |
115 | it(
116 | 'is canvas element',
117 | function () {
118 | expect(el).toBeA(HTMLCanvasElement);
119 | },
120 | true,
121 | );
122 |
123 | it(
124 | 'has "hyperbolic" class',
125 | function () {
126 | expect(el.className).toBe('hyperbolic');
127 | },
128 | true,
129 | );
130 |
131 | it(
132 | 'has absolute position style',
133 | function () {
134 | expect(el.style['position']).toBe('absolute');
135 | },
136 | true,
137 | );
138 | });
139 |
140 | it(
141 | 'has a radius and diameter',
142 | function () {
143 | expect(canvas.getRadius()).toBeARealNumber();
144 | expect(canvas.getDiameter()).toBeARealNumber();
145 | expect(canvas.getDiameter()).toBe(canvas.getRadius() * 2);
146 | },
147 | true,
148 | );
149 |
150 | it(
151 | 'has a canvas context',
152 | function () {
153 | expect(canvas.getContext()).toBeA(CanvasRenderingContext2D);
154 | },
155 | true,
156 | );
157 |
158 | it(
159 | 'sets multiple context properties',
160 | function () {
161 | var ctx = canvas.getContext();
162 | var properties = {
163 | lineJoin: 'round',
164 | lineWidth: 2,
165 | shadowBlur: 20,
166 | shadowColor: '#ffffff',
167 | strokeStyle: '#dd4814',
168 | fillStyle: '#333333',
169 | };
170 | canvas.setContextProperties(properties);
171 |
172 | for (var property in properties) {
173 | expect(ctx[property]).toBe(properties[property]);
174 | }
175 | },
176 | true,
177 | );
178 |
179 | it(
180 | 'sets single context property',
181 | function () {
182 | var ctx = canvas.getContext();
183 | var properties = {
184 | lineJoin: 'round',
185 | lineWidth: 2,
186 | shadowBlur: 20,
187 | shadowColor: '#ffffff',
188 | strokeStyle: '#dd4814',
189 | fillStyle: '#333333',
190 | };
191 | for (var property in properties) {
192 | canvas.setContextProperty(property, properties[property]);
193 | expect(ctx[property]).toBe(properties[property]);
194 | }
195 | },
196 | true,
197 | );
198 |
199 | describe('when converting canvas coordinates to a Point', function () {
200 | it('returns a Point', function () {
201 | var coordinates = [
202 | canvas.getRadius() * Math.random(),
203 | canvas.getRadius() * Math.random(),
204 | ];
205 | var point = canvas.at(coordinates);
206 | expect(point).toBeA(HyperbolicCanvas.Point);
207 | });
208 | });
209 |
210 | describe('when converting a Point to canvas coordinates', function () {
211 | it('returns an Array with length of 2', function () {
212 | var point = HyperbolicCanvas.Point.random();
213 | var coordinates = canvas.at(point);
214 | expect(coordinates).toBeA(Array);
215 | expect(coordinates.length).toBe(2);
216 | coordinates.forEach(function (n) {
217 | expect(n).toBeGreaterThan(0);
218 | expect(n).toBeLessThan(canvas.getDiameter());
219 | });
220 | });
221 | });
222 |
223 | describe('when generating path', function () {
224 | var object;
225 | beforeEach(function () {
226 | object = HyperbolicCanvas.Line.givenTwoPoints(
227 | HyperbolicCanvas.Point.random(),
228 | HyperbolicCanvas.Point.random(),
229 | );
230 | });
231 |
232 | it(
233 | 'returns CanvasRenderingContext2D by default',
234 | function () {
235 | expect(canvas.pathForEuclidean(object)).toBeA(CanvasRenderingContext2D);
236 | expect(canvas.pathForHyperbolic(object)).toBeA(
237 | CanvasRenderingContext2D,
238 | );
239 | },
240 | true,
241 | );
242 |
243 | describe('with Path2D available to current browser', function () {
244 | it(
245 | 'returns input if given',
246 | function () {
247 | var options = { path2D: true, path: new Path2D() };
248 | expect(canvas.pathForEuclidean(object, options)).toBe(options.path);
249 | expect(canvas.pathForHyperbolic(object, options)).toBe(options.path);
250 | },
251 | true,
252 | );
253 |
254 | it(
255 | 'returns Path2D if requested',
256 | function () {
257 | var options = { path2D: true, path: false };
258 | expect(canvas.pathForEuclidean(object, options)).toBeA(Path2D);
259 | expect(canvas.pathForHyperbolic(object, options)).toBeA(Path2D);
260 | },
261 | true,
262 | );
263 | });
264 |
265 | describe('with Path2D unavailable to current browser', function () {
266 | var _Path2D;
267 | beforeAll(function () {
268 | _Path2D = window.Path2D;
269 | window.Path2D = undefined;
270 | });
271 |
272 | afterAll(function () {
273 | window.Path2D = _Path2D;
274 | });
275 |
276 | it(
277 | 'returns input if given',
278 | function () {
279 | var options = { path2D: false, path: canvas.getContext() };
280 | expect(canvas.pathForEuclidean(object, options)).toBe(options.path);
281 | expect(canvas.pathForHyperbolic(object, options)).toBe(options.path);
282 | },
283 | true,
284 | );
285 |
286 | it(
287 | 'returns CanvasRenderingContext2D if Path2D is requested',
288 | function () {
289 | var options = { path2D: true, path: false };
290 | expect(canvas.pathForEuclidean(object, options)).toBeA(
291 | CanvasRenderingContext2D,
292 | );
293 | expect(canvas.pathForHyperbolic(object, options)).toBeA(
294 | CanvasRenderingContext2D,
295 | );
296 | },
297 | true,
298 | );
299 | });
300 | });
301 | });
302 |
--------------------------------------------------------------------------------
/jasmine/spec/HyperbolicCanvasSpec.js:
--------------------------------------------------------------------------------
1 | describe('HyperbolicCanvas', function () {
2 | it(
3 | 'defines the natural and just constant tau',
4 | function () {
5 | expect(Math.TAU).toBe(Math.PI * 2);
6 | },
7 | true,
8 | );
9 |
10 | it(
11 | 'defines the threshold for effective zero values',
12 | function () {
13 | expect(HyperbolicCanvas.ZERO).toBeA(Number);
14 | },
15 | true,
16 | );
17 |
18 | it(
19 | 'defines the threshold for effective infinity values',
20 | function () {
21 | expect(HyperbolicCanvas.INFINITY).toBeA(Number);
22 | },
23 | true,
24 | );
25 |
26 | it(
27 | 'creates a Canvas',
28 | function () {
29 | expect(HyperbolicCanvas.create()).toBeA(HyperbolicCanvas.Canvas);
30 | },
31 | true,
32 | );
33 | });
34 |
--------------------------------------------------------------------------------
/jasmine/spec/JasmineSpec.js:
--------------------------------------------------------------------------------
1 | describe('Jasmine', function () {
2 | var randomNumbers = {};
3 | var n;
4 | var testRunCount = Math.floor(Math.random() * 6) + 5;
5 | beforeEach(function () {
6 | n = Math.random();
7 | });
8 |
9 | it(
10 | 'has defined random seed on the Math object',
11 | function () {
12 | expect(Math.seed).toBeDefined();
13 | },
14 | true,
15 | );
16 |
17 | it(
18 | 'has displaySymbols property',
19 | function () {
20 | expect(jasmine.displaySymbols).toBeA(Boolean);
21 | },
22 | true,
23 | );
24 |
25 | it(
26 | 'has testRunCount property',
27 | function () {
28 | expect(jasmine.runCount).toBeARealNumber();
29 | },
30 | true,
31 | );
32 |
33 | it(
34 | 'generates multiple unique numbers for a single spec',
35 | function () {
36 | expect(randomNumbers[n]).toBeUndefined();
37 | randomNumbers[n] = n;
38 | },
39 | testRunCount,
40 | );
41 |
42 | it(
43 | 'has generated ' + testRunCount + ' numbers with previous spec',
44 | function () {
45 | expect(Object.keys(randomNumbers).length).toBe(testRunCount);
46 | },
47 | true,
48 | );
49 | });
50 |
--------------------------------------------------------------------------------
/jasmine/spec/PointSpec.js:
--------------------------------------------------------------------------------
1 | describe('Point', function () {
2 | var Point = HyperbolicCanvas.Point;
3 | var point;
4 |
5 | describe('generated at random on hyperbolic plane', function () {
6 | var quadrant;
7 | beforeEach(function () {
8 | quadrant = Math.floor(Math.random() * 4) + 1;
9 | point = Point.random(quadrant);
10 | });
11 |
12 | it('is on plane', function () {
13 | expect(point.isOnPlane()).toBe(true);
14 | expect(point.getEuclideanRadius()).toBeLessThan(1);
15 | });
16 |
17 | it('is not ideal', function () {
18 | expect(point.isIdeal()).toBe(false);
19 | });
20 |
21 | it('has Cartesian coordinates, angle, and Euclidean and hyperbolic radii', function () {
22 | expect(point.getX()).toBeARealNumber();
23 | expect(point.getY()).toBeARealNumber();
24 | expect(point.getAngle()).toBeARealNumber();
25 | expect(point.getEuclideanRadius()).toBeARealNumber();
26 | expect(point.getHyperbolicRadius()).toBeARealNumber();
27 | });
28 |
29 | it('is equal to identical Point', function () {
30 | var otherPoint = Point.givenCoordinates(point.getX(), point.getY());
31 | expect(point.equals(otherPoint)).toBe(true);
32 | });
33 |
34 | it('is clonable', function () {
35 | var clone = point.clone();
36 | expect(clone).toBeA(Point);
37 | expect(clone).not.toBe(point);
38 | expect(point.equals(clone)).toBe(true);
39 |
40 | expect(clone.getX()).toApproximate(point.getX());
41 | expect(clone.getY()).toApproximate(point.getY());
42 | expect(clone.getAngle()).toApproximate(point.getAngle());
43 | expect(clone.getEuclideanRadius()).toApproximate(
44 | point.getEuclideanRadius(),
45 | );
46 | expect(clone.getHyperbolicRadius()).toApproximate(
47 | point.getHyperbolicRadius(),
48 | );
49 | });
50 |
51 | it('calculates Cartesian quadrant', function () {
52 | expect(point.quadrant()).toBe(quadrant);
53 | });
54 |
55 | it('has opposite Point', function () {
56 | var opposite = point.opposite();
57 | expect(opposite.getEuclideanRadius()).toApproximate(
58 | point.getEuclideanRadius(),
59 | );
60 | expect(opposite.getAngle()).toApproximate(
61 | HyperbolicCanvas.Angle.opposite(point.getAngle()),
62 | );
63 | });
64 |
65 | it('has Point rotated about origin', function () {
66 | var angle = HyperbolicCanvas.Angle.random();
67 | var rotatedPoint = point.rotateAboutOrigin(angle);
68 | expect(rotatedPoint).toBeA(Point);
69 | expect(rotatedPoint.getEuclideanRadius()).toApproximate(
70 | point.getEuclideanRadius(),
71 | );
72 | expect(rotatedPoint.getAngle()).toApproximate(
73 | HyperbolicCanvas.Angle.normalize(point.getAngle() + angle),
74 | );
75 | var rotatedBackPoint = rotatedPoint.rotateAboutOrigin(angle * -1);
76 | expect(rotatedBackPoint.equals(point)).toBe(true);
77 | });
78 |
79 | describe('relative to other Point', function () {
80 | var otherPoint;
81 | beforeEach(function () {
82 | otherPoint = Point.random();
83 | });
84 |
85 | it('calculates Euclidean distance to other Point', function () {
86 | var d = point.euclideanDistanceTo(otherPoint);
87 | expect(d).toBeARealNumber();
88 | });
89 |
90 | it('calculates Euclidean angle towards and away from other Point', function () {
91 | var angleTo = point.euclideanAngleTo(otherPoint);
92 | var angleFrom = point.euclideanAngleFrom(otherPoint);
93 | expect(angleTo).toBeARealNumber();
94 | expect(angleFrom).toApproximate(
95 | HyperbolicCanvas.Angle.opposite(angleTo),
96 | );
97 | });
98 |
99 | it('calculates hyperbolic distance to other Point', function () {
100 | var d = point.hyperbolicDistanceTo(otherPoint);
101 | expect(d).toBeARealNumber();
102 | });
103 |
104 | it('calculates hyperbolic angle towards and away from other Point', function () {
105 | var angleTo = point.hyperbolicAngleTo(otherPoint);
106 | var angleFrom = point.hyperbolicAngleFrom(otherPoint);
107 | expect(angleTo).toBeARealNumber();
108 | expect(angleFrom).toBeARealNumber();
109 | });
110 | });
111 |
112 | describe('when calculating Euclidean distant Point', function () {
113 | var distance, direction, distantPoint;
114 | beforeEach(function () {
115 | distance = Math.random();
116 | direction = HyperbolicCanvas.Angle.random();
117 | distantPoint = point.euclideanDistantPoint(distance, direction);
118 | });
119 |
120 | it('calculates location of distant point along Euclidean geodesic', function () {
121 | expect(distantPoint).toBeA(Point);
122 | });
123 |
124 | it('stores angle of travel', function () {
125 | expect(distantPoint.getDirection()).toBe(direction);
126 | });
127 |
128 | it('is reversible', function () {
129 | expect(
130 | distantPoint
131 | .euclideanDistantPoint(
132 | distance,
133 | HyperbolicCanvas.Angle.opposite(direction),
134 | )
135 | .equals(point),
136 | ).toBe(true);
137 | });
138 | });
139 |
140 | describe('when calculating hyperbolic distant Point', function () {
141 | var distance, direction;
142 | describe('in general', function () {
143 | beforeEach(function () {
144 | distance = Math.random();
145 | direction = HyperbolicCanvas.Angle.random();
146 | });
147 |
148 | it('calculates location of distant point along hyperbolic geodesic', function () {
149 | var distantPoint = point.hyperbolicDistantPoint(distance, direction);
150 | expect(distantPoint).toBeA(Point);
151 | expect(distantPoint.isOnPlane()).toBe(true);
152 |
153 | expect(point.hyperbolicDistanceTo(distantPoint)).toApproximate(
154 | distance,
155 | );
156 | });
157 |
158 | it('stores instantaneous angle of travel at destination on distant Point', function () {
159 | var distantPoint = point.hyperbolicDistantPoint(distance, direction);
160 | expect(distantPoint.getDirection()).toBeARealNumber();
161 | });
162 |
163 | it('calculates accurate distant Point regardless of number of intermediate steps', function () {
164 | var distantPoint0 = point.hyperbolicDistantPoint(
165 | distance * 3,
166 | direction,
167 | );
168 | var distantPoint1 = point
169 | .hyperbolicDistantPoint(distance, direction)
170 | .hyperbolicDistantPoint(distance)
171 | .hyperbolicDistantPoint(distance);
172 | expect(distantPoint0.equals(distantPoint1)).toBe(true);
173 | });
174 |
175 | it('is reversible', function () {
176 | var distantPoint0 = point.hyperbolicDistantPoint(distance, direction);
177 | var distantPoint1 = distantPoint0.hyperbolicDistantPoint(
178 | distance,
179 | HyperbolicCanvas.Angle.opposite(distantPoint0.getDirection()),
180 | );
181 | expect(point.equals(distantPoint1)).toBe(true);
182 | });
183 | });
184 |
185 | describe('along diameter of hyperbolic plane', function () {
186 | describe('away from origin', function () {
187 | it('calculates Point away from origin', function () {
188 | distance = Math.random();
189 | direction = point.getAngle();
190 | var distantPoint = point.hyperbolicDistantPoint(
191 | distance,
192 | direction,
193 | );
194 | expect(distantPoint.getAngle()).toBe(direction);
195 | expect(distantPoint.getDirection()).toBe(direction);
196 | expect(distantPoint.quadrant()).toBe(point.quadrant());
197 |
198 | expect(distantPoint.getHyperbolicRadius()).toBe(
199 | point.getHyperbolicRadius() + distance,
200 | );
201 | });
202 | });
203 |
204 | describe('towards origin', function () {
205 | beforeEach(function () {
206 | direction = HyperbolicCanvas.Angle.opposite(point.getAngle());
207 | });
208 |
209 | it('calculates Point towards but not across origin', function () {
210 | distance = Math.random() * point.getHyperbolicRadius();
211 | var distantPoint = point.hyperbolicDistantPoint(
212 | distance,
213 | direction,
214 | );
215 | expect(distantPoint.getAngle()).toBe(point.getAngle());
216 | expect(distantPoint.quadrant()).toBe(point.quadrant());
217 | expect(distantPoint.getDirection()).toBe(direction);
218 |
219 | expect(distantPoint.getHyperbolicRadius()).toBe(
220 | point.getHyperbolicRadius() - distance,
221 | );
222 | });
223 |
224 | it('calculates Point towards and across origin', function () {
225 | distance = (Math.random() + 1) * point.getHyperbolicRadius();
226 | var distantPoint = point.hyperbolicDistantPoint(
227 | distance,
228 | direction,
229 | );
230 | expect(distantPoint.getAngle()).toBe(direction);
231 | expect(distantPoint.quadrant() % 4).toBe(
232 | (point.quadrant() + 2) % 4,
233 | );
234 | expect(distantPoint.getDirection()).toBe(direction);
235 |
236 | expect(distantPoint.getHyperbolicRadius()).toBe(
237 | distance - point.getHyperbolicRadius(),
238 | );
239 | });
240 |
241 | it('calculates origin', function () {
242 | distance = point.getHyperbolicRadius();
243 | var distantPoint = point.hyperbolicDistantPoint(
244 | distance,
245 | direction,
246 | );
247 | expect(distantPoint.getX()).toBe(0);
248 | expect(distantPoint.getY()).toBe(0);
249 | expect(distantPoint.getEuclideanRadius()).toBe(0);
250 | expect(distantPoint.getHyperbolicRadius()).toBe(0);
251 | expect(distantPoint.getDirection()).toBe(direction);
252 | });
253 | });
254 | });
255 | });
256 |
257 | describe('when comparing to distant Point', function () {
258 | var distance, direction, distantPoint;
259 | beforeEach(function () {
260 | distance = Math.random();
261 | direction = HyperbolicCanvas.Angle.random();
262 | distantPoint = point.hyperbolicDistantPoint(distance, direction);
263 | });
264 |
265 | it('calculates angle of hyperbolic geodesic towards self from perspective of other Point', function () {
266 | expect(point.hyperbolicAngleFrom(distantPoint)).toApproximate(
267 | HyperbolicCanvas.Angle.opposite(distantPoint.getDirection()),
268 | );
269 | });
270 |
271 | it('calculates angle of hyperbolic geodesic towards other Point from perspective of self', function () {
272 | expect(point.hyperbolicAngleTo(distantPoint)).toApproximate(direction);
273 | });
274 | });
275 | });
276 |
277 | describe('between two other Points along Euclidean geodesic', function () {
278 | var p0, p1;
279 | beforeEach(function () {
280 | p0 = Point.random();
281 | p1 = Point.random();
282 | point = Point.euclideanBetween(p0, p1);
283 | });
284 |
285 | it('has mean Cartesian coordinates', function () {
286 | expect(point.getX()).toBe((p0.getX() + p1.getX()) / 2);
287 | expect(point.getY()).toBe((p0.getY() + p1.getY()) / 2);
288 | });
289 | });
290 |
291 | describe('between two other points along hyperbolic geodesic', function () {
292 | var p0, p1;
293 | beforeEach(function () {
294 | p0 = Point.random();
295 | p1 = Point.random();
296 | point = Point.hyperbolicBetween(p0, p1);
297 | });
298 |
299 | it('is equidistant to other points at half total distance', function () {
300 | var d = p0.hyperbolicDistanceTo(p1);
301 | var d0 = point.hyperbolicDistanceTo(p0);
302 | var d1 = point.hyperbolicDistanceTo(p1);
303 | expect(d0).toApproximate(d1);
304 | expect(d0 + d1).toApproximate(d);
305 | });
306 | });
307 |
308 | describe('given Cartesian coordinates', function () {
309 | beforeEach(function () {
310 | var angle = HyperbolicCanvas.Angle.random();
311 | var radius = Math.random();
312 | point = Point.givenCoordinates(
313 | Math.cos(angle) * radius,
314 | Math.sin(angle) * radius,
315 | );
316 | });
317 |
318 | it('has Cartesian coordinates', function () {
319 | expect(point.getX()).toBeARealNumber();
320 | expect(point.getY()).toBeARealNumber();
321 | });
322 |
323 | it('has angle', function () {
324 | expect(point.getAngle()).toBeARealNumber();
325 | });
326 |
327 | it('has Euclidean radius', function () {
328 | expect(point.getEuclideanRadius()).toBeARealNumber();
329 | });
330 |
331 | it('has hyperbolic radius', function () {
332 | expect(point.getHyperbolicRadius()).toBeARealNumber();
333 | });
334 |
335 | it('is not ideal', function () {
336 | expect(point.isIdeal()).toBe(false);
337 | });
338 | });
339 |
340 | describe('given Euclidean polar coordinates', function () {
341 | describe('in general', function () {
342 | beforeEach(function () {
343 | point = Point.givenEuclideanPolarCoordinates(
344 | Math.random() + 0.5,
345 | HyperbolicCanvas.Angle.random(),
346 | );
347 | });
348 |
349 | it('has angle and Euclidean radius', function () {
350 | expect(point.getAngle()).toBeARealNumber();
351 | expect(point.getEuclideanRadius()).toBeARealNumber();
352 | });
353 |
354 | it('has Cartesian coordinates', function () {
355 | expect(point.getX()).toBeARealNumber();
356 | expect(point.getY()).toBeARealNumber();
357 | });
358 |
359 | it('equals Point defined by opposite angle and negative radius', function () {
360 | var otherPoint = Point.givenEuclideanPolarCoordinates(
361 | point.getEuclideanRadius() * -1,
362 | HyperbolicCanvas.Angle.opposite(point.getAngle()),
363 | );
364 | expect(point.equals(otherPoint)).toBe(true);
365 | });
366 | });
367 |
368 | describe('with radius >= 1', function () {
369 | beforeEach(function () {
370 | point = Point.givenEuclideanPolarCoordinates(
371 | Math.random() + 1,
372 | HyperbolicCanvas.Angle.random(),
373 | );
374 | });
375 |
376 | it('does not have hyperbolic radius', function () {
377 | expect(point.getHyperbolicRadius()).toBeNaN();
378 | });
379 |
380 | it('is not on hyperbolic plane', function () {
381 | expect(point.isOnPlane()).toBe(false);
382 | });
383 | });
384 |
385 | describe('with 0 <= radius < 1', function () {
386 | beforeEach(function () {
387 | point = Point.givenEuclideanPolarCoordinates(
388 | Math.random(),
389 | HyperbolicCanvas.Angle.random(),
390 | );
391 | });
392 |
393 | it('has hyperbolic radius', function () {
394 | expect(point.getHyperbolicRadius()).toBeARealNumber();
395 | });
396 |
397 | it('is on hyperbolic plane', function () {
398 | expect(point.isOnPlane()).toBe(true);
399 | });
400 | });
401 | });
402 |
403 | describe('given hyperbolic polar coordinates', function () {
404 | beforeEach(function () {
405 | point = Point.givenHyperbolicPolarCoordinates(
406 | Math.random() * 10,
407 | HyperbolicCanvas.Angle.random(),
408 | );
409 | });
410 |
411 | it('has angle and hyperbolic radius', function () {
412 | expect(point.getAngle()).toBeARealNumber();
413 | expect(point.getHyperbolicRadius()).toBeARealNumber();
414 | });
415 |
416 | it('has Euclidean radius', function () {
417 | expect(point.getEuclideanRadius()).toBeARealNumber();
418 | expect(point.getEuclideanRadius()).toBeLessThan(1);
419 | });
420 |
421 | it('has Cartesian coordinates', function () {
422 | expect(point.getX()).toBeARealNumber();
423 | expect(point.getY()).toBeARealNumber();
424 | });
425 |
426 | it('is on hyperbolic plane', function () {
427 | expect(point.isOnPlane()).toBe(true);
428 | });
429 |
430 | it('equals Point defined by opposite angle and negative radius', function () {
431 | var otherPoint = Point.givenHyperbolicPolarCoordinates(
432 | point.getHyperbolicRadius() * -1,
433 | HyperbolicCanvas.Angle.opposite(point.getAngle()),
434 | );
435 | expect(point.equals(otherPoint)).toBe(true);
436 | });
437 | });
438 |
439 | describe('given ideal angle', function () {
440 | beforeEach(function () {
441 | point = Point.givenIdealAngle(HyperbolicCanvas.Angle.random());
442 | });
443 |
444 | it('is Point', function () {
445 | expect(point).toBeA(Point);
446 | });
447 |
448 | it('is not on plane', function () {
449 | expect(point.isOnPlane()).toBe(false);
450 | });
451 |
452 | it('is ideal', function () {
453 | expect(point.isIdeal()).toBe(true);
454 | });
455 |
456 | it('has Euclidean radius of 1', function () {
457 | expect(point.getEuclideanRadius()).toApproximate(1);
458 | });
459 | });
460 |
461 | describe('ORIGIN', function () {
462 | beforeEach(function () {
463 | point = Point.ORIGIN;
464 | });
465 |
466 | it(
467 | 'is Point',
468 | function () {
469 | expect(point).toBeA(Point);
470 | },
471 | true,
472 | );
473 |
474 | it(
475 | 'has Cartesian coordinates (0, 0)',
476 | function () {
477 | expect(point.getX()).toBe(0);
478 | expect(point.getY()).toBe(0);
479 | },
480 | true,
481 | );
482 |
483 | it(
484 | 'has Euclidean and hyperbolic radii of 0',
485 | function () {
486 | expect(point.getEuclideanRadius()).toBe(0);
487 | expect(point.getHyperbolicRadius()).toBe(0);
488 | },
489 | true,
490 | );
491 |
492 | it(
493 | 'has angle of 0',
494 | function () {
495 | expect(point.getAngle()).toBe(0);
496 | },
497 | true,
498 | );
499 |
500 | it(
501 | 'is on hyperbolic plane',
502 | function () {
503 | expect(point.isOnPlane()).toBe(true);
504 | },
505 | true,
506 | );
507 | });
508 | });
509 |
--------------------------------------------------------------------------------
/jasmine/spec/PolygonSpec.js:
--------------------------------------------------------------------------------
1 | describe('Polygon', function () {
2 | var Polygon = HyperbolicCanvas.Polygon;
3 | var polygon;
4 |
5 | describe('given n vertices', function () {
6 | var vertices, n;
7 | beforeEach(function () {
8 | n = Math.floor(Math.random() * 10) + 3;
9 | vertices = [];
10 | for (var i = 0; i < n; i++) {
11 | vertices.push(HyperbolicCanvas.Point.random());
12 | }
13 | polygon = Polygon.givenVertices(vertices);
14 | });
15 |
16 | it('has n vertices of type Point', function () {
17 | var vertices = polygon.getVertices();
18 | expect(vertices).toBeA(Array);
19 | expect(vertices.length).toBe(n);
20 | vertices.forEach(function (vertex) {
21 | expect(vertex).toBeA(HyperbolicCanvas.Point);
22 | });
23 | });
24 |
25 | it('has n lines of type Line', function () {
26 | var lines = polygon.getLines();
27 | expect(lines.length).toBe(n);
28 | expect(lines).toBeA(Array);
29 | lines.forEach(function (line) {
30 | expect(line).toBeA(HyperbolicCanvas.Line);
31 | });
32 | });
33 | });
34 |
35 | describe('given n angles of ideal points', function () {
36 | var n;
37 | beforeEach(function () {
38 | n = Math.floor(Math.random() * 10) + 3;
39 | var baseAngles = [];
40 | var total = 0;
41 | for (var i = 0; i < n; i++) {
42 | var angle = HyperbolicCanvas.Angle.random();
43 | baseAngles.push(angle);
44 | total += angle;
45 | }
46 | var angles = [];
47 | var currentAngle = 0;
48 | for (var i = 0; i < baseAngles.length; i++) {
49 | var angle = (baseAngles[i] * Math.TAU) / total;
50 | angles.push((currentAngle += angle));
51 | }
52 | polygon = Polygon.givenAnglesOfIdealVertices(angles);
53 | });
54 |
55 | it('has n lines of infinite hyperbolic length', function () {
56 | var lines = polygon.getLines();
57 | expect(lines).toBeA(Array);
58 | lines.forEach(function (line) {
59 | expect(line).toBeA(HyperbolicCanvas.Line);
60 | expect(line.getHyperbolicLength()).toBe(Infinity);
61 | });
62 | });
63 | });
64 |
65 | describe('given side count, center, radius', function () {
66 | var n, center, radius, rotation;
67 | beforeEach(function () {
68 | n = Math.floor(Math.random() * 10) + 3;
69 | center = HyperbolicCanvas.Point.random();
70 | rotation = HyperbolicCanvas.Angle.random();
71 | });
72 |
73 | describe('in Euclidean context', function () {
74 | beforeEach(function () {
75 | radius = Math.random();
76 | polygon = Polygon.givenEuclideanNCenterRadius(
77 | n,
78 | center,
79 | radius,
80 | rotation,
81 | );
82 | });
83 |
84 | it('has n vertices of type Point', function () {
85 | var vertices = polygon.getVertices();
86 | expect(vertices).toBeA(Array);
87 | expect(vertices.length).toBe(n);
88 | vertices.forEach(function (vertex) {
89 | expect(vertex).toBeA(HyperbolicCanvas.Point);
90 | });
91 | });
92 |
93 | it('has first vertex at given rotation angle', function () {
94 | expect(
95 | polygon.getVertices()[0].euclideanAngleFrom(center),
96 | ).toApproximate(rotation);
97 | });
98 |
99 | it('has n lines of type Line', function () {
100 | var lines = polygon.getLines();
101 | expect(lines.length).toBe(n);
102 | expect(lines).toBeA(Array);
103 | lines.forEach(function (line) {
104 | expect(line).toBeA(HyperbolicCanvas.Line);
105 | });
106 | });
107 |
108 | it('has lines of equal Euclidean length', function () {
109 | var lengths = [];
110 | polygon.getLines().forEach(function (line) {
111 | lengths.push(line.getEuclideanLength());
112 | });
113 | var n = lengths.length;
114 | for (var i = 0; i < n; i++) {
115 | expect(lengths[i]).toApproximate(lengths[(i + 1) % n]);
116 | }
117 | });
118 | });
119 |
120 | describe('in hyperbolic context', function () {
121 | beforeEach(function () {
122 | radius = Math.random() * 10;
123 | polygon = Polygon.givenHyperbolicNCenterRadius(
124 | n,
125 | center,
126 | radius,
127 | rotation,
128 | );
129 | });
130 |
131 | it('has n vertices of type Point', function () {
132 | var vertices = polygon.getVertices();
133 | expect(vertices).toBeA(Array);
134 | expect(vertices.length).toBe(n);
135 | vertices.forEach(function (vertex) {
136 | expect(vertex).toBeA(HyperbolicCanvas.Point);
137 | });
138 | });
139 |
140 | it('has first vertex at given rotation angle', function () {
141 | expect(
142 | polygon.getVertices()[0].hyperbolicAngleFrom(center),
143 | ).toApproximate(rotation);
144 | });
145 |
146 | it('has n lines of type Line', function () {
147 | var lines = polygon.getLines();
148 | expect(lines.length).toBe(n);
149 | expect(lines).toBeA(Array);
150 | lines.forEach(function (line) {
151 | expect(line).toBeA(HyperbolicCanvas.Line);
152 | });
153 | });
154 |
155 | it('has lines of equal hyperbolic length', function () {
156 | var lengths = [];
157 | polygon.getLines().forEach(function (line) {
158 | lengths.push(line.getHyperbolicLength());
159 | });
160 | var n = lengths.length;
161 | for (var i = 0; i < n; i++) {
162 | expect(lengths[i]).toApproximate(lengths[(i + 1) % n]);
163 | }
164 | });
165 | });
166 | });
167 | });
168 |
--------------------------------------------------------------------------------
/jasmine/spec/SpecHelper.js:
--------------------------------------------------------------------------------
1 | // display visualization of specs at top of page or not
2 | jasmine.displaySymbols = jasmine.runCount <= 10;
3 |
4 | beforeEach(function () {
5 | jasmine.addMatchers({
6 | toApproximate: function () {
7 | return {
8 | compare: function (actual, expected) {
9 | return {
10 | pass:
11 | actual === expected ||
12 | Math.abs(actual - expected) < HyperbolicCanvas.ZERO ||
13 | (isNaN(actual) && isNaN(expected)),
14 | };
15 | },
16 | };
17 | },
18 | toBeA: function () {
19 | return {
20 | compare: function (actual, expected) {
21 | return {
22 | pass:
23 | actual instanceof Object
24 | ? actual instanceof expected
25 | : actual.__proto__ === expected.prototype,
26 | };
27 | },
28 | };
29 | },
30 | toBeARealNumber: function () {
31 | return {
32 | compare: function (actual) {
33 | return {
34 | pass:
35 | typeof actual === 'number' &&
36 | !isNaN(actual) &&
37 | actual !== Infinity &&
38 | actual !== -Infinity,
39 | };
40 | },
41 | };
42 | },
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hyperbolic-canvas",
3 | "version": "1.0.1",
4 | "description": "The Poincaré disk model of the hyperbolic plane on the HTML canvas.",
5 | "main": "index.js",
6 | "directories": {
7 | "lib": "lib"
8 | },
9 | "scripts": {
10 | "test": "yarn compile && firefox jasmine/SpecRunner.html",
11 | "compile": "rm -rf dist/ && mkdir dist/ && browserify index.js > dist/hyperbolic_canvas.js && prettier --write dist/",
12 | "prepare": "husky",
13 | "prettier": "prettier --write ."
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/itsnickbarry/hyperbolic-canvas.git"
18 | },
19 | "keywords": [
20 | "hyperbolic",
21 | "geometry",
22 | "canvas",
23 | "wow",
24 | "visualization",
25 | "non-euclidean",
26 | "poincare",
27 | "math",
28 | "maths"
29 | ],
30 | "author": "Nick Barry",
31 | "license": "MIT",
32 | "bugs": {
33 | "url": "https://github.com/itsnickbarry/hyperbolic-canvas/issues"
34 | },
35 | "homepage": "https://github.com/itsnickbarry/hyperbolic-canvas#readme",
36 | "devDependencies": {
37 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
38 | "browserify": "^17.0.0",
39 | "husky": "^9.0.11",
40 | "lint-staged": "^15.2.7",
41 | "prettier": "^3.3.2"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/scripts/comets.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | if (typeof HyperbolicCanvas === 'undefined') {
3 | window.HyperbolicCanvas = {};
4 | }
5 | if (typeof HyperbolicCanvas.scripts === 'undefined') {
6 | window.HyperbolicCanvas.scripts = {};
7 | }
8 |
9 | HyperbolicCanvas.scripts['comets'] = function (canvas) {
10 | comets = [];
11 | var spawnDistance = 0.99;
12 |
13 | canvas.setContextProperties({ fillStyle: '#DD4814' });
14 |
15 | var step = function (event) {
16 | canvas.clear();
17 |
18 | var oldComets = comets;
19 | var newComets = [];
20 |
21 | var path;
22 |
23 | for (var i = 0; i < oldComets.length; i++) {
24 | comet = oldComets[i];
25 | if (comet.getEuclideanRadius() <= spawnDistance) {
26 | var distance = comet.distance || Math.random() * 0.05 + 0.01;
27 | var newComet = comet.hyperbolicDistantPoint(distance);
28 | newComet.distance = distance;
29 |
30 | newComets.push(newComet);
31 | var circle = HyperbolicCanvas.Circle.givenHyperbolicCenterRadius(
32 | newComet,
33 | 0.02,
34 | );
35 | path = canvas.pathForHyperbolic(circle, { path2D: true, path: path });
36 | }
37 | }
38 | canvas.fillAndStroke(path);
39 |
40 | comets = newComets;
41 | requestAnimationFrame(step);
42 | };
43 |
44 | var onClick = function (event) {
45 | if (event) {
46 | x = event.clientX;
47 | y = event.clientY;
48 | var point = canvas.at([x, y]);
49 | point._setDirection(HyperbolicCanvas.Angle.opposite(point.getAngle()));
50 | if (point.isOnPlane) {
51 | comets.push(point);
52 | }
53 | }
54 | };
55 |
56 | canvas.getCanvasElement().addEventListener('click', onClick);
57 |
58 | requestAnimationFrame(step);
59 | };
60 | })();
61 |
--------------------------------------------------------------------------------
/scripts/concentric-circles.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | if (typeof HyperbolicCanvas === 'undefined') {
3 | window.HyperbolicCanvas = {};
4 | }
5 | if (typeof HyperbolicCanvas.scripts === 'undefined') {
6 | window.HyperbolicCanvas.scripts = {};
7 | }
8 |
9 | var curry = function (fn, obj, numArgs) {
10 | var firstArgs = Array.prototype.slice.call(arguments, 3);
11 |
12 | return function curriedFunction() {
13 | var args = firstArgs.concat(Array.prototype.slice.call(arguments));
14 |
15 | if (args.length >= numArgs) {
16 | return fn.apply(obj, args);
17 | } else {
18 | return curriedFunction;
19 | }
20 | };
21 | };
22 |
23 | HyperbolicCanvas.scripts['concentric-circles'] = function (canvas) {
24 | var location = HyperbolicCanvas.Point.ORIGIN;
25 |
26 | colors = [
27 | '#DD4814',
28 | '#E05A2B',
29 | '#E36C43',
30 | '#E77E5A',
31 | '#EA9172',
32 | '#EEA389',
33 | '#EFAC95',
34 | '#F1B5A1',
35 | '#F3BEAC',
36 | '#F4C8B8',
37 | '#F6D1C4',
38 | '#F8DAD0',
39 | '#F9E3DB',
40 | '#FBECE7',
41 | // reverse
42 | '#F9E3DB',
43 | '#F8DAD0',
44 | '#F6D1C4',
45 | '#F4C8B8',
46 | '#F3BEAC',
47 | '#F1B5A1',
48 | '#EFAC95',
49 | '#EEA389',
50 | '#EA9172',
51 | '#E77E5A',
52 | '#E36C43',
53 | '#E05A2B',
54 | ];
55 |
56 | maxRadius = 6;
57 |
58 | var render = function (event) {
59 | canvas.clear();
60 |
61 | circles = [];
62 |
63 | for (var i = 26; i > 0; i--) {
64 | canvas.setContextProperties({ fillStyle: colors[i] });
65 | var circle = HyperbolicCanvas.Circle.givenHyperbolicCenterRadius(
66 | location,
67 | i * 0.5,
68 | );
69 | if (circle) {
70 | var path = canvas.pathForHyperbolic(circle);
71 | canvas.fill(path);
72 | }
73 | }
74 | };
75 |
76 | var onClick = function (event) {
77 | canvas.getCanvasElement().removeEventListener('click', onClick);
78 | incrementColor(1);
79 | };
80 |
81 | var incrementColor = function (ms) {
82 | colors.unshift(colors.pop());
83 | requestAnimationFrame(render);
84 | ms += 1;
85 | if (ms < 75) {
86 | setTimeout(curry(incrementColor, null, 1, ms), ms);
87 | } else {
88 | canvas.getCanvasElement().addEventListener('click', onClick);
89 | }
90 | };
91 |
92 | var onMouseMove = function (event) {
93 | if (event) {
94 | x = event.clientX;
95 | y = event.clientY;
96 | }
97 | location = canvas.at([x, y]);
98 | if (!location.isOnPlane()) {
99 | location = HyperbolicCanvas.Point.givenEuclideanPolarCoordinates(
100 | 0.9999,
101 | location.getAngle(),
102 | );
103 | }
104 | requestAnimationFrame(render);
105 | };
106 |
107 | var onScroll = function (event) {
108 | if (event.deltaY < 0) {
109 | colors.unshift(colors.pop());
110 | } else if (event.deltaY > 0) {
111 | colors.push(colors.shift());
112 | }
113 | requestAnimationFrame(render);
114 | };
115 |
116 | canvas.getCanvasElement().addEventListener('click', onClick);
117 | canvas.getContainerElement().addEventListener('mousemove', onMouseMove);
118 | document.addEventListener('wheel', onScroll);
119 | };
120 | })();
121 |
--------------------------------------------------------------------------------
/scripts/hand-drawn-polygon.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | if (typeof HyperbolicCanvas === 'undefined') {
3 | window.HyperbolicCanvas = {};
4 | }
5 | if (typeof HyperbolicCanvas.scripts === 'undefined') {
6 | window.HyperbolicCanvas.scripts = {};
7 | }
8 |
9 | HyperbolicCanvas.scripts['hand-drawn-polygon'] = function (canvas) {
10 | canvas.setContextProperties({ fillStyle: '#DD4814' });
11 |
12 | var vertices = [];
13 | var lines = [];
14 |
15 | var render = function (event) {
16 | canvas.clear();
17 |
18 | var point = canvas.at([event.clientX, event.clientY]);
19 |
20 | if (vertices.length >= 2) {
21 | vertices.push(point);
22 | lines.push(
23 | HyperbolicCanvas.Line.givenTwoPoints(
24 | vertices[vertices.length - 2],
25 | point,
26 | ),
27 | );
28 | lines.push(HyperbolicCanvas.Line.givenTwoPoints(point, vertices[0]));
29 |
30 | var polygon = HyperbolicCanvas.Polygon.givenVertices(vertices);
31 | polygon._lines = lines;
32 | var path = canvas.pathForHyperbolic(polygon);
33 | canvas.fill(path);
34 |
35 | path = canvas.pathForHyperbolic(polygon, { infinite: true });
36 | canvas.stroke(path);
37 |
38 | vertices.pop();
39 | lines.pop();
40 | lines.pop();
41 | } else if (vertices.length == 1) {
42 | var line = HyperbolicCanvas.Line.givenTwoPoints(vertices[0], point);
43 | var path = canvas.pathForHyperbolic(line);
44 | canvas.stroke(path);
45 | }
46 | };
47 |
48 | var addVertex = function (event) {
49 | var point = canvas.at([event.clientX, event.clientY]);
50 | if (
51 | !(vertices.length > 0 && point.equals(vertices[vertices.length - 1]))
52 | ) {
53 | vertices.push(point);
54 |
55 | if (vertices.length > 1) {
56 | lines.push(
57 | HyperbolicCanvas.Line.givenTwoPoints(
58 | vertices[vertices.length - 2],
59 | point,
60 | ),
61 | );
62 | }
63 | }
64 | };
65 |
66 | var onMouseMove = function (event) {
67 | requestAnimationFrame(function () {
68 | render(event);
69 | });
70 | };
71 |
72 | canvas.getCanvasElement().addEventListener('click', addVertex);
73 | canvas.getCanvasElement().addEventListener('mousemove', onMouseMove);
74 | };
75 | })();
76 |
--------------------------------------------------------------------------------
/scripts/hexagons.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | if (typeof HyperbolicCanvas === 'undefined') {
3 | window.HyperbolicCanvas = {};
4 | }
5 | if (typeof HyperbolicCanvas.scripts === 'undefined') {
6 | window.HyperbolicCanvas.scripts = {};
7 | }
8 |
9 | HyperbolicCanvas.scripts['hexagons'] = function (canvas) {
10 | var counter = 0;
11 | canvas.setContextProperties({ fillStyle: '#DD4814' });
12 |
13 | var sideCount = 6;
14 | var radius = 0.9;
15 | var ringCount = 4;
16 | var rotation = 0;
17 | var rotationDenominator = Math.pow(sideCount, 4) * 6;
18 | var rotationInterval = Math.TAU / rotationDenominator;
19 |
20 | var render = function () {
21 | if (counter < 4) {
22 | counter += 1;
23 | requestAnimationFrame(render);
24 | return;
25 | }
26 | counter = 0;
27 | var polygons = [];
28 | for (var i = 0; i < ringCount; i++) {
29 | for (var j = 0; j < sideCount; j++) {
30 | var center = HyperbolicCanvas.Point.givenHyperbolicPolarCoordinates(
31 | i * radius * 2,
32 | (Math.TAU / sideCount) * j +
33 | (i % 2 === 0 ? rotation : rotation * -1),
34 | );
35 | var gon = HyperbolicCanvas.Polygon.givenHyperbolicNCenterRadius(
36 | sideCount,
37 | center,
38 | radius,
39 | ((Math.TAU / sideCount) * j + rotation) * (i + 1), // + Math.PI * i // if sideCount is odd
40 | );
41 | polygons.push(gon);
42 | }
43 | }
44 | rotation += rotationInterval;
45 |
46 | canvas.clear();
47 |
48 | var path;
49 | polygons.forEach(function (polygon) {
50 | path = canvas.pathForHyperbolic(polygon, { path2D: true, path: path });
51 | });
52 | canvas.fill(path);
53 |
54 | requestAnimationFrame(render);
55 | };
56 | requestAnimationFrame(render);
57 | };
58 | })();
59 |
--------------------------------------------------------------------------------
/scripts/laser-spaceship.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | if (typeof HyperbolicCanvas === 'undefined') {
3 | window.HyperbolicCanvas = {};
4 | }
5 | if (typeof HyperbolicCanvas.scripts === 'undefined') {
6 | window.HyperbolicCanvas.scripts = {};
7 | }
8 |
9 | var randomColor = function () {
10 | return '#' + (Math.random().toString(16) + '0000000').slice(2, 8);
11 | };
12 |
13 | HyperbolicCanvas.scripts['laser-spaceship'] = function (canvas) {
14 | var keysDown = {};
15 | var keysUp = {};
16 |
17 | var ctx = canvas.getContext();
18 | var defaultProperties = {
19 | lineDash: [],
20 | lineJoin: 'round',
21 | lineWidth: 2,
22 | shadowBlur: 20,
23 | shadowColor: 'white',
24 | strokeStyle: '#DD4814',
25 | fillStyle: '#333333',
26 | };
27 |
28 | canvas.setContextProperties(defaultProperties);
29 |
30 | var heading = HyperbolicCanvas.Angle.random();
31 | var headingIncrement = Math.TAU / 100;
32 | var velocity = 0;
33 | var velocityIncrement = 0.002;
34 | var maxVelocity = 0.05;
35 |
36 | var wingAngle = Math.TAU / 3;
37 |
38 | var bullets = [];
39 | var framesSinceBullet = 0;
40 | var bulletFrameCooldown = 1;
41 | var lastBulletTime = new Date();
42 | var bulletCooldown = 200;
43 |
44 | var location = HyperbolicCanvas.Point.givenEuclideanPolarCoordinates(
45 | 0.5,
46 | HyperbolicCanvas.Angle.opposite(heading),
47 | );
48 | var front;
49 |
50 | var drawShip = function () {
51 | front = location.hyperbolicDistantPoint(0.1, heading);
52 | var left = location.hyperbolicDistantPoint(0.05, heading + wingAngle);
53 | var right = location.hyperbolicDistantPoint(0.05, heading - wingAngle);
54 |
55 | // draw heading line
56 | canvas.setContextProperties({
57 | lineDash: [5],
58 | lineWidth: 1,
59 | shadowBlur: 0,
60 | strokeStyle: 'white',
61 | });
62 | var path = canvas.pathForHyperbolic(
63 | HyperbolicCanvas.Line.givenTwoPoints(
64 | front,
65 | location.hyperbolicDistantPoint(30),
66 | ),
67 | );
68 | canvas.stroke(path);
69 | canvas.setContextProperties(defaultProperties);
70 |
71 | // draw ship
72 | path = canvas.pathForHyperbolic(
73 | HyperbolicCanvas.Polygon.givenVertices([front, left, location, right]),
74 | );
75 | canvas.stroke(path);
76 | };
77 |
78 | var drawBullets = function () {
79 | var path;
80 | for (var i in bullets) {
81 | var bullet = bullets[i];
82 |
83 | // use reach bullet's random color
84 | // canvas.setContextProperties({
85 | // fillStyle: bullet.color
86 | // });
87 |
88 | path = canvas.pathForHyperbolic(
89 | HyperbolicCanvas.Circle.givenHyperbolicCenterRadius(bullet, 0.01),
90 | { path2D: true, path: path },
91 | );
92 | }
93 | if (path) {
94 | canvas.fill(path);
95 | }
96 | };
97 |
98 | var drawRangeCircles = function () {
99 | // draw range circles
100 | canvas.setContextProperties({
101 | strokeStyle: 'black',
102 | });
103 | var circle;
104 |
105 | for (var i = 0; i < 3; i++) {
106 | circle = HyperbolicCanvas.Circle.givenHyperbolicCenterRadius(
107 | location,
108 | i + 1,
109 | );
110 | canvas.setContextProperties({
111 | lineDash: [
112 | circle.getEuclideanCircumference() * 0.1,
113 | circle.getEuclideanCircumference() * 0.9,
114 | ],
115 | });
116 | canvas.stroke(canvas.pathForHyperbolic(circle));
117 | }
118 | for (var i = 0; i < 3; i++) {
119 | circle = HyperbolicCanvas.Circle.givenHyperbolicCenterRadius(
120 | location,
121 | i + 0.5,
122 | );
123 | canvas.setContextProperties({
124 | lineDash: [
125 | circle.getEuclideanCircumference() * 0.1,
126 | circle.getEuclideanCircumference() * 9.9,
127 | ],
128 | });
129 | canvas.stroke(canvas.pathForHyperbolic(circle));
130 | }
131 |
132 | canvas.setContextProperties(defaultProperties);
133 | };
134 |
135 | var render = function (event) {
136 | canvas.clear();
137 | drawShip();
138 | drawBullets();
139 | drawRangeCircles();
140 | };
141 |
142 | var shouldRender = true;
143 | var fn = function () {
144 | if (shouldRender) {
145 | shouldRender ^= true;
146 | requestAnimationFrame(fn);
147 | return;
148 | }
149 | shouldRender ^= true;
150 | boost = 16 in keysDown ? 3 : 1;
151 |
152 | if (37 in keysDown || 65 in keysDown) {
153 | heading += headingIncrement * boost;
154 | }
155 | if (39 in keysDown || 68 in keysDown) {
156 | heading -= headingIncrement * boost;
157 | }
158 |
159 | if (38 in keysDown || 87 in keysDown) {
160 | if (velocity < maxVelocity) {
161 | velocity += velocityIncrement * boost;
162 | }
163 | }
164 | if (40 in keysDown || 83 in keysDown) {
165 | if (velocity > 0) {
166 | velocity -= velocityIncrement;
167 | if (velocity < 0) {
168 | velocity = 0;
169 | }
170 | }
171 | }
172 |
173 | // var now = new Date()
174 | // if (32 in keysDown && now - lastBulletTime > bulletCooldown) {
175 | if (32 in keysDown && framesSinceBullet > bulletFrameCooldown) {
176 | // fire
177 | var bullet = HyperbolicCanvas.Point.givenCoordinates(
178 | front.getX(),
179 | front.getY(),
180 | );
181 | bullet._setDirection(
182 | front.getDirection() + ((Math.random() - 0.5) * Math.TAU) / 100,
183 | );
184 | bullet.color = randomColor();
185 | bullets.push(bullet);
186 |
187 | // lastBulletTime = now;
188 | framesSinceBullet = 0;
189 | } else {
190 | framesSinceBullet += 1;
191 | }
192 |
193 | location = location.hyperbolicDistantPoint(velocity, heading);
194 | heading = location.getDirection();
195 | velocity *= 0.99;
196 |
197 | // update bullet locations
198 | var newBullets = [];
199 | for (var i in bullets) {
200 | var bullet = bullets[i];
201 | var newBullet = bullet.hyperbolicDistantPoint(0.1);
202 | newBullet.color = bullet.color;
203 | if (newBullet.getEuclideanRadius() < 0.99) {
204 | newBullets.push(newBullet);
205 | }
206 | }
207 | bullets = newBullets;
208 |
209 | render();
210 | requestAnimationFrame(fn);
211 | };
212 |
213 | fn();
214 |
215 | addEventListener(
216 | 'keydown',
217 | function (e) {
218 | // if (e.keyCode === 32) {
219 | // var now = new Date();
220 | // if (now - lastBulletTime < bulletCooldown) {
221 | // return;
222 | // }
223 | // lastBulletTime = now;
224 | // // only fire on keydown, don't store in keysDown
225 | // var bullet = HyperbolicCanvas.Point.givenCoordinates(
226 | // front.getX(),
227 | // front.getY()
228 | // );
229 | // bullet._setDirection(front.getDirection());
230 | // bullet.color = randomColor();
231 | // bullets.push(bullet);
232 | //
233 | // var audioName = 'mod blaster';
234 | // var audio = new Audio('https://github.com/endless-sky/endless-sky/raw/master/sounds/' + audioName +'.wav');
235 | // audio.play();ss
236 | // } else {
237 | // keysDown[e.keyCode] = true;
238 | // }
239 | keysDown[e.keyCode] = true;
240 | },
241 | false,
242 | );
243 |
244 | addEventListener(
245 | 'keyup',
246 | function (e) {
247 | delete keysDown[e.keyCode];
248 | },
249 | false,
250 | );
251 | };
252 | })();
253 |
--------------------------------------------------------------------------------
/scripts/mouse-interaction.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | if (typeof HyperbolicCanvas === 'undefined') {
3 | window.HyperbolicCanvas = {};
4 | }
5 | if (typeof HyperbolicCanvas.scripts === 'undefined') {
6 | window.HyperbolicCanvas.scripts = {};
7 | }
8 |
9 | HyperbolicCanvas.scripts['mouse-interaction'] = function (canvas) {
10 | var maxN = 12;
11 | var n = 3;
12 | var location = HyperbolicCanvas.Point.ORIGIN;
13 | var rotation = 0;
14 | var rotationInterval = Math.TAU / 800;
15 | var radius = 1;
16 |
17 | canvas.setContextProperties({ fillStyle: '#DD4814' });
18 |
19 | var render = function (event) {
20 | canvas.clear();
21 |
22 | var polygon = HyperbolicCanvas.Polygon.givenHyperbolicNCenterRadius(
23 | n,
24 | location,
25 | radius,
26 | rotation,
27 | );
28 |
29 | if (polygon) {
30 | var path = canvas.pathForHyperbolic(polygon);
31 |
32 | polygon.getVertices().forEach(function (v) {
33 | var angle = location.hyperbolicAngleTo(v);
34 | path = canvas.pathForHyperbolic(
35 | HyperbolicCanvas.Polygon.givenHyperbolicNCenterRadius(
36 | n,
37 | location.hyperbolicDistantPoint(radius * 1.5, angle),
38 | radius / 2,
39 | angle + rotation,
40 | ),
41 | { path2D: true, path: path },
42 | );
43 | });
44 |
45 | canvas.fillAndStroke(path);
46 | }
47 | rotation += rotationInterval;
48 | if (rotation > Math.TAU) {
49 | rotation -= Math.TAU;
50 | }
51 | requestAnimationFrame(render);
52 | };
53 |
54 | var resetLocation = function (event) {
55 | if (event) {
56 | x = event.clientX;
57 | y = event.clientY;
58 | }
59 | location = canvas.at([x, y]);
60 | };
61 |
62 | var incrementN = function () {
63 | n += 1;
64 | n %= maxN;
65 | n = n < 3 ? 3 : n;
66 | };
67 |
68 | var scroll = function (event) {
69 | radius += event.deltaY * 0.01;
70 | if (radius < 0.05) {
71 | radius = 0.05;
72 | } else if (radius > 20) {
73 | radius = 20;
74 | }
75 | };
76 |
77 | canvas.getCanvasElement().addEventListener('click', incrementN);
78 | canvas.getCanvasElement().addEventListener('mousemove', resetLocation);
79 | document.addEventListener('wheel', scroll);
80 |
81 | requestAnimationFrame(render);
82 | };
83 | })();
84 |
--------------------------------------------------------------------------------
/scripts/web.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | if (typeof HyperbolicCanvas === 'undefined') {
3 | window.HyperbolicCanvas = {};
4 | }
5 | if (typeof HyperbolicCanvas.scripts === 'undefined') {
6 | window.HyperbolicCanvas.scripts = {};
7 | }
8 |
9 | HyperbolicCanvas.scripts['web'] = function (canvas) {
10 | var unitCircle = HyperbolicCanvas.Circle.givenEuclideanCenterRadius(
11 | HyperbolicCanvas.Point.ORIGIN,
12 | 0.9999,
13 | );
14 |
15 | var location = null;
16 | var angles = [];
17 |
18 | for (var i = 0; i < 30; i++) {
19 | angles.push(Math.random() * Math.TAU);
20 | }
21 |
22 | var step = function (event) {
23 | canvas.clear();
24 |
25 | if (location) {
26 | var path;
27 | angles.forEach(function (angle, index, array) {
28 | var point = unitCircle.euclideanPointAt(angle);
29 | var line = HyperbolicCanvas.Line.givenTwoPoints(location, point);
30 | path = canvas.pathForHyperbolic(line, {
31 | path2D: true,
32 | path: path,
33 | infinite: true,
34 | });
35 | array[index] = array[index] + Math.random() * 0.1;
36 | });
37 | canvas.stroke(path);
38 | }
39 | requestAnimationFrame(step);
40 | };
41 |
42 | var resetLocation = function (event) {
43 | if (event) {
44 | x = event.clientX;
45 | y = event.clientY;
46 | }
47 | var point = canvas.at([x, y]);
48 | location = point.isOnPlane ? point : null;
49 | };
50 |
51 | canvas.getCanvasElement().addEventListener('mousemove', resetLocation);
52 |
53 | requestAnimationFrame(step);
54 | };
55 | })();
56 |
--------------------------------------------------------------------------------
/src/angle.js:
--------------------------------------------------------------------------------
1 | const HyperbolicCanvas = require('./hyperbolic_canvas.js');
2 |
3 | let Angle = (HyperbolicCanvas.Angle = {});
4 |
5 | Angle.normalize = function (angle) {
6 | if (angle < 0) {
7 | return Math.abs(Math.floor(angle / Math.TAU)) * Math.TAU + angle;
8 | } else if (angle >= Math.TAU) {
9 | return angle % Math.TAU;
10 | } else {
11 | return angle;
12 | }
13 | };
14 |
15 | Angle.fromDegrees = function (degrees) {
16 | return Angle.normalize(degrees * Angle.DEGREES_TO_RADIANS);
17 | };
18 |
19 | Angle.toDegrees = function (radians) {
20 | return (radians / Angle.DEGREES_TO_RADIANS) % 360;
21 | };
22 |
23 | Angle.opposite = function (angle) {
24 | return Angle.normalize(angle + Math.PI);
25 | };
26 |
27 | Angle.toSlope = function (angle) {
28 | // TODO should this return Infinity?
29 | return Math.tan(angle);
30 | };
31 |
32 | Angle.fromSlope = function (slope) {
33 | return Math.atan(slope);
34 | };
35 |
36 | Angle.random = function (quadrant) {
37 | let angle = Math.random() * Math.TAU;
38 | return quadrant ? angle / 4 + (Math.PI / 2) * ((quadrant - 1) % 4) : angle;
39 | };
40 |
41 | Angle.DEGREES_TO_RADIANS = Math.PI / 180;
42 |
--------------------------------------------------------------------------------
/src/canvas.js:
--------------------------------------------------------------------------------
1 | const HyperbolicCanvas = require('./hyperbolic_canvas.js');
2 |
3 | // TODO store polygons and circles as hit regions
4 |
5 | let Canvas = (HyperbolicCanvas.Canvas = function (options) {
6 | this._setupElements(options);
7 | this._setupSize();
8 | });
9 |
10 | Canvas.prototype.getBackdropElement = function () {
11 | return this._backdrop;
12 | };
13 |
14 | Canvas.prototype.getContainerElement = function () {
15 | return this._el;
16 | };
17 |
18 | Canvas.prototype.getCanvasElement = function () {
19 | return this._canvas;
20 | };
21 |
22 | Canvas.prototype.getUnderlayElement = function () {
23 | return this._underlay;
24 | };
25 |
26 | Canvas.prototype.getContext = function () {
27 | return this._ctx;
28 | };
29 |
30 | Canvas.prototype.getRadius = function () {
31 | return this._radius;
32 | };
33 |
34 | Canvas.prototype.getDiameter = function () {
35 | return this._diameter;
36 | };
37 |
38 | Canvas.prototype.setContextProperties = function (options) {
39 | for (let attribute in options) {
40 | this.setContextProperty(attribute, options[attribute]);
41 | }
42 | };
43 |
44 | Canvas.prototype.setContextProperty = function (property, value) {
45 | let ctx = this.getContext();
46 | if (property === 'lineDash') {
47 | ctx.setLineDash(value);
48 | }
49 | ctx[property] = value;
50 | };
51 |
52 | Canvas.prototype.at = function (loc) {
53 | if (loc.__proto__ === HyperbolicCanvas.Point.prototype) {
54 | // scale up
55 | let x = (loc.getX() + 1) * this.getRadius();
56 | let y = (loc.getY() + 1) * this.getRadius();
57 | return [x, this.getDiameter() - y];
58 | } else if (loc.__proto__ === Array.prototype) {
59 | // scale down
60 | return new HyperbolicCanvas.Point({
61 | x: loc[0] / this.getRadius() - 1,
62 | y: (this.getDiameter() - loc[1]) / this.getRadius() - 1,
63 | });
64 | }
65 | };
66 |
67 | Canvas.prototype.clear = function () {
68 | this.getContext().clearRect(0, 0, this.getDiameter(), this.getDiameter());
69 | };
70 |
71 | Canvas.prototype.fill = function (path) {
72 | if (path && Path2D && path instanceof Path2D) {
73 | this.getContext().fill(path);
74 | } else {
75 | path = path || this.getContext();
76 | path.fill();
77 | }
78 | };
79 |
80 | Canvas.prototype.fillAndStroke = function (path) {
81 | if (path && Path2D && path instanceof Path2D) {
82 | this.getContext().fill(path);
83 | this.getContext().stroke(path);
84 | } else {
85 | path = path || this.getContext();
86 | path.fill();
87 | path.stroke();
88 | }
89 | };
90 |
91 | Canvas.prototype.stroke = function (path) {
92 | if (path && Path2D && path instanceof Path2D) {
93 | this.getContext().stroke(path);
94 | } else {
95 | path = path || this.getContext();
96 | path.stroke();
97 | }
98 | };
99 |
100 | Canvas.prototype.pathForReferenceAngles = function (n, rotation, options) {
101 | let path = this._getPathOrContext(options || {});
102 | let angle = rotation || 0;
103 | let r = this.getRadius();
104 | let difference = Math.TAU / n;
105 | for (let i = 0; i < n; i++) {
106 | let idealPoint = this.at(
107 | HyperbolicCanvas.Point.givenEuclideanPolarCoordinates(1, angle),
108 | );
109 | path.moveTo(r, r);
110 | path.lineTo(idealPoint[0], idealPoint[1]);
111 | angle += difference;
112 | }
113 | return path;
114 | };
115 |
116 | Canvas.prototype.pathForReferenceGrid = function (n, options) {
117 | let path = this._getPathOrContext(options || {});
118 | for (let i = 1; i < n; i++) {
119 | // x axis
120 | path.moveTo((this.getDiameter() * i) / n, 0);
121 | path.lineTo((this.getDiameter() * i) / n, this.getDiameter());
122 | // y axis
123 | path.moveTo(0, (this.getDiameter() * i) / n);
124 | path.lineTo(this.getDiameter(), (this.getDiameter() * i) / n);
125 | }
126 | return path;
127 | };
128 |
129 | Canvas.prototype.pathForReferenceRings = function (n, r, options) {
130 | let path = this._getPathOrContext(options || {});
131 | for (let i = 0; i < n; i++) {
132 | this._pathForCircle(
133 | HyperbolicCanvas.Circle.givenHyperbolicCenterRadius(
134 | HyperbolicCanvas.Point.ORIGIN,
135 | r * (i + 1),
136 | ),
137 | path,
138 | );
139 | }
140 | return path;
141 | };
142 |
143 | Canvas.prototype.pathForEuclidean = function (object, options) {
144 | options = options || {};
145 | return this._pathFunctionForEuclidean(object)(
146 | object,
147 | this._getPathOrContext(options),
148 | options,
149 | );
150 | };
151 |
152 | Canvas.prototype.pathForHyperbolic = function (object, options) {
153 | options = options || {};
154 | return this._pathFunctionForHyperbolic(object)(
155 | object,
156 | this._getPathOrContext(options),
157 | options,
158 | );
159 | };
160 |
161 | Canvas.prototype._pathForCircle = function (c, path) {
162 | let center = this.at(c.getEuclideanCenter());
163 | let start = this.at(c.euclideanPointAt(0));
164 |
165 | path.moveTo(start[0], start[1]);
166 |
167 | path.arc(
168 | center[0],
169 | center[1],
170 | c.getEuclideanRadius() * this.getRadius(),
171 | 0,
172 | Math.TAU,
173 | );
174 | return path;
175 | };
176 |
177 | Canvas.prototype._pathForEuclideanLine = function (l, path, options) {
178 | let p1 = this.at(l.getP1());
179 |
180 | if (!options.connected) {
181 | let p0 = this.at(l.getP0());
182 | path.moveTo(p0[0], p0[1]);
183 | }
184 | path.lineTo(p1[0], p1[1]);
185 | return path;
186 | };
187 |
188 | Canvas.prototype._pathForEuclideanPoint = function (p, path) {
189 | let point = this.at(p);
190 | path.lineTo(point[0], point[1]);
191 | return path;
192 | };
193 |
194 | Canvas.prototype._pathForEuclideanPolygon = function (p, path) {
195 | let start = this.at(p.getVertices()[0]);
196 | path.moveTo(start[0], start[1]);
197 |
198 | let lines = p.getLines();
199 | for (let i = 0; i < lines.length; i++) {
200 | this._pathForEuclideanLine(lines[i], path, { connected: true });
201 | }
202 | return path;
203 | };
204 |
205 | Canvas.prototype._pathForHyperbolicLine = function (l, path, options) {
206 | let geodesic = l.getHyperbolicGeodesic();
207 |
208 | if (geodesic instanceof HyperbolicCanvas.Circle) {
209 | let p0 = this.at(l.getP0());
210 | let p1 = this.at(l.getP1());
211 |
212 | if (options.connected) {
213 | // not clear why this is necessary
214 | path.lineTo(p0[0], p0[1]);
215 | } else {
216 | // do not connect line to previous point on path
217 | path.moveTo(p0[0], p0[1]);
218 | }
219 |
220 | let control = this.at(
221 | HyperbolicCanvas.Line.euclideanIntersect(
222 | geodesic.euclideanTangentAtPoint(l.getP0()),
223 | geodesic.euclideanTangentAtPoint(l.getP1()),
224 | ),
225 | );
226 |
227 | if (control) {
228 | path.arcTo(
229 | control[0],
230 | control[1],
231 | p1[0],
232 | p1[1],
233 | geodesic.getEuclideanRadius() * this.getRadius(),
234 | );
235 | } else {
236 | path.lineTo(p1[0], p1[1]);
237 | }
238 | return path;
239 | } else if (geodesic instanceof HyperbolicCanvas.Line) {
240 | return this._pathForEuclideanLine(geodesic, path, options);
241 | } else {
242 | return false;
243 | }
244 | };
245 |
246 | Canvas.prototype._pathForHyperbolicPolygon = function (p, path, options) {
247 | let lines = p.getLines();
248 | let start = this.at(p.getVertices()[0]);
249 | if (options.infinite) {
250 | for (let i = 0; i < lines.length; i++) {
251 | this._pathForHyperbolicLine(lines[i].getIdealLine(), path, options);
252 | }
253 | } else {
254 | path.moveTo(start[0], start[1]);
255 |
256 | for (let i = 0; i < lines.length; i++) {
257 | this._pathForHyperbolicLine(lines[i], path, { connected: true });
258 | }
259 | }
260 | return path;
261 | };
262 |
263 | Canvas.prototype._pathFunctionForEuclidean = function (object) {
264 | let fn;
265 | switch (object.__proto__) {
266 | case HyperbolicCanvas.Line.prototype:
267 | fn = this._pathForEuclideanLine;
268 | break;
269 | case HyperbolicCanvas.Circle.prototype:
270 | fn = this._pathForCircle;
271 | break;
272 | case HyperbolicCanvas.Polygon.prototype:
273 | fn = this._pathForEuclideanPolygon;
274 | break;
275 | case HyperbolicCanvas.Point.prototype:
276 | fn = this._pathForEuclideanPoint;
277 | break;
278 | default:
279 | fn = function () {
280 | return false;
281 | };
282 | break;
283 | }
284 | return fn.bind(this);
285 | };
286 |
287 | Canvas.prototype._pathFunctionForHyperbolic = function (object) {
288 | let fn;
289 | switch (object.__proto__) {
290 | case HyperbolicCanvas.Circle.prototype:
291 | fn = this._pathForCircle;
292 | break;
293 | case HyperbolicCanvas.Line.prototype:
294 | fn = this._pathForHyperbolicLine;
295 | break;
296 | case HyperbolicCanvas.Polygon.prototype:
297 | fn = this._pathForHyperbolicPolygon;
298 | break;
299 | default:
300 | fn = function () {
301 | return false;
302 | };
303 | break;
304 | }
305 | return fn.bind(this);
306 | };
307 |
308 | Canvas.prototype._getPathOrContext = function (options) {
309 | // options:
310 | // path2D: [boolean] -> use Path2D instead of CanvasRenderingContext2D
311 | // path: [Path2D] -> Path2D to add to
312 | if (options.path) {
313 | return options.path;
314 | } else if (options.path2D && Path2D) {
315 | return new Path2D();
316 | } else {
317 | this.getContext().beginPath();
318 | return this.getContext();
319 | }
320 | };
321 |
322 | Canvas.prototype._setupElements = function (options) {
323 | let el = (this._el = options.el);
324 | while (el.firstChild) {
325 | el.removeChild(el.firstChild);
326 | }
327 |
328 | let backdrop = (this._backdrop = document.createElement('div'));
329 | backdrop.className = 'backdrop';
330 |
331 | let underlay = (this._underlay = document.createElement('div'));
332 | underlay.className = 'underlay';
333 | underlay.style.display = 'block';
334 |
335 | let canvas = (this._canvas = document.createElement('canvas'));
336 | canvas.className = 'hyperbolic';
337 | canvas.style.position = 'absolute';
338 |
339 | this._ctx = canvas.getContext('2d', options.contextAttributes);
340 |
341 | el.appendChild(backdrop);
342 | backdrop.appendChild(underlay);
343 | underlay.appendChild(canvas);
344 | };
345 |
346 | Canvas.prototype._setupSize = function () {
347 | let container = this.getContainerElement();
348 | let underlay = this.getUnderlayElement();
349 | let canvas = this.getCanvasElement();
350 | let backdrop = this.getBackdropElement();
351 |
352 | let w = container.clientWidth;
353 | let h = container.clientHeight;
354 | let d = (this._diameter = w > h ? h : w);
355 | let r = (this._radius = d / 2);
356 |
357 | underlay.style['width'] = underlay.style['height'] = '' + d + 'px';
358 | backdrop.style['width'] = backdrop.style['height'] = '' + d + 'px';
359 | underlay.style['border-radius'] = '' + Math.floor(r) + 'px';
360 | canvas.style['border-radius'] = '' + Math.floor(r) + 'px';
361 | canvas.width = canvas.height = d;
362 | };
363 |
--------------------------------------------------------------------------------
/src/circle.js:
--------------------------------------------------------------------------------
1 | const HyperbolicCanvas = require('./hyperbolic_canvas.js');
2 |
3 | let Circle = (HyperbolicCanvas.Circle = function (options) {
4 | this._euclideanCenter = options.euclideanCenter;
5 | if (options.euclideanRadius < 0) {
6 | this._euclideanRadius = Math.abs(options.euclideanRadius);
7 | } else {
8 | this._euclideanRadius = options.euclideanRadius;
9 | }
10 | this._hyperbolicCenter = options.hyperbolicCenter;
11 | if (options.hyperbolicRadius < 0) {
12 | this._hyperbolicRadius = Math.abs(options.hyperbolicRadius);
13 | } else {
14 | this._hyperbolicRadius = options.hyperbolicRadius;
15 | }
16 | });
17 |
18 | Circle.prototype.getEuclideanArea = function () {
19 | if (typeof this._euclideanArea === 'undefined') {
20 | this._euclideanArea = Math.PI * Math.pow(this.getEuclideanRadius(), 2);
21 | }
22 | return this._euclideanArea;
23 | };
24 |
25 | Circle.prototype.getEuclideanCenter = function () {
26 | if (typeof this._euclideanCenter === 'undefined') {
27 | this._calculateEuclideanCenterRadius();
28 | }
29 | return this._euclideanCenter;
30 | };
31 |
32 | Circle.prototype.getEuclideanCircumference = function () {
33 | if (typeof this._euclideanCircumference === 'undefined') {
34 | this._euclideanCircumference = Math.TAU * this.getEuclideanRadius();
35 | }
36 | return this._euclideanCircumference;
37 | };
38 |
39 | Circle.prototype.getEuclideanDiameter = function () {
40 | return this.getEuclideanRadius() * 2;
41 | };
42 |
43 | Circle.prototype.getEuclideanRadius = function () {
44 | if (typeof this._euclideanRadius === 'undefined') {
45 | this._calculateEuclideanCenterRadius();
46 | }
47 | return this._euclideanRadius;
48 | };
49 |
50 | Circle.prototype.getHyperbolicArea = function () {
51 | if (typeof this._hyperbolicArea === 'undefined') {
52 | this._hyperbolicArea =
53 | Math.TAU * (Math.cosh(this.getHyperbolicRadius()) - 1);
54 | }
55 | return this._hyperbolicArea;
56 | };
57 |
58 | Circle.prototype.getHyperbolicCenter = function () {
59 | if (typeof this._hyperbolicCenter === 'undefined') {
60 | this._calculateHyperbolicCenterRadius();
61 | }
62 | return this._hyperbolicCenter;
63 | };
64 |
65 | Circle.prototype.getHyperbolicCircumference = function () {
66 | if (typeof this._hyperbolicCircumference === 'undefined') {
67 | this._hyperbolicCircumference =
68 | Math.TAU * Math.sinh(this.getHyperbolicRadius());
69 | }
70 | return this._hyperbolicCircumference;
71 | };
72 |
73 | Circle.prototype.getHyperbolicDiameter = function () {
74 | return this.getHyperbolicRadius() * 2;
75 | };
76 |
77 | Circle.prototype.getHyperbolicRadius = function () {
78 | if (typeof this._hyperbolicRadius === 'undefined') {
79 | this._calculateHyperbolicCenterRadius();
80 | }
81 | return this._hyperbolicRadius;
82 | };
83 |
84 | Circle.prototype.getUnitCircleIntersects = function () {
85 | if (typeof this._unitCircleIntersects === 'undefined') {
86 | this._unitCircleIntersects = Circle.intersects(this, Circle.UNIT);
87 | }
88 | return this._unitCircleIntersects;
89 | };
90 |
91 | Circle.prototype.clone = function () {
92 | return Circle.givenEuclideanCenterRadius(
93 | this.getEuclideanCenter(),
94 | this.getEuclideanRadius(),
95 | );
96 | };
97 |
98 | Circle.prototype.equals = function (otherCircle) {
99 | return (
100 | this.getEuclideanCenter().equals(otherCircle.getEuclideanCenter()) &&
101 | Math.abs(this.getEuclideanRadius() - otherCircle.getEuclideanRadius()) <
102 | HyperbolicCanvas.ZERO
103 | );
104 | };
105 |
106 | Circle.prototype.containsPoint = function (point) {
107 | return (
108 | this.getEuclideanRadius() >
109 | point.euclideanDistanceTo(this.getEuclideanCenter())
110 | );
111 | };
112 |
113 | Circle.prototype.includesPoint = function (point) {
114 | return (
115 | Math.abs(
116 | this.getEuclideanRadius() -
117 | point.euclideanDistanceTo(this.getEuclideanCenter()),
118 | ) < HyperbolicCanvas.ZERO
119 | );
120 | };
121 |
122 | Circle.prototype.euclideanAngleAt = function (p) {
123 | let dx = p.getX() - this.getEuclideanCenter().getX();
124 | let dy = p.getY() - this.getEuclideanCenter().getY();
125 | return HyperbolicCanvas.Angle.normalize(Math.atan2(dy, dx));
126 | };
127 |
128 | Circle.prototype.euclideanPointAt = function (angle) {
129 | return HyperbolicCanvas.Point.givenCoordinates(
130 | this.getEuclideanRadius() * Math.cos(angle) +
131 | this.getEuclideanCenter().getX(),
132 | this.getEuclideanRadius() * Math.sin(angle) +
133 | this.getEuclideanCenter().getY(),
134 | );
135 | };
136 |
137 | Circle.prototype.hyperbolicAngleAt = function (p) {
138 | return this.getHyperbolicCenter().hyperbolicAngleTo(p);
139 | };
140 |
141 | Circle.prototype.hyperbolicPointAt = function (angle) {
142 | return this.getHyperbolicCenter().hyperbolicDistantPoint(
143 | this.getHyperbolicRadius(),
144 | angle,
145 | );
146 | };
147 |
148 | Circle.prototype.pointsAtX = function (x) {
149 | let values = this.yAtX(x);
150 | let points = [];
151 | values.forEach(function (y) {
152 | points.push(HyperbolicCanvas.Point.givenCoordinates(x, y));
153 | });
154 | return points;
155 | };
156 |
157 | Circle.prototype.pointsAtY = function (y) {
158 | let values = this.xAtY(y);
159 | let points = [];
160 | values.forEach(function (x) {
161 | points.push(HyperbolicCanvas.Point.givenCoordinates(x, y));
162 | });
163 | return points;
164 | };
165 |
166 | Circle.prototype.euclideanTangentAtAngle = function (angle) {
167 | return HyperbolicCanvas.Line.givenPointSlope(
168 | this.euclideanPointAt(angle),
169 | -1 / HyperbolicCanvas.Angle.toSlope(angle),
170 | );
171 | };
172 |
173 | Circle.prototype.euclideanTangentAtPoint = function (p) {
174 | // not very mathematical; point is not necessarily on circle
175 | return this.euclideanTangentAtAngle(this.euclideanAngleAt(p));
176 | };
177 |
178 | Circle.prototype.xAtY = function (y) {
179 | let center = this.getEuclideanCenter();
180 | let a = this._pythagoreanTheorem(y - center.getY());
181 | if (a) {
182 | return Math.abs(a) < HyperbolicCanvas.ZERO
183 | ? [center.getX()]
184 | : [center.getX() + a, center.getX() - a];
185 | } else {
186 | return a === 0 ? [center.getX()] : [];
187 | }
188 | };
189 |
190 | Circle.prototype.yAtX = function (x) {
191 | let center = this.getEuclideanCenter();
192 | let a = this._pythagoreanTheorem(x - center.getX());
193 | if (a) {
194 | return Math.abs(a) < HyperbolicCanvas.ZERO
195 | ? [center.getY()]
196 | : [center.getY() + a, center.getY() - a];
197 | } else {
198 | return a === 0 ? [center.getY()] : [];
199 | }
200 | };
201 |
202 | Circle.prototype._pythagoreanTheorem = function (b) {
203 | let c = this.getEuclideanRadius();
204 | let aSquared = Math.pow(c, 2) - Math.pow(b, 2);
205 | return Math.abs(aSquared) < HyperbolicCanvas.ZERO ? 0 : Math.sqrt(aSquared);
206 | };
207 |
208 | Circle.prototype._calculateEuclideanCenterRadius = function () {
209 | let center = this.getHyperbolicCenter();
210 | let farPoint = this.hyperbolicPointAt(center.getAngle());
211 | let nearPoint = this.hyperbolicPointAt(
212 | HyperbolicCanvas.Angle.opposite(center.getAngle()),
213 | );
214 | let diameter = HyperbolicCanvas.Line.givenTwoPoints(farPoint, nearPoint);
215 | this._euclideanCenter = diameter.getEuclideanMidpoint();
216 | this._euclideanRadius = diameter.getEuclideanLength() / 2;
217 | };
218 |
219 | Circle.prototype._calculateHyperbolicCenterRadius = function () {
220 | let center = this.getEuclideanCenter();
221 |
222 | if (center.getEuclideanRadius() + this.getEuclideanRadius() >= 1) {
223 | // TODO horocycles
224 | if (this.equals(Circle.UNIT)) {
225 | this._hyperbolicCenter = center;
226 | this._hyperbolicRadius = Infinity;
227 | } else {
228 | this._hyperbolicCenter = false;
229 | this._hyperbolicRadius = NaN;
230 | }
231 | } else {
232 | let farPoint = this.euclideanPointAt(center.getAngle());
233 | let nearPoint = this.euclideanPointAt(
234 | HyperbolicCanvas.Angle.opposite(center.getAngle()),
235 | );
236 | let diameter = HyperbolicCanvas.Line.givenTwoPoints(farPoint, nearPoint);
237 | this._hyperbolicCenter = diameter.getHyperbolicMidpoint();
238 | this._hyperbolicRadius = diameter.getHyperbolicLength() / 2;
239 | }
240 | };
241 |
242 | Circle.intersects = function (c0, c1) {
243 | // this function adapted from a post on Stack Overflow by 01AutoMonkey
244 | // and licensed CC BY-SA 3.0:
245 | // https://creativecommons.org/licenses/by-sa/3.0/legalcode
246 | let x0 = c0.getEuclideanCenter().getX();
247 | let y0 = c0.getEuclideanCenter().getY();
248 | let r0 = c0.getEuclideanRadius();
249 | let x1 = c1.getEuclideanCenter().getX();
250 | let y1 = c1.getEuclideanCenter().getY();
251 | let r1 = c1.getEuclideanRadius();
252 |
253 | let a, dx, dy, d, h, rx, ry;
254 | let x2, y2;
255 |
256 | /* dx and dy are the vertical and horizontal distances between
257 | * the circle centers.
258 | */
259 | dx = x1 - x0;
260 | dy = y1 - y0;
261 |
262 | /* Determine the straight-line distance between the centers. */
263 | d = Math.sqrt(dy * dy + dx * dx);
264 |
265 | /* Check for solvability. */
266 | if (d > r0 + r1) {
267 | /* no solution. circles do not intersect. */
268 | return false;
269 | }
270 | if (d < Math.abs(r0 - r1)) {
271 | /* no solution. one circle is contained in the other */
272 | return false;
273 | }
274 |
275 | /* 'point 2' is the point where the line through the circle
276 | * intersection points crosses the line between the circle
277 | * centers.
278 | */
279 |
280 | /* Determine the distance from point 0 to point 2. */
281 | a = (r0 * r0 - r1 * r1 + d * d) / (2.0 * d);
282 |
283 | /* Determine the coordinates of point 2. */
284 | x2 = x0 + (dx * a) / d;
285 | y2 = y0 + (dy * a) / d;
286 |
287 | /* Determine the distance from point 2 to either of the
288 | * intersection points.
289 | */
290 | h = Math.sqrt(r0 * r0 - a * a);
291 |
292 | /* Now determine the offsets of the intersection points from
293 | * point 2.
294 | */
295 | rx = -dy * (h / d);
296 | ry = dx * (h / d);
297 |
298 | /* Determine the absolute intersection points. */
299 | let xi = x2 + rx;
300 | let xi_prime = x2 - rx;
301 | let yi = y2 + ry;
302 | let yi_prime = y2 - ry;
303 |
304 | let p0 = HyperbolicCanvas.Point.givenCoordinates(xi, yi);
305 | let p1 = HyperbolicCanvas.Point.givenCoordinates(xi_prime, yi_prime);
306 | return p0.equals(p1) ? [p0] : [p0, p1];
307 | };
308 |
309 | Circle.givenEuclideanCenterRadius = function (center, radius) {
310 | return new Circle({ euclideanCenter: center, euclideanRadius: radius });
311 | };
312 |
313 | Circle.givenHyperbolicCenterRadius = function (center, radius) {
314 | if (!center.isOnPlane()) {
315 | return false;
316 | }
317 | return new Circle({ hyperbolicCenter: center, hyperbolicRadius: radius });
318 | };
319 |
320 | Circle.givenTwoPoints = function (p0, p1) {
321 | let l = HyperbolicCanvas.Line.givenTwoPoints(p0, p1);
322 | return new Circle({
323 | euclideanCenter: l.getEuclideanMidpoint(),
324 | euclideanRadius: l.getEuclideanLength() / 2,
325 | });
326 | };
327 |
328 | Circle.givenThreePoints = function (p0, p1, p2) {
329 | if (!(p0 && p1 && p2)) {
330 | //not all points exist
331 | return false;
332 | }
333 | if (p0.equals(p1) || p0.equals(p2) || p1.equals(p2)) {
334 | // points are not unique
335 | return false;
336 | }
337 | let b0 = HyperbolicCanvas.Line.givenTwoPoints(p0, p1);
338 | let b1 = HyperbolicCanvas.Line.givenTwoPoints(p1, p2);
339 | if (b0.equals(b1)) {
340 | // all three points are colinear
341 | return false;
342 | }
343 | let center = HyperbolicCanvas.Line.euclideanIntersect(
344 | b0.euclideanPerpindicularBisector(),
345 | b1.euclideanPerpindicularBisector(),
346 | );
347 | let radius = HyperbolicCanvas.Line.givenTwoPoints(
348 | p0,
349 | center,
350 | ).getEuclideanLength();
351 | return new Circle({ euclideanCenter: center, euclideanRadius: radius });
352 | };
353 |
354 | Circle.UNIT = new Circle({});
355 |
356 | Circle.UNIT.getEuclideanArea = function () {
357 | return Math.PI;
358 | };
359 |
360 | Circle.UNIT.getEuclideanCenter = Circle.UNIT.getHyperbolicCenter = function () {
361 | return HyperbolicCanvas.Point.ORIGIN;
362 | };
363 |
364 | Circle.UNIT.getEuclideanCircumference = function () {
365 | return Math.TAU;
366 | };
367 |
368 | Circle.UNIT.getEuclideanDiameter = function () {
369 | return 2;
370 | };
371 |
372 | Circle.UNIT.getEuclideanRadius = function () {
373 | return 1;
374 | };
375 |
376 | Circle.UNIT.getHyperbolicArea =
377 | Circle.UNIT.getHyperbolicCircumference =
378 | Circle.UNIT.getHyperbolicDiameter =
379 | Circle.UNIT.getHyperbolicRadius =
380 | function () {
381 | return Infinity;
382 | };
383 |
384 | Circle.UNIT.hyperbolicAngleAt = Circle.UNIT.euclideanAngleAt = function (
385 | point,
386 | ) {
387 | return point.getAngle();
388 | };
389 |
390 | Circle.UNIT.hyperbolicPointAt = Circle.UNIT.euclideanPointAt = function (
391 | angle,
392 | ) {
393 | return HyperbolicCanvas.Point.givenEuclideanPolarCoordinates(1, angle);
394 | };
395 |
--------------------------------------------------------------------------------
/src/hyperbolic_canvas.js:
--------------------------------------------------------------------------------
1 | const HyperbolicCanvas = (module.exports = global.HyperbolicCanvas = {});
2 |
3 | // modules attach themselves to HyperbolicCanvas and do not include exports
4 | require('./angle.js');
5 | require('./point.js');
6 | require('./line.js');
7 | require('./circle.js');
8 | require('./polygon.js');
9 | require('./canvas.js');
10 |
11 | HyperbolicCanvas.create = function (selector) {
12 | return new HyperbolicCanvas.Canvas({
13 | el: document.querySelector(selector) || document.createElement('div'),
14 | });
15 | };
16 |
17 | HyperbolicCanvas.INFINITY = 1e12;
18 | HyperbolicCanvas.ZERO = 1e-6;
19 |
20 | Math.TAU = 2 * Math.PI;
21 |
--------------------------------------------------------------------------------
/src/line.js:
--------------------------------------------------------------------------------
1 | const HyperbolicCanvas = require('./hyperbolic_canvas.js');
2 |
3 | let Line = (HyperbolicCanvas.Line = function (options) {
4 | this._p0 = options.p0;
5 | this._p1 = options.p1;
6 | this._m = options.m === -Infinity ? Infinity : options.m;
7 | this._euclideanUnitCircleIntersects = options.euclideanUnitCircleIntersects;
8 | this._idealPoints = options.idealPoints;
9 | });
10 |
11 | Line.prototype.getHyperbolicGeodesic = function () {
12 | if (typeof this._geodesic === 'undefined') {
13 | if (this.getP0().isIdeal() || this.getP1().isIdeal()) {
14 | if (this.getP0().isIdeal() && this.getP1().isIdeal()) {
15 | // both points are ideal
16 | this._calculateGeodesicThroughTwoIdealPoints();
17 | } else {
18 | // one point is on plane
19 | this._calculateGeodesicThroughOnePointOnPlane();
20 | }
21 | } else if (!this.isOnPlane()) {
22 | // either Point is not well defined, so geodesic is not defined
23 | this._geodesic = false;
24 | } else if (
25 | this.getP0().equals(HyperbolicCanvas.Point.ORIGIN) ||
26 | this.getP1().equals(HyperbolicCanvas.Point.ORIGIN)
27 | ) {
28 | // either Point is at origin, so geodesic is a Line
29 | this._geodesic = this;
30 | } else {
31 | // both Points are on plane; only need one
32 | this._calculateGeodesicThroughOnePointOnPlane();
33 | }
34 | }
35 | return this._geodesic;
36 | };
37 |
38 | Line.prototype.getEuclideanLength = function () {
39 | if (typeof this._euclideanLength === 'undefined') {
40 | this._euclideanLength = this.getP0().euclideanDistanceTo(this.getP1());
41 | }
42 | return this._euclideanLength;
43 | };
44 |
45 | Line.prototype.getEuclideanMidpoint = function () {
46 | if (typeof this._euclideanMidpoint === 'undefined') {
47 | this._euclideanMidpoint = HyperbolicCanvas.Point.euclideanBetween(
48 | this.getP0(),
49 | this.getP1(),
50 | );
51 | }
52 | return this._euclideanMidpoint;
53 | };
54 |
55 | Line.prototype.getHyperbolicLength = function () {
56 | if (typeof this._hyperbolicLength === 'undefined') {
57 | this._hyperbolicLength = this.getP0().hyperbolicDistanceTo(this.getP1());
58 | }
59 | return this._hyperbolicLength;
60 | };
61 |
62 | Line.prototype.getHyperbolicMidpoint = function () {
63 | if (typeof this._hyperbolicMidpoint === 'undefined') {
64 | if (this.isOnPlane()) {
65 | this._hyperbolicMidpoint = this.getP0().hyperbolicDistantPoint(
66 | this.getHyperbolicLength() / 2,
67 | this.getP0().hyperbolicAngleTo(this.getP1()),
68 | );
69 | } else {
70 | this._hyperbolicMidpoint = false;
71 | }
72 | }
73 | return this._hyperbolicMidpoint;
74 | };
75 |
76 | Line.prototype.getIdealLine = function () {
77 | if (typeof this._idealLine === 'undefined') {
78 | this._idealLine = Line.givenTwoPoints(
79 | this.getIdealPoints()[0],
80 | this.getIdealPoints()[1],
81 | );
82 | }
83 | return this._idealLine;
84 | };
85 |
86 | Line.prototype.getIdealPoints = function () {
87 | if (typeof this._idealPoints === 'undefined') {
88 | let g = this.getHyperbolicGeodesic();
89 | if (g === false) {
90 | this._idealPoints = false;
91 | } else if (g === this) {
92 | this._idealPoints = this.getEuclideanUnitCircleIntersects();
93 | } else {
94 | this._idealPoints = g.getUnitCircleIntersects();
95 | }
96 | }
97 | return this._idealPoints;
98 | };
99 |
100 | Line.prototype.getP0 = function () {
101 | return this._p0;
102 | };
103 |
104 | Line.prototype.getP1 = function () {
105 | if (typeof this._p1 === 'undefined') {
106 | let x, y;
107 | let p = this.getP0();
108 | let m = this.getSlope();
109 | if (m === Infinity) {
110 | x = p.getX();
111 | y = p.getY() ? p.getY() * 2 : p.getY() + 1;
112 | } else if (m === 0) {
113 | x = p.getX() ? p.getX() * 2 : p.getX() + 1;
114 | y = p.getY();
115 | } else {
116 | x = 0;
117 | y = p.getY() - m * p.getX();
118 | }
119 | this._p1 = HyperbolicCanvas.Point.givenCoordinates(x, y);
120 | }
121 | return this._p1;
122 | };
123 |
124 | Line.prototype.getSlope = function () {
125 | if (typeof this._m === 'undefined') {
126 | this._m =
127 | (this.getP0().getY() - this.getP1().getY()) /
128 | (this.getP0().getX() - this.getP1().getX());
129 | if (Math.abs(this._m) > HyperbolicCanvas.INFINITY) {
130 | this._m = Infinity;
131 | }
132 | }
133 | return this._m;
134 | };
135 |
136 | Line.prototype.getEuclideanUnitCircleIntersects = function () {
137 | if (typeof this._euclideanUnitCircleIntersects === 'undefined') {
138 | let m = this.getSlope();
139 |
140 | if (m > HyperbolicCanvas.INFINITY) {
141 | let x0, x1, y0, y1;
142 | x0 = x1 = this.getP0().getX();
143 | y0 = Math.sqrt(1 - x0 * x0);
144 | y1 = -1 * y0;
145 | return [
146 | HyperbolicCanvas.Point.givenCoordinates(x0, y0),
147 | HyperbolicCanvas.Point.givenCoordinates(x1, y1),
148 | ];
149 | }
150 |
151 | //quadratic formula
152 | let a = Math.pow(m, 2) + 1;
153 |
154 | // calculate discriminant
155 | let x = this.getP0().getX();
156 | let y = this.getP0().getY();
157 |
158 | let b = m * 2 * (y - m * x);
159 | let c = Math.pow(y, 2) + Math.pow(x * m, 2) - 2 * m * x * y - 1;
160 |
161 | let discriminant = b * b - 4 * a * c;
162 |
163 | if (discriminant < 0) {
164 | return false;
165 | }
166 |
167 | let x0 = (-1 * b - Math.sqrt(discriminant)) / (2 * a);
168 | let y0 = this.euclideanYAtX(x0);
169 | let p0 = HyperbolicCanvas.Point.givenCoordinates(x0, y0);
170 |
171 | if (discriminant === 0) {
172 | return [p0];
173 | }
174 |
175 | let x1 = (-1 * b + Math.sqrt(discriminant)) / (2 * a);
176 | let y1 = this.euclideanYAtX(x1);
177 |
178 | let p1 = HyperbolicCanvas.Point.givenCoordinates(x1, y1);
179 |
180 | this._euclideanUnitCircleIntersects = [p0, p1];
181 | }
182 | return this._euclideanUnitCircleIntersects;
183 | };
184 |
185 | Line.prototype.clone = function () {
186 | return Line.givenTwoPoints(this.getP0(), this.getP1());
187 | };
188 |
189 | Line.prototype.euclideanIncludesPoint = function (point) {
190 | if (this.getSlope() === Infinity) {
191 | return this.getP0().getX() === point.getX();
192 | }
193 | return (
194 | Math.abs(
195 | point.getY() -
196 | this.getP0().getY() -
197 | this.getSlope() * (point.getX() - this.getP0().getX()),
198 | ) < HyperbolicCanvas.ZERO
199 | );
200 | };
201 |
202 | Line.prototype.hyperbolicIncludesPoint = function (point) {
203 | if (!(point.isOnPlane() ^ point.isIdeal())) {
204 | return false;
205 | }
206 | let g = this.getHyperbolicGeodesic();
207 | if (g instanceof HyperbolicCanvas.Circle) {
208 | return g.includesPoint(point);
209 | } else if (g instanceof Line) {
210 | return this.euclideanIncludesPoint(point);
211 | } else {
212 | return false;
213 | }
214 | };
215 |
216 | Line.prototype.equals = function (otherLine) {
217 | return (
218 | this.isEuclideanParallelTo(otherLine) &&
219 | this.euclideanIncludesPoint(otherLine.getP0())
220 | );
221 | };
222 |
223 | Line.prototype.hyperbolicEquals = function (otherLine) {
224 | let g = this.getHyperbolicGeodesic();
225 | let otherG = otherLine.getHyperbolicGeodesic();
226 | if (
227 | g instanceof HyperbolicCanvas.Circle &&
228 | otherG instanceof HyperbolicCanvas.Circle
229 | ) {
230 | return g.equals(otherG);
231 | } else if (g instanceof Line && otherG instanceof Line) {
232 | return g.equals(otherG);
233 | } else {
234 | return false;
235 | }
236 | };
237 |
238 | Line.prototype.euclideanIntersectsWithCircle = function (circle) {
239 | // rotate circle and line by same amount about origin such that line
240 | // becomes the x-axis
241 |
242 | let angleOffset = HyperbolicCanvas.Angle.fromSlope(this.getSlope()) * -1;
243 |
244 | let offsetCircle = HyperbolicCanvas.Circle.givenEuclideanCenterRadius(
245 | circle.getEuclideanCenter().rotateAboutOrigin(angleOffset),
246 | circle.getEuclideanRadius(),
247 | );
248 |
249 | // distance from line to origin
250 | let lineOffset = Line.euclideanIntersect(
251 | this,
252 | this.euclideanPerpindicularLineAt(HyperbolicCanvas.Point.ORIGIN),
253 | ).euclideanDistanceTo(HyperbolicCanvas.Point.ORIGIN);
254 |
255 | // line passes above or below origin
256 | lineOffset *= this.euclideanYAtX(0) > 0 ? 1 : -1;
257 |
258 | let offsetIntersects = offsetCircle.pointsAtY(lineOffset);
259 | let intersects = [];
260 | for (let i = 0; i < offsetIntersects.length; i++) {
261 | intersects.push(offsetIntersects[i].rotateAboutOrigin(angleOffset * -1));
262 | }
263 | return intersects;
264 | };
265 |
266 | Line.prototype.hyperbolicIntersectsWithCircle = function (circle) {
267 | let g = this.getHyperbolicGeodesic();
268 |
269 | if (g instanceof HyperbolicCanvas.Circle) {
270 | return HyperbolicCanvas.Circle.intersects(g, circle);
271 | } else if (g instanceof Line) {
272 | return this.euclideanIntersectsWithCircle(circle);
273 | } else {
274 | return false;
275 | }
276 | };
277 |
278 | Line.prototype.euclideanXAtY = function (y) {
279 | if (this.getSlope() === 0) {
280 | // TODO use Infinity/undefined instead of true/false ?
281 | return y === this.getP0().getY();
282 | } else {
283 | return (y - this.getP0().getY()) / this.getSlope() + this.getP0().getX();
284 | }
285 | };
286 |
287 | Line.prototype.euclideanYAtX = function (x) {
288 | if (this.getSlope() === Infinity) {
289 | return x === this.getP0().getX();
290 | } else {
291 | return (x - this.getP0().getX()) * this.getSlope() + this.getP0().getY();
292 | }
293 | };
294 |
295 | Line.prototype.isIdeal = function () {
296 | return this.getP0().isIdeal() || this.getP1().isIdeal();
297 | };
298 |
299 | Line.prototype.isOnPlane = function () {
300 | return this.getP0().isOnPlane() && this.getP1().isOnPlane();
301 | };
302 |
303 | Line.prototype.isEuclideanParallelTo = function (otherLine) {
304 | return (
305 | Math.abs(this.getSlope() - otherLine.getSlope()) < HyperbolicCanvas.ZERO
306 | );
307 | };
308 |
309 | Line.prototype.isHyperbolicParallelTo = function (otherLine) {
310 | return Line.hyperbolicIntersect(this, otherLine) === false;
311 | };
312 |
313 | Line.prototype.euclideanPerpindicularBisector = function () {
314 | return this.euclideanPerpindicularLineAt(this.getEuclideanMidpoint());
315 | };
316 |
317 | Line.prototype.euclideanPerpindicularLineAt = function (point) {
318 | return Line.givenPointSlope(point, this.euclideanPerpindicularSlope());
319 | };
320 |
321 | Line.prototype.euclideanPerpindicularSlope = function () {
322 | let slope = this.getSlope();
323 | if (slope === Infinity) {
324 | return 0;
325 | } else if (slope === 0) {
326 | return Infinity;
327 | } else {
328 | return -1 / slope;
329 | }
330 | };
331 |
332 | Line.prototype.pointAtEuclideanX = function (x) {
333 | let y = this.euclideanYAtX(x);
334 | if (typeof y === 'boolean') {
335 | if (y) {
336 | y = 0;
337 | } else {
338 | return y;
339 | }
340 | }
341 | return HyperbolicCanvas.Point.givenCoordinates(x, y);
342 | };
343 |
344 | Line.prototype.pointAtEuclideanY = function (y) {
345 | let x = this.euclideanXAtY(y);
346 | if (typeof x === 'boolean') {
347 | if (x) {
348 | x = 0;
349 | } else {
350 | return x;
351 | }
352 | }
353 | return HyperbolicCanvas.Point.givenCoordinates(x, y);
354 | };
355 |
356 | Line.prototype._calculateGeodesicThroughTwoIdealPoints = function () {
357 | let a0 = this.getP0().getAngle();
358 | let a1 = this.getP1().getAngle();
359 | if (
360 | Math.abs(a0 - HyperbolicCanvas.Angle.opposite(a1)) < HyperbolicCanvas.ZERO
361 | ) {
362 | this._geodesic = this;
363 | } else {
364 | let t0 = HyperbolicCanvas.Circle.UNIT.euclideanTangentAtPoint(this.getP0());
365 | let t1 = HyperbolicCanvas.Circle.UNIT.euclideanTangentAtPoint(this.getP1());
366 | let center = Line.euclideanIntersect(t0, t1);
367 | this._geodesic = HyperbolicCanvas.Circle.givenEuclideanCenterRadius(
368 | center,
369 | center.euclideanDistanceTo(this.getP0()),
370 | );
371 | }
372 | };
373 |
374 | Line.prototype._calculateGeodesicThroughOnePointOnPlane = function () {
375 | let l0 = Line.givenTwoPoints(this.getP0(), HyperbolicCanvas.Point.ORIGIN);
376 | let l1 = Line.givenTwoPoints(this.getP1(), HyperbolicCanvas.Point.ORIGIN);
377 |
378 | if (l0.equals(l1)) {
379 | // both points are colinear with origin, so geodesic is a Line, itself
380 | return (this._geodesic = this);
381 | }
382 |
383 | // get the line through point on plane, which is perpindicular to origin
384 | // get intersects of that line with unit circle
385 | let intersects;
386 |
387 | if (this.getP0().isIdeal()) {
388 | intersects = l1
389 | .euclideanPerpindicularLineAt(this.getP1())
390 | .getEuclideanUnitCircleIntersects();
391 | } else {
392 | intersects = l0
393 | .euclideanPerpindicularLineAt(this.getP0())
394 | .getEuclideanUnitCircleIntersects();
395 | }
396 |
397 | if (!intersects || intersects.length < 2) {
398 | // line is outside of or tangent to unit circle
399 | return (this._geodesic = false);
400 | }
401 |
402 | let t0 = HyperbolicCanvas.Circle.UNIT.euclideanTangentAtPoint(intersects[0]);
403 | let t1 = HyperbolicCanvas.Circle.UNIT.euclideanTangentAtPoint(intersects[1]);
404 |
405 | let c = Line.euclideanIntersect(t0, t1);
406 |
407 | this._geodesic = HyperbolicCanvas.Circle.givenThreePoints(
408 | this.getP0(),
409 | this.getP1(),
410 | c,
411 | );
412 | };
413 |
414 | Line.euclideanIntersect = function (l0, l1) {
415 | let x, y;
416 |
417 | let l0m = l0.getSlope();
418 | let l1m = l1.getSlope();
419 |
420 | if (l0m === l1m) {
421 | // lines are parallel; lines may also be the same line
422 | return false;
423 | }
424 |
425 | let l0x = l0.getP0().getX();
426 | let l1x = l1.getP0().getX();
427 | let l0y = l0.getP0().getY();
428 | let l1y = l1.getP0().getY();
429 |
430 | if (l0m === Infinity) {
431 | x = l0x;
432 | } else if (l1m === Infinity) {
433 | x = l1x;
434 | } else {
435 | x = (l0x * l0m - l1x * l1m + l1y - l0y) / (l0m - l1m);
436 | }
437 |
438 | if (l0m === 0) {
439 | y = l0y;
440 | } else if (l1m === 0) {
441 | y = l1y;
442 | } else {
443 | y = l0m === Infinity ? l1m * (x - l1x) + l1y : l0m * (x - l0x) + l0y;
444 | }
445 |
446 | return HyperbolicCanvas.Point.givenCoordinates(x, y);
447 | };
448 |
449 | Line.hyperbolicIntersect = function (l0, l1) {
450 | if (!(l0.isOnPlane() && l1.isOnPlane())) {
451 | return false;
452 | }
453 | let g0 = l0.getHyperbolicGeodesic();
454 | let g1 = l1.getHyperbolicGeodesic();
455 |
456 | let g0IsLine = g0 instanceof Line;
457 | let g1IsLine = g1 instanceof Line;
458 |
459 | if (g0IsLine || g1IsLine) {
460 | if (g0IsLine && g1IsLine) {
461 | return HyperbolicCanvas.Point.ORIGIN;
462 | }
463 | let circle = g0IsLine ? g1 : g0;
464 | let line = g0IsLine ? g0 : g1;
465 |
466 | let angleOffset = HyperbolicCanvas.Angle.fromSlope(line.getSlope()) * -1;
467 |
468 | let offsetCircle = HyperbolicCanvas.Circle.givenEuclideanCenterRadius(
469 | circle.getEuclideanCenter().rotateAboutOrigin(angleOffset),
470 | circle.getEuclideanRadius(),
471 | );
472 |
473 | let offsetIntersects = offsetCircle.pointsAtY(0);
474 | let intersects = [];
475 | for (let i = 0; i < offsetIntersects.length; i++) {
476 | intersects.push(offsetIntersects[i].rotateAboutOrigin(angleOffset * -1));
477 | }
478 |
479 | for (let i = 0; i < intersects.length; i++) {
480 | if (intersects[i].isOnPlane()) {
481 | return intersects[i];
482 | }
483 | }
484 | return false;
485 | }
486 |
487 | let intersects = HyperbolicCanvas.Circle.intersects(g0, g1);
488 |
489 | if (!intersects) {
490 | return false;
491 | }
492 |
493 | for (let i = 0; i < intersects.length; i++) {
494 | if (intersects[i].isOnPlane()) {
495 | return intersects[i];
496 | }
497 | }
498 | // this should never happen
499 | return false;
500 | };
501 |
502 | Line.randomSlope = function () {
503 | return HyperbolicCanvas.Angle.toSlope(HyperbolicCanvas.Angle.random());
504 | };
505 |
506 | Line.givenPointSlope = function (p, slope) {
507 | return new Line({
508 | p0: p,
509 | m: Math.abs(slope) > HyperbolicCanvas.INFINITY ? Infinity : slope,
510 | });
511 | };
512 |
513 | Line.givenTwoPoints = function (p0, p1) {
514 | return new Line({ p0: p0, p1: p1 });
515 | };
516 |
517 | Line.givenAnglesOfIdealPoints = function (a0, a1) {
518 | let points = [
519 | HyperbolicCanvas.Point.givenIdealAngle(a0),
520 | HyperbolicCanvas.Point.givenIdealAngle(a1),
521 | ];
522 | return new Line({
523 | p0: points[0],
524 | p1: points[1],
525 | euclideanUnitCircleIntersects: points,
526 | idealPoints: points,
527 | });
528 | };
529 |
530 | Line.X_AXIS = new Line({ p0: HyperbolicCanvas.Point.ORIGIN, m: 0 });
531 |
532 | Line.Y_AXIS = new Line({ p0: HyperbolicCanvas.Point.ORIGIN, m: Infinity });
533 |
--------------------------------------------------------------------------------
/src/point.js:
--------------------------------------------------------------------------------
1 | const HyperbolicCanvas = require('./hyperbolic_canvas.js');
2 |
3 | let Point = (HyperbolicCanvas.Point = function (options) {
4 | this._angle = options.angle;
5 | this._euclideanRadius = options.euclideanRadius;
6 | this._direction = options.direction;
7 | this._hyperbolicRadius = options.hyperbolicRadius;
8 | this._x = options.x;
9 | this._y = options.y;
10 | });
11 |
12 | Point.prototype.getAngle = function () {
13 | if (typeof this._angle === 'undefined') {
14 | this._angle = HyperbolicCanvas.Angle.normalize(
15 | Math.atan2(this.getY(), this.getX()),
16 | );
17 | }
18 | return this._angle;
19 | };
20 |
21 | Point.prototype.getDirection = function (direction) {
22 | if (typeof direction !== 'undefined') {
23 | return HyperbolicCanvas.Angle.normalize(direction);
24 | }
25 | if (typeof this._direction !== 'undefined') {
26 | return this._direction;
27 | }
28 | return this.getAngle();
29 | };
30 |
31 | Point.prototype.getEuclideanRadius = function () {
32 | if (typeof this._euclideanRadius === 'undefined') {
33 | if (typeof this._x === 'undefined' || this._y === 'undefined') {
34 | if (this.getHyperbolicRadius() === Infinity) {
35 | this._euclideanRadius = 1;
36 | } else {
37 | this._euclideanRadius =
38 | (Math.exp(this.getHyperbolicRadius()) - 1) /
39 | (Math.exp(this.getHyperbolicRadius()) + 1);
40 | }
41 | } else {
42 | this._euclideanRadius = Math.sqrt(
43 | Math.pow(this.getX(), 2) + Math.pow(this.getY(), 2),
44 | );
45 | }
46 | if (Math.abs(this._euclideanRadius - 1) < HyperbolicCanvas.ZERO) {
47 | this._euclideanRadius = 1;
48 | }
49 | }
50 | return this._euclideanRadius;
51 | };
52 |
53 | Point.prototype.getHyperbolicRadius = function () {
54 | if (typeof this._hyperbolicRadius === 'undefined') {
55 | if (this.isIdeal()) {
56 | this._hyperbolicRadius = Infinity;
57 | } else {
58 | this._hyperbolicRadius = 2 * Math.atanh(this.getEuclideanRadius());
59 | }
60 | }
61 | return this._hyperbolicRadius;
62 | };
63 |
64 | Point.prototype.getX = function () {
65 | if (typeof this._x === 'undefined') {
66 | this._x = this.getEuclideanRadius() * Math.cos(this.getAngle());
67 | }
68 | return this._x;
69 | };
70 |
71 | Point.prototype.getY = function () {
72 | if (typeof this._y === 'undefined') {
73 | this._y = this.getEuclideanRadius() * Math.sin(this.getAngle());
74 | }
75 | return this._y;
76 | };
77 |
78 | Point.prototype.equals = function (otherPoint) {
79 | return (
80 | Math.abs(this.getX() - otherPoint.getX()) < HyperbolicCanvas.ZERO &&
81 | Math.abs(this.getY() - otherPoint.getY()) < HyperbolicCanvas.ZERO
82 | );
83 | };
84 |
85 | Point.prototype.clone = function () {
86 | return new Point({
87 | angle: this._angle,
88 | direction: this._direction,
89 | euclideanRadius: this._euclideanRadius,
90 | hyperbolicRadius: this._hyperbolicRadius,
91 | x: this._x,
92 | y: this._y,
93 | });
94 | };
95 |
96 | Point.prototype.euclideanAngleFrom = function (otherPoint) {
97 | return otherPoint.euclideanAngleTo(this);
98 | };
99 |
100 | Point.prototype.euclideanAngleTo = function (otherPoint) {
101 | return HyperbolicCanvas.Angle.normalize(
102 | Math.atan2(
103 | otherPoint.getY() - this.getY(),
104 | otherPoint.getX() - this.getX(),
105 | ),
106 | );
107 | };
108 |
109 | Point.prototype.euclideanDistanceTo = function (otherPoint) {
110 | return Math.sqrt(
111 | Math.pow(this.getX() - otherPoint.getX(), 2) +
112 | Math.pow(this.getY() - otherPoint.getY(), 2),
113 | );
114 | };
115 |
116 | Point.prototype.euclideanDistantPoint = function (distance, direction) {
117 | let bearing = this.getDirection(direction);
118 | let distantPoint = Point.givenCoordinates(
119 | this.getX() + Math.cos(bearing) * distance,
120 | this.getY() + Math.sin(bearing) * distance,
121 | );
122 | distantPoint._setDirection(bearing);
123 | return distantPoint;
124 | };
125 |
126 | Point.prototype.hyperbolicAngleFrom = function (otherPoint) {
127 | return otherPoint.hyperbolicAngleTo(this);
128 | };
129 |
130 | Point.prototype.hyperbolicAngleTo = function (otherPoint) {
131 | let geodesic = HyperbolicCanvas.Line.givenTwoPoints(
132 | this,
133 | otherPoint,
134 | ).getHyperbolicGeodesic();
135 |
136 | let intersect;
137 |
138 | if (geodesic instanceof HyperbolicCanvas.Circle) {
139 | let t0 = geodesic.euclideanTangentAtPoint(this);
140 | let t1 = geodesic.euclideanTangentAtPoint(otherPoint);
141 | intersect = HyperbolicCanvas.Line.euclideanIntersect(t0, t1);
142 | } else {
143 | intersect = otherPoint;
144 | }
145 | return this.euclideanAngleTo(intersect);
146 | };
147 |
148 | Point.prototype.hyperbolicDistanceTo = function (otherPoint) {
149 | if (this.isIdeal() || otherPoint.isIdeal()) {
150 | return Infinity;
151 | }
152 | let b = this.getHyperbolicRadius();
153 | let c = otherPoint.getHyperbolicRadius();
154 | let alpha = this.getAngle() - otherPoint.getAngle();
155 |
156 | return Math.acosh(
157 | Math.cosh(b) * Math.cosh(c) - Math.sinh(b) * Math.sinh(c) * Math.cos(alpha),
158 | );
159 | };
160 |
161 | Point.prototype.hyperbolicDistantPoint = function (distance, direction) {
162 | /*
163 | Hyperbolic Law of Cosines
164 | cosh(a) === cosh(b)cosh(c) - sinh(b)sinh(c)cos(alpha)
165 |
166 | A: this point
167 | B: distant point
168 | C: origin
169 | a: hyperbolic radius of distant point
170 | b: hyperbolic radius of this point
171 | c: distance from this point to distant point
172 | alpha, beta, gamma: angles of triangle ABC at A, B, and C, respectively
173 |
174 | bearing: direction from this point to distant point
175 | aAngle: direction from origin to this point
176 | bAngle: direction from origin to distant point
177 | */
178 | // TODO hyperbolic law of haversines
179 | // TODO throw exception if direction is not provided or stored; do not default to this.getAngle()
180 | // TODO allow distance of Infinity, return ideal Point
181 | let bearing = this.getDirection(direction);
182 |
183 | let c = distance;
184 |
185 | if (Math.abs(c) < HyperbolicCanvas.ZERO) {
186 | let point = this.clone();
187 | point._setDirection(bearing);
188 | return point;
189 | }
190 | if (this.equals(Point.ORIGIN)) {
191 | let point = Point.givenHyperbolicPolarCoordinates(c, bearing);
192 | point._setDirection(bearing);
193 | return point;
194 | }
195 |
196 | let aAngle = this.getAngle();
197 | let b = this.getHyperbolicRadius();
198 |
199 | if (Math.abs(aAngle - bearing) < HyperbolicCanvas.ZERO) {
200 | let point = Point.givenHyperbolicPolarCoordinates(b + c, aAngle);
201 | point._setDirection(bearing);
202 | return point;
203 | }
204 |
205 | let alpha = Math.abs(Math.PI - Math.abs(aAngle - bearing));
206 |
207 | if (alpha < HyperbolicCanvas.ZERO) {
208 | let point = Point.givenHyperbolicPolarCoordinates(b - c, aAngle);
209 | point._setDirection(bearing);
210 | return point;
211 | }
212 |
213 | // save hyperbolic functions which are called more than once
214 | let coshb = Math.cosh(b);
215 | let coshc = Math.cosh(c);
216 | let sinhb = Math.sinh(b);
217 | let sinhc = Math.sinh(c);
218 |
219 | let a = Math.acosh(coshb * coshc - sinhb * sinhc * Math.cos(alpha));
220 |
221 | let cosha = Math.cosh(a);
222 | let sinha = Math.sinh(a);
223 |
224 | // correct potential floating point error before calling acos
225 | let cosgamma = (cosha * coshb - coshc) / (sinha * sinhb);
226 | cosgamma = cosgamma > 1 ? 1 : cosgamma < -1 ? -1 : cosgamma;
227 | let gamma = Math.acos(cosgamma);
228 |
229 | // determine whether aAngle is +/- gamma
230 | let aAngleOpposite = HyperbolicCanvas.Angle.opposite(aAngle);
231 | let dir =
232 | aAngle > aAngleOpposite
233 | ? bearing > aAngleOpposite && bearing < aAngle
234 | ? -1
235 | : 1
236 | : bearing > aAngle && bearing < aAngleOpposite
237 | ? 1
238 | : -1;
239 | let bAngle = HyperbolicCanvas.Angle.normalize(aAngle + gamma * dir);
240 | let distantPoint = Point.givenHyperbolicPolarCoordinates(a, bAngle);
241 |
242 | // correct potential floating point error before calling acos
243 | let cosbeta = (cosha * coshc - coshb) / (sinha * sinhc);
244 | cosbeta = cosbeta > 1 ? 1 : cosbeta < -1 ? -1 : cosbeta;
245 | let beta = Math.acos(cosbeta);
246 |
247 | distantPoint._setDirection(bAngle + beta * dir);
248 |
249 | return distantPoint;
250 | };
251 |
252 | Point.prototype.isIdeal = function () {
253 | return (
254 | this._euclideanRadius === 1 ||
255 | this._hyperbolicRadius === Infinity ||
256 | this.getEuclideanRadius() === 1
257 | );
258 | };
259 |
260 | Point.prototype.isOnPlane = function () {
261 | return (
262 | this._euclideanRadius < 1 ||
263 | this._hyperbolicRadius < Infinity ||
264 | this.getEuclideanRadius() < 1
265 | );
266 | };
267 |
268 | Point.prototype.opposite = function () {
269 | return Point.givenEuclideanPolarCoordinates(
270 | this.getEuclideanRadius(),
271 | HyperbolicCanvas.Angle.opposite(this.getAngle()),
272 | );
273 | };
274 |
275 | Point.prototype.quadrant = function () {
276 | return Math.floor(this.getAngle() / (Math.PI / 2) + 1);
277 | };
278 |
279 | Point.prototype.rotateAboutOrigin = function (angle) {
280 | return Point.givenEuclideanPolarCoordinates(
281 | this.getEuclideanRadius(),
282 | this.getAngle() + angle,
283 | );
284 | };
285 |
286 | Point.prototype._setDirection = function (direction) {
287 | this._direction = HyperbolicCanvas.Angle.normalize(direction);
288 | };
289 |
290 | Point.euclideanBetween = function (p0, p1) {
291 | return new Point({
292 | x: (p0.getX() + p1.getX()) / 2,
293 | y: (p0.getY() + p1.getY()) / 2,
294 | });
295 | };
296 |
297 | Point.hyperbolicBetween = function (p0, p1) {
298 | if (!(p0.isOnPlane() && p1.isOnPlane())) {
299 | return false;
300 | }
301 | let d = p0.hyperbolicDistanceTo(p1);
302 | return p0.hyperbolicDistantPoint(d / 2, p0.hyperbolicAngleTo(p1));
303 | };
304 |
305 | Point.givenCoordinates = function (x, y) {
306 | return new Point({ x: x, y: y });
307 | };
308 |
309 | Point.givenEuclideanPolarCoordinates = function (radius, angle) {
310 | if (radius < 0) {
311 | angle += Math.PI;
312 | radius = Math.abs(radius);
313 | }
314 |
315 | return new Point({
316 | angle: HyperbolicCanvas.Angle.normalize(angle),
317 | euclideanRadius: radius,
318 | });
319 | };
320 |
321 | Point.givenHyperbolicPolarCoordinates = function (radius, angle) {
322 | // returns NaN coordinates at distance > 709
323 | // at angle 0, indistinguishable from limit at distance > 36
324 | if (radius < 0) {
325 | angle += Math.PI;
326 | radius = Math.abs(radius);
327 | }
328 |
329 | return new Point({
330 | angle: HyperbolicCanvas.Angle.normalize(angle),
331 | hyperbolicRadius: radius,
332 | });
333 | };
334 |
335 | Point.givenIdealAngle = function (angle) {
336 | return Point.givenEuclideanPolarCoordinates(1, angle);
337 | };
338 |
339 | Point.random = function (quadrant) {
340 | return Point.givenEuclideanPolarCoordinates(
341 | Math.random(),
342 | HyperbolicCanvas.Angle.random(quadrant),
343 | );
344 | };
345 |
346 | Point.ORIGIN = Point.CENTER = new Point({ x: 0, y: 0 });
347 |
348 | Point.ORIGIN.getAngle =
349 | Point.ORIGIN.getEuclideanRadius =
350 | Point.ORIGIN.getHyperbolicRadius =
351 | Point.ORIGIN.getX =
352 | Point.ORIGIN.getY =
353 | function () {
354 | return 0;
355 | };
356 |
357 | Point.ORIGIN.euclideanAngleFrom = Point.ORIGIN.hyperbolicAngleFrom = function (
358 | otherPoint,
359 | ) {
360 | return HyperbolicCanvas.Angle.opposite(this.euclideanAngleTo(otherPoint));
361 | };
362 |
363 | Point.ORIGIN.euclideanAngleTo = Point.ORIGIN.hyperbolicAngleTo = function (
364 | otherPoint,
365 | ) {
366 | return otherPoint.getAngle();
367 | };
368 |
369 | Point.ORIGIN.euclideanDistanceTo = function (otherPoint) {
370 | return otherPoint.getEuclideanRadius();
371 | };
372 |
373 | Point.ORIGIN.hyperbolicDistanceTo = function (otherPoint) {
374 | return otherPoint.getHyperbolicRadius();
375 | };
376 |
377 | Point.ORIGIN.euclideanDistantPoint = function (distance, direction) {
378 | return Point.givenEuclideanCenterRadius(
379 | distance,
380 | this.getDirection(direction),
381 | );
382 | };
383 |
384 | Point.ORIGIN.hyperbolicDistantPoint = function (distance, direction) {
385 | return Point.givenHyperbolicPolarCoordinates(
386 | distance,
387 | this.getDirection(direction),
388 | );
389 | };
390 |
--------------------------------------------------------------------------------
/src/polygon.js:
--------------------------------------------------------------------------------
1 | const HyperbolicCanvas = require('./hyperbolic_canvas.js');
2 |
3 | let Polygon = (HyperbolicCanvas.Polygon = function (options) {
4 | this._vertices = options.vertices;
5 | this._lines = options.lines;
6 | });
7 |
8 | Polygon.prototype.getLines = function () {
9 | if (typeof this._lines === 'undefined') {
10 | this._lines = [];
11 | let vertices = this.getVertices();
12 | let n = vertices.length;
13 | for (let i = 0; i < vertices.length; i++) {
14 | this._lines.push(
15 | HyperbolicCanvas.Line.givenTwoPoints(
16 | vertices[i],
17 | vertices[(i + 1) % n],
18 | ),
19 | );
20 | }
21 | }
22 | return this._lines;
23 | };
24 |
25 | Polygon.prototype.getVertices = function () {
26 | return this._vertices;
27 | };
28 |
29 | Polygon.givenAnglesOfIdealVertices = function (angles) {
30 | if (angles.length < 3) {
31 | return false;
32 | }
33 |
34 | let vertices = [];
35 |
36 | angles.forEach(function (angle) {
37 | vertices.push(HyperbolicCanvas.Point.givenIdealAngle(angle));
38 | });
39 |
40 | return Polygon.givenVertices(vertices);
41 | };
42 |
43 | Polygon.givenVertices = function (vertices) {
44 | if (vertices.length < 3) {
45 | return false;
46 | }
47 |
48 | return new Polygon({ vertices: vertices });
49 | };
50 |
51 | Polygon.givenEuclideanNCenterRadius = function (n, center, radius, rotation) {
52 | if (n < 3) {
53 | return false;
54 | }
55 | rotation = rotation ? HyperbolicCanvas.Angle.normalize(rotation) : 0;
56 |
57 | let increment = Math.TAU / n;
58 | let vertices = [];
59 |
60 | for (let i = 0; i < n; i++) {
61 | vertices.push(center.euclideanDistantPoint(radius, rotation));
62 | rotation = HyperbolicCanvas.Angle.normalize(rotation + increment);
63 | }
64 |
65 | return new Polygon({ vertices: vertices });
66 | };
67 |
68 | Polygon.givenHyperbolicNCenterRadius = function (n, center, radius, rotation) {
69 | if (n < 3) {
70 | return false;
71 | }
72 | if (!center.isOnPlane()) {
73 | return false;
74 | }
75 | rotation = rotation ? HyperbolicCanvas.Angle.normalize(rotation) : 0;
76 |
77 | let increment = Math.TAU / n;
78 | let vertices = [];
79 |
80 | for (let i = 0; i < n; i++) {
81 | vertices.push(center.hyperbolicDistantPoint(radius, rotation));
82 | rotation = HyperbolicCanvas.Angle.normalize(rotation + increment);
83 | }
84 |
85 | return new Polygon({ vertices: vertices });
86 | };
87 |
--------------------------------------------------------------------------------