├── .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 | 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 | 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 | 55 |
56 | 57 |
58 |
59 |

Examples

60 | 72 | 84 |
88 |

Hand-Drawn Polygon

89 |

90 | Click to draw your own hyperbolic polygon. 91 |

92 |
93 | 105 |
109 |

Concentric Circles

110 |

111 | Hyperbolic circles look like circles. 112 |

113 |
114 | 115 |

Bad Examples

116 |
117 |

Very Bad Comets

118 |

119 | It's not that bad any more. Point and click. 120 |

121 |
122 |
123 |

Annoying Web

124 |

125 | Ill-conditioned functions and floating point rounding 127 | errors. 129 |

130 |
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 | 52 |
53 | 54 | 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 | --------------------------------------------------------------------------------