├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── lee.js ├── libs ├── tinycolor.js └── tinygradient.js ├── package.json └── spec ├── LeeSpec.js └── support └── jasmine.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 James Milner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :runner: Lee Algorithm in JavaScript 2 | 3 | # Demo 4 | Check out a little demo [here](https://jameslmilner.github.io/lee-algorithm-js/) :) 5 | 6 | # What is it? 7 | 8 | A version of the [Lee Algorithm](https://en.wikipedia.org/wiki/Lee_algorithm) implemented in JavaScript. The Lee algorithm is a route finding algorithm that is suited to solving maze problems. 9 | 10 |

11 | 12 |

13 | 14 | # Background 15 | 16 | This implementation came out of the problem set at London Algorithms (which is a fantastic meetup by the way!) and was a lot of fun to try and code. 17 | 18 | # How To 19 | 20 | The implementation takes a matrix n * m, a starting cell and an end cell. It can return the completed matrix with the distances and also a single best path for the matrix. 21 | 22 | ```javascript 23 | 24 | var aMatrix = [ 25 | [0, 0, 0, 0, 0], 26 | [0, -1, -1, -1, 0], 27 | [0, 0, -1, 0, -1], 28 | [-1, 0, 0, 0, -1], 29 | [-1, -1, -1, 0, -1] 30 | ]; 31 | 32 | console.log("FINAL MATRIX : \n", pathfinder(aMatrix, 0, 0, 4, 3)); 33 | console.log("BEST PATH : \n", backtrace(aMatrix, 0, 0, 4, 3)); 34 | 35 | ``` 36 | 37 | # Tests 38 | 39 | The test suite uses Jasmine. You can use: 40 | 41 | npm install 42 | jasmine 43 | 44 | to test the code. 45 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Maze Path Finding Using the Lee Algorithm 5 | 6 | 66 | 67 | 68 | 69 |

Maze Path Finding

70 | 71 |
72 |

73 | Let us imagine the Maze as a matrix made up of 0s (empty space) and -1s (walls/obstacles) like so: 74 |

75 |
76 | 77 |
78 |
79 |
 80 | 
 81 | var aMatrix = [
 82 |     [0,  0, 0,  0, 0],
 83 |     [0, -1, -1, -1, 0],
 84 |     [0,  0, 0, 0, -1],
 85 |     [-1, 0, -1, 0, -1],
 86 |     [-1, -1, -1, 0, -1]
 87 | ];
 88 |             
89 |
90 |
91 | 92 |
93 |

94 | The Lee Algorithm allows us to take an arbitary start and end cell within the matrix and attempt to calculate a viable route 95 | through between the two cells. In this case we are starting at 0,1 and aiming to get to 6,6. 96 | Try it below! 97 |

98 |
99 | 100 |
101 | 102 |
103 | 104 |
105 |
106 |

A little project by James Milner

107 |
108 | 109 | 110 | 111 | 112 | 113 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /lee.js: -------------------------------------------------------------------------------- 1 | 2 | var lee = (function() { 3 | 4 | var pathfinder = function (matrix, x1, y1, x2, y2) { 5 | 6 | var toVisit = [[x1, y1]]; // Initialise at the start square 7 | 8 | while(toVisit.length) { // While there are still squares to visit 9 | 10 | x = toVisit[0][0]; 11 | y = toVisit[0][1]; 12 | 13 | for (var i = x-1; i < x+2; i++) { // -1, 0, 1 14 | for (var j = y-1; j < y+2; j++) { // -1, 0, 1 15 | 16 | if (neighbourCheck(matrix, i,j, x1, y1, 0)) { 17 | 18 | matrix[i][j] = matrix[x][y] + 1; 19 | toVisit.push([i, j]); 20 | 21 | } 22 | 23 | } 24 | } 25 | 26 | var shift = toVisit.shift(); 27 | 28 | } 29 | 30 | var distance = matrix[x2][y2]; 31 | return [matrix, distance]; 32 | 33 | }; 34 | 35 | var backtrace = function(matrix, x1, y1, x2, y2) { 36 | 37 | var previousValue = matrix[x2][y2]; 38 | var successfulRoute = []; 39 | 40 | var x = x2; 41 | var y = y2; 42 | 43 | while ( !(x === x1 && y === y1) ) { 44 | 45 | for (var i = x-1; i < x+2; i++) { // -1, 0, 1 46 | for (var j = y-1; j < y+2; j++) { // -1, 0, 1 47 | 48 | if ( 49 | matrix[i] && (matrix[i][j] === previousValue -1) && // If array x array defined and the matrix value is 0 50 | !(i === x && j === y) ) { 51 | 52 | previousValue = matrix[i][j]; 53 | successfulRoute.push([i, j]); 54 | x = i; 55 | y = j; 56 | 57 | } else if (successfulRoute.length == matrix[x2][y2] - 1) { // If we got to the end of the route 58 | x = x1; 59 | y = y1; // Break the while loop 60 | } 61 | 62 | } 63 | } 64 | 65 | } 66 | 67 | successfulRoute.unshift([x2, y2]); // Add end point 68 | successfulRoute.push([x1, y1]); // Add start point 69 | return successfulRoute.reverse(); // Reverse the array so it's at the start 70 | 71 | }; 72 | 73 | var neighbourCheck = function(matrix, i, j, x1, y1, value) { 74 | return matrix[i] && (matrix[i][j] === value) && // If array x array defined and the matrix value is 0 75 | !(i === x && j === y) && // If it's not the center square 76 | !(i === x1 && j === y1); // If it's not the first square 77 | }; 78 | 79 | return { 80 | pathfinder : pathfinder, 81 | backtrace : backtrace, 82 | neighbourCheck : neighbourCheck 83 | }; 84 | 85 | })(); 86 | -------------------------------------------------------------------------------- /libs/tinycolor.js: -------------------------------------------------------------------------------- 1 | // TinyColor v1.3.0 2 | // https://github.com/bgrins/TinyColor 3 | // Brian Grinstead, MIT License 4 | 5 | (function(Math) { 6 | 7 | var trimLeft = /^\s+/, 8 | trimRight = /\s+$/, 9 | tinyCounter = 0, 10 | mathRound = Math.round, 11 | mathMin = Math.min, 12 | mathMax = Math.max, 13 | mathRandom = Math.random; 14 | 15 | function tinycolor (color, opts) { 16 | 17 | color = (color) ? color : ''; 18 | opts = opts || { }; 19 | 20 | // If input is already a tinycolor, return itself 21 | if (color instanceof tinycolor) { 22 | return color; 23 | } 24 | // If we are called as a function, call using new instead 25 | if (!(this instanceof tinycolor)) { 26 | return new tinycolor(color, opts); 27 | } 28 | 29 | var rgb = inputToRGB(color); 30 | this._originalInput = color, 31 | this._r = rgb.r, 32 | this._g = rgb.g, 33 | this._b = rgb.b, 34 | this._a = rgb.a, 35 | this._roundA = mathRound(100*this._a) / 100, 36 | this._format = opts.format || rgb.format; 37 | this._gradientType = opts.gradientType; 38 | 39 | // Don't let the range of [0,255] come back in [0,1]. 40 | // Potentially lose a little bit of precision here, but will fix issues where 41 | // .5 gets interpreted as half of the total, instead of half of 1 42 | // If it was supposed to be 128, this was already taken care of by `inputToRgb` 43 | if (this._r < 1) { this._r = mathRound(this._r); } 44 | if (this._g < 1) { this._g = mathRound(this._g); } 45 | if (this._b < 1) { this._b = mathRound(this._b); } 46 | 47 | this._ok = rgb.ok; 48 | this._tc_id = tinyCounter++; 49 | } 50 | 51 | tinycolor.prototype = { 52 | isDark: function() { 53 | return this.getBrightness() < 128; 54 | }, 55 | isLight: function() { 56 | return !this.isDark(); 57 | }, 58 | isValid: function() { 59 | return this._ok; 60 | }, 61 | getOriginalInput: function() { 62 | return this._originalInput; 63 | }, 64 | getFormat: function() { 65 | return this._format; 66 | }, 67 | getAlpha: function() { 68 | return this._a; 69 | }, 70 | getBrightness: function() { 71 | //http://www.w3.org/TR/AERT#color-contrast 72 | var rgb = this.toRgb(); 73 | return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000; 74 | }, 75 | getLuminance: function() { 76 | //http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef 77 | var rgb = this.toRgb(); 78 | var RsRGB, GsRGB, BsRGB, R, G, B; 79 | RsRGB = rgb.r/255; 80 | GsRGB = rgb.g/255; 81 | BsRGB = rgb.b/255; 82 | 83 | if (RsRGB <= 0.03928) {R = RsRGB / 12.92;} else {R = Math.pow(((RsRGB + 0.055) / 1.055), 2.4);} 84 | if (GsRGB <= 0.03928) {G = GsRGB / 12.92;} else {G = Math.pow(((GsRGB + 0.055) / 1.055), 2.4);} 85 | if (BsRGB <= 0.03928) {B = BsRGB / 12.92;} else {B = Math.pow(((BsRGB + 0.055) / 1.055), 2.4);} 86 | return (0.2126 * R) + (0.7152 * G) + (0.0722 * B); 87 | }, 88 | setAlpha: function(value) { 89 | this._a = boundAlpha(value); 90 | this._roundA = mathRound(100*this._a) / 100; 91 | return this; 92 | }, 93 | toHsv: function() { 94 | var hsv = rgbToHsv(this._r, this._g, this._b); 95 | return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a }; 96 | }, 97 | toHsvString: function() { 98 | var hsv = rgbToHsv(this._r, this._g, this._b); 99 | var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100); 100 | return (this._a == 1) ? 101 | "hsv(" + h + ", " + s + "%, " + v + "%)" : 102 | "hsva(" + h + ", " + s + "%, " + v + "%, "+ this._roundA + ")"; 103 | }, 104 | toHsl: function() { 105 | var hsl = rgbToHsl(this._r, this._g, this._b); 106 | return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a }; 107 | }, 108 | toHslString: function() { 109 | var hsl = rgbToHsl(this._r, this._g, this._b); 110 | var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100); 111 | return (this._a == 1) ? 112 | "hsl(" + h + ", " + s + "%, " + l + "%)" : 113 | "hsla(" + h + ", " + s + "%, " + l + "%, "+ this._roundA + ")"; 114 | }, 115 | toHex: function(allow3Char) { 116 | return rgbToHex(this._r, this._g, this._b, allow3Char); 117 | }, 118 | toHexString: function(allow3Char) { 119 | return '#' + this.toHex(allow3Char); 120 | }, 121 | toHex8: function() { 122 | return rgbaToHex(this._r, this._g, this._b, this._a); 123 | }, 124 | toHex8String: function() { 125 | return '#' + this.toHex8(); 126 | }, 127 | toRgb: function() { 128 | return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a }; 129 | }, 130 | toRgbString: function() { 131 | return (this._a == 1) ? 132 | "rgb(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" : 133 | "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")"; 134 | }, 135 | toPercentageRgb: function() { 136 | return { r: mathRound(bound01(this._r, 255) * 100) + "%", g: mathRound(bound01(this._g, 255) * 100) + "%", b: mathRound(bound01(this._b, 255) * 100) + "%", a: this._a }; 137 | }, 138 | toPercentageRgbString: function() { 139 | return (this._a == 1) ? 140 | "rgb(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" : 141 | "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")"; 142 | }, 143 | toName: function() { 144 | if (this._a === 0) { 145 | return "transparent"; 146 | } 147 | 148 | if (this._a < 1) { 149 | return false; 150 | } 151 | 152 | return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false; 153 | }, 154 | toFilter: function(secondColor) { 155 | var hex8String = '#' + rgbaToHex(this._r, this._g, this._b, this._a); 156 | var secondHex8String = hex8String; 157 | var gradientType = this._gradientType ? "GradientType = 1, " : ""; 158 | 159 | if (secondColor) { 160 | var s = tinycolor(secondColor); 161 | secondHex8String = s.toHex8String(); 162 | } 163 | 164 | return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")"; 165 | }, 166 | toString: function(format) { 167 | var formatSet = !!format; 168 | format = format || this._format; 169 | 170 | var formattedString = false; 171 | var hasAlpha = this._a < 1 && this._a >= 0; 172 | var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "name"); 173 | 174 | if (needsAlphaFormat) { 175 | // Special case for "transparent", all other non-alpha formats 176 | // will return rgba when there is transparency. 177 | if (format === "name" && this._a === 0) { 178 | return this.toName(); 179 | } 180 | return this.toRgbString(); 181 | } 182 | if (format === "rgb") { 183 | formattedString = this.toRgbString(); 184 | } 185 | if (format === "prgb") { 186 | formattedString = this.toPercentageRgbString(); 187 | } 188 | if (format === "hex" || format === "hex6") { 189 | formattedString = this.toHexString(); 190 | } 191 | if (format === "hex3") { 192 | formattedString = this.toHexString(true); 193 | } 194 | if (format === "hex8") { 195 | formattedString = this.toHex8String(); 196 | } 197 | if (format === "name") { 198 | formattedString = this.toName(); 199 | } 200 | if (format === "hsl") { 201 | formattedString = this.toHslString(); 202 | } 203 | if (format === "hsv") { 204 | formattedString = this.toHsvString(); 205 | } 206 | 207 | return formattedString || this.toHexString(); 208 | }, 209 | clone: function() { 210 | return tinycolor(this.toString()); 211 | }, 212 | 213 | _applyModification: function(fn, args) { 214 | var color = fn.apply(null, [this].concat([].slice.call(args))); 215 | this._r = color._r; 216 | this._g = color._g; 217 | this._b = color._b; 218 | this.setAlpha(color._a); 219 | return this; 220 | }, 221 | lighten: function() { 222 | return this._applyModification(lighten, arguments); 223 | }, 224 | brighten: function() { 225 | return this._applyModification(brighten, arguments); 226 | }, 227 | darken: function() { 228 | return this._applyModification(darken, arguments); 229 | }, 230 | desaturate: function() { 231 | return this._applyModification(desaturate, arguments); 232 | }, 233 | saturate: function() { 234 | return this._applyModification(saturate, arguments); 235 | }, 236 | greyscale: function() { 237 | return this._applyModification(greyscale, arguments); 238 | }, 239 | spin: function() { 240 | return this._applyModification(spin, arguments); 241 | }, 242 | 243 | _applyCombination: function(fn, args) { 244 | return fn.apply(null, [this].concat([].slice.call(args))); 245 | }, 246 | analogous: function() { 247 | return this._applyCombination(analogous, arguments); 248 | }, 249 | complement: function() { 250 | return this._applyCombination(complement, arguments); 251 | }, 252 | monochromatic: function() { 253 | return this._applyCombination(monochromatic, arguments); 254 | }, 255 | splitcomplement: function() { 256 | return this._applyCombination(splitcomplement, arguments); 257 | }, 258 | triad: function() { 259 | return this._applyCombination(triad, arguments); 260 | }, 261 | tetrad: function() { 262 | return this._applyCombination(tetrad, arguments); 263 | } 264 | }; 265 | 266 | // If input is an object, force 1 into "1.0" to handle ratios properly 267 | // String input requires "1.0" as input, so 1 will be treated as 1 268 | tinycolor.fromRatio = function(color, opts) { 269 | if (typeof color == "object") { 270 | var newColor = {}; 271 | for (var i in color) { 272 | if (color.hasOwnProperty(i)) { 273 | if (i === "a") { 274 | newColor[i] = color[i]; 275 | } 276 | else { 277 | newColor[i] = convertToPercentage(color[i]); 278 | } 279 | } 280 | } 281 | color = newColor; 282 | } 283 | 284 | return tinycolor(color, opts); 285 | }; 286 | 287 | // Given a string or object, convert that input to RGB 288 | // Possible string inputs: 289 | // 290 | // "red" 291 | // "#f00" or "f00" 292 | // "#ff0000" or "ff0000" 293 | // "#ff000000" or "ff000000" 294 | // "rgb 255 0 0" or "rgb (255, 0, 0)" 295 | // "rgb 1.0 0 0" or "rgb (1, 0, 0)" 296 | // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1" 297 | // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1" 298 | // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%" 299 | // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1" 300 | // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%" 301 | // 302 | function inputToRGB(color) { 303 | 304 | var rgb = { r: 0, g: 0, b: 0 }; 305 | var a = 1; 306 | var s = null; 307 | var v = null; 308 | var l = null; 309 | var ok = false; 310 | var format = false; 311 | 312 | if (typeof color == "string") { 313 | color = stringInputToObject(color); 314 | } 315 | 316 | if (typeof color == "object") { 317 | if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) { 318 | rgb = rgbToRgb(color.r, color.g, color.b); 319 | ok = true; 320 | format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb"; 321 | } 322 | else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) { 323 | s = convertToPercentage(color.s); 324 | v = convertToPercentage(color.v); 325 | rgb = hsvToRgb(color.h, s, v); 326 | ok = true; 327 | format = "hsv"; 328 | } 329 | else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) { 330 | s = convertToPercentage(color.s); 331 | l = convertToPercentage(color.l); 332 | rgb = hslToRgb(color.h, s, l); 333 | ok = true; 334 | format = "hsl"; 335 | } 336 | 337 | if (color.hasOwnProperty("a")) { 338 | a = color.a; 339 | } 340 | } 341 | 342 | a = boundAlpha(a); 343 | 344 | return { 345 | ok: ok, 346 | format: color.format || format, 347 | r: mathMin(255, mathMax(rgb.r, 0)), 348 | g: mathMin(255, mathMax(rgb.g, 0)), 349 | b: mathMin(255, mathMax(rgb.b, 0)), 350 | a: a 351 | }; 352 | } 353 | 354 | 355 | // Conversion Functions 356 | // -------------------- 357 | 358 | // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from: 359 | // 360 | 361 | // `rgbToRgb` 362 | // Handle bounds / percentage checking to conform to CSS color spec 363 | // 364 | // *Assumes:* r, g, b in [0, 255] or [0, 1] 365 | // *Returns:* { r, g, b } in [0, 255] 366 | function rgbToRgb(r, g, b){ 367 | return { 368 | r: bound01(r, 255) * 255, 369 | g: bound01(g, 255) * 255, 370 | b: bound01(b, 255) * 255 371 | }; 372 | } 373 | 374 | // `rgbToHsl` 375 | // Converts an RGB color value to HSL. 376 | // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1] 377 | // *Returns:* { h, s, l } in [0,1] 378 | function rgbToHsl(r, g, b) { 379 | 380 | r = bound01(r, 255); 381 | g = bound01(g, 255); 382 | b = bound01(b, 255); 383 | 384 | var max = mathMax(r, g, b), min = mathMin(r, g, b); 385 | var h, s, l = (max + min) / 2; 386 | 387 | if(max == min) { 388 | h = s = 0; // achromatic 389 | } 390 | else { 391 | var d = max - min; 392 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 393 | switch(max) { 394 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 395 | case g: h = (b - r) / d + 2; break; 396 | case b: h = (r - g) / d + 4; break; 397 | } 398 | 399 | h /= 6; 400 | } 401 | 402 | return { h: h, s: s, l: l }; 403 | } 404 | 405 | // `hslToRgb` 406 | // Converts an HSL color value to RGB. 407 | // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100] 408 | // *Returns:* { r, g, b } in the set [0, 255] 409 | function hslToRgb(h, s, l) { 410 | var r, g, b; 411 | 412 | h = bound01(h, 360); 413 | s = bound01(s, 100); 414 | l = bound01(l, 100); 415 | 416 | function hue2rgb(p, q, t) { 417 | if(t < 0) t += 1; 418 | if(t > 1) t -= 1; 419 | if(t < 1/6) return p + (q - p) * 6 * t; 420 | if(t < 1/2) return q; 421 | if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; 422 | return p; 423 | } 424 | 425 | if(s === 0) { 426 | r = g = b = l; // achromatic 427 | } 428 | else { 429 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s; 430 | var p = 2 * l - q; 431 | r = hue2rgb(p, q, h + 1/3); 432 | g = hue2rgb(p, q, h); 433 | b = hue2rgb(p, q, h - 1/3); 434 | } 435 | 436 | return { r: r * 255, g: g * 255, b: b * 255 }; 437 | } 438 | 439 | // `rgbToHsv` 440 | // Converts an RGB color value to HSV 441 | // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1] 442 | // *Returns:* { h, s, v } in [0,1] 443 | function rgbToHsv(r, g, b) { 444 | 445 | r = bound01(r, 255); 446 | g = bound01(g, 255); 447 | b = bound01(b, 255); 448 | 449 | var max = mathMax(r, g, b), min = mathMin(r, g, b); 450 | var h, s, v = max; 451 | 452 | var d = max - min; 453 | s = max === 0 ? 0 : d / max; 454 | 455 | if(max == min) { 456 | h = 0; // achromatic 457 | } 458 | else { 459 | switch(max) { 460 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 461 | case g: h = (b - r) / d + 2; break; 462 | case b: h = (r - g) / d + 4; break; 463 | } 464 | h /= 6; 465 | } 466 | return { h: h, s: s, v: v }; 467 | } 468 | 469 | // `hsvToRgb` 470 | // Converts an HSV color value to RGB. 471 | // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100] 472 | // *Returns:* { r, g, b } in the set [0, 255] 473 | function hsvToRgb(h, s, v) { 474 | 475 | h = bound01(h, 360) * 6; 476 | s = bound01(s, 100); 477 | v = bound01(v, 100); 478 | 479 | var i = Math.floor(h), 480 | f = h - i, 481 | p = v * (1 - s), 482 | q = v * (1 - f * s), 483 | t = v * (1 - (1 - f) * s), 484 | mod = i % 6, 485 | r = [v, q, p, p, t, v][mod], 486 | g = [t, v, v, q, p, p][mod], 487 | b = [p, p, t, v, v, q][mod]; 488 | 489 | return { r: r * 255, g: g * 255, b: b * 255 }; 490 | } 491 | 492 | // `rgbToHex` 493 | // Converts an RGB color to hex 494 | // Assumes r, g, and b are contained in the set [0, 255] 495 | // Returns a 3 or 6 character hex 496 | function rgbToHex(r, g, b, allow3Char) { 497 | 498 | var hex = [ 499 | pad2(mathRound(r).toString(16)), 500 | pad2(mathRound(g).toString(16)), 501 | pad2(mathRound(b).toString(16)) 502 | ]; 503 | 504 | // Return a 3 character hex if possible 505 | if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) { 506 | return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0); 507 | } 508 | 509 | return hex.join(""); 510 | } 511 | 512 | // `rgbaToHex` 513 | // Converts an RGBA color plus alpha transparency to hex 514 | // Assumes r, g, b and a are contained in the set [0, 255] 515 | // Returns an 8 character hex 516 | function rgbaToHex(r, g, b, a) { 517 | 518 | var hex = [ 519 | pad2(convertDecimalToHex(a)), 520 | pad2(mathRound(r).toString(16)), 521 | pad2(mathRound(g).toString(16)), 522 | pad2(mathRound(b).toString(16)) 523 | ]; 524 | 525 | return hex.join(""); 526 | } 527 | 528 | // `equals` 529 | // Can be called with any tinycolor input 530 | tinycolor.equals = function (color1, color2) { 531 | if (!color1 || !color2) { return false; } 532 | return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString(); 533 | }; 534 | 535 | tinycolor.random = function() { 536 | return tinycolor.fromRatio({ 537 | r: mathRandom(), 538 | g: mathRandom(), 539 | b: mathRandom() 540 | }); 541 | }; 542 | 543 | 544 | // Modification Functions 545 | // ---------------------- 546 | // Thanks to less.js for some of the basics here 547 | // 548 | 549 | function desaturate(color, amount) { 550 | amount = (amount === 0) ? 0 : (amount || 10); 551 | var hsl = tinycolor(color).toHsl(); 552 | hsl.s -= amount / 100; 553 | hsl.s = clamp01(hsl.s); 554 | return tinycolor(hsl); 555 | } 556 | 557 | function saturate(color, amount) { 558 | amount = (amount === 0) ? 0 : (amount || 10); 559 | var hsl = tinycolor(color).toHsl(); 560 | hsl.s += amount / 100; 561 | hsl.s = clamp01(hsl.s); 562 | return tinycolor(hsl); 563 | } 564 | 565 | function greyscale(color) { 566 | return tinycolor(color).desaturate(100); 567 | } 568 | 569 | function lighten (color, amount) { 570 | amount = (amount === 0) ? 0 : (amount || 10); 571 | var hsl = tinycolor(color).toHsl(); 572 | hsl.l += amount / 100; 573 | hsl.l = clamp01(hsl.l); 574 | return tinycolor(hsl); 575 | } 576 | 577 | function brighten(color, amount) { 578 | amount = (amount === 0) ? 0 : (amount || 10); 579 | var rgb = tinycolor(color).toRgb(); 580 | rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100)))); 581 | rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100)))); 582 | rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100)))); 583 | return tinycolor(rgb); 584 | } 585 | 586 | function darken (color, amount) { 587 | amount = (amount === 0) ? 0 : (amount || 10); 588 | var hsl = tinycolor(color).toHsl(); 589 | hsl.l -= amount / 100; 590 | hsl.l = clamp01(hsl.l); 591 | return tinycolor(hsl); 592 | } 593 | 594 | // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue. 595 | // Values outside of this range will be wrapped into this range. 596 | function spin(color, amount) { 597 | var hsl = tinycolor(color).toHsl(); 598 | var hue = (hsl.h + amount) % 360; 599 | hsl.h = hue < 0 ? 360 + hue : hue; 600 | return tinycolor(hsl); 601 | } 602 | 603 | // Combination Functions 604 | // --------------------- 605 | // Thanks to jQuery xColor for some of the ideas behind these 606 | // 607 | 608 | function complement(color) { 609 | var hsl = tinycolor(color).toHsl(); 610 | hsl.h = (hsl.h + 180) % 360; 611 | return tinycolor(hsl); 612 | } 613 | 614 | function triad(color) { 615 | var hsl = tinycolor(color).toHsl(); 616 | var h = hsl.h; 617 | return [ 618 | tinycolor(color), 619 | tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }), 620 | tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l }) 621 | ]; 622 | } 623 | 624 | function tetrad(color) { 625 | var hsl = tinycolor(color).toHsl(); 626 | var h = hsl.h; 627 | return [ 628 | tinycolor(color), 629 | tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }), 630 | tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }), 631 | tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l }) 632 | ]; 633 | } 634 | 635 | function splitcomplement(color) { 636 | var hsl = tinycolor(color).toHsl(); 637 | var h = hsl.h; 638 | return [ 639 | tinycolor(color), 640 | tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}), 641 | tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l}) 642 | ]; 643 | } 644 | 645 | function analogous(color, results, slices) { 646 | results = results || 6; 647 | slices = slices || 30; 648 | 649 | var hsl = tinycolor(color).toHsl(); 650 | var part = 360 / slices; 651 | var ret = [tinycolor(color)]; 652 | 653 | for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) { 654 | hsl.h = (hsl.h + part) % 360; 655 | ret.push(tinycolor(hsl)); 656 | } 657 | return ret; 658 | } 659 | 660 | function monochromatic(color, results) { 661 | results = results || 6; 662 | var hsv = tinycolor(color).toHsv(); 663 | var h = hsv.h, s = hsv.s, v = hsv.v; 664 | var ret = []; 665 | var modification = 1 / results; 666 | 667 | while (results--) { 668 | ret.push(tinycolor({ h: h, s: s, v: v})); 669 | v = (v + modification) % 1; 670 | } 671 | 672 | return ret; 673 | } 674 | 675 | // Utility Functions 676 | // --------------------- 677 | 678 | tinycolor.mix = function(color1, color2, amount) { 679 | amount = (amount === 0) ? 0 : (amount || 50); 680 | 681 | var rgb1 = tinycolor(color1).toRgb(); 682 | var rgb2 = tinycolor(color2).toRgb(); 683 | 684 | var p = amount / 100; 685 | var w = p * 2 - 1; 686 | var a = rgb2.a - rgb1.a; 687 | 688 | var w1; 689 | 690 | if (w * a == -1) { 691 | w1 = w; 692 | } else { 693 | w1 = (w + a) / (1 + w * a); 694 | } 695 | 696 | w1 = (w1 + 1) / 2; 697 | 698 | var w2 = 1 - w1; 699 | 700 | var rgba = { 701 | r: rgb2.r * w1 + rgb1.r * w2, 702 | g: rgb2.g * w1 + rgb1.g * w2, 703 | b: rgb2.b * w1 + rgb1.b * w2, 704 | a: rgb2.a * p + rgb1.a * (1 - p) 705 | }; 706 | 707 | return tinycolor(rgba); 708 | }; 709 | 710 | 711 | // Readability Functions 712 | // --------------------- 713 | // false 732 | // tinycolor.isReadable("#000", "#111",{level:"AA",size:"large"}) => false 733 | tinycolor.isReadable = function(color1, color2, wcag2) { 734 | var readability = tinycolor.readability(color1, color2); 735 | var wcag2Parms, out; 736 | 737 | out = false; 738 | 739 | wcag2Parms = validateWCAG2Parms(wcag2); 740 | switch (wcag2Parms.level + wcag2Parms.size) { 741 | case "AAsmall": 742 | case "AAAlarge": 743 | out = readability >= 4.5; 744 | break; 745 | case "AAlarge": 746 | out = readability >= 3; 747 | break; 748 | case "AAAsmall": 749 | out = readability >= 7; 750 | break; 751 | } 752 | return out; 753 | 754 | }; 755 | 756 | // `mostReadable` 757 | // Given a base color and a list of possible foreground or background 758 | // colors for that base, returns the most readable color. 759 | // Optionally returns Black or White if the most readable color is unreadable. 760 | // *Example* 761 | // tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255" 762 | // tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(); // "#ffffff" 763 | // tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(); // "#faf3f3" 764 | // tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(); // "#ffffff" 765 | tinycolor.mostReadable = function(baseColor, colorList, args) { 766 | var bestColor = null; 767 | var bestScore = 0; 768 | var readability; 769 | var includeFallbackColors, level, size ; 770 | args = args || {}; 771 | includeFallbackColors = args.includeFallbackColors ; 772 | level = args.level; 773 | size = args.size; 774 | 775 | for (var i= 0; i < colorList.length ; i++) { 776 | readability = tinycolor.readability(baseColor, colorList[i]); 777 | if (readability > bestScore) { 778 | bestScore = readability; 779 | bestColor = tinycolor(colorList[i]); 780 | } 781 | } 782 | 783 | if (tinycolor.isReadable(baseColor, bestColor, {"level":level,"size":size}) || !includeFallbackColors) { 784 | return bestColor; 785 | } 786 | else { 787 | args.includeFallbackColors=false; 788 | return tinycolor.mostReadable(baseColor,["#fff", "#000"],args); 789 | } 790 | }; 791 | 792 | 793 | // Big List of Colors 794 | // ------------------ 795 | // 796 | var names = tinycolor.names = { 797 | aliceblue: "f0f8ff", 798 | antiquewhite: "faebd7", 799 | aqua: "0ff", 800 | aquamarine: "7fffd4", 801 | azure: "f0ffff", 802 | beige: "f5f5dc", 803 | bisque: "ffe4c4", 804 | black: "000", 805 | blanchedalmond: "ffebcd", 806 | blue: "00f", 807 | blueviolet: "8a2be2", 808 | brown: "a52a2a", 809 | burlywood: "deb887", 810 | burntsienna: "ea7e5d", 811 | cadetblue: "5f9ea0", 812 | chartreuse: "7fff00", 813 | chocolate: "d2691e", 814 | coral: "ff7f50", 815 | cornflowerblue: "6495ed", 816 | cornsilk: "fff8dc", 817 | crimson: "dc143c", 818 | cyan: "0ff", 819 | darkblue: "00008b", 820 | darkcyan: "008b8b", 821 | darkgoldenrod: "b8860b", 822 | darkgray: "a9a9a9", 823 | darkgreen: "006400", 824 | darkgrey: "a9a9a9", 825 | darkkhaki: "bdb76b", 826 | darkmagenta: "8b008b", 827 | darkolivegreen: "556b2f", 828 | darkorange: "ff8c00", 829 | darkorchid: "9932cc", 830 | darkred: "8b0000", 831 | darksalmon: "e9967a", 832 | darkseagreen: "8fbc8f", 833 | darkslateblue: "483d8b", 834 | darkslategray: "2f4f4f", 835 | darkslategrey: "2f4f4f", 836 | darkturquoise: "00ced1", 837 | darkviolet: "9400d3", 838 | deeppink: "ff1493", 839 | deepskyblue: "00bfff", 840 | dimgray: "696969", 841 | dimgrey: "696969", 842 | dodgerblue: "1e90ff", 843 | firebrick: "b22222", 844 | floralwhite: "fffaf0", 845 | forestgreen: "228b22", 846 | fuchsia: "f0f", 847 | gainsboro: "dcdcdc", 848 | ghostwhite: "f8f8ff", 849 | gold: "ffd700", 850 | goldenrod: "daa520", 851 | gray: "808080", 852 | green: "008000", 853 | greenyellow: "adff2f", 854 | grey: "808080", 855 | honeydew: "f0fff0", 856 | hotpink: "ff69b4", 857 | indianred: "cd5c5c", 858 | indigo: "4b0082", 859 | ivory: "fffff0", 860 | khaki: "f0e68c", 861 | lavender: "e6e6fa", 862 | lavenderblush: "fff0f5", 863 | lawngreen: "7cfc00", 864 | lemonchiffon: "fffacd", 865 | lightblue: "add8e6", 866 | lightcoral: "f08080", 867 | lightcyan: "e0ffff", 868 | lightgoldenrodyellow: "fafad2", 869 | lightgray: "d3d3d3", 870 | lightgreen: "90ee90", 871 | lightgrey: "d3d3d3", 872 | lightpink: "ffb6c1", 873 | lightsalmon: "ffa07a", 874 | lightseagreen: "20b2aa", 875 | lightskyblue: "87cefa", 876 | lightslategray: "789", 877 | lightslategrey: "789", 878 | lightsteelblue: "b0c4de", 879 | lightyellow: "ffffe0", 880 | lime: "0f0", 881 | limegreen: "32cd32", 882 | linen: "faf0e6", 883 | magenta: "f0f", 884 | maroon: "800000", 885 | mediumaquamarine: "66cdaa", 886 | mediumblue: "0000cd", 887 | mediumorchid: "ba55d3", 888 | mediumpurple: "9370db", 889 | mediumseagreen: "3cb371", 890 | mediumslateblue: "7b68ee", 891 | mediumspringgreen: "00fa9a", 892 | mediumturquoise: "48d1cc", 893 | mediumvioletred: "c71585", 894 | midnightblue: "191970", 895 | mintcream: "f5fffa", 896 | mistyrose: "ffe4e1", 897 | moccasin: "ffe4b5", 898 | navajowhite: "ffdead", 899 | navy: "000080", 900 | oldlace: "fdf5e6", 901 | olive: "808000", 902 | olivedrab: "6b8e23", 903 | orange: "ffa500", 904 | orangered: "ff4500", 905 | orchid: "da70d6", 906 | palegoldenrod: "eee8aa", 907 | palegreen: "98fb98", 908 | paleturquoise: "afeeee", 909 | palevioletred: "db7093", 910 | papayawhip: "ffefd5", 911 | peachpuff: "ffdab9", 912 | peru: "cd853f", 913 | pink: "ffc0cb", 914 | plum: "dda0dd", 915 | powderblue: "b0e0e6", 916 | purple: "800080", 917 | rebeccapurple: "663399", 918 | red: "f00", 919 | rosybrown: "bc8f8f", 920 | royalblue: "4169e1", 921 | saddlebrown: "8b4513", 922 | salmon: "fa8072", 923 | sandybrown: "f4a460", 924 | seagreen: "2e8b57", 925 | seashell: "fff5ee", 926 | sienna: "a0522d", 927 | silver: "c0c0c0", 928 | skyblue: "87ceeb", 929 | slateblue: "6a5acd", 930 | slategray: "708090", 931 | slategrey: "708090", 932 | snow: "fffafa", 933 | springgreen: "00ff7f", 934 | steelblue: "4682b4", 935 | tan: "d2b48c", 936 | teal: "008080", 937 | thistle: "d8bfd8", 938 | tomato: "ff6347", 939 | turquoise: "40e0d0", 940 | violet: "ee82ee", 941 | wheat: "f5deb3", 942 | white: "fff", 943 | whitesmoke: "f5f5f5", 944 | yellow: "ff0", 945 | yellowgreen: "9acd32" 946 | }; 947 | 948 | // Make it easy to access colors via `hexNames[hex]` 949 | var hexNames = tinycolor.hexNames = flip(names); 950 | 951 | 952 | // Utilities 953 | // --------- 954 | 955 | // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }` 956 | function flip(o) { 957 | var flipped = { }; 958 | for (var i in o) { 959 | if (o.hasOwnProperty(i)) { 960 | flipped[o[i]] = i; 961 | } 962 | } 963 | return flipped; 964 | } 965 | 966 | // Return a valid alpha value [0,1] with all invalid values being set to 1 967 | function boundAlpha(a) { 968 | a = parseFloat(a); 969 | 970 | if (isNaN(a) || a < 0 || a > 1) { 971 | a = 1; 972 | } 973 | 974 | return a; 975 | } 976 | 977 | // Take input from [0, n] and return it as [0, 1] 978 | function bound01(n, max) { 979 | if (isOnePointZero(n)) { n = "100%"; } 980 | 981 | var processPercent = isPercentage(n); 982 | n = mathMin(max, mathMax(0, parseFloat(n))); 983 | 984 | // Automatically convert percentage into number 985 | if (processPercent) { 986 | n = parseInt(n * max, 10) / 100; 987 | } 988 | 989 | // Handle floating point rounding errors 990 | if ((Math.abs(n - max) < 0.000001)) { 991 | return 1; 992 | } 993 | 994 | // Convert into [0, 1] range if it isn't already 995 | return (n % max) / parseFloat(max); 996 | } 997 | 998 | // Force a number between 0 and 1 999 | function clamp01(val) { 1000 | return mathMin(1, mathMax(0, val)); 1001 | } 1002 | 1003 | // Parse a base-16 hex value into a base-10 integer 1004 | function parseIntFromHex(val) { 1005 | return parseInt(val, 16); 1006 | } 1007 | 1008 | // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1 1009 | // 1010 | function isOnePointZero(n) { 1011 | return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1; 1012 | } 1013 | 1014 | // Check to see if string passed in is a percentage 1015 | function isPercentage(n) { 1016 | return typeof n === "string" && n.indexOf('%') != -1; 1017 | } 1018 | 1019 | // Force a hex value to have 2 characters 1020 | function pad2(c) { 1021 | return c.length == 1 ? '0' + c : '' + c; 1022 | } 1023 | 1024 | // Replace a decimal with it's percentage value 1025 | function convertToPercentage(n) { 1026 | if (n <= 1) { 1027 | n = (n * 100) + "%"; 1028 | } 1029 | 1030 | return n; 1031 | } 1032 | 1033 | // Converts a decimal to a hex value 1034 | function convertDecimalToHex(d) { 1035 | return Math.round(parseFloat(d) * 255).toString(16); 1036 | } 1037 | // Converts a hex value to a decimal 1038 | function convertHexToDecimal(h) { 1039 | return (parseIntFromHex(h) / 255); 1040 | } 1041 | 1042 | var matchers = (function() { 1043 | 1044 | // 1045 | var CSS_INTEGER = "[-\\+]?\\d+%?"; 1046 | 1047 | // 1048 | var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?"; 1049 | 1050 | // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome. 1051 | var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")"; 1052 | 1053 | // Actual matching. 1054 | // Parentheses and commas are optional, but not required. 1055 | // Whitespace can take the place of commas or opening paren 1056 | var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"; 1057 | var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"; 1058 | 1059 | return { 1060 | CSS_UNIT: new RegExp(CSS_UNIT), 1061 | rgb: new RegExp("rgb" + PERMISSIVE_MATCH3), 1062 | rgba: new RegExp("rgba" + PERMISSIVE_MATCH4), 1063 | hsl: new RegExp("hsl" + PERMISSIVE_MATCH3), 1064 | hsla: new RegExp("hsla" + PERMISSIVE_MATCH4), 1065 | hsv: new RegExp("hsv" + PERMISSIVE_MATCH3), 1066 | hsva: new RegExp("hsva" + PERMISSIVE_MATCH4), 1067 | hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, 1068 | hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, 1069 | hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/ 1070 | }; 1071 | })(); 1072 | 1073 | // `isValidCSSUnit` 1074 | // Take in a single string / number and check to see if it looks like a CSS unit 1075 | // (see `matchers` above for definition). 1076 | function isValidCSSUnit(color) { 1077 | return !!matchers.CSS_UNIT.exec(color); 1078 | } 1079 | 1080 | // `stringInputToObject` 1081 | // Permissive string parsing. Take in a number of formats, and output an object 1082 | // based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}` 1083 | function stringInputToObject(color) { 1084 | 1085 | color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase(); 1086 | var named = false; 1087 | if (names[color]) { 1088 | color = names[color]; 1089 | named = true; 1090 | } 1091 | else if (color == 'transparent') { 1092 | return { r: 0, g: 0, b: 0, a: 0, format: "name" }; 1093 | } 1094 | 1095 | // Try to match string input using regular expressions. 1096 | // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360] 1097 | // Just return an object and let the conversion functions handle that. 1098 | // This way the result will be the same whether the tinycolor is initialized with string or object. 1099 | var match; 1100 | if ((match = matchers.rgb.exec(color))) { 1101 | return { r: match[1], g: match[2], b: match[3] }; 1102 | } 1103 | if ((match = matchers.rgba.exec(color))) { 1104 | return { r: match[1], g: match[2], b: match[3], a: match[4] }; 1105 | } 1106 | if ((match = matchers.hsl.exec(color))) { 1107 | return { h: match[1], s: match[2], l: match[3] }; 1108 | } 1109 | if ((match = matchers.hsla.exec(color))) { 1110 | return { h: match[1], s: match[2], l: match[3], a: match[4] }; 1111 | } 1112 | if ((match = matchers.hsv.exec(color))) { 1113 | return { h: match[1], s: match[2], v: match[3] }; 1114 | } 1115 | if ((match = matchers.hsva.exec(color))) { 1116 | return { h: match[1], s: match[2], v: match[3], a: match[4] }; 1117 | } 1118 | if ((match = matchers.hex8.exec(color))) { 1119 | return { 1120 | a: convertHexToDecimal(match[1]), 1121 | r: parseIntFromHex(match[2]), 1122 | g: parseIntFromHex(match[3]), 1123 | b: parseIntFromHex(match[4]), 1124 | format: named ? "name" : "hex8" 1125 | }; 1126 | } 1127 | if ((match = matchers.hex6.exec(color))) { 1128 | return { 1129 | r: parseIntFromHex(match[1]), 1130 | g: parseIntFromHex(match[2]), 1131 | b: parseIntFromHex(match[3]), 1132 | format: named ? "name" : "hex" 1133 | }; 1134 | } 1135 | if ((match = matchers.hex3.exec(color))) { 1136 | return { 1137 | r: parseIntFromHex(match[1] + '' + match[1]), 1138 | g: parseIntFromHex(match[2] + '' + match[2]), 1139 | b: parseIntFromHex(match[3] + '' + match[3]), 1140 | format: named ? "name" : "hex" 1141 | }; 1142 | } 1143 | 1144 | return false; 1145 | } 1146 | 1147 | function validateWCAG2Parms(parms) { 1148 | // return valid WCAG2 parms for isReadable. 1149 | // If input parms are invalid, return {"level":"AA", "size":"small"} 1150 | var level, size; 1151 | parms = parms || {"level":"AA", "size":"small"}; 1152 | level = (parms.level || "AA").toUpperCase(); 1153 | size = (parms.size || "small").toLowerCase(); 1154 | if (level !== "AA" && level !== "AAA") { 1155 | level = "AA"; 1156 | } 1157 | if (size !== "small" && size !== "large") { 1158 | size = "small"; 1159 | } 1160 | return {"level":level, "size":size}; 1161 | } 1162 | 1163 | // Node: Export function 1164 | if (typeof module !== "undefined" && module.exports) { 1165 | module.exports = tinycolor; 1166 | } 1167 | // AMD/requirejs: Define the module 1168 | else if (typeof define === 'function' && define.amd) { 1169 | define(function () {return tinycolor;}); 1170 | } 1171 | // Browser: Expose to window 1172 | else { 1173 | window.tinycolor = tinycolor; 1174 | } 1175 | 1176 | })(Math); 1177 | -------------------------------------------------------------------------------- /libs/tinygradient.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * TinyGradient 0.3.1 3 | * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) 4 | * Licensed under MIT (http://opensource.org/licenses/MIT) 5 | */ 6 | 7 | (function(root, factory) { 8 | if (typeof module !== "undefined" && module.exports) { 9 | module.exports = factory(require('tinycolor2')); 10 | } 11 | else if (typeof define === 'function' && define.amd) { 12 | define(['tinycolor'], factory); 13 | } 14 | else { 15 | root.tinygradient = factory(root.tinycolor); 16 | } 17 | }(this, function(tinycolor) { 18 | "use strict"; 19 | 20 | var Utils = { 21 | rgba_max: { r: 256, g: 256, b: 256, a: 1 }, 22 | hsva_max: { h: 360, s: 1, v: 1, a: 1 }, 23 | 24 | /** 25 | * Linearly compute the step size between start and end (not normalized) 26 | * @param {Object} start - rgba or hsva 27 | * @param {Object} end - rgba or hsva 28 | * @param {Integer} steps - number of desired steps 29 | * @return {Object} rgba or hsva 30 | */ 31 | stepize: function(start, end, steps) { 32 | var step = {}; 33 | 34 | for (var k in start) { 35 | if (start.hasOwnProperty(k)) { 36 | step[k] = (end[k]-start[k]) / steps; 37 | } 38 | } 39 | 40 | return step; 41 | }, 42 | 43 | /** 44 | * Compute the final step color 45 | * @param {Object} step - rgba or hsva from `stepize` 46 | * @param {Object} start - rgba or hsva 47 | * @param {Integer} i - color index 48 | * @param {Object} max - rgba or hsva of maximum values for each channel 49 | * @return {Object} rgba or hsva 50 | */ 51 | interpolate: function(step, start, i, max) { 52 | var color = {}; 53 | 54 | for (var k in start) { 55 | if (start.hasOwnProperty(k)) { 56 | color[k] = step[k] * i + start[k]; 57 | color[k] = color[k]<0 ? color[k]+max[k] : ( max[k]!=1 ? color[k]%max[k] : color[k] ); 58 | } 59 | } 60 | 61 | return color; 62 | }, 63 | 64 | /** 65 | * Generate gradient with RGBa interpolation 66 | * @param {Object} stop1 67 | * @param {Object} stop2 68 | * @param {Integer} steps 69 | * @param {tinycolor[]} color1 included, color2 excluded 70 | */ 71 | rgb: function(stop1, stop2, steps) { 72 | var start = stop1.color.toRgb(), 73 | end = stop2.color.toRgb(), 74 | gradient = [stop1.color], 75 | step = Utils.stepize(start, end, steps), 76 | color; 77 | 78 | for (var i=1; i= end.h && trigonometric)) { 103 | diff = end.h-start.h; 104 | } 105 | else if (trigonometric) { 106 | diff = 360-end.h+start.h; 107 | } 108 | else { 109 | diff = 360-start.h+end.h; 110 | } 111 | step.h = Math.pow(-1, trigonometric) * Math.abs(diff)*1.0 / steps; 112 | 113 | for (var i=1; i 1) { 212 | throw new Error('Color stops positions must be between 0 and 1'); 213 | } 214 | else if (stop.pos <= p) { 215 | throw new Error('Color stops positions are not ordered'); 216 | } 217 | p = stop.pos; 218 | } 219 | else { 220 | stop = { 221 | color: tinycolor(stop), 222 | pos: i/(l-1) 223 | }; 224 | } 225 | 226 | return stop; 227 | }); 228 | 229 | if (this.stops[0].pos !== 0) { 230 | this.stops.unshift({ 231 | color: this.stops[0].color, 232 | pos: 0 233 | }); 234 | } 235 | if (this.stops[this.stops.length-1].pos !== 1) { 236 | this.stops.push({ 237 | color: this.stops[this.stops.length-1].color, 238 | pos: 1 239 | }); 240 | } 241 | }; 242 | 243 | /** 244 | * Return new instance with reversed stops 245 | * @return {tinygradient} 246 | */ 247 | TinyGradient.prototype.reverse = function() { 248 | var stops = []; 249 | 250 | this.stops.forEach(function(stop) { 251 | stops.push({ 252 | color: stop.color, 253 | pos: 1 - stop.pos 254 | }); 255 | }); 256 | 257 | return new TinyGradient(stops.reverse()); 258 | }; 259 | 260 | /** 261 | * Generate gradient with RGBa interpolation 262 | * @param {Integer} steps 263 | * @return {tinycolor[]} 264 | */ 265 | TinyGradient.prototype.rgb = function(steps) { 266 | var substeps = Utils.substeps(this.stops, steps), 267 | gradient = []; 268 | 269 | for (var i=0, l=this.stops.length; i end.h && start.h-end.h > 180); 301 | } 302 | 303 | // rgb interpolation if one of the steps in grayscale 304 | if (start.s===0 || end.s===0) { 305 | gradient = gradient.concat(Utils.rgb(this.stops[i], this.stops[i+1], substeps[i])); 306 | } 307 | else { 308 | gradient = gradient.concat(Utils.hsv(this.stops[i], this.stops[i+1], substeps[i], 309 | (mode==='long' && trig) || (mode==='short' && !trig) || (!parametrized && trigonometric) 310 | )); 311 | } 312 | } 313 | 314 | gradient.push(this.stops[l-1].color); 315 | 316 | return gradient; 317 | }; 318 | 319 | /** 320 | * Generate CSS3 command (no prefix) for this gradient 321 | * @param {String} [mode=linear] - 'linear' or 'radial' 322 | * @param {String} [direction] - default is 'to right' or 'ellipse at center' 323 | * @return {String} 324 | */ 325 | TinyGradient.prototype.css = function(mode, direction) { 326 | mode = mode || 'linear'; 327 | direction = direction || (mode=='linear' ? 'to right' : 'ellipse at center'); 328 | 329 | var css = mode + '-gradient(' + direction; 330 | this.stops.forEach(function(stop) { 331 | css+= ', ' + stop.color.toRgbString() + ' ' + (stop.pos*100) + '%'; 332 | }); 333 | css+= ')'; 334 | return css; 335 | }; 336 | 337 | 338 | /** 339 | * Initialize and create gradient with RGBa interpolation 340 | * @see TinyGradient::rgb 341 | */ 342 | TinyGradient.rgb = function(colors, steps) { 343 | colors = Array.prototype.slice.call(arguments); 344 | steps = colors.pop(); 345 | 346 | return TinyGradient.apply(null, colors).rgb(steps); 347 | }; 348 | 349 | /** 350 | * Initialize and create gradient with HSVa interpolation 351 | * @see TinyGradient::hsv 352 | */ 353 | TinyGradient.hsv = function(colors, steps, mode) { 354 | colors = Array.prototype.slice.call(arguments); 355 | mode = colors.pop(); 356 | steps = colors.pop(); 357 | 358 | return TinyGradient.apply(null, colors).hsv(steps, mode); 359 | }; 360 | 361 | /** 362 | * Initialize and generate CSS3 command for gradient 363 | * @see TinyGradient::css 364 | */ 365 | TinyGradient.css = function(colors, mode, direction) { 366 | colors = Array.prototype.slice.call(arguments); 367 | direction = colors.pop(); 368 | mode = colors.pop(); 369 | 370 | return TinyGradient.apply(null, colors).css(mode, direction); 371 | }; 372 | 373 | 374 | // export 375 | return TinyGradient; 376 | })); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lee-algorithm", 3 | "version": "1.0.0", 4 | "description": "A JS implementation of the Lee Algorithm", 5 | "main": "lee.js", 6 | "scripts": { 7 | "test": "test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/JamesMilnerUK/lee-algorithm-js.git" 12 | }, 13 | "keywords": [ 14 | "Lee", 15 | "Algorithm", 16 | "Maze" 17 | ], 18 | "author": "James Milner", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/JamesMilnerUK/lee-algorithm-js/issues" 22 | }, 23 | "homepage": "https://github.com/JamesMilnerUK/lee-algorithm-js#readme", 24 | "devDependencies": { 25 | "jasmine": "^2.4.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/LeeSpec.js: -------------------------------------------------------------------------------- 1 | 2 | describe("Lee", function() { 3 | 4 | fs = require('fs'); 5 | leeCode = fs.readFileSync('lee.js','utf-8'); // depends on the file encoding 6 | eval(leeCode); 7 | 8 | it("Check pathfinder defined", function() { 9 | 10 | expect(lee.pathfinder).toBeDefined(); 11 | 12 | }); 13 | 14 | it("Check backtrace defined", function() { 15 | 16 | expect(lee.neighbourCheck).toBeDefined(); 17 | 18 | }); 19 | 20 | it("Check pathfinder defined", function() { 21 | 22 | expect(lee.backtrace).toBeDefined(); 23 | 24 | }); 25 | 26 | it("Checks to make sure the returned matrix is correct", function() { 27 | 28 | var aMatrix = [ 29 | [0, 0, 0, 0, 0], 30 | [0, -1, -1, -1, 0], 31 | [0, 0, 0, 0, -1], 32 | [-1, 0, -1, 0, -1], 33 | [-1, -1, -1, 0, -1] 34 | ]; 35 | 36 | var checkedMatrix = lee.pathfinder(aMatrix, 0, 0, 4, 3); 37 | console.log("Checked Matrix: \n", checkedMatrix); 38 | 39 | // Check the structure of the matrix is correct 40 | expect(checkedMatrix).toBeDefined(); 41 | expect(checkedMatrix.length).toBe(2); 42 | expect(checkedMatrix[0].length).toBe(5); 43 | expect(checkedMatrix[0][0].length).toBe(5); 44 | 45 | expect(checkedMatrix[0][0][0]).toBe(0); // Make sure the start is 0 46 | expect(checkedMatrix[1]).toBe(5); // Make sure the end is 5 47 | 48 | 49 | }); 50 | 51 | it("Checks to make sure the returned route is correct", function() { 52 | 53 | var aMatrix = [ 54 | [0, 0, 0, 0, 0], 55 | [0, -1, -1, -1, 0], 56 | [0, 0, 0, 0, -1], 57 | [-1, 0, -1, 0, -1], 58 | [-1, -1, -1, 0, -1] 59 | ]; 60 | 61 | var checkedMatrix = lee.pathfinder(aMatrix, 0, 0, 4, 3); 62 | var route = lee.backtrace(aMatrix, 0, 0, 4, 3); 63 | 64 | expect(route.length).toBe(6); // Make sure the length is correct 65 | 66 | expect(route[0][0]).toBe(0); // Make sure the start is 0, 0 67 | expect(route[0][1]).toBe(0); 68 | expect(route[5][0]).toBe(4); // Make sure the end is 4,3 69 | expect(route[5][1]).toBe(3); 70 | }); 71 | 72 | // END OF TESTS 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | --------------------------------------------------------------------------------