├── .gitignore ├── .travis.yml ├── Procfile ├── README.md ├── app.js ├── package.json ├── public ├── fonts │ └── dice.ttf ├── javascripts │ ├── backgammon.js │ ├── d3.js │ ├── jquery.js │ ├── spectrum.js │ └── underscore.js └── stylesheets │ └── style.less ├── routes └── index.js ├── src ├── game.js ├── run.js └── server.js ├── test ├── game.js ├── mocha.opts └── server.js └── views ├── index.html └── layouts ├── main.html └── main.template /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | *.swp 4 | 5 | *.swo 6 | 7 | *.log 8 | 9 | public/stylesheets/style.css 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | deploy: 5 | provider: heroku 6 | api_key: 7 | secure: E3Ley4R5203xhlQpIC623lt6xwFFzlkoDpd7g+P8yrqdwjEoFIs6fAS8wtfV24L9M018HgNG+Tlm8HiM4quwc4UG3ekHTww29gf+KFgi/TToO5acag7a8zyZ0KWIMRgz3jtvsjNU5FSk/FLNSKY4orreAIjExnmlzwXnb+IgDNY= 8 | app: backgammon-js 9 | on: 10 | repo: poulter7/backgammon-js 11 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | backgammon-js 2 | ============= 3 | 4 | Multiplayer backgammon in node js. UI is largely from wbillingsley/play-backgammon 5 | 6 | [![Build Status](https://travis-ci.org/poulter7/backgammon-js.png?branch=master)](https://travis-ci.org/poulter7/backgammon-js) 7 | 8 | 9 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | console.log('launching'); 7 | require('./src/server.js').start(3000, null, null, false) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "^1.17.1", 4 | "chance": "*", 5 | "d3": "*", 6 | "errorhandler": "^1.5.0", 7 | "express": "^4.15.2", 8 | "express-session": "^1.15.2", 9 | "express3-handlebars": "*", 10 | "less-middleware": "*", 11 | "method-override": "^2.3.8", 12 | "morgan": "^1.8.1", 13 | "multer": "^1.3.0", 14 | "mustache": "*", 15 | "node-inspector": "~0.6.2", 16 | "pug": "^2.0.0-beta11", 17 | "serve-favicon": "^2.4.2", 18 | "socket.io": "*", 19 | "underscore": "~1.5.2" 20 | }, 21 | "devDependencies": { 22 | "jquery": "*", 23 | "mocha": "*", 24 | "growl": "*", 25 | "node-inspector": "*", 26 | "should": ">= 0.0.1", 27 | "supervisor": "*", 28 | "socket.io-client": "*", 29 | "zombie": "*", 30 | "grunt": "*" 31 | }, 32 | "scripts": { 33 | "start": "node app.js", 34 | "test": "node_modules/.bin/mocha -G", 35 | "watchtest": "node_modules/.bin/mocha -w -G" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/fonts/dice.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poulter7/backgammon-js/919b53f52d9f0ae6b7c6becec81155c3efc21005/public/fonts/dice.ttf -------------------------------------------------------------------------------- /public/javascripts/backgammon.js: -------------------------------------------------------------------------------- 1 | radius = 22; 2 | buffer = 2; 3 | margin = 25 4 | var socketURL = 'http://' + window.location.host; 5 | var options ={ 6 | transports: ['websocket'], 7 | 'force new connection': true 8 | }; 9 | 10 | $(window).load(function(){ 11 | client = io.connect();//socketURL, options); 12 | client.on("connect", function(data){ 13 | client.emit("status"); 14 | client.emit("dice"); 15 | client.emit("player"); 16 | }); 17 | client.on("player", function(player){ 18 | update_player(player); 19 | }); 20 | client.on("status", function(board){ 21 | render_board(board); 22 | }); 23 | client.on("dice", function(move_status){ 24 | dice = move_status.dice; 25 | render_dice(move_status.dice); 26 | render_skip_message(move_status.dice, move_status.playable); 27 | }); 28 | }) 29 | $(window).keydown(function(event){ 30 | keyCode = event.which || event.keyCode 31 | console.log(keyCode) 32 | if (keyCode >= 49 && keyCode <= 56){ // number pressed 33 | diceNumber = String.fromCharCode(keyCode); 34 | selectDiceValue(diceNumber); 35 | event.preventDefault(); 36 | } else if (keyCode === 32){ 37 | requestRoll(); 38 | } 39 | 40 | }) 41 | validDiceIndex = function(val){ 42 | console.log('Val', val) 43 | for (var i in dice){ 44 | var d = dice[i]; 45 | if (d.val == val && !d.rolled){ 46 | return i 47 | } 48 | } 49 | } 50 | selectDiceValue = function(val){ 51 | var index = validDiceIndex(val); 52 | if (!(index === undefined)){ 53 | selectDice(index); 54 | } 55 | } 56 | 57 | px = { 58 | 1: 625, 59 | 2: 575, 60 | 3: 525, 61 | 4: 475, 62 | 5: 425, 63 | 6: 375, 64 | 7: 275, 65 | 8: 225, 66 | 9: 175, 67 | 10: 125, 68 | 11: 75, 69 | 12: 25, 70 | 24: 625, 71 | 23: 575, 72 | 22: 525, 73 | 21: 475, 74 | 20: 425, 75 | 19: 375, 76 | 18: 275, 77 | 17: 225, 78 | 16: 175, 79 | 15: 125, 80 | 14: 75, 81 | 13: 25, 82 | home: 685, 83 | bar: 325 84 | } 85 | 86 | cx = function(p){ 87 | return px[p.position]; 88 | } 89 | cy = function(p){ 90 | positionIndent = (2*radius + buffer)*(p.index % 5) + Math.floor(p.index/ 5) * 4 * buffer; 91 | if (_.isNumber(p.position)){ 92 | if (p.position <= 12){ // bottom row 93 | return 500 - positionIndent - margin 94 | } else { 95 | return positionIndent + margin 96 | } 97 | } else if (p.position=='home') { 98 | if(p.color == 'red'){ 99 | return positionIndent + margin 100 | } else { 101 | return 500 - positionIndent - margin 102 | } 103 | } else { // bar 104 | var middle = 250 105 | if(p.color == 'red'){ 106 | return middle + buffer + radius + positionIndent 107 | } else { 108 | return middle - buffer - radius - positionIndent 109 | } 110 | } 111 | return 0 112 | } 113 | 114 | selected = undefined; 115 | 116 | var drag = d3.behavior.drag(); 117 | 118 | drag.on("drag", function() { 119 | d3.select(this).attr("cx", +d3.select(this).attr("cx") + d3.event.dx); 120 | d3.select(this).attr("cy", +d3.select(this).attr("cy") + d3.event.dy); 121 | }) 122 | 123 | deselect = function() { 124 | d3.select(".selected").classed("selected", false); 125 | selected = undefined; 126 | } 127 | 128 | selectDice = function(dieIndex){ 129 | console.log('Dice click - pos ', selected.position, ' ', parseInt(dieIndex)) 130 | client.emit('move', selected.position, dieIndex); 131 | } 132 | 133 | update_player = function(player){ 134 | $('#player').text(player.charAt(0).toUpperCase() + player.slice(1)) 135 | } 136 | 137 | selectPiece = function(circle){ 138 | var sel = d3.select(circle); 139 | var clicked = sel.datum(); 140 | 141 | if (clicked.selectable){ 142 | deselect(); 143 | sel.classed("selected", true); 144 | 145 | selected = clicked; 146 | } 147 | } 148 | passTurn = function(){ 149 | console.debug('passing'); 150 | client.emit('pass'); 151 | } 152 | render_skip_message = function(dice, playable){ 153 | var playableDiv = $('#playable'); 154 | playableDiv.empty(); 155 | 156 | if (!playable){ 157 | var link = $('Cannot move - skip turn'); 158 | link.click(passTurn); 159 | link.appendTo(playableDiv); 160 | } 161 | 162 | } 163 | render_dice = function(dice){ 164 | d3.select("#dice") 165 | .selectAll("a") 166 | .remove(); 167 | 168 | var diceDiv = $("#dice"); 169 | diceDiv.empty(); 170 | 171 | if (typeof dice != 'undefined'){ 172 | console.debug('Render dice') 173 | 174 | console.debug(dice) 175 | pieces = d3.select("#dice") 176 | .selectAll("a") 177 | .data(dice) 178 | .enter() 179 | .append("a") 180 | .classed("dice", function(d){return true}) 181 | .classed("used", function(d){ return d.rolled}) 182 | .on('click', function(d, i){console.debug('Dice', d); selectDice(i)}) 183 | .text(function(d){return d.val}) 184 | } else { 185 | var link = $('Perform roll)'); 186 | link.click(requestRoll); 187 | link.appendTo(diceDiv) 188 | } 189 | } 190 | requestRoll = function(){ 191 | client.emit('roll') 192 | } 193 | 194 | render_board = function(data){ 195 | deselect(); 196 | console.debug('Rendering Board'); 197 | 198 | // position -> count map 199 | var perPositionCount = _.countBy(data, _.values); 200 | // position -> piece map 201 | var perPositionPiece = _.indexBy(data, _.values); 202 | 203 | // f(count, piece) -> [pieces with index...] 204 | var indexedPieceFunction = function (countAndPiece) { 205 | return _.times( 206 | countAndPiece[0], 207 | function(index) { 208 | return _.extend({index:index, selectable: index === countAndPiece[0] -1}, countAndPiece[1]); 209 | } 210 | ) 211 | } 212 | var indexedPieces = _.map(_.zip(_.values(perPositionCount), _.values(perPositionPiece)), indexedPieceFunction ) 213 | var indexedPieces = _.flatten(indexedPieces) 214 | 215 | d3.select("#pieces") 216 | .selectAll("circle") 217 | .data(indexedPieces) 218 | .enter() 219 | .append("circle") 220 | 221 | d3.select("#pieces") 222 | .selectAll("circle") 223 | .data(indexedPieces) 224 | .attr("r", radius) 225 | .attr("cx", cx) 226 | .attr("cy", cy) 227 | .attr("index", function(d){ return d.index}) 228 | .attr("pos", function(d){ return d.position}) 229 | .classed("red", function(d){ return d.color === "red"}) 230 | .classed("black", function(d){ return d.color === "black"}) 231 | .on('click', function(){selectPiece(this)}) 232 | 233 | 234 | } 235 | -------------------------------------------------------------------------------- /public/javascripts/spectrum.js: -------------------------------------------------------------------------------- 1 | // Spectrum Colorpicker v1.2.0 2 | // https://github.com/bgrins/spectrum 3 | // Author: Brian Grinstead 4 | // License: MIT 5 | 6 | (function (window, $, undefined) { 7 | var defaultOpts = { 8 | 9 | // Callbacks 10 | beforeShow: noop, 11 | move: noop, 12 | change: noop, 13 | show: noop, 14 | hide: noop, 15 | 16 | // Options 17 | color: false, 18 | flat: false, 19 | showInput: false, 20 | allowEmpty: false, 21 | showButtons: true, 22 | clickoutFiresChange: false, 23 | showInitial: false, 24 | showPalette: false, 25 | showPaletteOnly: false, 26 | showSelectionPalette: true, 27 | localStorageKey: false, 28 | appendTo: "body", 29 | maxSelectionSize: 7, 30 | cancelText: "cancel", 31 | chooseText: "choose", 32 | preferredFormat: false, 33 | className: "", 34 | showAlpha: false, 35 | theme: "sp-light", 36 | palette: ['fff', '000'], 37 | selectionPalette: [], 38 | disabled: false 39 | }, 40 | spectrums = [], 41 | IE = !!/msie/i.exec( window.navigator.userAgent ), 42 | rgbaSupport = (function() { 43 | function contains( str, substr ) { 44 | return !!~('' + str).indexOf(substr); 45 | } 46 | 47 | var elem = document.createElement('div'); 48 | var style = elem.style; 49 | style.cssText = 'background-color:rgba(0,0,0,.5)'; 50 | return contains(style.backgroundColor, 'rgba') || contains(style.backgroundColor, 'hsla'); 51 | })(), 52 | inputTypeColorSupport = (function() { 53 | var colorInput = $("")[0]; 54 | return colorInput.type === "color" && colorInput.value !== "!"; 55 | })(), 56 | replaceInput = [ 57 | "
", 58 | "
", 59 | "
", 60 | "
" 61 | ].join(''), 62 | markup = (function () { 63 | 64 | // IE does not support gradients with multiple stops, so we need to simulate 65 | // that for the rainbow slider with 8 divs that each have a single gradient 66 | var gradientFix = ""; 67 | if (IE) { 68 | for (var i = 1; i <= 6; i++) { 69 | gradientFix += "
"; 70 | } 71 | } 72 | 73 | return [ 74 | "
", 75 | "
", 76 | "
", 77 | "
", 78 | "
", 79 | "
", 80 | "
", 81 | "
", 82 | "
", 83 | "
", 84 | "
", 85 | "
", 86 | "
", 87 | "
", 88 | "
", 89 | "
", 90 | "
", 91 | "
", 92 | "
", 93 | gradientFix, 94 | "
", 95 | "
", 96 | "
", 97 | "
", 98 | "
", 99 | "", 100 | "
", 101 | "
", 102 | "
", 103 | "", 104 | "", 105 | "
", 106 | "
", 107 | "
" 108 | ].join(""); 109 | })(); 110 | 111 | function paletteTemplate (p, color, className) { 112 | var html = []; 113 | for (var i = 0; i < p.length; i++) { 114 | var current = p[i]; 115 | if(current) { 116 | var tiny = tinycolor(current); 117 | var c = tiny.toHsl().l < 0.5 ? "sp-thumb-el sp-thumb-dark" : "sp-thumb-el sp-thumb-light"; 118 | c += (tinycolor.equals(color, current)) ? " sp-thumb-active" : ""; 119 | 120 | var swatchStyle = rgbaSupport ? ("background-color:" + tiny.toRgbString()) : "filter:" + tiny.toFilter(); 121 | html.push(''); 122 | } else { 123 | var cls = 'sp-clear-display'; 124 | html.push(''); 125 | } 126 | } 127 | return "
" + html.join('') + "
"; 128 | } 129 | 130 | function hideAll() { 131 | for (var i = 0; i < spectrums.length; i++) { 132 | if (spectrums[i]) { 133 | spectrums[i].hide(); 134 | } 135 | } 136 | } 137 | 138 | function instanceOptions(o, callbackContext) { 139 | var opts = $.extend({}, defaultOpts, o); 140 | opts.callbacks = { 141 | 'move': bind(opts.move, callbackContext), 142 | 'change': bind(opts.change, callbackContext), 143 | 'show': bind(opts.show, callbackContext), 144 | 'hide': bind(opts.hide, callbackContext), 145 | 'beforeShow': bind(opts.beforeShow, callbackContext) 146 | }; 147 | 148 | return opts; 149 | } 150 | 151 | function spectrum(element, o) { 152 | 153 | var opts = instanceOptions(o, element), 154 | flat = opts.flat, 155 | showSelectionPalette = opts.showSelectionPalette, 156 | localStorageKey = opts.localStorageKey, 157 | theme = opts.theme, 158 | callbacks = opts.callbacks, 159 | resize = throttle(reflow, 10), 160 | visible = false, 161 | dragWidth = 0, 162 | dragHeight = 0, 163 | dragHelperHeight = 0, 164 | slideHeight = 0, 165 | slideWidth = 0, 166 | alphaWidth = 0, 167 | alphaSlideHelperWidth = 0, 168 | slideHelperHeight = 0, 169 | currentHue = 0, 170 | currentSaturation = 0, 171 | currentValue = 0, 172 | currentAlpha = 1, 173 | palette = opts.palette.slice(0), 174 | paletteArray = $.isArray(palette[0]) ? palette : [palette], 175 | selectionPalette = opts.selectionPalette.slice(0), 176 | maxSelectionSize = opts.maxSelectionSize, 177 | draggingClass = "sp-dragging", 178 | shiftMovementDirection = null; 179 | 180 | var doc = element.ownerDocument, 181 | body = doc.body, 182 | boundElement = $(element), 183 | disabled = false, 184 | container = $(markup, doc).addClass(theme), 185 | dragger = container.find(".sp-color"), 186 | dragHelper = container.find(".sp-dragger"), 187 | slider = container.find(".sp-hue"), 188 | slideHelper = container.find(".sp-slider"), 189 | alphaSliderInner = container.find(".sp-alpha-inner"), 190 | alphaSlider = container.find(".sp-alpha"), 191 | alphaSlideHelper = container.find(".sp-alpha-handle"), 192 | textInput = container.find(".sp-input"), 193 | paletteContainer = container.find(".sp-palette"), 194 | initialColorContainer = container.find(".sp-initial"), 195 | cancelButton = container.find(".sp-cancel"), 196 | clearButton = container.find(".sp-clear"), 197 | chooseButton = container.find(".sp-choose"), 198 | isInput = boundElement.is("input"), 199 | isInputTypeColor = isInput && inputTypeColorSupport && boundElement.attr("type") === "color", 200 | shouldReplace = isInput && !flat, 201 | replacer = (shouldReplace) ? $(replaceInput).addClass(theme).addClass(opts.className) : $([]), 202 | offsetElement = (shouldReplace) ? replacer : boundElement, 203 | previewElement = replacer.find(".sp-preview-inner"), 204 | initialColor = opts.color || (isInput && boundElement.val()), 205 | colorOnShow = false, 206 | preferredFormat = opts.preferredFormat, 207 | currentPreferredFormat = preferredFormat, 208 | clickoutFiresChange = !opts.showButtons || opts.clickoutFiresChange, 209 | isEmpty = !initialColor, 210 | allowEmpty = opts.allowEmpty && !isInputTypeColor; 211 | 212 | function applyOptions() { 213 | 214 | if (opts.showPaletteOnly) { 215 | opts.showPalette = true; 216 | } 217 | 218 | container.toggleClass("sp-flat", flat); 219 | container.toggleClass("sp-input-disabled", !opts.showInput); 220 | container.toggleClass("sp-alpha-enabled", opts.showAlpha); 221 | container.toggleClass("sp-clear-enabled", allowEmpty); 222 | container.toggleClass("sp-buttons-disabled", !opts.showButtons); 223 | container.toggleClass("sp-palette-disabled", !opts.showPalette); 224 | container.toggleClass("sp-palette-only", opts.showPaletteOnly); 225 | container.toggleClass("sp-initial-disabled", !opts.showInitial); 226 | container.addClass(opts.className); 227 | 228 | reflow(); 229 | } 230 | 231 | function initialize() { 232 | 233 | if (IE) { 234 | container.find("*:not(input)").attr("unselectable", "on"); 235 | } 236 | 237 | applyOptions(); 238 | 239 | if (shouldReplace) { 240 | boundElement.after(replacer).hide(); 241 | } 242 | 243 | if (!allowEmpty) { 244 | clearButton.hide(); 245 | } 246 | 247 | if (flat) { 248 | boundElement.after(container).hide(); 249 | } 250 | else { 251 | 252 | var appendTo = opts.appendTo === "parent" ? boundElement.parent() : $(opts.appendTo); 253 | if (appendTo.length !== 1) { 254 | appendTo = $("body"); 255 | } 256 | 257 | appendTo.append(container); 258 | } 259 | 260 | if (localStorageKey && window.localStorage) { 261 | 262 | // Migrate old palettes over to new format. May want to remove this eventually. 263 | try { 264 | var oldPalette = window.localStorage[localStorageKey].split(",#"); 265 | if (oldPalette.length > 1) { 266 | delete window.localStorage[localStorageKey]; 267 | $.each(oldPalette, function(i, c) { 268 | addColorToSelectionPalette(c); 269 | }); 270 | } 271 | } 272 | catch(e) { } 273 | 274 | try { 275 | selectionPalette = window.localStorage[localStorageKey].split(";"); 276 | } 277 | catch (e) { } 278 | } 279 | 280 | offsetElement.bind("click.spectrum touchstart.spectrum", function (e) { 281 | if (!disabled) { 282 | toggle(); 283 | } 284 | 285 | e.stopPropagation(); 286 | 287 | if (!$(e.target).is("input")) { 288 | e.preventDefault(); 289 | } 290 | }); 291 | 292 | if(boundElement.is(":disabled") || (opts.disabled === true)) { 293 | disable(); 294 | } 295 | 296 | // Prevent clicks from bubbling up to document. This would cause it to be hidden. 297 | container.click(stopPropagation); 298 | 299 | // Handle user typed input 300 | textInput.change(setFromTextInput); 301 | textInput.bind("paste", function () { 302 | setTimeout(setFromTextInput, 1); 303 | }); 304 | textInput.keydown(function (e) { if (e.keyCode == 13) { setFromTextInput(); } }); 305 | 306 | cancelButton.text(opts.cancelText); 307 | cancelButton.bind("click.spectrum", function (e) { 308 | e.stopPropagation(); 309 | e.preventDefault(); 310 | hide("cancel"); 311 | }); 312 | 313 | 314 | clearButton.bind("click.spectrum", function (e) { 315 | e.stopPropagation(); 316 | e.preventDefault(); 317 | 318 | isEmpty = true; 319 | 320 | move(); 321 | if(flat) { 322 | //for the flat style, this is a change event 323 | updateOriginalInput(true); 324 | } 325 | }); 326 | 327 | 328 | chooseButton.text(opts.chooseText); 329 | chooseButton.bind("click.spectrum", function (e) { 330 | e.stopPropagation(); 331 | e.preventDefault(); 332 | 333 | if (isValid()) { 334 | updateOriginalInput(true); 335 | hide(); 336 | } 337 | }); 338 | 339 | draggable(alphaSlider, function (dragX, dragY, e) { 340 | currentAlpha = (dragX / alphaWidth); 341 | isEmpty = false; 342 | if (e.shiftKey) { 343 | currentAlpha = Math.round(currentAlpha * 10) / 10; 344 | } 345 | 346 | move(); 347 | }); 348 | 349 | draggable(slider, function (dragX, dragY) { 350 | currentHue = parseFloat(dragY / slideHeight); 351 | isEmpty = false; 352 | move(); 353 | }, dragStart, dragStop); 354 | 355 | draggable(dragger, function (dragX, dragY, e) { 356 | 357 | // shift+drag should snap the movement to either the x or y axis. 358 | if (!e.shiftKey) { 359 | shiftMovementDirection = null; 360 | } 361 | else if (!shiftMovementDirection) { 362 | var oldDragX = currentSaturation * dragWidth; 363 | var oldDragY = dragHeight - (currentValue * dragHeight); 364 | var furtherFromX = Math.abs(dragX - oldDragX) > Math.abs(dragY - oldDragY); 365 | 366 | shiftMovementDirection = furtherFromX ? "x" : "y"; 367 | } 368 | 369 | var setSaturation = !shiftMovementDirection || shiftMovementDirection === "x"; 370 | var setValue = !shiftMovementDirection || shiftMovementDirection === "y"; 371 | 372 | if (setSaturation) { 373 | currentSaturation = parseFloat(dragX / dragWidth); 374 | } 375 | if (setValue) { 376 | currentValue = parseFloat((dragHeight - dragY) / dragHeight); 377 | } 378 | 379 | isEmpty = false; 380 | 381 | move(); 382 | 383 | }, dragStart, dragStop); 384 | 385 | if (!!initialColor) { 386 | set(initialColor); 387 | 388 | // In case color was black - update the preview UI and set the format 389 | // since the set function will not run (default color is black). 390 | updateUI(); 391 | currentPreferredFormat = preferredFormat || tinycolor(initialColor).format; 392 | 393 | addColorToSelectionPalette(initialColor); 394 | } 395 | else { 396 | updateUI(); 397 | } 398 | 399 | if (flat) { 400 | show(); 401 | } 402 | 403 | function palletElementClick(e) { 404 | if (e.data && e.data.ignore) { 405 | set($(this).data("color")); 406 | move(); 407 | } 408 | else { 409 | set($(this).data("color")); 410 | updateOriginalInput(true); 411 | move(); 412 | hide(); 413 | } 414 | 415 | return false; 416 | } 417 | 418 | var paletteEvent = IE ? "mousedown.spectrum" : "click.spectrum touchstart.spectrum"; 419 | paletteContainer.delegate(".sp-thumb-el", paletteEvent, palletElementClick); 420 | initialColorContainer.delegate(".sp-thumb-el:nth-child(1)", paletteEvent, { ignore: true }, palletElementClick); 421 | } 422 | 423 | function addColorToSelectionPalette(color) { 424 | if (showSelectionPalette) { 425 | var colorRgb = tinycolor(color).toRgbString(); 426 | if ($.inArray(colorRgb, selectionPalette) === -1) { 427 | selectionPalette.push(colorRgb); 428 | while(selectionPalette.length > maxSelectionSize) { 429 | selectionPalette.shift(); 430 | } 431 | } 432 | 433 | if (localStorageKey && window.localStorage) { 434 | try { 435 | window.localStorage[localStorageKey] = selectionPalette.join(";"); 436 | } 437 | catch(e) { } 438 | } 439 | } 440 | } 441 | 442 | function getUniqueSelectionPalette() { 443 | var unique = []; 444 | var p = selectionPalette; 445 | var paletteLookup = {}; 446 | var rgb; 447 | 448 | if (opts.showPalette) { 449 | 450 | for (var i = 0; i < paletteArray.length; i++) { 451 | for (var j = 0; j < paletteArray[i].length; j++) { 452 | rgb = tinycolor(paletteArray[i][j]).toRgbString(); 453 | paletteLookup[rgb] = true; 454 | } 455 | } 456 | 457 | for (i = 0; i < p.length; i++) { 458 | rgb = tinycolor(p[i]).toRgbString(); 459 | 460 | if (!paletteLookup.hasOwnProperty(rgb)) { 461 | unique.push(p[i]); 462 | paletteLookup[rgb] = true; 463 | } 464 | } 465 | } 466 | 467 | return unique.reverse().slice(0, opts.maxSelectionSize); 468 | } 469 | 470 | function drawPalette() { 471 | 472 | var currentColor = get(); 473 | 474 | var html = $.map(paletteArray, function (palette, i) { 475 | return paletteTemplate(palette, currentColor, "sp-palette-row sp-palette-row-" + i); 476 | }); 477 | 478 | if (selectionPalette) { 479 | html.push(paletteTemplate(getUniqueSelectionPalette(), currentColor, "sp-palette-row sp-palette-row-selection")); 480 | } 481 | 482 | paletteContainer.html(html.join("")); 483 | } 484 | 485 | function drawInitial() { 486 | if (opts.showInitial) { 487 | var initial = colorOnShow; 488 | var current = get(); 489 | initialColorContainer.html(paletteTemplate([initial, current], current, "sp-palette-row-initial")); 490 | } 491 | } 492 | 493 | function dragStart() { 494 | if (dragHeight <= 0 || dragWidth <= 0 || slideHeight <= 0) { 495 | reflow(); 496 | } 497 | container.addClass(draggingClass); 498 | shiftMovementDirection = null; 499 | } 500 | 501 | function dragStop() { 502 | container.removeClass(draggingClass); 503 | } 504 | 505 | function setFromTextInput() { 506 | 507 | var value = textInput.val(); 508 | 509 | if ((value === null || value === "") && allowEmpty) { 510 | set(null); 511 | } 512 | else { 513 | var tiny = tinycolor(value); 514 | if (tiny.ok) { 515 | set(tiny); 516 | } 517 | else { 518 | textInput.addClass("sp-validation-error"); 519 | } 520 | } 521 | } 522 | 523 | function toggle() { 524 | if (visible) { 525 | hide(); 526 | } 527 | else { 528 | show(); 529 | } 530 | } 531 | 532 | function show() { 533 | var event = $.Event('beforeShow.spectrum'); 534 | 535 | if (visible) { 536 | reflow(); 537 | return; 538 | } 539 | 540 | boundElement.trigger(event, [ get() ]); 541 | 542 | if (callbacks.beforeShow(get()) === false || event.isDefaultPrevented()) { 543 | return; 544 | } 545 | 546 | hideAll(); 547 | visible = true; 548 | 549 | $(doc).bind("click.spectrum", hide); 550 | $(window).bind("resize.spectrum", resize); 551 | replacer.addClass("sp-active"); 552 | container.removeClass("sp-hidden"); 553 | 554 | if (opts.showPalette) { 555 | drawPalette(); 556 | } 557 | reflow(); 558 | updateUI(); 559 | 560 | colorOnShow = get(); 561 | 562 | drawInitial(); 563 | callbacks.show(colorOnShow); 564 | boundElement.trigger('show.spectrum', [ colorOnShow ]); 565 | } 566 | 567 | function hide(e) { 568 | 569 | // Return on right click 570 | if (e && e.type == "click" && e.button == 2) { return; } 571 | 572 | // Return if hiding is unnecessary 573 | if (!visible || flat) { return; } 574 | visible = false; 575 | 576 | $(doc).unbind("click.spectrum", hide); 577 | $(window).unbind("resize.spectrum", resize); 578 | 579 | replacer.removeClass("sp-active"); 580 | container.addClass("sp-hidden"); 581 | 582 | var colorHasChanged = !tinycolor.equals(get(), colorOnShow); 583 | 584 | if (colorHasChanged) { 585 | if (clickoutFiresChange && e !== "cancel") { 586 | updateOriginalInput(true); 587 | } 588 | else { 589 | revert(); 590 | } 591 | } 592 | 593 | callbacks.hide(get()); 594 | boundElement.trigger('hide.spectrum', [ get() ]); 595 | } 596 | 597 | function revert() { 598 | set(colorOnShow, true); 599 | } 600 | 601 | function set(color, ignoreFormatChange) { 602 | if (tinycolor.equals(color, get())) { 603 | return; 604 | } 605 | 606 | var newColor; 607 | if (!color && allowEmpty) { 608 | isEmpty = true; 609 | } else { 610 | isEmpty = false; 611 | newColor = tinycolor(color); 612 | var newHsv = newColor.toHsv(); 613 | 614 | currentHue = (newHsv.h % 360) / 360; 615 | currentSaturation = newHsv.s; 616 | currentValue = newHsv.v; 617 | currentAlpha = newHsv.a; 618 | } 619 | updateUI(); 620 | 621 | if (newColor && newColor.ok && !ignoreFormatChange) { 622 | currentPreferredFormat = preferredFormat || newColor.format; 623 | } 624 | } 625 | 626 | function get(opts) { 627 | opts = opts || { }; 628 | 629 | if (allowEmpty && isEmpty) { 630 | return null; 631 | } 632 | 633 | return tinycolor.fromRatio({ 634 | h: currentHue, 635 | s: currentSaturation, 636 | v: currentValue, 637 | a: Math.round(currentAlpha * 100) / 100 638 | }, { format: opts.format || currentPreferredFormat }); 639 | } 640 | 641 | function isValid() { 642 | return !textInput.hasClass("sp-validation-error"); 643 | } 644 | 645 | function move() { 646 | updateUI(); 647 | 648 | callbacks.move(get()); 649 | boundElement.trigger('move.spectrum', [ get() ]); 650 | } 651 | 652 | function updateUI() { 653 | 654 | textInput.removeClass("sp-validation-error"); 655 | 656 | updateHelperLocations(); 657 | 658 | // Update dragger background color (gradients take care of saturation and value). 659 | var flatColor = tinycolor.fromRatio({ h: currentHue, s: 1, v: 1 }); 660 | dragger.css("background-color", flatColor.toHexString()); 661 | 662 | // Get a format that alpha will be included in (hex and names ignore alpha) 663 | var format = currentPreferredFormat; 664 | if (currentAlpha < 1) { 665 | if (format === "hex" || format === "hex3" || format === "hex6" || format === "name") { 666 | format = "rgb"; 667 | } 668 | } 669 | 670 | var realColor = get({ format: format }), 671 | displayColor = ''; 672 | 673 | //reset background info for preview element 674 | previewElement.removeClass("sp-clear-display"); 675 | previewElement.css('background-color', 'transparent'); 676 | 677 | if (!realColor && allowEmpty) { 678 | // Update the replaced elements background with icon indicating no color selection 679 | previewElement.addClass("sp-clear-display"); 680 | } 681 | else { 682 | var realHex = realColor.toHexString(), 683 | realRgb = realColor.toRgbString(); 684 | 685 | // Update the replaced elements background color (with actual selected color) 686 | if (rgbaSupport || realColor.alpha === 1) { 687 | previewElement.css("background-color", realRgb); 688 | } 689 | else { 690 | previewElement.css("background-color", "transparent"); 691 | previewElement.css("filter", realColor.toFilter()); 692 | } 693 | 694 | if (opts.showAlpha) { 695 | var rgb = realColor.toRgb(); 696 | rgb.a = 0; 697 | var realAlpha = tinycolor(rgb).toRgbString(); 698 | var gradient = "linear-gradient(left, " + realAlpha + ", " + realHex + ")"; 699 | 700 | if (IE) { 701 | alphaSliderInner.css("filter", tinycolor(realAlpha).toFilter({ gradientType: 1 }, realHex)); 702 | } 703 | else { 704 | alphaSliderInner.css("background", "-webkit-" + gradient); 705 | alphaSliderInner.css("background", "-moz-" + gradient); 706 | alphaSliderInner.css("background", "-ms-" + gradient); 707 | alphaSliderInner.css("background", gradient); 708 | } 709 | } 710 | 711 | displayColor = realColor.toString(format); 712 | } 713 | // Update the text entry input as it changes happen 714 | if (opts.showInput) { 715 | textInput.val(displayColor); 716 | } 717 | 718 | if (opts.showPalette) { 719 | drawPalette(); 720 | } 721 | 722 | drawInitial(); 723 | } 724 | 725 | function updateHelperLocations() { 726 | var s = currentSaturation; 727 | var v = currentValue; 728 | 729 | if(allowEmpty && isEmpty) { 730 | //if selected color is empty, hide the helpers 731 | alphaSlideHelper.hide(); 732 | slideHelper.hide(); 733 | dragHelper.hide(); 734 | } 735 | else { 736 | //make sure helpers are visible 737 | alphaSlideHelper.show(); 738 | slideHelper.show(); 739 | dragHelper.show(); 740 | 741 | // Where to show the little circle in that displays your current selected color 742 | var dragX = s * dragWidth; 743 | var dragY = dragHeight - (v * dragHeight); 744 | dragX = Math.max( 745 | -dragHelperHeight, 746 | Math.min(dragWidth - dragHelperHeight, dragX - dragHelperHeight) 747 | ); 748 | dragY = Math.max( 749 | -dragHelperHeight, 750 | Math.min(dragHeight - dragHelperHeight, dragY - dragHelperHeight) 751 | ); 752 | dragHelper.css({ 753 | "top": dragY, 754 | "left": dragX 755 | }); 756 | 757 | var alphaX = currentAlpha * alphaWidth; 758 | alphaSlideHelper.css({ 759 | "left": alphaX - (alphaSlideHelperWidth / 2) 760 | }); 761 | 762 | // Where to show the bar that displays your current selected hue 763 | var slideY = (currentHue) * slideHeight; 764 | slideHelper.css({ 765 | "top": slideY - slideHelperHeight 766 | }); 767 | } 768 | } 769 | 770 | function updateOriginalInput(fireCallback) { 771 | var color = get(), 772 | displayColor = '', 773 | hasChanged = !tinycolor.equals(color, colorOnShow); 774 | 775 | if(color) { 776 | displayColor = color.toString(currentPreferredFormat); 777 | // Update the selection palette with the current color 778 | addColorToSelectionPalette(color); 779 | } 780 | 781 | if (isInput) { 782 | boundElement.val(displayColor); 783 | } 784 | 785 | colorOnShow = color; 786 | 787 | if (fireCallback && hasChanged) { 788 | callbacks.change(color); 789 | boundElement.trigger('change', [ color ]); 790 | } 791 | } 792 | 793 | function reflow() { 794 | dragWidth = dragger.width(); 795 | dragHeight = dragger.height(); 796 | dragHelperHeight = dragHelper.height(); 797 | slideWidth = slider.width(); 798 | slideHeight = slider.height(); 799 | slideHelperHeight = slideHelper.height(); 800 | alphaWidth = alphaSlider.width(); 801 | alphaSlideHelperWidth = alphaSlideHelper.width(); 802 | 803 | if (!flat) { 804 | container.css("position", "absolute"); 805 | container.offset(getOffset(container, offsetElement)); 806 | } 807 | 808 | updateHelperLocations(); 809 | } 810 | 811 | function destroy() { 812 | boundElement.show(); 813 | offsetElement.unbind("click.spectrum touchstart.spectrum"); 814 | container.remove(); 815 | replacer.remove(); 816 | spectrums[spect.id] = null; 817 | } 818 | 819 | function option(optionName, optionValue) { 820 | if (optionName === undefined) { 821 | return $.extend({}, opts); 822 | } 823 | if (optionValue === undefined) { 824 | return opts[optionName]; 825 | } 826 | 827 | opts[optionName] = optionValue; 828 | applyOptions(); 829 | } 830 | 831 | function enable() { 832 | disabled = false; 833 | boundElement.attr("disabled", false); 834 | offsetElement.removeClass("sp-disabled"); 835 | } 836 | 837 | function disable() { 838 | hide(); 839 | disabled = true; 840 | boundElement.attr("disabled", true); 841 | offsetElement.addClass("sp-disabled"); 842 | } 843 | 844 | initialize(); 845 | 846 | var spect = { 847 | show: show, 848 | hide: hide, 849 | toggle: toggle, 850 | reflow: reflow, 851 | option: option, 852 | enable: enable, 853 | disable: disable, 854 | set: function (c) { 855 | set(c); 856 | updateOriginalInput(); 857 | }, 858 | get: get, 859 | destroy: destroy, 860 | container: container 861 | }; 862 | 863 | spect.id = spectrums.push(spect) - 1; 864 | 865 | return spect; 866 | } 867 | 868 | /** 869 | * checkOffset - get the offset below/above and left/right element depending on screen position 870 | * Thanks https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js 871 | */ 872 | function getOffset(picker, input) { 873 | var extraY = 0; 874 | var dpWidth = picker.outerWidth(); 875 | var dpHeight = picker.outerHeight(); 876 | var inputHeight = input.outerHeight(); 877 | var doc = picker[0].ownerDocument; 878 | var docElem = doc.documentElement; 879 | var viewWidth = docElem.clientWidth + $(doc).scrollLeft(); 880 | var viewHeight = docElem.clientHeight + $(doc).scrollTop(); 881 | var offset = input.offset(); 882 | offset.top += inputHeight; 883 | 884 | offset.left -= 885 | Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ? 886 | Math.abs(offset.left + dpWidth - viewWidth) : 0); 887 | 888 | offset.top -= 889 | Math.min(offset.top, ((offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ? 890 | Math.abs(dpHeight + inputHeight - extraY) : extraY)); 891 | 892 | return offset; 893 | } 894 | 895 | /** 896 | * noop - do nothing 897 | */ 898 | function noop() { 899 | 900 | } 901 | 902 | /** 903 | * stopPropagation - makes the code only doing this a little easier to read in line 904 | */ 905 | function stopPropagation(e) { 906 | e.stopPropagation(); 907 | } 908 | 909 | /** 910 | * Create a function bound to a given object 911 | * Thanks to underscore.js 912 | */ 913 | function bind(func, obj) { 914 | var slice = Array.prototype.slice; 915 | var args = slice.call(arguments, 2); 916 | return function () { 917 | return func.apply(obj, args.concat(slice.call(arguments))); 918 | }; 919 | } 920 | 921 | /** 922 | * Lightweight drag helper. Handles containment within the element, so that 923 | * when dragging, the x is within [0,element.width] and y is within [0,element.height] 924 | */ 925 | function draggable(element, onmove, onstart, onstop) { 926 | onmove = onmove || function () { }; 927 | onstart = onstart || function () { }; 928 | onstop = onstop || function () { }; 929 | var doc = element.ownerDocument || document; 930 | var dragging = false; 931 | var offset = {}; 932 | var maxHeight = 0; 933 | var maxWidth = 0; 934 | var hasTouch = ('ontouchstart' in window); 935 | 936 | var duringDragEvents = {}; 937 | duringDragEvents["selectstart"] = prevent; 938 | duringDragEvents["dragstart"] = prevent; 939 | duringDragEvents["touchmove mousemove"] = move; 940 | duringDragEvents["touchend mouseup"] = stop; 941 | 942 | function prevent(e) { 943 | if (e.stopPropagation) { 944 | e.stopPropagation(); 945 | } 946 | if (e.preventDefault) { 947 | e.preventDefault(); 948 | } 949 | e.returnValue = false; 950 | } 951 | 952 | function move(e) { 953 | if (dragging) { 954 | // Mouseup happened outside of window 955 | if (IE && document.documentMode < 9 && !e.button) { 956 | return stop(); 957 | } 958 | 959 | var touches = e.originalEvent.touches; 960 | var pageX = touches ? touches[0].pageX : e.pageX; 961 | var pageY = touches ? touches[0].pageY : e.pageY; 962 | 963 | var dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth)); 964 | var dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight)); 965 | 966 | if (hasTouch) { 967 | // Stop scrolling in iOS 968 | prevent(e); 969 | } 970 | 971 | onmove.apply(element, [dragX, dragY, e]); 972 | } 973 | } 974 | function start(e) { 975 | var rightclick = (e.which) ? (e.which == 3) : (e.button == 2); 976 | var touches = e.originalEvent.touches; 977 | 978 | if (!rightclick && !dragging) { 979 | if (onstart.apply(element, arguments) !== false) { 980 | dragging = true; 981 | maxHeight = $(element).height(); 982 | maxWidth = $(element).width(); 983 | offset = $(element).offset(); 984 | 985 | $(doc).bind(duringDragEvents); 986 | $(doc.body).addClass("sp-dragging"); 987 | 988 | if (!hasTouch) { 989 | move(e); 990 | } 991 | 992 | prevent(e); 993 | } 994 | } 995 | } 996 | function stop() { 997 | if (dragging) { 998 | $(doc).unbind(duringDragEvents); 999 | $(doc.body).removeClass("sp-dragging"); 1000 | onstop.apply(element, arguments); 1001 | } 1002 | dragging = false; 1003 | } 1004 | 1005 | $(element).bind("touchstart mousedown", start); 1006 | } 1007 | 1008 | function throttle(func, wait, debounce) { 1009 | var timeout; 1010 | return function () { 1011 | var context = this, args = arguments; 1012 | var throttler = function () { 1013 | timeout = null; 1014 | func.apply(context, args); 1015 | }; 1016 | if (debounce) clearTimeout(timeout); 1017 | if (debounce || !timeout) timeout = setTimeout(throttler, wait); 1018 | }; 1019 | } 1020 | 1021 | 1022 | function log(){/* jshint -W021 */if(window.console){if(Function.prototype.bind)log=Function.prototype.bind.call(console.log,console);else log=function(){Function.prototype.apply.call(console.log,console,arguments);};log.apply(this,arguments);}} 1023 | 1024 | /** 1025 | * Define a jQuery plugin 1026 | */ 1027 | var dataID = "spectrum.id"; 1028 | $.fn.spectrum = function (opts, extra) { 1029 | 1030 | if (typeof opts == "string") { 1031 | 1032 | var returnValue = this; 1033 | var args = Array.prototype.slice.call( arguments, 1 ); 1034 | 1035 | this.each(function () { 1036 | var spect = spectrums[$(this).data(dataID)]; 1037 | if (spect) { 1038 | 1039 | var method = spect[opts]; 1040 | if (!method) { 1041 | throw new Error( "Spectrum: no such method: '" + opts + "'" ); 1042 | } 1043 | 1044 | if (opts == "get") { 1045 | returnValue = spect.get(); 1046 | } 1047 | else if (opts == "container") { 1048 | returnValue = spect.container; 1049 | } 1050 | else if (opts == "option") { 1051 | returnValue = spect.option.apply(spect, args); 1052 | } 1053 | else if (opts == "destroy") { 1054 | spect.destroy(); 1055 | $(this).removeData(dataID); 1056 | } 1057 | else { 1058 | method.apply(spect, args); 1059 | } 1060 | } 1061 | }); 1062 | 1063 | return returnValue; 1064 | } 1065 | 1066 | // Initializing a new instance of spectrum 1067 | return this.spectrum("destroy").each(function () { 1068 | var spect = spectrum(this, opts); 1069 | $(this).data(dataID, spect.id); 1070 | }); 1071 | }; 1072 | 1073 | $.fn.spectrum.load = true; 1074 | $.fn.spectrum.loadOpts = {}; 1075 | $.fn.spectrum.draggable = draggable; 1076 | $.fn.spectrum.defaults = defaultOpts; 1077 | 1078 | $.spectrum = { }; 1079 | $.spectrum.localization = { }; 1080 | $.spectrum.palettes = { }; 1081 | 1082 | $.fn.spectrum.processNativeColorInputs = function () { 1083 | if (!inputTypeColorSupport) { 1084 | $("input[type=color]").spectrum({ 1085 | preferredFormat: "hex6" 1086 | }); 1087 | } 1088 | }; 1089 | 1090 | // TinyColor v0.9.16 1091 | // https://github.com/bgrins/TinyColor 1092 | // 2013-08-10, Brian Grinstead, MIT License 1093 | 1094 | (function() { 1095 | 1096 | var trimLeft = /^[\s,#]+/, 1097 | trimRight = /\s+$/, 1098 | tinyCounter = 0, 1099 | math = Math, 1100 | mathRound = math.round, 1101 | mathMin = math.min, 1102 | mathMax = math.max, 1103 | mathRandom = math.random; 1104 | 1105 | function tinycolor (color, opts) { 1106 | 1107 | color = (color) ? color : ''; 1108 | opts = opts || { }; 1109 | 1110 | // If input is already a tinycolor, return itself 1111 | if (typeof color == "object" && color.hasOwnProperty("_tc_id")) { 1112 | return color; 1113 | } 1114 | 1115 | var rgb = inputToRGB(color); 1116 | var r = rgb.r, 1117 | g = rgb.g, 1118 | b = rgb.b, 1119 | a = rgb.a, 1120 | roundA = mathRound(100*a) / 100, 1121 | format = opts.format || rgb.format; 1122 | 1123 | // Don't let the range of [0,255] come back in [0,1]. 1124 | // Potentially lose a little bit of precision here, but will fix issues where 1125 | // .5 gets interpreted as half of the total, instead of half of 1 1126 | // If it was supposed to be 128, this was already taken care of by `inputToRgb` 1127 | if (r < 1) { r = mathRound(r); } 1128 | if (g < 1) { g = mathRound(g); } 1129 | if (b < 1) { b = mathRound(b); } 1130 | 1131 | return { 1132 | ok: rgb.ok, 1133 | format: format, 1134 | _tc_id: tinyCounter++, 1135 | alpha: a, 1136 | getAlpha: function() { 1137 | return a; 1138 | }, 1139 | setAlpha: function(value) { 1140 | a = boundAlpha(value); 1141 | roundA = mathRound(100*a) / 100; 1142 | }, 1143 | toHsv: function() { 1144 | var hsv = rgbToHsv(r, g, b); 1145 | return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: a }; 1146 | }, 1147 | toHsvString: function() { 1148 | var hsv = rgbToHsv(r, g, b); 1149 | var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100); 1150 | return (a == 1) ? 1151 | "hsv(" + h + ", " + s + "%, " + v + "%)" : 1152 | "hsva(" + h + ", " + s + "%, " + v + "%, "+ roundA + ")"; 1153 | }, 1154 | toHsl: function() { 1155 | var hsl = rgbToHsl(r, g, b); 1156 | return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: a }; 1157 | }, 1158 | toHslString: function() { 1159 | var hsl = rgbToHsl(r, g, b); 1160 | var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100); 1161 | return (a == 1) ? 1162 | "hsl(" + h + ", " + s + "%, " + l + "%)" : 1163 | "hsla(" + h + ", " + s + "%, " + l + "%, "+ roundA + ")"; 1164 | }, 1165 | toHex: function(allow3Char) { 1166 | return rgbToHex(r, g, b, allow3Char); 1167 | }, 1168 | toHexString: function(allow3Char) { 1169 | return '#' + rgbToHex(r, g, b, allow3Char); 1170 | }, 1171 | toRgb: function() { 1172 | return { r: mathRound(r), g: mathRound(g), b: mathRound(b), a: a }; 1173 | }, 1174 | toRgbString: function() { 1175 | return (a == 1) ? 1176 | "rgb(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ")" : 1177 | "rgba(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ", " + roundA + ")"; 1178 | }, 1179 | toPercentageRgb: function() { 1180 | return { r: mathRound(bound01(r, 255) * 100) + "%", g: mathRound(bound01(g, 255) * 100) + "%", b: mathRound(bound01(b, 255) * 100) + "%", a: a }; 1181 | }, 1182 | toPercentageRgbString: function() { 1183 | return (a == 1) ? 1184 | "rgb(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%)" : 1185 | "rgba(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%, " + roundA + ")"; 1186 | }, 1187 | toName: function() { 1188 | if (a === 0) { 1189 | return "transparent"; 1190 | } 1191 | 1192 | return hexNames[rgbToHex(r, g, b, true)] || false; 1193 | }, 1194 | toFilter: function(secondColor) { 1195 | var hex = rgbToHex(r, g, b); 1196 | var secondHex = hex; 1197 | var alphaHex = Math.round(parseFloat(a) * 255).toString(16); 1198 | var secondAlphaHex = alphaHex; 1199 | var gradientType = opts && opts.gradientType ? "GradientType = 1, " : ""; 1200 | 1201 | if (secondColor) { 1202 | var s = tinycolor(secondColor); 1203 | secondHex = s.toHex(); 1204 | secondAlphaHex = Math.round(parseFloat(s.alpha) * 255).toString(16); 1205 | } 1206 | 1207 | return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr=#" + pad2(alphaHex) + hex + ",endColorstr=#" + pad2(secondAlphaHex) + secondHex + ")"; 1208 | }, 1209 | toString: function(format) { 1210 | var formatSet = !!format; 1211 | format = format || this.format; 1212 | 1213 | var formattedString = false; 1214 | var hasAlphaAndFormatNotSet = !formatSet && a < 1 && a > 0; 1215 | var formatWithAlpha = hasAlphaAndFormatNotSet && (format === "hex" || format === "hex6" || format === "hex3" || format === "name"); 1216 | 1217 | if (format === "rgb") { 1218 | formattedString = this.toRgbString(); 1219 | } 1220 | if (format === "prgb") { 1221 | formattedString = this.toPercentageRgbString(); 1222 | } 1223 | if (format === "hex" || format === "hex6") { 1224 | formattedString = this.toHexString(); 1225 | } 1226 | if (format === "hex3") { 1227 | formattedString = this.toHexString(true); 1228 | } 1229 | if (format === "name") { 1230 | formattedString = this.toName(); 1231 | } 1232 | if (format === "hsl") { 1233 | formattedString = this.toHslString(); 1234 | } 1235 | if (format === "hsv") { 1236 | formattedString = this.toHsvString(); 1237 | } 1238 | 1239 | if (formatWithAlpha) { 1240 | return this.toRgbString(); 1241 | } 1242 | 1243 | return formattedString || this.toHexString(); 1244 | } 1245 | }; 1246 | } 1247 | 1248 | // If input is an object, force 1 into "1.0" to handle ratios properly 1249 | // String input requires "1.0" as input, so 1 will be treated as 1 1250 | tinycolor.fromRatio = function(color, opts) { 1251 | if (typeof color == "object") { 1252 | var newColor = {}; 1253 | for (var i in color) { 1254 | if (color.hasOwnProperty(i)) { 1255 | if (i === "a") { 1256 | newColor[i] = color[i]; 1257 | } 1258 | else { 1259 | newColor[i] = convertToPercentage(color[i]); 1260 | } 1261 | } 1262 | } 1263 | color = newColor; 1264 | } 1265 | 1266 | return tinycolor(color, opts); 1267 | }; 1268 | 1269 | // Given a string or object, convert that input to RGB 1270 | // Possible string inputs: 1271 | // 1272 | // "red" 1273 | // "#f00" or "f00" 1274 | // "#ff0000" or "ff0000" 1275 | // "rgb 255 0 0" or "rgb (255, 0, 0)" 1276 | // "rgb 1.0 0 0" or "rgb (1, 0, 0)" 1277 | // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1" 1278 | // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1" 1279 | // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%" 1280 | // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1" 1281 | // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%" 1282 | // 1283 | function inputToRGB(color) { 1284 | 1285 | var rgb = { r: 0, g: 0, b: 0 }; 1286 | var a = 1; 1287 | var ok = false; 1288 | var format = false; 1289 | 1290 | if (typeof color == "string") { 1291 | color = stringInputToObject(color); 1292 | } 1293 | 1294 | if (typeof color == "object") { 1295 | if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) { 1296 | rgb = rgbToRgb(color.r, color.g, color.b); 1297 | ok = true; 1298 | format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb"; 1299 | } 1300 | else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) { 1301 | color.s = convertToPercentage(color.s); 1302 | color.v = convertToPercentage(color.v); 1303 | rgb = hsvToRgb(color.h, color.s, color.v); 1304 | ok = true; 1305 | format = "hsv"; 1306 | } 1307 | else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) { 1308 | color.s = convertToPercentage(color.s); 1309 | color.l = convertToPercentage(color.l); 1310 | rgb = hslToRgb(color.h, color.s, color.l); 1311 | ok = true; 1312 | format = "hsl"; 1313 | } 1314 | 1315 | if (color.hasOwnProperty("a")) { 1316 | a = color.a; 1317 | } 1318 | } 1319 | 1320 | a = boundAlpha(a); 1321 | 1322 | return { 1323 | ok: ok, 1324 | format: color.format || format, 1325 | r: mathMin(255, mathMax(rgb.r, 0)), 1326 | g: mathMin(255, mathMax(rgb.g, 0)), 1327 | b: mathMin(255, mathMax(rgb.b, 0)), 1328 | a: a 1329 | }; 1330 | } 1331 | 1332 | 1333 | // Conversion Functions 1334 | // -------------------- 1335 | 1336 | // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from: 1337 | // 1338 | 1339 | // `rgbToRgb` 1340 | // Handle bounds / percentage checking to conform to CSS color spec 1341 | // 1342 | // *Assumes:* r, g, b in [0, 255] or [0, 1] 1343 | // *Returns:* { r, g, b } in [0, 255] 1344 | function rgbToRgb(r, g, b){ 1345 | return { 1346 | r: bound01(r, 255) * 255, 1347 | g: bound01(g, 255) * 255, 1348 | b: bound01(b, 255) * 255 1349 | }; 1350 | } 1351 | 1352 | // `rgbToHsl` 1353 | // Converts an RGB color value to HSL. 1354 | // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1] 1355 | // *Returns:* { h, s, l } in [0,1] 1356 | function rgbToHsl(r, g, b) { 1357 | 1358 | r = bound01(r, 255); 1359 | g = bound01(g, 255); 1360 | b = bound01(b, 255); 1361 | 1362 | var max = mathMax(r, g, b), min = mathMin(r, g, b); 1363 | var h, s, l = (max + min) / 2; 1364 | 1365 | if(max == min) { 1366 | h = s = 0; // achromatic 1367 | } 1368 | else { 1369 | var d = max - min; 1370 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 1371 | switch(max) { 1372 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 1373 | case g: h = (b - r) / d + 2; break; 1374 | case b: h = (r - g) / d + 4; break; 1375 | } 1376 | 1377 | h /= 6; 1378 | } 1379 | 1380 | return { h: h, s: s, l: l }; 1381 | } 1382 | 1383 | // `hslToRgb` 1384 | // Converts an HSL color value to RGB. 1385 | // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100] 1386 | // *Returns:* { r, g, b } in the set [0, 255] 1387 | function hslToRgb(h, s, l) { 1388 | var r, g, b; 1389 | 1390 | h = bound01(h, 360); 1391 | s = bound01(s, 100); 1392 | l = bound01(l, 100); 1393 | 1394 | function hue2rgb(p, q, t) { 1395 | if(t < 0) t += 1; 1396 | if(t > 1) t -= 1; 1397 | if(t < 1/6) return p + (q - p) * 6 * t; 1398 | if(t < 1/2) return q; 1399 | if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; 1400 | return p; 1401 | } 1402 | 1403 | if(s === 0) { 1404 | r = g = b = l; // achromatic 1405 | } 1406 | else { 1407 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s; 1408 | var p = 2 * l - q; 1409 | r = hue2rgb(p, q, h + 1/3); 1410 | g = hue2rgb(p, q, h); 1411 | b = hue2rgb(p, q, h - 1/3); 1412 | } 1413 | 1414 | return { r: r * 255, g: g * 255, b: b * 255 }; 1415 | } 1416 | 1417 | // `rgbToHsv` 1418 | // Converts an RGB color value to HSV 1419 | // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1] 1420 | // *Returns:* { h, s, v } in [0,1] 1421 | function rgbToHsv(r, g, b) { 1422 | 1423 | r = bound01(r, 255); 1424 | g = bound01(g, 255); 1425 | b = bound01(b, 255); 1426 | 1427 | var max = mathMax(r, g, b), min = mathMin(r, g, b); 1428 | var h, s, v = max; 1429 | 1430 | var d = max - min; 1431 | s = max === 0 ? 0 : d / max; 1432 | 1433 | if(max == min) { 1434 | h = 0; // achromatic 1435 | } 1436 | else { 1437 | switch(max) { 1438 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 1439 | case g: h = (b - r) / d + 2; break; 1440 | case b: h = (r - g) / d + 4; break; 1441 | } 1442 | h /= 6; 1443 | } 1444 | return { h: h, s: s, v: v }; 1445 | } 1446 | 1447 | // `hsvToRgb` 1448 | // Converts an HSV color value to RGB. 1449 | // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100] 1450 | // *Returns:* { r, g, b } in the set [0, 255] 1451 | function hsvToRgb(h, s, v) { 1452 | 1453 | h = bound01(h, 360) * 6; 1454 | s = bound01(s, 100); 1455 | v = bound01(v, 100); 1456 | 1457 | var i = math.floor(h), 1458 | f = h - i, 1459 | p = v * (1 - s), 1460 | q = v * (1 - f * s), 1461 | t = v * (1 - (1 - f) * s), 1462 | mod = i % 6, 1463 | r = [v, q, p, p, t, v][mod], 1464 | g = [t, v, v, q, p, p][mod], 1465 | b = [p, p, t, v, v, q][mod]; 1466 | 1467 | return { r: r * 255, g: g * 255, b: b * 255 }; 1468 | } 1469 | 1470 | // `rgbToHex` 1471 | // Converts an RGB color to hex 1472 | // Assumes r, g, and b are contained in the set [0, 255] 1473 | // Returns a 3 or 6 character hex 1474 | function rgbToHex(r, g, b, allow3Char) { 1475 | 1476 | var hex = [ 1477 | pad2(mathRound(r).toString(16)), 1478 | pad2(mathRound(g).toString(16)), 1479 | pad2(mathRound(b).toString(16)) 1480 | ]; 1481 | 1482 | // Return a 3 character hex if possible 1483 | 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)) { 1484 | return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0); 1485 | } 1486 | 1487 | return hex.join(""); 1488 | } 1489 | 1490 | // `equals` 1491 | // Can be called with any tinycolor input 1492 | tinycolor.equals = function (color1, color2) { 1493 | if (!color1 || !color2) { return false; } 1494 | return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString(); 1495 | }; 1496 | tinycolor.random = function() { 1497 | return tinycolor.fromRatio({ 1498 | r: mathRandom(), 1499 | g: mathRandom(), 1500 | b: mathRandom() 1501 | }); 1502 | }; 1503 | 1504 | 1505 | // Modification Functions 1506 | // ---------------------- 1507 | // Thanks to less.js for some of the basics here 1508 | // 1509 | 1510 | tinycolor.desaturate = function (color, amount) { 1511 | amount = (amount === 0) ? 0 : (amount || 10); 1512 | var hsl = tinycolor(color).toHsl(); 1513 | hsl.s -= amount / 100; 1514 | hsl.s = clamp01(hsl.s); 1515 | return tinycolor(hsl); 1516 | }; 1517 | tinycolor.saturate = function (color, amount) { 1518 | amount = (amount === 0) ? 0 : (amount || 10); 1519 | var hsl = tinycolor(color).toHsl(); 1520 | hsl.s += amount / 100; 1521 | hsl.s = clamp01(hsl.s); 1522 | return tinycolor(hsl); 1523 | }; 1524 | tinycolor.greyscale = function(color) { 1525 | return tinycolor.desaturate(color, 100); 1526 | }; 1527 | tinycolor.lighten = function(color, amount) { 1528 | amount = (amount === 0) ? 0 : (amount || 10); 1529 | var hsl = tinycolor(color).toHsl(); 1530 | hsl.l += amount / 100; 1531 | hsl.l = clamp01(hsl.l); 1532 | return tinycolor(hsl); 1533 | }; 1534 | tinycolor.darken = function (color, amount) { 1535 | amount = (amount === 0) ? 0 : (amount || 10); 1536 | var hsl = tinycolor(color).toHsl(); 1537 | hsl.l -= amount / 100; 1538 | hsl.l = clamp01(hsl.l); 1539 | return tinycolor(hsl); 1540 | }; 1541 | tinycolor.complement = function(color) { 1542 | var hsl = tinycolor(color).toHsl(); 1543 | hsl.h = (hsl.h + 180) % 360; 1544 | return tinycolor(hsl); 1545 | }; 1546 | 1547 | 1548 | // Combination Functions 1549 | // --------------------- 1550 | // Thanks to jQuery xColor for some of the ideas behind these 1551 | // 1552 | 1553 | tinycolor.triad = function(color) { 1554 | var hsl = tinycolor(color).toHsl(); 1555 | var h = hsl.h; 1556 | return [ 1557 | tinycolor(color), 1558 | tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }), 1559 | tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l }) 1560 | ]; 1561 | }; 1562 | tinycolor.tetrad = function(color) { 1563 | var hsl = tinycolor(color).toHsl(); 1564 | var h = hsl.h; 1565 | return [ 1566 | tinycolor(color), 1567 | tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }), 1568 | tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }), 1569 | tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l }) 1570 | ]; 1571 | }; 1572 | tinycolor.splitcomplement = function(color) { 1573 | var hsl = tinycolor(color).toHsl(); 1574 | var h = hsl.h; 1575 | return [ 1576 | tinycolor(color), 1577 | tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}), 1578 | tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l}) 1579 | ]; 1580 | }; 1581 | tinycolor.analogous = function(color, results, slices) { 1582 | results = results || 6; 1583 | slices = slices || 30; 1584 | 1585 | var hsl = tinycolor(color).toHsl(); 1586 | var part = 360 / slices; 1587 | var ret = [tinycolor(color)]; 1588 | 1589 | for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) { 1590 | hsl.h = (hsl.h + part) % 360; 1591 | ret.push(tinycolor(hsl)); 1592 | } 1593 | return ret; 1594 | }; 1595 | tinycolor.monochromatic = function(color, results) { 1596 | results = results || 6; 1597 | var hsv = tinycolor(color).toHsv(); 1598 | var h = hsv.h, s = hsv.s, v = hsv.v; 1599 | var ret = []; 1600 | var modification = 1 / results; 1601 | 1602 | while (results--) { 1603 | ret.push(tinycolor({ h: h, s: s, v: v})); 1604 | v = (v + modification) % 1; 1605 | } 1606 | 1607 | return ret; 1608 | }; 1609 | 1610 | 1611 | // Readability Functions 1612 | // --------------------- 1613 | // 1614 | 1615 | // `readability` 1616 | // Analyze the 2 colors and returns an object with the following properties: 1617 | // `brightness`: difference in brightness between the two colors 1618 | // `color`: difference in color/hue between the two colors 1619 | tinycolor.readability = function(color1, color2) { 1620 | var a = tinycolor(color1).toRgb(); 1621 | var b = tinycolor(color2).toRgb(); 1622 | var brightnessA = (a.r * 299 + a.g * 587 + a.b * 114) / 1000; 1623 | var brightnessB = (b.r * 299 + b.g * 587 + b.b * 114) / 1000; 1624 | var colorDiff = ( 1625 | Math.max(a.r, b.r) - Math.min(a.r, b.r) + 1626 | Math.max(a.g, b.g) - Math.min(a.g, b.g) + 1627 | Math.max(a.b, b.b) - Math.min(a.b, b.b) 1628 | ); 1629 | 1630 | return { 1631 | brightness: Math.abs(brightnessA - brightnessB), 1632 | color: colorDiff 1633 | }; 1634 | }; 1635 | 1636 | // `readable` 1637 | // http://www.w3.org/TR/AERT#color-contrast 1638 | // Ensure that foreground and background color combinations provide sufficient contrast. 1639 | // *Example* 1640 | // tinycolor.readable("#000", "#111") => false 1641 | tinycolor.readable = function(color1, color2) { 1642 | var readability = tinycolor.readability(color1, color2); 1643 | return readability.brightness > 125 && readability.color > 500; 1644 | }; 1645 | 1646 | // `mostReadable` 1647 | // Given a base color and a list of possible foreground or background 1648 | // colors for that base, returns the most readable color. 1649 | // *Example* 1650 | // tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000" 1651 | tinycolor.mostReadable = function(baseColor, colorList) { 1652 | var bestColor = null; 1653 | var bestScore = 0; 1654 | var bestIsReadable = false; 1655 | for (var i=0; i < colorList.length; i++) { 1656 | 1657 | // We normalize both around the "acceptable" breaking point, 1658 | // but rank brightness constrast higher than hue. 1659 | 1660 | var readability = tinycolor.readability(baseColor, colorList[i]); 1661 | var readable = readability.brightness > 125 && readability.color > 500; 1662 | var score = 3 * (readability.brightness / 125) + (readability.color / 500); 1663 | 1664 | if ((readable && ! bestIsReadable) || 1665 | (readable && bestIsReadable && score > bestScore) || 1666 | ((! readable) && (! bestIsReadable) && score > bestScore)) { 1667 | bestIsReadable = readable; 1668 | bestScore = score; 1669 | bestColor = tinycolor(colorList[i]); 1670 | } 1671 | } 1672 | return bestColor; 1673 | }; 1674 | 1675 | 1676 | // Big List of Colors 1677 | // ------------------ 1678 | // 1679 | var names = tinycolor.names = { 1680 | aliceblue: "f0f8ff", 1681 | antiquewhite: "faebd7", 1682 | aqua: "0ff", 1683 | aquamarine: "7fffd4", 1684 | azure: "f0ffff", 1685 | beige: "f5f5dc", 1686 | bisque: "ffe4c4", 1687 | black: "000", 1688 | blanchedalmond: "ffebcd", 1689 | blue: "00f", 1690 | blueviolet: "8a2be2", 1691 | brown: "a52a2a", 1692 | burlywood: "deb887", 1693 | burntsienna: "ea7e5d", 1694 | cadetblue: "5f9ea0", 1695 | chartreuse: "7fff00", 1696 | chocolate: "d2691e", 1697 | coral: "ff7f50", 1698 | cornflowerblue: "6495ed", 1699 | cornsilk: "fff8dc", 1700 | crimson: "dc143c", 1701 | cyan: "0ff", 1702 | darkblue: "00008b", 1703 | darkcyan: "008b8b", 1704 | darkgoldenrod: "b8860b", 1705 | darkgray: "a9a9a9", 1706 | darkgreen: "006400", 1707 | darkgrey: "a9a9a9", 1708 | darkkhaki: "bdb76b", 1709 | darkmagenta: "8b008b", 1710 | darkolivegreen: "556b2f", 1711 | darkorange: "ff8c00", 1712 | darkorchid: "9932cc", 1713 | darkred: "8b0000", 1714 | darksalmon: "e9967a", 1715 | darkseagreen: "8fbc8f", 1716 | darkslateblue: "483d8b", 1717 | darkslategray: "2f4f4f", 1718 | darkslategrey: "2f4f4f", 1719 | darkturquoise: "00ced1", 1720 | darkviolet: "9400d3", 1721 | deeppink: "ff1493", 1722 | deepskyblue: "00bfff", 1723 | dimgray: "696969", 1724 | dimgrey: "696969", 1725 | dodgerblue: "1e90ff", 1726 | firebrick: "b22222", 1727 | floralwhite: "fffaf0", 1728 | forestgreen: "228b22", 1729 | fuchsia: "f0f", 1730 | gainsboro: "dcdcdc", 1731 | ghostwhite: "f8f8ff", 1732 | gold: "ffd700", 1733 | goldenrod: "daa520", 1734 | gray: "808080", 1735 | green: "008000", 1736 | greenyellow: "adff2f", 1737 | grey: "808080", 1738 | honeydew: "f0fff0", 1739 | hotpink: "ff69b4", 1740 | indianred: "cd5c5c", 1741 | indigo: "4b0082", 1742 | ivory: "fffff0", 1743 | khaki: "f0e68c", 1744 | lavender: "e6e6fa", 1745 | lavenderblush: "fff0f5", 1746 | lawngreen: "7cfc00", 1747 | lemonchiffon: "fffacd", 1748 | lightblue: "add8e6", 1749 | lightcoral: "f08080", 1750 | lightcyan: "e0ffff", 1751 | lightgoldenrodyellow: "fafad2", 1752 | lightgray: "d3d3d3", 1753 | lightgreen: "90ee90", 1754 | lightgrey: "d3d3d3", 1755 | lightpink: "ffb6c1", 1756 | lightsalmon: "ffa07a", 1757 | lightseagreen: "20b2aa", 1758 | lightskyblue: "87cefa", 1759 | lightslategray: "789", 1760 | lightslategrey: "789", 1761 | lightsteelblue: "b0c4de", 1762 | lightyellow: "ffffe0", 1763 | lime: "0f0", 1764 | limegreen: "32cd32", 1765 | linen: "faf0e6", 1766 | magenta: "f0f", 1767 | maroon: "800000", 1768 | mediumaquamarine: "66cdaa", 1769 | mediumblue: "0000cd", 1770 | mediumorchid: "ba55d3", 1771 | mediumpurple: "9370db", 1772 | mediumseagreen: "3cb371", 1773 | mediumslateblue: "7b68ee", 1774 | mediumspringgreen: "00fa9a", 1775 | mediumturquoise: "48d1cc", 1776 | mediumvioletred: "c71585", 1777 | midnightblue: "191970", 1778 | mintcream: "f5fffa", 1779 | mistyrose: "ffe4e1", 1780 | moccasin: "ffe4b5", 1781 | navajowhite: "ffdead", 1782 | navy: "000080", 1783 | oldlace: "fdf5e6", 1784 | olive: "808000", 1785 | olivedrab: "6b8e23", 1786 | orange: "ffa500", 1787 | orangered: "ff4500", 1788 | orchid: "da70d6", 1789 | palegoldenrod: "eee8aa", 1790 | palegreen: "98fb98", 1791 | paleturquoise: "afeeee", 1792 | palevioletred: "db7093", 1793 | papayawhip: "ffefd5", 1794 | peachpuff: "ffdab9", 1795 | peru: "cd853f", 1796 | pink: "ffc0cb", 1797 | plum: "dda0dd", 1798 | powderblue: "b0e0e6", 1799 | purple: "800080", 1800 | red: "f00", 1801 | rosybrown: "bc8f8f", 1802 | royalblue: "4169e1", 1803 | saddlebrown: "8b4513", 1804 | salmon: "fa8072", 1805 | sandybrown: "f4a460", 1806 | seagreen: "2e8b57", 1807 | seashell: "fff5ee", 1808 | sienna: "a0522d", 1809 | silver: "c0c0c0", 1810 | skyblue: "87ceeb", 1811 | slateblue: "6a5acd", 1812 | slategray: "708090", 1813 | slategrey: "708090", 1814 | snow: "fffafa", 1815 | springgreen: "00ff7f", 1816 | steelblue: "4682b4", 1817 | tan: "d2b48c", 1818 | teal: "008080", 1819 | thistle: "d8bfd8", 1820 | tomato: "ff6347", 1821 | turquoise: "40e0d0", 1822 | violet: "ee82ee", 1823 | wheat: "f5deb3", 1824 | white: "fff", 1825 | whitesmoke: "f5f5f5", 1826 | yellow: "ff0", 1827 | yellowgreen: "9acd32" 1828 | }; 1829 | 1830 | // Make it easy to access colors via `hexNames[hex]` 1831 | var hexNames = tinycolor.hexNames = flip(names); 1832 | 1833 | 1834 | // Utilities 1835 | // --------- 1836 | 1837 | // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }` 1838 | function flip(o) { 1839 | var flipped = { }; 1840 | for (var i in o) { 1841 | if (o.hasOwnProperty(i)) { 1842 | flipped[o[i]] = i; 1843 | } 1844 | } 1845 | return flipped; 1846 | } 1847 | 1848 | // Return a valid alpha value [0,1] with all invalid values being set to 1 1849 | function boundAlpha(a) { 1850 | a = parseFloat(a); 1851 | 1852 | if (isNaN(a) || a < 0 || a > 1) { 1853 | a = 1; 1854 | } 1855 | 1856 | return a; 1857 | } 1858 | 1859 | // Take input from [0, n] and return it as [0, 1] 1860 | function bound01(n, max) { 1861 | if (isOnePointZero(n)) { n = "100%"; } 1862 | 1863 | var processPercent = isPercentage(n); 1864 | n = mathMin(max, mathMax(0, parseFloat(n))); 1865 | 1866 | // Automatically convert percentage into number 1867 | if (processPercent) { 1868 | n = parseInt(n * max, 10) / 100; 1869 | } 1870 | 1871 | // Handle floating point rounding errors 1872 | if ((math.abs(n - max) < 0.000001)) { 1873 | return 1; 1874 | } 1875 | 1876 | // Convert into [0, 1] range if it isn't already 1877 | return (n % max) / parseFloat(max); 1878 | } 1879 | 1880 | // Force a number between 0 and 1 1881 | function clamp01(val) { 1882 | return mathMin(1, mathMax(0, val)); 1883 | } 1884 | 1885 | // Parse an integer into hex 1886 | function parseHex(val) { 1887 | return parseInt(val, 16); 1888 | } 1889 | 1890 | // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1 1891 | // 1892 | function isOnePointZero(n) { 1893 | return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1; 1894 | } 1895 | 1896 | // Check to see if string passed in is a percentage 1897 | function isPercentage(n) { 1898 | return typeof n === "string" && n.indexOf('%') != -1; 1899 | } 1900 | 1901 | // Force a hex value to have 2 characters 1902 | function pad2(c) { 1903 | return c.length == 1 ? '0' + c : '' + c; 1904 | } 1905 | 1906 | // Replace a decimal with it's percentage value 1907 | function convertToPercentage(n) { 1908 | if (n <= 1) { 1909 | n = (n * 100) + "%"; 1910 | } 1911 | 1912 | return n; 1913 | } 1914 | 1915 | var matchers = (function() { 1916 | 1917 | // 1918 | var CSS_INTEGER = "[-\\+]?\\d+%?"; 1919 | 1920 | // 1921 | var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?"; 1922 | 1923 | // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome. 1924 | var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")"; 1925 | 1926 | // Actual matching. 1927 | // Parentheses and commas are optional, but not required. 1928 | // Whitespace can take the place of commas or opening paren 1929 | var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"; 1930 | var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"; 1931 | 1932 | return { 1933 | rgb: new RegExp("rgb" + PERMISSIVE_MATCH3), 1934 | rgba: new RegExp("rgba" + PERMISSIVE_MATCH4), 1935 | hsl: new RegExp("hsl" + PERMISSIVE_MATCH3), 1936 | hsla: new RegExp("hsla" + PERMISSIVE_MATCH4), 1937 | hsv: new RegExp("hsv" + PERMISSIVE_MATCH3), 1938 | hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, 1939 | hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/ 1940 | }; 1941 | })(); 1942 | 1943 | // `stringInputToObject` 1944 | // Permissive string parsing. Take in a number of formats, and output an object 1945 | // based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}` 1946 | function stringInputToObject(color) { 1947 | 1948 | color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase(); 1949 | var named = false; 1950 | if (names[color]) { 1951 | color = names[color]; 1952 | named = true; 1953 | } 1954 | else if (color == 'transparent') { 1955 | return { r: 0, g: 0, b: 0, a: 0, format: "name" }; 1956 | } 1957 | 1958 | // Try to match string input using regular expressions. 1959 | // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360] 1960 | // Just return an object and let the conversion functions handle that. 1961 | // This way the result will be the same whether the tinycolor is initialized with string or object. 1962 | var match; 1963 | if ((match = matchers.rgb.exec(color))) { 1964 | return { r: match[1], g: match[2], b: match[3] }; 1965 | } 1966 | if ((match = matchers.rgba.exec(color))) { 1967 | return { r: match[1], g: match[2], b: match[3], a: match[4] }; 1968 | } 1969 | if ((match = matchers.hsl.exec(color))) { 1970 | return { h: match[1], s: match[2], l: match[3] }; 1971 | } 1972 | if ((match = matchers.hsla.exec(color))) { 1973 | return { h: match[1], s: match[2], l: match[3], a: match[4] }; 1974 | } 1975 | if ((match = matchers.hsv.exec(color))) { 1976 | return { h: match[1], s: match[2], v: match[3] }; 1977 | } 1978 | if ((match = matchers.hex6.exec(color))) { 1979 | return { 1980 | r: parseHex(match[1]), 1981 | g: parseHex(match[2]), 1982 | b: parseHex(match[3]), 1983 | format: named ? "name" : "hex" 1984 | }; 1985 | } 1986 | if ((match = matchers.hex3.exec(color))) { 1987 | return { 1988 | r: parseHex(match[1] + '' + match[1]), 1989 | g: parseHex(match[2] + '' + match[2]), 1990 | b: parseHex(match[3] + '' + match[3]), 1991 | format: named ? "name" : "hex" 1992 | }; 1993 | } 1994 | 1995 | return false; 1996 | } 1997 | 1998 | // Expose tinycolor to window, does not need to run in non-browser context. 1999 | window.tinycolor = tinycolor; 2000 | 2001 | })(); 2002 | 2003 | 2004 | $(function () { 2005 | if ($.fn.spectrum.load) { 2006 | $.fn.spectrum.processNativeColorInputs(); 2007 | } 2008 | }); 2009 | 2010 | })(window, jQuery); 2011 | -------------------------------------------------------------------------------- /public/javascripts/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.5.2 2 | // http://underscorejs.org 3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | 6 | (function() { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, `window` in the browser, or `exports` on the server. 12 | var root = this; 13 | 14 | // Save the previous value of the `_` variable. 15 | var previousUnderscore = root._; 16 | 17 | // Establish the object that gets returned to break out of a loop iteration. 18 | var breaker = {}; 19 | 20 | // Save bytes in the minified (but not gzipped) version: 21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 22 | 23 | //use the faster Date.now if available. 24 | var getTime = (Date.now || function() { 25 | return new Date().getTime(); 26 | }); 27 | 28 | // Create quick reference variables for speed access to core prototypes. 29 | var 30 | push = ArrayProto.push, 31 | slice = ArrayProto.slice, 32 | concat = ArrayProto.concat, 33 | toString = ObjProto.toString, 34 | hasOwnProperty = ObjProto.hasOwnProperty; 35 | 36 | // All **ECMAScript 5** native function implementations that we hope to use 37 | // are declared here. 38 | var 39 | nativeForEach = ArrayProto.forEach, 40 | nativeMap = ArrayProto.map, 41 | nativeReduce = ArrayProto.reduce, 42 | nativeReduceRight = ArrayProto.reduceRight, 43 | nativeFilter = ArrayProto.filter, 44 | nativeEvery = ArrayProto.every, 45 | nativeSome = ArrayProto.some, 46 | nativeIndexOf = ArrayProto.indexOf, 47 | nativeLastIndexOf = ArrayProto.lastIndexOf, 48 | nativeIsArray = Array.isArray, 49 | nativeKeys = Object.keys, 50 | nativeBind = FuncProto.bind; 51 | 52 | // Create a safe reference to the Underscore object for use below. 53 | var _ = function(obj) { 54 | if (obj instanceof _) return obj; 55 | if (!(this instanceof _)) return new _(obj); 56 | this._wrapped = obj; 57 | }; 58 | 59 | // Export the Underscore object for **Node.js**, with 60 | // backwards-compatibility for the old `require()` API. If we're in 61 | // the browser, add `_` as a global object via a string identifier, 62 | // for Closure Compiler "advanced" mode. 63 | if (typeof exports !== 'undefined') { 64 | if (typeof module !== 'undefined' && module.exports) { 65 | exports = module.exports = _; 66 | } 67 | exports._ = _; 68 | } else { 69 | root._ = _; 70 | } 71 | 72 | // Current version. 73 | _.VERSION = '1.5.2'; 74 | 75 | // Collection Functions 76 | // -------------------- 77 | 78 | // The cornerstone, an `each` implementation, aka `forEach`. 79 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 80 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 81 | var each = _.each = _.forEach = function(obj, iterator, context) { 82 | if (obj == null) return; 83 | if (nativeForEach && obj.forEach === nativeForEach) { 84 | obj.forEach(iterator, context); 85 | } else if (obj.length === +obj.length) { 86 | for (var i = 0, length = obj.length; i < length; i++) { 87 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 88 | } 89 | } else { 90 | var keys = _.keys(obj); 91 | for (var i = 0, length = keys.length; i < length; i++) { 92 | if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; 93 | } 94 | } 95 | }; 96 | 97 | // Return the results of applying the iterator to each element. 98 | // Delegates to **ECMAScript 5**'s native `map` if available. 99 | _.map = _.collect = function(obj, iterator, context) { 100 | var results = []; 101 | if (obj == null) return results; 102 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 103 | each(obj, function(value, index, list) { 104 | results.push(iterator.call(context, value, index, list)); 105 | }); 106 | return results; 107 | }; 108 | 109 | var reduceError = 'Reduce of empty array with no initial value'; 110 | 111 | // **Reduce** builds up a single result from a list of values, aka `inject`, 112 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 113 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 114 | var initial = arguments.length > 2; 115 | if (obj == null) obj = []; 116 | if (nativeReduce && obj.reduce === nativeReduce) { 117 | if (context) iterator = _.bind(iterator, context); 118 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 119 | } 120 | each(obj, function(value, index, list) { 121 | if (!initial) { 122 | memo = value; 123 | initial = true; 124 | } else { 125 | memo = iterator.call(context, memo, value, index, list); 126 | } 127 | }); 128 | if (!initial) throw new TypeError(reduceError); 129 | return memo; 130 | }; 131 | 132 | // The right-associative version of reduce, also known as `foldr`. 133 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 134 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 135 | var initial = arguments.length > 2; 136 | if (obj == null) obj = []; 137 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 138 | if (context) iterator = _.bind(iterator, context); 139 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 140 | } 141 | var length = obj.length; 142 | if (length !== +length) { 143 | var keys = _.keys(obj); 144 | length = keys.length; 145 | } 146 | each(obj, function(value, index, list) { 147 | index = keys ? keys[--length] : --length; 148 | if (!initial) { 149 | memo = obj[index]; 150 | initial = true; 151 | } else { 152 | memo = iterator.call(context, memo, obj[index], index, list); 153 | } 154 | }); 155 | if (!initial) throw new TypeError(reduceError); 156 | return memo; 157 | }; 158 | 159 | // Return the first value which passes a truth test. Aliased as `detect`. 160 | _.find = _.detect = function(obj, iterator, context) { 161 | var result; 162 | any(obj, function(value, index, list) { 163 | if (iterator.call(context, value, index, list)) { 164 | result = value; 165 | return true; 166 | } 167 | }); 168 | return result; 169 | }; 170 | 171 | // Return all the elements that pass a truth test. 172 | // Delegates to **ECMAScript 5**'s native `filter` if available. 173 | // Aliased as `select`. 174 | _.filter = _.select = function(obj, iterator, context) { 175 | var results = []; 176 | if (obj == null) return results; 177 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 178 | each(obj, function(value, index, list) { 179 | if (iterator.call(context, value, index, list)) results.push(value); 180 | }); 181 | return results; 182 | }; 183 | 184 | // Return all the elements for which a truth test fails. 185 | _.reject = function(obj, iterator, context) { 186 | return _.filter(obj, function(value, index, list) { 187 | return !iterator.call(context, value, index, list); 188 | }, context); 189 | }; 190 | 191 | // Determine whether all of the elements match a truth test. 192 | // Delegates to **ECMAScript 5**'s native `every` if available. 193 | // Aliased as `all`. 194 | _.every = _.all = function(obj, iterator, context) { 195 | iterator || (iterator = _.identity); 196 | var result = true; 197 | if (obj == null) return result; 198 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 199 | each(obj, function(value, index, list) { 200 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 201 | }); 202 | return !!result; 203 | }; 204 | 205 | // Determine if at least one element in the object matches a truth test. 206 | // Delegates to **ECMAScript 5**'s native `some` if available. 207 | // Aliased as `any`. 208 | var any = _.some = _.any = function(obj, iterator, context) { 209 | iterator || (iterator = _.identity); 210 | var result = false; 211 | if (obj == null) return result; 212 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 213 | each(obj, function(value, index, list) { 214 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 215 | }); 216 | return !!result; 217 | }; 218 | 219 | // Determine if the array or object contains a given value (using `===`). 220 | // Aliased as `include`. 221 | _.contains = _.include = function(obj, target) { 222 | if (obj == null) return false; 223 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 224 | return any(obj, function(value) { 225 | return value === target; 226 | }); 227 | }; 228 | 229 | // Invoke a method (with arguments) on every item in a collection. 230 | _.invoke = function(obj, method) { 231 | var args = slice.call(arguments, 2); 232 | var isFunc = _.isFunction(method); 233 | return _.map(obj, function(value) { 234 | return (isFunc ? method : value[method]).apply(value, args); 235 | }); 236 | }; 237 | 238 | // Convenience version of a common use case of `map`: fetching a property. 239 | _.pluck = function(obj, key) { 240 | return _.map(obj, _.property(key)); 241 | }; 242 | 243 | // Convenience version of a common use case of `filter`: selecting only objects 244 | // containing specific `key:value` pairs. 245 | _.where = function(obj, attrs, first) { 246 | if (_.isEmpty(attrs)) return first ? void 0 : []; 247 | return _[first ? 'find' : 'filter'](obj, function(value) { 248 | for (var key in attrs) { 249 | if (attrs[key] !== value[key]) return false; 250 | } 251 | return true; 252 | }); 253 | }; 254 | 255 | // Convenience version of a common use case of `find`: getting the first object 256 | // containing specific `key:value` pairs. 257 | _.findWhere = function(obj, attrs) { 258 | return _.where(obj, attrs, true); 259 | }; 260 | 261 | // Return the maximum element or (element-based computation). 262 | // Can't optimize arrays of integers longer than 65,535 elements. 263 | // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) 264 | _.max = function(obj, iterator, context) { 265 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 266 | return Math.max.apply(Math, obj); 267 | } 268 | if (!iterator && _.isEmpty(obj)) return -Infinity; 269 | var result = {computed : -Infinity, value: -Infinity}; 270 | each(obj, function(value, index, list) { 271 | var computed = iterator ? iterator.call(context, value, index, list) : value; 272 | computed > result.computed && (result = {value : value, computed : computed}); 273 | }); 274 | return result.value; 275 | }; 276 | 277 | // Return the minimum element (or element-based computation). 278 | _.min = function(obj, iterator, context) { 279 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 280 | return Math.min.apply(Math, obj); 281 | } 282 | if (!iterator && _.isEmpty(obj)) return Infinity; 283 | var result = {computed : Infinity, value: Infinity}; 284 | each(obj, function(value, index, list) { 285 | var computed = iterator ? iterator.call(context, value, index, list) : value; 286 | computed < result.computed && (result = {value : value, computed : computed}); 287 | }); 288 | return result.value; 289 | }; 290 | 291 | // Shuffle an array, using the modern version of the 292 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). 293 | _.shuffle = function(obj) { 294 | var rand; 295 | var index = 0; 296 | var shuffled = []; 297 | each(obj, function(value) { 298 | rand = _.random(index++); 299 | shuffled[index - 1] = shuffled[rand]; 300 | shuffled[rand] = value; 301 | }); 302 | return shuffled; 303 | }; 304 | 305 | // Sample **n** random values from a collection. 306 | // If **n** is not specified, returns a single random element. 307 | // The internal `guard` argument allows it to work with `map`. 308 | _.sample = function(obj, n, guard) { 309 | if (n == null || guard) { 310 | if (obj.length !== +obj.length) obj = _.values(obj); 311 | return obj[_.random(obj.length - 1)]; 312 | } 313 | return _.shuffle(obj).slice(0, Math.max(0, n)); 314 | }; 315 | 316 | // An internal function to generate lookup iterators. 317 | var lookupIterator = function(value) { 318 | if (value == null) return _.identity; 319 | if (_.isFunction(value)) return value; 320 | return _.property(value); 321 | }; 322 | 323 | // Sort the object's values by a criterion produced by an iterator. 324 | _.sortBy = function(obj, iterator, context) { 325 | iterator = lookupIterator(iterator); 326 | return _.pluck(_.map(obj, function(value, index, list) { 327 | return { 328 | value: value, 329 | index: index, 330 | criteria: iterator.call(context, value, index, list) 331 | }; 332 | }).sort(function(left, right) { 333 | var a = left.criteria; 334 | var b = right.criteria; 335 | if (a !== b) { 336 | if (a > b || a === void 0) return 1; 337 | if (a < b || b === void 0) return -1; 338 | } 339 | return left.index - right.index; 340 | }), 'value'); 341 | }; 342 | 343 | // An internal function used for aggregate "group by" operations. 344 | var group = function(behavior) { 345 | return function(obj, iterator, context) { 346 | var result = {}; 347 | iterator = lookupIterator(iterator); 348 | each(obj, function(value, index) { 349 | var key = iterator.call(context, value, index, obj); 350 | behavior(result, key, value); 351 | }); 352 | return result; 353 | }; 354 | }; 355 | 356 | // Groups the object's values by a criterion. Pass either a string attribute 357 | // to group by, or a function that returns the criterion. 358 | _.groupBy = group(function(result, key, value) { 359 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value); 360 | }); 361 | 362 | // Indexes the object's values by a criterion, similar to `groupBy`, but for 363 | // when you know that your index values will be unique. 364 | _.indexBy = group(function(result, key, value) { 365 | result[key] = value; 366 | }); 367 | 368 | // Counts instances of an object that group by a certain criterion. Pass 369 | // either a string attribute to count by, or a function that returns the 370 | // criterion. 371 | _.countBy = group(function(result, key) { 372 | _.has(result, key) ? result[key]++ : result[key] = 1; 373 | }); 374 | 375 | // Use a comparator function to figure out the smallest index at which 376 | // an object should be inserted so as to maintain order. Uses binary search. 377 | _.sortedIndex = function(array, obj, iterator, context) { 378 | iterator = lookupIterator(iterator); 379 | var value = iterator.call(context, obj); 380 | var low = 0, high = array.length; 381 | while (low < high) { 382 | var mid = (low + high) >>> 1; 383 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; 384 | } 385 | return low; 386 | }; 387 | 388 | // Safely create a real, live array from anything iterable. 389 | _.toArray = function(obj) { 390 | if (!obj) return []; 391 | if (_.isArray(obj)) return slice.call(obj); 392 | if (obj.length === +obj.length) return _.map(obj, _.identity); 393 | return _.values(obj); 394 | }; 395 | 396 | // Return the number of elements in an object. 397 | _.size = function(obj) { 398 | if (obj == null) return 0; 399 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; 400 | }; 401 | 402 | // Array Functions 403 | // --------------- 404 | 405 | // Get the first element of an array. Passing **n** will return the first N 406 | // values in the array. Aliased as `head` and `take`. The **guard** check 407 | // allows it to work with `_.map`. 408 | _.first = _.head = _.take = function(array, n, guard) { 409 | if (array == null) return void 0; 410 | if ((n == null) || guard) return array[0]; 411 | if (n < 0) return []; 412 | return slice.call(array, 0, n); 413 | }; 414 | 415 | // Returns everything but the last entry of the array. Especially useful on 416 | // the arguments object. Passing **n** will return all the values in 417 | // the array, excluding the last N. The **guard** check allows it to work with 418 | // `_.map`. 419 | _.initial = function(array, n, guard) { 420 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 421 | }; 422 | 423 | // Get the last element of an array. Passing **n** will return the last N 424 | // values in the array. The **guard** check allows it to work with `_.map`. 425 | _.last = function(array, n, guard) { 426 | if (array == null) return void 0; 427 | if ((n == null) || guard) return array[array.length - 1]; 428 | return slice.call(array, Math.max(array.length - n, 0)); 429 | }; 430 | 431 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 432 | // Especially useful on the arguments object. Passing an **n** will return 433 | // the rest N values in the array. The **guard** 434 | // check allows it to work with `_.map`. 435 | _.rest = _.tail = _.drop = function(array, n, guard) { 436 | return slice.call(array, (n == null) || guard ? 1 : n); 437 | }; 438 | 439 | // Trim out all falsy values from an array. 440 | _.compact = function(array) { 441 | return _.filter(array, _.identity); 442 | }; 443 | 444 | // Internal implementation of a recursive `flatten` function. 445 | var flatten = function(input, shallow, output) { 446 | if (shallow && _.every(input, _.isArray)) { 447 | return concat.apply(output, input); 448 | } 449 | each(input, function(value) { 450 | if (_.isArray(value) || _.isArguments(value)) { 451 | shallow ? push.apply(output, value) : flatten(value, shallow, output); 452 | } else { 453 | output.push(value); 454 | } 455 | }); 456 | return output; 457 | }; 458 | 459 | // Flatten out an array, either recursively (by default), or just one level. 460 | _.flatten = function(array, shallow) { 461 | return flatten(array, shallow, []); 462 | }; 463 | 464 | // Return a version of the array that does not contain the specified value(s). 465 | _.without = function(array) { 466 | return _.difference(array, slice.call(arguments, 1)); 467 | }; 468 | 469 | // Produce a duplicate-free version of the array. If the array has already 470 | // been sorted, you have the option of using a faster algorithm. 471 | // Aliased as `unique`. 472 | _.uniq = _.unique = function(array, isSorted, iterator, context) { 473 | if (_.isFunction(isSorted)) { 474 | context = iterator; 475 | iterator = isSorted; 476 | isSorted = false; 477 | } 478 | var initial = iterator ? _.map(array, iterator, context) : array; 479 | var results = []; 480 | var seen = []; 481 | each(initial, function(value, index) { 482 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { 483 | seen.push(value); 484 | results.push(array[index]); 485 | } 486 | }); 487 | return results; 488 | }; 489 | 490 | // Produce an array that contains the union: each distinct element from all of 491 | // the passed-in arrays. 492 | _.union = function() { 493 | return _.uniq(_.flatten(arguments, true)); 494 | }; 495 | 496 | // Produce an array that contains every item shared between all the 497 | // passed-in arrays. 498 | _.intersection = function(array) { 499 | var rest = slice.call(arguments, 1); 500 | return _.filter(_.uniq(array), function(item) { 501 | return _.every(rest, function(other) { 502 | return _.indexOf(other, item) >= 0; 503 | }); 504 | }); 505 | }; 506 | 507 | // Take the difference between one array and a number of other arrays. 508 | // Only the elements present in just the first array will remain. 509 | _.difference = function(array) { 510 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); 511 | return _.filter(array, function(value){ return !_.contains(rest, value); }); 512 | }; 513 | 514 | // Zip together multiple lists into a single array -- elements that share 515 | // an index go together. 516 | _.zip = function() { 517 | var length = _.max(_.pluck(arguments, "length").concat(0)); 518 | var results = new Array(length); 519 | for (var i = 0; i < length; i++) { 520 | results[i] = _.pluck(arguments, '' + i); 521 | } 522 | return results; 523 | }; 524 | 525 | // Converts lists into objects. Pass either a single array of `[key, value]` 526 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 527 | // the corresponding values. 528 | _.object = function(list, values) { 529 | if (list == null) return {}; 530 | var result = {}; 531 | for (var i = 0, length = list.length; i < length; i++) { 532 | if (values) { 533 | result[list[i]] = values[i]; 534 | } else { 535 | result[list[i][0]] = list[i][1]; 536 | } 537 | } 538 | return result; 539 | }; 540 | 541 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 542 | // we need this function. Return the position of the first occurrence of an 543 | // item in an array, or -1 if the item is not included in the array. 544 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 545 | // If the array is large and already in sort order, pass `true` 546 | // for **isSorted** to use binary search. 547 | _.indexOf = function(array, item, isSorted) { 548 | if (array == null) return -1; 549 | var i = 0, length = array.length; 550 | if (isSorted) { 551 | if (typeof isSorted == 'number') { 552 | i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); 553 | } else { 554 | i = _.sortedIndex(array, item); 555 | return array[i] === item ? i : -1; 556 | } 557 | } 558 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); 559 | for (; i < length; i++) if (array[i] === item) return i; 560 | return -1; 561 | }; 562 | 563 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 564 | _.lastIndexOf = function(array, item, from) { 565 | if (array == null) return -1; 566 | var hasIndex = from != null; 567 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { 568 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); 569 | } 570 | var i = (hasIndex ? from : array.length); 571 | while (i--) if (array[i] === item) return i; 572 | return -1; 573 | }; 574 | 575 | // Generate an integer Array containing an arithmetic progression. A port of 576 | // the native Python `range()` function. See 577 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 578 | _.range = function(start, stop, step) { 579 | if (arguments.length <= 1) { 580 | stop = start || 0; 581 | start = 0; 582 | } 583 | step = arguments[2] || 1; 584 | 585 | var length = Math.max(Math.ceil((stop - start) / step), 0); 586 | var idx = 0; 587 | var range = new Array(length); 588 | 589 | while(idx < length) { 590 | range[idx++] = start; 591 | start += step; 592 | } 593 | 594 | return range; 595 | }; 596 | 597 | // Function (ahem) Functions 598 | // ------------------ 599 | 600 | // Reusable constructor function for prototype setting. 601 | var ctor = function(){}; 602 | 603 | // Create a function bound to a given object (assigning `this`, and arguments, 604 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 605 | // available. 606 | _.bind = function(func, context) { 607 | var args, bound; 608 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 609 | if (!_.isFunction(func)) throw new TypeError; 610 | args = slice.call(arguments, 2); 611 | return bound = function() { 612 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 613 | ctor.prototype = func.prototype; 614 | var self = new ctor; 615 | ctor.prototype = null; 616 | var result = func.apply(self, args.concat(slice.call(arguments))); 617 | if (Object(result) === result) return result; 618 | return self; 619 | }; 620 | }; 621 | 622 | // Partially apply a function by creating a version that has had some of its 623 | // arguments pre-filled, without changing its dynamic `this` context. _ acts 624 | // as a placeholder, allowing any combination of arguments to be pre-filled. 625 | _.partial = function(func) { 626 | var boundArgs = slice.call(arguments, 1); 627 | return function() { 628 | var args = slice.call(boundArgs); 629 | _.each(arguments, function(arg) { 630 | var index = args.indexOf(_); 631 | args[index >= 0 ? index : args.length] = arg; 632 | }); 633 | return func.apply(this, _.map(args, function(value) { 634 | return value === _ ? void 0 : value; 635 | })); 636 | }; 637 | }; 638 | 639 | // Bind a number of an object's methods to that object. Remaining arguments 640 | // are the method names to be bound. Useful for ensuring that all callbacks 641 | // defined on an object belong to it. 642 | _.bindAll = function(obj) { 643 | var funcs = slice.call(arguments, 1); 644 | if (funcs.length === 0) throw new Error("bindAll must be passed function names"); 645 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 646 | return obj; 647 | }; 648 | 649 | // Memoize an expensive function by storing its results. 650 | _.memoize = function(func, hasher) { 651 | var memo = {}; 652 | hasher || (hasher = _.identity); 653 | return function() { 654 | var key = hasher.apply(this, arguments); 655 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 656 | }; 657 | }; 658 | 659 | // Delays a function for the given number of milliseconds, and then calls 660 | // it with the arguments supplied. 661 | _.delay = function(func, wait) { 662 | var args = slice.call(arguments, 2); 663 | return setTimeout(function(){ return func.apply(null, args); }, wait); 664 | }; 665 | 666 | // Defers a function, scheduling it to run after the current call stack has 667 | // cleared. 668 | _.defer = function(func) { 669 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 670 | }; 671 | 672 | // Returns a function, that, when invoked, will only be triggered at most once 673 | // during a given window of time. Normally, the throttled function will run 674 | // as much as it can, without ever going more than once per `wait` duration; 675 | // but if you'd like to disable the execution on the leading edge, pass 676 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 677 | _.throttle = function(func, wait, options) { 678 | var context, args, result; 679 | var timeout = null; 680 | var previous = 0; 681 | options || (options = {}); 682 | var later = function() { 683 | previous = options.leading === false ? 0 : getTime(); 684 | timeout = null; 685 | result = func.apply(context, args); 686 | context = args = null; 687 | }; 688 | return function() { 689 | var now = getTime(); 690 | if (!previous && options.leading === false) previous = now; 691 | var remaining = wait - (now - previous); 692 | context = this; 693 | args = arguments; 694 | if (remaining <= 0) { 695 | clearTimeout(timeout); 696 | timeout = null; 697 | previous = now; 698 | result = func.apply(context, args); 699 | context = args = null; 700 | } else if (!timeout && options.trailing !== false) { 701 | timeout = setTimeout(later, remaining); 702 | } 703 | return result; 704 | }; 705 | }; 706 | 707 | // Returns a function, that, as long as it continues to be invoked, will not 708 | // be triggered. The function will be called after it stops being called for 709 | // N milliseconds. If `immediate` is passed, trigger the function on the 710 | // leading edge, instead of the trailing. 711 | _.debounce = function(func, wait, immediate) { 712 | var timeout, args, context, timestamp, result; 713 | return function() { 714 | context = this; 715 | args = arguments; 716 | timestamp = getTime(); 717 | var later = function() { 718 | var last = getTime() - timestamp; 719 | if (last < wait) { 720 | timeout = setTimeout(later, wait - last); 721 | } else { 722 | timeout = null; 723 | if (!immediate) { 724 | result = func.apply(context, args); 725 | context = args = null; 726 | } 727 | } 728 | }; 729 | var callNow = immediate && !timeout; 730 | if (!timeout) { 731 | timeout = setTimeout(later, wait); 732 | } 733 | if (callNow) { 734 | result = func.apply(context, args); 735 | context = args = null; 736 | } 737 | 738 | return result; 739 | }; 740 | }; 741 | 742 | // Returns a function that will be executed at most one time, no matter how 743 | // often you call it. Useful for lazy initialization. 744 | _.once = function(func) { 745 | var ran = false, memo; 746 | return function() { 747 | if (ran) return memo; 748 | ran = true; 749 | memo = func.apply(this, arguments); 750 | func = null; 751 | return memo; 752 | }; 753 | }; 754 | 755 | // Returns the first function passed as an argument to the second, 756 | // allowing you to adjust arguments, run code before and after, and 757 | // conditionally execute the original function. 758 | _.wrap = function(func, wrapper) { 759 | return _.partial(wrapper, func); 760 | }; 761 | 762 | // Returns a function that is the composition of a list of functions, each 763 | // consuming the return value of the function that follows. 764 | _.compose = function() { 765 | var funcs = arguments; 766 | return function() { 767 | var args = arguments; 768 | for (var i = funcs.length - 1; i >= 0; i--) { 769 | args = [funcs[i].apply(this, args)]; 770 | } 771 | return args[0]; 772 | }; 773 | }; 774 | 775 | // Returns a function that will only be executed after being called N times. 776 | _.after = function(times, func) { 777 | return function() { 778 | if (--times < 1) { 779 | return func.apply(this, arguments); 780 | } 781 | }; 782 | }; 783 | 784 | // Object Functions 785 | // ---------------- 786 | 787 | // Retrieve the names of an object's properties. 788 | // Delegates to **ECMAScript 5**'s native `Object.keys` 789 | _.keys = nativeKeys || function(obj) { 790 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 791 | var keys = []; 792 | for (var key in obj) if (_.has(obj, key)) keys.push(key); 793 | return keys; 794 | }; 795 | 796 | // Retrieve the values of an object's properties. 797 | _.values = function(obj) { 798 | var keys = _.keys(obj); 799 | var length = keys.length; 800 | var values = new Array(length); 801 | for (var i = 0; i < length; i++) { 802 | values[i] = obj[keys[i]]; 803 | } 804 | return values; 805 | }; 806 | 807 | // Convert an object into a list of `[key, value]` pairs. 808 | _.pairs = function(obj) { 809 | var keys = _.keys(obj); 810 | var length = keys.length; 811 | var pairs = new Array(length); 812 | for (var i = 0; i < length; i++) { 813 | pairs[i] = [keys[i], obj[keys[i]]]; 814 | } 815 | return pairs; 816 | }; 817 | 818 | // Invert the keys and values of an object. The values must be serializable. 819 | _.invert = function(obj) { 820 | var result = {}; 821 | var keys = _.keys(obj); 822 | for (var i = 0, length = keys.length; i < length; i++) { 823 | result[obj[keys[i]]] = keys[i]; 824 | } 825 | return result; 826 | }; 827 | 828 | // Return a sorted list of the function names available on the object. 829 | // Aliased as `methods` 830 | _.functions = _.methods = function(obj) { 831 | var names = []; 832 | for (var key in obj) { 833 | if (_.isFunction(obj[key])) names.push(key); 834 | } 835 | return names.sort(); 836 | }; 837 | 838 | // Extend a given object with all the properties in passed-in object(s). 839 | _.extend = function(obj) { 840 | each(slice.call(arguments, 1), function(source) { 841 | if (source) { 842 | for (var prop in source) { 843 | obj[prop] = source[prop]; 844 | } 845 | } 846 | }); 847 | return obj; 848 | }; 849 | 850 | // Return a copy of the object only containing the whitelisted properties. 851 | _.pick = function(obj) { 852 | var copy = {}; 853 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 854 | each(keys, function(key) { 855 | if (key in obj) copy[key] = obj[key]; 856 | }); 857 | return copy; 858 | }; 859 | 860 | // Return a copy of the object without the blacklisted properties. 861 | _.omit = function(obj) { 862 | var copy = {}; 863 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 864 | for (var key in obj) { 865 | if (!_.contains(keys, key)) copy[key] = obj[key]; 866 | } 867 | return copy; 868 | }; 869 | 870 | // Fill in a given object with default properties. 871 | _.defaults = function(obj) { 872 | each(slice.call(arguments, 1), function(source) { 873 | if (source) { 874 | for (var prop in source) { 875 | if (obj[prop] === void 0) obj[prop] = source[prop]; 876 | } 877 | } 878 | }); 879 | return obj; 880 | }; 881 | 882 | // Create a (shallow-cloned) duplicate of an object. 883 | _.clone = function(obj) { 884 | if (!_.isObject(obj)) return obj; 885 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 886 | }; 887 | 888 | // Invokes interceptor with the obj, and then returns obj. 889 | // The primary purpose of this method is to "tap into" a method chain, in 890 | // order to perform operations on intermediate results within the chain. 891 | _.tap = function(obj, interceptor) { 892 | interceptor(obj); 893 | return obj; 894 | }; 895 | 896 | // Internal recursive comparison function for `isEqual`. 897 | var eq = function(a, b, aStack, bStack) { 898 | // Identical objects are equal. `0 === -0`, but they aren't identical. 899 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). 900 | if (a === b) return a !== 0 || 1 / a == 1 / b; 901 | // A strict comparison is necessary because `null == undefined`. 902 | if (a == null || b == null) return a === b; 903 | // Unwrap any wrapped objects. 904 | if (a instanceof _) a = a._wrapped; 905 | if (b instanceof _) b = b._wrapped; 906 | // Compare `[[Class]]` names. 907 | var className = toString.call(a); 908 | if (className != toString.call(b)) return false; 909 | switch (className) { 910 | // Strings, numbers, dates, and booleans are compared by value. 911 | case '[object String]': 912 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 913 | // equivalent to `new String("5")`. 914 | return a == String(b); 915 | case '[object Number]': 916 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 917 | // other numeric values. 918 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 919 | case '[object Date]': 920 | case '[object Boolean]': 921 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 922 | // millisecond representations. Note that invalid dates with millisecond representations 923 | // of `NaN` are not equivalent. 924 | return +a == +b; 925 | // RegExps are compared by their source patterns and flags. 926 | case '[object RegExp]': 927 | return a.source == b.source && 928 | a.global == b.global && 929 | a.multiline == b.multiline && 930 | a.ignoreCase == b.ignoreCase; 931 | } 932 | if (typeof a != 'object' || typeof b != 'object') return false; 933 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 934 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 935 | var length = aStack.length; 936 | while (length--) { 937 | // Linear search. Performance is inversely proportional to the number of 938 | // unique nested structures. 939 | if (aStack[length] == a) return bStack[length] == b; 940 | } 941 | // Objects with different constructors are not equivalent, but `Object`s 942 | // from different frames are. 943 | var aCtor = a.constructor, bCtor = b.constructor; 944 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && 945 | _.isFunction(bCtor) && (bCtor instanceof bCtor)) 946 | && ('constructor' in a && 'constructor' in b)) { 947 | return false; 948 | } 949 | // Add the first object to the stack of traversed objects. 950 | aStack.push(a); 951 | bStack.push(b); 952 | var size = 0, result = true; 953 | // Recursively compare objects and arrays. 954 | if (className == '[object Array]') { 955 | // Compare array lengths to determine if a deep comparison is necessary. 956 | size = a.length; 957 | result = size == b.length; 958 | if (result) { 959 | // Deep compare the contents, ignoring non-numeric properties. 960 | while (size--) { 961 | if (!(result = eq(a[size], b[size], aStack, bStack))) break; 962 | } 963 | } 964 | } else { 965 | // Deep compare objects. 966 | for (var key in a) { 967 | if (_.has(a, key)) { 968 | // Count the expected number of properties. 969 | size++; 970 | // Deep compare each member. 971 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; 972 | } 973 | } 974 | // Ensure that both objects contain the same number of properties. 975 | if (result) { 976 | for (key in b) { 977 | if (_.has(b, key) && !(size--)) break; 978 | } 979 | result = !size; 980 | } 981 | } 982 | // Remove the first object from the stack of traversed objects. 983 | aStack.pop(); 984 | bStack.pop(); 985 | return result; 986 | }; 987 | 988 | // Perform a deep comparison to check if two objects are equal. 989 | _.isEqual = function(a, b) { 990 | return eq(a, b, [], []); 991 | }; 992 | 993 | // Is a given array, string, or object empty? 994 | // An "empty" object has no enumerable own-properties. 995 | _.isEmpty = function(obj) { 996 | if (obj == null) return true; 997 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 998 | for (var key in obj) if (_.has(obj, key)) return false; 999 | return true; 1000 | }; 1001 | 1002 | // Is a given value a DOM element? 1003 | _.isElement = function(obj) { 1004 | return !!(obj && obj.nodeType === 1); 1005 | }; 1006 | 1007 | // Is a given value an array? 1008 | // Delegates to ECMA5's native Array.isArray 1009 | _.isArray = nativeIsArray || function(obj) { 1010 | return toString.call(obj) == '[object Array]'; 1011 | }; 1012 | 1013 | // Is a given variable an object? 1014 | _.isObject = function(obj) { 1015 | return obj === Object(obj); 1016 | }; 1017 | 1018 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. 1019 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { 1020 | _['is' + name] = function(obj) { 1021 | return toString.call(obj) == '[object ' + name + ']'; 1022 | }; 1023 | }); 1024 | 1025 | // Define a fallback version of the method in browsers (ahem, IE), where 1026 | // there isn't any inspectable "Arguments" type. 1027 | if (!_.isArguments(arguments)) { 1028 | _.isArguments = function(obj) { 1029 | return !!(obj && _.has(obj, 'callee')); 1030 | }; 1031 | } 1032 | 1033 | // Optimize `isFunction` if appropriate. 1034 | if (typeof (/./) !== 'function') { 1035 | _.isFunction = function(obj) { 1036 | return typeof obj === 'function'; 1037 | }; 1038 | } 1039 | 1040 | // Is a given object a finite number? 1041 | _.isFinite = function(obj) { 1042 | return isFinite(obj) && !isNaN(parseFloat(obj)); 1043 | }; 1044 | 1045 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 1046 | _.isNaN = function(obj) { 1047 | return _.isNumber(obj) && obj != +obj; 1048 | }; 1049 | 1050 | // Is a given value a boolean? 1051 | _.isBoolean = function(obj) { 1052 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 1053 | }; 1054 | 1055 | // Is a given value equal to null? 1056 | _.isNull = function(obj) { 1057 | return obj === null; 1058 | }; 1059 | 1060 | // Is a given variable undefined? 1061 | _.isUndefined = function(obj) { 1062 | return obj === void 0; 1063 | }; 1064 | 1065 | // Shortcut function for checking if an object has a given property directly 1066 | // on itself (in other words, not on a prototype). 1067 | _.has = function(obj, key) { 1068 | return hasOwnProperty.call(obj, key); 1069 | }; 1070 | 1071 | // Utility Functions 1072 | // ----------------- 1073 | 1074 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1075 | // previous owner. Returns a reference to the Underscore object. 1076 | _.noConflict = function() { 1077 | root._ = previousUnderscore; 1078 | return this; 1079 | }; 1080 | 1081 | // Keep the identity function around for default iterators. 1082 | _.identity = function(value) { 1083 | return value; 1084 | }; 1085 | 1086 | _.constant = function(value) { 1087 | return function () { 1088 | return value; 1089 | }; 1090 | }; 1091 | 1092 | _.property = function(key) { 1093 | return function(obj) { 1094 | return obj[key]; 1095 | }; 1096 | }; 1097 | 1098 | // Run a function **n** times. 1099 | _.times = function(n, iterator, context) { 1100 | var accum = Array(Math.max(0, n)); 1101 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); 1102 | return accum; 1103 | }; 1104 | 1105 | // Return a random integer between min and max (inclusive). 1106 | _.random = function(min, max) { 1107 | if (max == null) { 1108 | max = min; 1109 | min = 0; 1110 | } 1111 | return min + Math.floor(Math.random() * (max - min + 1)); 1112 | }; 1113 | 1114 | // List of HTML entities for escaping. 1115 | var entityMap = { 1116 | escape: { 1117 | '&': '&', 1118 | '<': '<', 1119 | '>': '>', 1120 | '"': '"', 1121 | "'": ''' 1122 | } 1123 | }; 1124 | entityMap.unescape = _.invert(entityMap.escape); 1125 | 1126 | // Regexes containing the keys and values listed immediately above. 1127 | var entityRegexes = { 1128 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), 1129 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') 1130 | }; 1131 | 1132 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1133 | _.each(['escape', 'unescape'], function(method) { 1134 | _[method] = function(string) { 1135 | if (string == null) return ''; 1136 | return ('' + string).replace(entityRegexes[method], function(match) { 1137 | return entityMap[method][match]; 1138 | }); 1139 | }; 1140 | }); 1141 | 1142 | // If the value of the named `property` is a function then invoke it with the 1143 | // `object` as context; otherwise, return it. 1144 | _.result = function(object, property) { 1145 | if (object == null) return void 0; 1146 | var value = object[property]; 1147 | return _.isFunction(value) ? value.call(object) : value; 1148 | }; 1149 | 1150 | // Add your own custom functions to the Underscore object. 1151 | _.mixin = function(obj) { 1152 | each(_.functions(obj), function(name) { 1153 | var func = _[name] = obj[name]; 1154 | _.prototype[name] = function() { 1155 | var args = [this._wrapped]; 1156 | push.apply(args, arguments); 1157 | return result.call(this, func.apply(_, args)); 1158 | }; 1159 | }); 1160 | }; 1161 | 1162 | // Generate a unique integer id (unique within the entire client session). 1163 | // Useful for temporary DOM ids. 1164 | var idCounter = 0; 1165 | _.uniqueId = function(prefix) { 1166 | var id = ++idCounter + ''; 1167 | return prefix ? prefix + id : id; 1168 | }; 1169 | 1170 | // By default, Underscore uses ERB-style template delimiters, change the 1171 | // following template settings to use alternative delimiters. 1172 | _.templateSettings = { 1173 | evaluate : /<%([\s\S]+?)%>/g, 1174 | interpolate : /<%=([\s\S]+?)%>/g, 1175 | escape : /<%-([\s\S]+?)%>/g 1176 | }; 1177 | 1178 | // When customizing `templateSettings`, if you don't want to define an 1179 | // interpolation, evaluation or escaping regex, we need one that is 1180 | // guaranteed not to match. 1181 | var noMatch = /(.)^/; 1182 | 1183 | // Certain characters need to be escaped so that they can be put into a 1184 | // string literal. 1185 | var escapes = { 1186 | "'": "'", 1187 | '\\': '\\', 1188 | '\r': 'r', 1189 | '\n': 'n', 1190 | '\t': 't', 1191 | '\u2028': 'u2028', 1192 | '\u2029': 'u2029' 1193 | }; 1194 | 1195 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 1196 | 1197 | // JavaScript micro-templating, similar to John Resig's implementation. 1198 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1199 | // and correctly escapes quotes within interpolated code. 1200 | _.template = function(text, data, settings) { 1201 | var render; 1202 | settings = _.defaults({}, settings, _.templateSettings); 1203 | 1204 | // Combine delimiters into one regular expression via alternation. 1205 | var matcher = new RegExp([ 1206 | (settings.escape || noMatch).source, 1207 | (settings.interpolate || noMatch).source, 1208 | (settings.evaluate || noMatch).source 1209 | ].join('|') + '|$', 'g'); 1210 | 1211 | // Compile the template source, escaping string literals appropriately. 1212 | var index = 0; 1213 | var source = "__p+='"; 1214 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1215 | source += text.slice(index, offset) 1216 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 1217 | 1218 | if (escape) { 1219 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1220 | } 1221 | if (interpolate) { 1222 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1223 | } 1224 | if (evaluate) { 1225 | source += "';\n" + evaluate + "\n__p+='"; 1226 | } 1227 | index = offset + match.length; 1228 | return match; 1229 | }); 1230 | source += "';\n"; 1231 | 1232 | // If a variable is not specified, place data values in local scope. 1233 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1234 | 1235 | source = "var __t,__p='',__j=Array.prototype.join," + 1236 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1237 | source + "return __p;\n"; 1238 | 1239 | try { 1240 | render = new Function(settings.variable || 'obj', '_', source); 1241 | } catch (e) { 1242 | e.source = source; 1243 | throw e; 1244 | } 1245 | 1246 | if (data) return render(data, _); 1247 | var template = function(data) { 1248 | return render.call(this, data, _); 1249 | }; 1250 | 1251 | // Provide the compiled function source as a convenience for precompilation. 1252 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 1253 | 1254 | return template; 1255 | }; 1256 | 1257 | // Add a "chain" function, which will delegate to the wrapper. 1258 | _.chain = function(obj) { 1259 | return _(obj).chain(); 1260 | }; 1261 | 1262 | // OOP 1263 | // --------------- 1264 | // If Underscore is called as a function, it returns a wrapped object that 1265 | // can be used OO-style. This wrapper holds altered versions of all the 1266 | // underscore functions. Wrapped objects may be chained. 1267 | 1268 | // Helper function to continue chaining intermediate results. 1269 | var result = function(obj) { 1270 | return this._chain ? _(obj).chain() : obj; 1271 | }; 1272 | 1273 | // Add all of the Underscore functions to the wrapper object. 1274 | _.mixin(_); 1275 | 1276 | // Add all mutator Array functions to the wrapper. 1277 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1278 | var method = ArrayProto[name]; 1279 | _.prototype[name] = function() { 1280 | var obj = this._wrapped; 1281 | method.apply(obj, arguments); 1282 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; 1283 | return result.call(this, obj); 1284 | }; 1285 | }); 1286 | 1287 | // Add all accessor Array functions to the wrapper. 1288 | each(['concat', 'join', 'slice'], function(name) { 1289 | var method = ArrayProto[name]; 1290 | _.prototype[name] = function() { 1291 | return result.call(this, method.apply(this._wrapped, arguments)); 1292 | }; 1293 | }); 1294 | 1295 | _.extend(_.prototype, { 1296 | 1297 | // Start chaining a wrapped Underscore object. 1298 | chain: function() { 1299 | this._chain = true; 1300 | return this; 1301 | }, 1302 | 1303 | // Extracts the result from a wrapped and chained object. 1304 | value: function() { 1305 | return this._wrapped; 1306 | } 1307 | 1308 | }); 1309 | 1310 | // AMD registration happens at the end for compatibility with AMD loaders 1311 | // that may not enforce next-turn semantics on modules. Even though general 1312 | // practice for AMD registration is to be anonymous, underscore registers 1313 | // as a named module because, like jQuery, it is a base library that is 1314 | // popular enough to be bundled in a third party lib, but not be part of 1315 | // an AMD load request. Those cases could generate an error when an 1316 | // anonymous define() is called outside of a loader request. 1317 | if (typeof define === 'function' && define.amd) { 1318 | define('underscore', [], function() { 1319 | return _; 1320 | }); 1321 | } 1322 | }).call(this); 1323 | -------------------------------------------------------------------------------- /public/stylesheets/style.less: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Alegreya Sans', sans-serif; 3 | } 4 | @board-border: #964B00; 5 | 6 | #game-board { 7 | padding: 10px; 8 | display: inline-block; 9 | background-color: @board-border; 10 | border-radius: 4px; 11 | 12 | } 13 | @font-face { 14 | font-family: "Dice"; 15 | src: url(../fonts/dice.ttf) format("truetype"); 16 | } 17 | 18 | @beize: beige; 19 | .background rect { 20 | fill: @beize; 21 | } 22 | @col:#5C3F39; 23 | @col2: darken(@col, 20%); 24 | 25 | .border rect { 26 | fill: darken(@board-border, 20%); 27 | } 28 | .places { 29 | 30 | polyline:nth-child(even) { 31 | fill: @col2; 32 | } 33 | 34 | polyline:nth-child(odd) { 35 | fill: @col; 36 | } 37 | polyline { 38 | stroke: @col; 39 | stroke-width: 2px; 40 | stroke-style: solid; 41 | } 42 | 43 | } 44 | 45 | .bar rect { 46 | fill: @beize; 47 | stroke: @col; 48 | stroke-width: 2px; 49 | stroke-style: solid; 50 | } 51 | 52 | .home rect { 53 | fill: @beize; 54 | stroke: @col; 55 | stroke-width: 2px; 56 | stroke-style: solid; 57 | } 58 | 59 | #pieces { 60 | padding: 10px; 61 | circle { 62 | fill: lighten(black, 45%); 63 | stroke: black; 64 | stroke-width: 2px; 65 | stroke-style: solid; 66 | r: 25; 67 | transition: cx 1s, cy 1s; 68 | } 69 | circle.red { 70 | fill: lighten(red, 15%); 71 | } 72 | circle.selected { 73 | fill: yellow; 74 | } 75 | circle:hover { 76 | stroke: yellow; 77 | } 78 | } 79 | 80 | a.dice { 81 | color: black; 82 | font-family: "Dice"; 83 | font-size: 60px; 84 | margin: 2px; 85 | &.used { 86 | color: lighten(grey, 20%); 87 | } 88 | &:hover { 89 | color: yellow; 90 | cursor: pointer; 91 | } 92 | } 93 | #dice { 94 | margin:8px; 95 | } 96 | #diceroll { 97 | font-size: 20px; 98 | color: white; 99 | } 100 | #playlink { 101 | margin:8px; 102 | font-size: 20px; 103 | color: white; 104 | } 105 | 106 | text { 107 | display:inline-block; 108 | vertical-align:middle; 109 | } 110 | span #chat{ 111 | display: inline-block; 112 | } 113 | div #chat{ 114 | padding: 10px; 115 | } 116 | #input { 117 | width: 200px; 118 | padding: 5px; 119 | } 120 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * GET home page. 4 | */ 5 | 6 | exports.index = function(req, res){ 7 | res.render('index', { title: 'Express' }); 8 | }; 9 | -------------------------------------------------------------------------------- /src/game.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var _ = require('underscore') 3 | 4 | _.any = function(ls){ 5 | return _.contains(ls, true); 6 | } 7 | 8 | function Board(redState, blackState, bar, home) { 9 | var bs = typeof blackState !== 'undefined' ? blackState : {}; 10 | var rs = typeof redState !== 'undefined' ? redState : {}; 11 | blankState = _.object( 12 | _.range(1, 25), 13 | _.times(24,function(){return 0} ) 14 | ) 15 | this.blackState = _.extend({}, blankState, bs) 16 | this.redState = _.extend({}, blankState, rs) 17 | this.bar = typeof bar !== 'undefined' ? bar : { 18 | black: 0, 19 | red: 0 20 | }; 21 | this.home = typeof home !== 'undefined' ? home : { 22 | black: 0, 23 | red: 0 24 | }; 25 | } 26 | 27 | String.prototype.opponent = function(){ 28 | if (this == 'red'){ 29 | return 'black' 30 | } else if (this == 'black'){ 31 | return 'red' 32 | } 33 | } 34 | 35 | function Player(color, board) { 36 | this.color = color; 37 | this.board = board; 38 | } 39 | 40 | Player.prototype = { 41 | get ownedPositions(){ 42 | var t = this; 43 | var positions = _.filter( 44 | _.map( 45 | _.keys(this.state), 46 | function(i){return parseInt(i)} 47 | ), 48 | function(d){ 49 | return t.piecesAt(d) > 0; 50 | } 51 | ); 52 | var barPositions = this.bar > 0 ? ['bar'] : []; 53 | return positions.concat(barPositions); 54 | }, 55 | get state() { 56 | if (this.color == 'red'){ 57 | return this.board.redState; 58 | } else { 59 | return this.board.blackState; 60 | } 61 | }, 62 | get opponent(){ 63 | return this.board[this.color.opponent()]; 64 | }, 65 | get bar(){ 66 | return this.board.bar[this.color]; 67 | }, 68 | set bar(val){ 69 | this.board.bar[this.color] = val; 70 | }, 71 | get home(){ 72 | return this.board.home[this.color]; 73 | }, 74 | set home(val){ 75 | this.board.home[this.color] = val; 76 | }, 77 | canMovePieceAt: function(pos){ 78 | if (pos == 'bar'){ 79 | return true; 80 | } else if (pos == 'home') { 81 | return false; 82 | } else { 83 | return this.bar == 0; 84 | } 85 | }, 86 | canMoveWith: function(roll){ 87 | var that = this 88 | return _.any(_.map( 89 | this.ownedPositions, 90 | function(d){return that.validMove(d, roll)} 91 | )) 92 | }, 93 | canMoveToTarget: function(target){ 94 | return this.opponent.piecesAt(target) < 2; 95 | }, 96 | canBearOff: function(pos, roll){ 97 | var nonHomeIndices = this.color == 'red'? _.range(1, 19) : _.range(7, 25); 98 | var nonHomeValues = _.values(_.pick(this.state, nonHomeIndices)); 99 | var nonHomeCount = _.reduce(nonHomeValues, function(x, y){return x + y}, 0); 100 | var homeIndices = this.color == 'red'? _.range(19, 25) : _.range(1, 7); 101 | var homeSubBoard = _.pick(this.state, homeIndices); 102 | 103 | var homeValues = []; 104 | 105 | for (var key in homeSubBoard){ 106 | var o = homeSubBoard[key]; 107 | if (o){ 108 | homeValues.push(key); 109 | } 110 | 111 | } 112 | f = this.color == 'red' ? _.min : _.max; 113 | furthestPip = f(homeValues); 114 | requiredRollToHome = this.color == 'red' ? 25 - pos : pos 115 | 116 | return nonHomeCount == 0 && this.bar == 0 && (furthestPip == pos || requiredRollToHome == roll); 117 | 118 | }, 119 | placePiece: function(target, roll){ 120 | if (this.board.wouldBearOff(target)){ 121 | // bearing off 122 | this.home += 1; 123 | } else { 124 | if (this.opponent.piecesAt(target) == 1){ 125 | this.opponent.state[target] = 0; 126 | this.opponent.bar += 1; 127 | } 128 | this.state[target] = this.piecesAt(target) + 1; 129 | } 130 | }, 131 | liftPiece: function(pos){ 132 | if (pos === 'bar'){ 133 | this.bar -= 1; 134 | } else { 135 | this.state[pos] -= 1; 136 | } 137 | }, 138 | piecesAt: function(pos){ 139 | if (pos === 'bar'){ 140 | return this.bar 141 | } else { 142 | if (typeof this.state[pos] === "undefined"){ 143 | this.state[pos] = 0; 144 | } 145 | return this.state[pos]; 146 | } 147 | }, 148 | validMove: function(pos, roll){ 149 | var target = this.targetPosition(pos, roll); 150 | var canBearOff = this.canBearOff(pos, roll); 151 | var notBearingOff = !this.board.wouldBearOff(target) 152 | var validIfBearOff = notBearingOff || canBearOff; 153 | var canMoveTo = this.canMoveToTarget(target); 154 | var canMoveFrom = this.canMovePieceAt(pos); 155 | 156 | return ( 157 | canMoveTo && 158 | canMoveFrom && 159 | validIfBearOff 160 | ) 161 | }, 162 | targetPosition: function(pos, roll){ 163 | var p = pos; 164 | if (pos == 'bar'){ 165 | p = this.color == 'red' ? 0: 25; 166 | } 167 | if (this.color == 'red'){ 168 | return p + roll; 169 | } else if (this.color == 'black'){ 170 | return p - roll; 171 | } 172 | }, 173 | progressPiece: function(pos, roll){ 174 | if (this.validMove(pos, roll)){ 175 | var target = this.targetPosition(pos, roll); 176 | this.liftPiece(pos); 177 | this.placePiece(target, roll); 178 | return true; 179 | } else { 180 | return false; 181 | } 182 | } 183 | 184 | } 185 | 186 | Board.prototype = { 187 | get red(){ 188 | return new Player('red', this); 189 | }, 190 | get black(){ 191 | return new Player('black', this); 192 | }, 193 | toString: function(){ 194 | return 'Red: ' + JSON.stringify(this.red.state) + '\nBlack: ' + JSON.stringify(this.black.state); 195 | }, 196 | state: function(){ 197 | var redBar = _(this.red.bar).times(function(){return {position: 'bar', color: 'red'}}) 198 | var blackBar = _(this.black.bar).times(function(){return {position: 'bar', color: 'black'}}) 199 | var redHome= _(this.red.home).times(function(){return {position: 'home', color: 'red'}}) 200 | var blackHome = _(this.black.home).times(function(){return {position: 'home', color: 'black'}}) 201 | 202 | var pieceGenerator = function(color, count, position){ 203 | return _(count).times( 204 | function(){return {position:parseInt(position), color:color}} 205 | ) 206 | } 207 | var redPieces = _.flatten(_.map( 208 | this.red.state, 209 | _.partial(pieceGenerator, 'red') 210 | )) 211 | 212 | var blackPieces = _.flatten(_.map( 213 | this.black.state, 214 | _.partial(pieceGenerator, 'black') 215 | )) 216 | 217 | return [].concat(redBar, blackBar, redHome, blackHome, redPieces, blackPieces) 218 | }, 219 | owner: function(pos){ 220 | if (this.red.piecesAt(pos) > 0){ 221 | return this.red; 222 | } else if (this.black.piecesAt(pos) > 0){ 223 | return this.black; 224 | } 225 | }, 226 | wouldBearOff: function(target){ 227 | return (target <= 0 || target > 24); 228 | }, 229 | } 230 | 231 | var initialBoard = function() { 232 | return new Board( 233 | {1: 2, 12: 5, 17: 3, 19: 5,}, 234 | {24: 2, 13: 5, 8: 3, 6: 5,} 235 | ); 236 | } 237 | 238 | module.exports.Board = Board; 239 | module.exports.initialBoard = initialBoard; 240 | -------------------------------------------------------------------------------- /src/run.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys') 2 | var Board = require('../src/game.js') 3 | 4 | console.log(Board) 5 | var express = require('express'); 6 | var app = express(); 7 | 8 | app.get('/', function(req, res){ 9 | res.send('hello world'); 10 | }); 11 | 12 | app.listen(3000); 13 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | var game = require('./game.js') 2 | var Chance = require('chance'); 3 | var _ = require('underscore'); 4 | 5 | currentGame = undefined; 6 | currentPlayer = undefined; 7 | currentDice = undefined; 8 | autoDiceRoll = false; 9 | 10 | rollDice = function(){ 11 | var x = chance.d6(); 12 | var y = chance.d6(); 13 | if (x === y){ 14 | return [ 15 | {'val':x, 'rolled':false}, 16 | {'val':y, 'rolled':false}, 17 | {'val':x, 'rolled':false}, 18 | {'val':y, 'rolled':false}, 19 | ] 20 | } else { 21 | return [ 22 | {'val':x, 'rolled':false}, 23 | {'val':y, 'rolled':false}, 24 | ] 25 | } 26 | } 27 | resetDice = function(seed, performAutoDiceRoll){ 28 | console.log('Seed', seed ) 29 | if (seed === parseInt(seed)){ 30 | chance = new Chance(seed); 31 | } else { 32 | chance = new Chance(); 33 | } 34 | currentDice = undefined; 35 | autoDiceRoll = typeof performAutoDiceRoll != 'undefined'? performAutoDiceRoll : true; 36 | if (autoDiceRoll){ 37 | currentDice = rollDice(); 38 | } 39 | } 40 | resetGame = function(){ 41 | currentGame = game.initialBoard(); 42 | currentPlayer = 'red'; 43 | } 44 | newGame = function(seed, performAutoDiceRoll){ 45 | resetDice(seed, performAutoDiceRoll); 46 | resetGame(); 47 | } 48 | 49 | module.exports.newGame = newGame; 50 | 51 | var ioModule = require('socket.io') 52 | , lessMiddleware = require('less-middleware') 53 | , express = require('express') 54 | , exphbs = require('express3-handlebars') 55 | , http = require('http') 56 | , path = require('path') 57 | 58 | loadApp = function(){ 59 | var app = express(); 60 | // all environments 61 | app.use(lessMiddleware(__dirname + '/../public')); 62 | app.use(express.static(__dirname + '/../public')); 63 | 64 | app.engine('html', exphbs({defaultLayout: 'main', extname: '.html'})); 65 | app.set('view engine', 'html'); 66 | 67 | app.get('/', function (req, res) { 68 | res.render('index'); 69 | }); 70 | 71 | var logger = require('morgan'); 72 | var bodyParser = require('body-parser') 73 | var methodOverride = require('method-override'); 74 | var errorHandler = require('errorhandler'); 75 | app.use(logger('dev')); 76 | app.use(bodyParser.json()); 77 | app.use(bodyParser.urlencoded({extended: true})); 78 | app.use(methodOverride()); 79 | //app.use(app.router); 80 | app.use(express.static(path.join(__dirname, '../public'))); 81 | // development only 82 | if ('development' == app.get('env')) { 83 | app.use(errorHandler()); 84 | } 85 | return app 86 | } 87 | 88 | launchApp = function(app, port){ 89 | port = process.env.PORT || port || 5000; 90 | return app.listen(port) 91 | } 92 | 93 | switchControl = function(){ 94 | console.log('switching') 95 | currentPlayer = currentPlayer.opponent(); 96 | console.log('roll again') 97 | if (autoDiceRoll){ 98 | currentDice = rollDice(); 99 | } else { 100 | currentDice = undefined; 101 | } 102 | announcePlayer(); 103 | } 104 | announceDice = function(){ 105 | return io.sockets.emit("dice", {dice:currentDice, playable:canMove()}); 106 | } 107 | announcePlayer = function(){ 108 | return io.sockets.emit("player", currentPlayer) 109 | } 110 | announcePlayable = function(){ 111 | return io.sockets.emit("playable", canMove()) 112 | } 113 | announceState = function(){ 114 | return io.sockets.emit("status", currentGame.state()) 115 | } 116 | performRoll = function(){ 117 | console.log("performing roll") 118 | if (typeof currentDice == 'undefined'){ 119 | currentDice = rollDice(); 120 | } 121 | announceDice(); 122 | } 123 | performPass = function(){ 124 | console.log('Can move', canMove()) 125 | if (!canMove()){ 126 | switchControl(); 127 | announcePlayer(); 128 | announceState(); 129 | announceDice(); 130 | return true; 131 | } else { 132 | return false; 133 | } 134 | 135 | } 136 | performMove = function(pos, rollIndex){ 137 | var selectedDice = currentDice[rollIndex]; 138 | var currentPlayerPieceSelected = currentGame.owner(pos).color == currentPlayer; 139 | if (!selectedDice.rolled && currentPlayerPieceSelected){ 140 | var roll = selectedDice.val; 141 | var success = currentGame[currentPlayer].progressPiece(pos, roll); 142 | if (success){ 143 | currentDice[rollIndex].rolled = true; 144 | } 145 | var incomplete = _.contains( 146 | _.pluck(currentDice, 'rolled'), 147 | false 148 | ); 149 | if (!incomplete){ 150 | switchControl(); 151 | } 152 | } 153 | announceState(); 154 | return announceDice(); 155 | } 156 | 157 | loadIO = function(server){ 158 | var io = require('socket.io').listen(server); 159 | io.sockets.on('connection', function (socket) { 160 | socket.on("pass", performPass); 161 | socket.on("status", announceState); 162 | socket.on("player", announcePlayer); 163 | socket.on("playable", announcePlayable); 164 | socket.on("move", performMove); 165 | socket.on("dice", announceDice); 166 | socket.on("roll", performRoll); 167 | }); 168 | return io; 169 | } 170 | 171 | canMove = function(){ 172 | if (currentDice){ 173 | var dice = _.pluck(_.where(currentDice, {rolled:false}), 'val'); 174 | var moveable = _.map( 175 | dice, 176 | function(d){ 177 | return currentGame[currentPlayer].canMoveWith(d); 178 | } 179 | ); 180 | return _.contains(moveable, true); 181 | } else { 182 | return true 183 | } 184 | } 185 | start = function(port, cb, seed, performAutoDiceRoll){ 186 | server = launchApp(loadApp(), port); 187 | io = loadIO(server); 188 | newGame(seed, performAutoDiceRoll); 189 | } 190 | 191 | dropAllClients = function(){ 192 | io.sockets.clients().forEach(function(socket){socket.disconnect(true)}); 193 | } 194 | 195 | stop = function(cb){ 196 | dropAllClients(); 197 | server.close(); 198 | cb() 199 | } 200 | module.exports.start = start; 201 | module.exports.resetServer = function(seed, performAutoDiceRoll){ 202 | dropAllClients(); 203 | newGame(seed, performAutoDiceRoll); 204 | } 205 | module.exports.stop = stop; 206 | module.exports.board = function(){return currentGame}; 207 | module.exports.dice = function(){return currentDice}; 208 | module.exports.setDice = function(d){currentDice = d;}; 209 | module.exports.canMove = canMove; 210 | module.exports.player = currentPlayer; 211 | module.exports.performPass = function(){return performPass()}; 212 | module.exports.io = function(){return io}; 213 | -------------------------------------------------------------------------------- /test/game.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var should = require('should') 3 | game = require('../src/game.js') 4 | var Board = game.Board 5 | var initialBoard = game.initialBoard 6 | describe('Board', function(){ 7 | describe('#start', function(){ 8 | it('should have the correct starting positions for red', function(){ 9 | var board = initialBoard(); 10 | board.red.piecesAt(1).should.equal(2); 11 | board.red.piecesAt(12).should.equal(5); 12 | board.red.piecesAt(17).should.equal(3); 13 | board.red.piecesAt(19).should.equal(5); 14 | }), 15 | it('should have the correct starting positions for black', function(){ 16 | var board = initialBoard(); 17 | board.black.piecesAt(24).should.equal(2); 18 | board.black.piecesAt(13).should.equal(5); 19 | board.black.piecesAt(8).should.equal(3); 20 | board.black.piecesAt(6).should.equal(5); 21 | }) 22 | }), 23 | describe('#move', function(){ 24 | it('should be able to move ', function(){ 25 | var board = new Board(); 26 | board.redState = {1:1}; 27 | board.red.piecesAt(1).should.equal(1); 28 | board.red.progressPiece(1, 1); 29 | board.red.piecesAt(1).should.equal(0); 30 | board.red.piecesAt(2).should.equal(1) 31 | }), 32 | it('should be able to move two points', function(){ 33 | var board = new Board(); 34 | board.redState = {1:1}; 35 | assert.equal(board.red.piecesAt(1), 1); 36 | board.red.progressPiece(1, 2); 37 | assert.equal(board.red.piecesAt(1), 0); 38 | assert.equal(board.red.piecesAt(3), 1); 39 | }), 40 | it('should be able to move splitting a stack', function(){ 41 | var board = new Board(); 42 | board.redState = {1:2}; 43 | board.red.progressPiece(1, 1); 44 | board.red.piecesAt(1).should.equal(1); 45 | board.red.piecesAt(2).should.equal(1); 46 | }), 47 | it('should not be able to move when an opposing stack is blocking', function(){ 48 | var board = new Board(); 49 | board.blackState = {2:2}; 50 | board.redState = {1:1}; 51 | board.red.validMove(1, 1).should.not.be.okay; 52 | board.red.canMoveToTarget(2).should.not.be.okay; 53 | board.red.progressPiece(1, 1); 54 | board.red.piecesAt(1).should.equal(1); 55 | board.red.piecesAt(2).should.equal(0); 56 | board.black.piecesAt(2).should.equal(2) 57 | }), 58 | it('should not be an invlid move returns false', function(){ 59 | var board = new Board(); 60 | board.blackState = {2:2}; 61 | board.redState = {1:1}; 62 | board.red.progressPiece(1, 1).should.be.false; 63 | }), 64 | it('should be able to determine what positions each player occupies', function(){ 65 | var board = new Board(); 66 | board.redState = {1:1}; 67 | board.blackState = {2:2, 10:2}; 68 | board.bar = {red:0 , black: 1}; 69 | board.red.ownedPositions.should.be.eql([1]); 70 | 71 | board.black.ownedPositions.should.be.eql([2, 10, 'bar']); 72 | }), 73 | it('should be able to determine if a player has no moves', function(){ 74 | var board = new Board(); 75 | board.blackState = {2:2, 10:2}; 76 | board.redState = {1:2}; 77 | board.red.canMoveWith(1).should.be.false; 78 | board.black.canMoveWith(1).should.be.true; 79 | }), 80 | it('should be able to move if off the bar', function(){ 81 | var board = new Board(); 82 | board.blackState = {}; 83 | board.redState = {}; 84 | board.bar = {red:0 , black: 1}; 85 | board.red.canMoveWith(3).should.be.false; 86 | board.black.canMoveWith(3).should.be.true; 87 | }) 88 | it('should be able to move if stack blocking', function(){ 89 | var board = new Board(); 90 | board.blackState = {4:2, 13:0}; 91 | board.redState = {1:2, 10:2}; 92 | board.bar = {red:0 , black: 0}; 93 | board.red.canMoveWith(3).should.be.true; 94 | board.black.canMoveWith(3).should.be.false; 95 | }), 96 | it('should be able to move if stack blocking from bar', function(){ 97 | var board = new Board(); 98 | board.blackState = {4:2}; 99 | board.redState = {10:2}; 100 | board.bar = {red:1, black: 0}; 101 | board.red.canMoveWith(4).should.be.false; 102 | }) 103 | }), 104 | describe('#player', function(){ 105 | it('should be possible from a player string to retreive the opponent', function(){ 106 | 'red'.opponent().should.equal('black'); 107 | 'black'.opponent().should.equal('red'); 108 | should.strictEqual('dave'.opponent(), undefined); 109 | }) 110 | }) 111 | describe('#validMove', function(){ 112 | it('should be invalid to move into a stack of opposing pieces', function(){ 113 | var board = new Board(); 114 | board.blackState = {2:2}; 115 | board.red.canMoveToTarget(2).should.be.false; 116 | 117 | }) 118 | it('should be invalid to move into a stack of opposing pieces', function(){ 119 | var board = new Board(); 120 | board.redState = {2:2}; 121 | board.black.canMoveToTarget(2).should.be.false; 122 | 123 | }) 124 | it('should be valid to move against a single piece', function(){ 125 | var board = new Board(); 126 | board.blackState = {2:1}; 127 | board.red.canMoveToTarget(2).should.be.true; 128 | }) 129 | it('should be valid to move into an empty space', function(){ 130 | var board = new Board(); 131 | board.blackState = {2:0}; 132 | board.red.canMoveToTarget(2).should.be.true; 133 | }) 134 | it('should be invalid to move anything but bar moves if available', function(){ 135 | var board = new Board(); 136 | board.bar = {red:1, black:0}; 137 | board.red.canMovePieceAt(2).should.be.false; 138 | board.red.canMovePieceAt('bar').should.be.true; 139 | }) 140 | it('should be valid to move anything if bar is free', function(){ 141 | var board = new Board(); 142 | board.redState = {1:1}; 143 | board.bar = {red:0, black:0}; 144 | board.red.canMovePieceAt(1).should.be.true; 145 | }) 146 | }) 147 | describe('#hitting', function(){ 148 | it('should be possible to hit a blot', function(){ 149 | var board = new Board(); 150 | board.blackState = {12:1}; 151 | board.redState = {11:1}; 152 | board.red.progressPiece(11, 1); 153 | board.red.piecesAt(11).should.equal(0); 154 | board.bar.black.should.equal(1) 155 | }) 156 | it('should be possible for black to hit a blot', function(){ 157 | var board = new Board(); 158 | board.blackState = {12:1}; 159 | board.redState = {11:1}; 160 | board.black.progressPiece(12, 1); 161 | board.red.piecesAt(11).should.equal(0); 162 | board.black.piecesAt(11).should.equal(1); 163 | board.bar.red.should.equal(1); 164 | }) 165 | }), 166 | describe('#bar', function(){ 167 | it('should empty at startup', function() { 168 | var board = initialBoard(); 169 | assert.equal(board.bar.black, 0); 170 | board.bar.black.should.equal(0); 171 | board.bar.red.should.equal(0); 172 | }) 173 | it('should be able to move a red piece off of the bar to create a stack', function(){ 174 | var board = new Board(); 175 | board.redState = {1:1}; 176 | board.bar = {red:1, black:0}; 177 | board.red.progressPiece('bar', 1).should.be.true; 178 | board.bar.red.should.equal(0); 179 | board.red.piecesAt(1).should.equal(2) 180 | }) 181 | it('should be able to move a red piece off of the bar', function(){ 182 | var board = new Board(); 183 | board.bar = {red:1, black: 0}; 184 | board.red.progressPiece('bar', 1).should.be.true; 185 | board.bar.red.should.equal(0); 186 | board.red.piecesAt(1).should.equal(1); 187 | }), 188 | it('should be able to move a red piece off of the bar using lift', function(){ 189 | var board = new Board(); 190 | board.bar = {red:1, black: 0}; 191 | board.red.progressPiece('bar', 1).should.be.true; 192 | board.bar.red.should.equal(0); 193 | board.red.piecesAt(1).should.equal(1); 194 | }), 195 | it('should be able to move a red piece off of the bar to a different first six point', function(){ 196 | var board = new Board(); 197 | board.bar = {red: 1, black: 0}; 198 | board.red.progressPiece('bar', 6).should.be.true 199 | board.bar.red.should.equal(0); 200 | board.red.piecesAt(6).should.equal(1); 201 | }), 202 | it('should be possible to hit a blot from entering', function(){ 203 | var board = new Board(); 204 | board.blackState = {1:1};; 205 | board.bar = {red:1, black:0}; 206 | board.red.progressPiece('bar', 1).should.be.true; 207 | board.red.piecesAt(1).should.equal(1, 'Pip successfully entered'); 208 | board.black.piecesAt(1).should.equal(0, 'Hit pip removed'); 209 | board.bar.black.should.equal(1, 'Hit pip on bar'); 210 | }), 211 | it('should not be able to pop a piece when a black stack is blocking', function(){ 212 | var board = new Board(); 213 | board.blackState = {1:2}; 214 | board.bar = {red:1, black:0}; 215 | board.red.progressPiece('bar', 1).should.be.false; 216 | board.bar.red.should.equal(1); 217 | board.red.piecesAt(1).should.equal(0); 218 | board.black.piecesAt(1).should.equal(2); 219 | }), 220 | it('should be possible to pop a pip from the black bar', function(){ 221 | var board = new Board(); 222 | board.redState = {19:1}; 223 | board.bar = {red:2, black:2}; 224 | board.black.progressPiece('bar', 6); 225 | board.black.piecesAt(19).should.equal(1); 226 | board.red.piecesAt(19).should.equal(0); 227 | board.bar.red.should.equal(3); 228 | }) 229 | }), 230 | describe('#bearingoff', function(){ 231 | /*Once you have moved all of your checkers onto any of the last six points on the board, you may begin bearing off - unless your opponent knocks one of your checkers to the bar, and then you must stop bearing off until you return that checker to one of the last six points. 232 | * 233 | * To bear off, move a piece into the rectangular goal at the end of the board. 234 | * If you can move into the goal using an exact number, and you have no checkers on the bar or points further back than the last 6, then you may bear that checker off. (On the final point, a roll of one is needed to bear off that checker.) 235 | * You may choose to make any other move instead of bearing off, if you are able. 236 | * If you do not have a number that allows you to bear a checker off with an exact move, you may not bear off and must move another checker on the board instead. 237 | * However, If there are no checkers to move because they are all closer to the goal than the number you rolled, you may then bear off with the checker that is farthest away from your goal, even if the number you rolled is too large for an exact move. 238 | */ 239 | it('should be able to bear off black at the end of a game', function() { 240 | var board = new Board(); 241 | board.blackState = {1:1}; 242 | board.black.canBearOff(1, 1).should.be.ok; 243 | board.black.progressPiece(1, 1); 244 | board.black.piecesAt(1).should.equal(0); 245 | board.home.black.should.equal(1); 246 | }), 247 | it('should not be able to bear off black with pieces outside the home zone', function() { 248 | var board = new Board(); 249 | board.blackState = {1:1, 8:2}; 250 | board.black.canBearOff(1, 1).should.not.be.ok; 251 | }), 252 | it('should not be able to bear off black with a larger move to play', function() { 253 | var board = new Board(); 254 | board.blackState = {1:1, 2:2}; 255 | board.black.canBearOff(1, 2).should.not.be.ok; 256 | board.black.canBearOff(2, 2).should.be.ok; 257 | }), 258 | it('should not be able to bear off black with a larger move to play, with a large dice roll', function() { 259 | var board = new Board(); 260 | board.blackState = {1:1, 2:2}; 261 | board.black.canBearOff(1, 6).should.not.be.ok; 262 | board.black.canBearOff(2, 6).should.be.ok; 263 | }), 264 | it('should be able to bear off red at the end of a game', function() { 265 | var board = new Board(); 266 | board.redState = {24:1}; 267 | board.red.canBearOff(24, 1).should.be.ok; 268 | board.red.progressPiece(24, 1); 269 | board.red.piecesAt(24).should.equal(0); 270 | board.home.red.should.equal(1); 271 | }), 272 | it('should be able to bear off successively', function() { 273 | var board = new Board(); 274 | board.redState = {24:2}; 275 | board.blackState = {1:2}; 276 | 277 | board.black.progressPiece(1, 1); 278 | board.black.piecesAt(1).should.equal(1); 279 | board.black.canBearOff(1, 1).should.be.true; 280 | board.home.black.should.equal(1); 281 | 282 | board.red.progressPiece(24, 1); 283 | board.red.canBearOff(24, 1).should.be.true; 284 | board.red.piecesAt(24).should.equal(1); 285 | board.home.red.should.equal(1); 286 | }), 287 | it('should not be able to bear off if there are pieces outside of the home board', function(){ 288 | var board = new Board(); 289 | board.redState = {1: 1, 24:1}; 290 | board.red.canBearOff(24, 2).should.not.be.ok; 291 | board.red.validMove(24, 2).should.not.be.ok; 292 | }), 293 | it('should not be able to bear off if there are pieces on the bar', function(){ 294 | var board = new Board(); 295 | board.redState = {1: 0, 24:1}; 296 | board.bar.red = 1; 297 | board.red.canBearOff(24, 1).should.not.be.ok; 298 | board.red.progressPiece(24, 1); 299 | board.home.red.should.equal(0); 300 | }), 301 | it('should not be able to bear off a piece when another piece should be moved first', function(){ 302 | var board = new Board(); 303 | board.redState = {23:1, 24:1}; 304 | board.red.validMove(24, 2).should.not.be.ok; 305 | board.red.validMove(23, 2).should.be.ok; 306 | }), 307 | it('should be able to bear off an exact move', function(){ 308 | var board = new Board(); 309 | board.redState = {22: 1, 23:1, 24:1}; 310 | board.red.validMove(24, 2).should.not.be.ok; 311 | board.red.validMove(23, 2).should.be.ok; 312 | board.red.validMove(22, 2).should.be.ok; 313 | }), 314 | it('should not be able to bear off a piece when another piece could be moved without bearing off', function(){ 315 | var board = new Board(); 316 | board.redState = {21:0, 22:1, 24:1}; 317 | board.red.validMove(24, 2).should.not.be.ok; 318 | board.red.validMove(22, 2).should.be.ok; 319 | }), 320 | it('should be able to bear off a piece with a larger number if it is the only piece remaining', function(){ 321 | var board = new Board(); 322 | board.redState = {21:0, 22:1, 24:1}; 323 | board.red.validMove(22, 5).should.be.ok; 324 | }), 325 | it('should not be able to be able to move a piece which is on the home col', function(){ 326 | var board = new Board() 327 | board.home = {red: 1, black: 3}; 328 | board.red.validMove('home', 1).should.not.be.ok; 329 | }) 330 | }), 331 | describe('#display-state', function(){ 332 | it('should be able to produce a displayable summary of a board', function(){ 333 | var board = new Board(); 334 | board.redState = {2:2, 3:1}; 335 | board.blackState = {23:1}; 336 | board.home = {red: 1, black: 3}; 337 | board.bar = {red: 2, black:4}; 338 | assert.deepEqual( 339 | board.state(), 340 | [ 341 | {position: 'bar', color:'red'}, 342 | {position: 'bar', color:'red'}, 343 | {position: 'bar', color:'black'}, 344 | {position: 'bar', color:'black'}, 345 | {position: 'bar', color:'black'}, 346 | {position: 'bar', color:'black'}, 347 | {position: 'home', color:'red'}, 348 | {position: 'home', color:'black'}, 349 | {position: 'home', color:'black'}, 350 | {position: 'home', color:'black'}, 351 | {position: 2, color:'red'}, 352 | {position: 2, color:'red'}, 353 | {position: 3, color:'red'}, 354 | {position: 23, color:'black'} 355 | ] 356 | ) 357 | }) 358 | }) 359 | }) 360 | 361 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --reporter spec 3 | --ui bdd 4 | --recursive 5 | --growl 6 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , should = require('should') 3 | , _ = require('underscore') 4 | , app_module = require('../src/server.js') 5 | , jQuery = require('jquery') 6 | , should = require('should') 7 | , ioClient= require('socket.io-client') 8 | , Browser = require("zombie") 9 | 10 | var browser = undefined 11 | var socketURL = 'http://0.0.0.0:5000' 12 | var options = { 13 | transports: ['websocket'], 14 | 'force new connection': true 15 | }; 16 | 17 | Browser.prototype.keydown = function(targetSelector, keyCode) { 18 | keyCode = keyCode.charCodeAt(0) 19 | var event = this.window.document.createEvent('HTMLEvents'); 20 | event.initEvent('keydown', true, true); 21 | event.which = keyCode; 22 | if (targetSelector === this.window){ 23 | var target = this.window 24 | } else{ 25 | var target = this.window.document.querySelector(targetSelector); 26 | } 27 | target && target.dispatchEvent(event); 28 | }; 29 | 30 | waitFor = function(browser, precondition, callback){ 31 | var condition = precondition(browser); 32 | if (precondition(browser)){ 33 | callback(browser) 34 | return; 35 | } 36 | setTimeout( 37 | function() { 38 | waitFor(browser, precondition, callback) 39 | }, 40 | 100 41 | ); 42 | } 43 | loadPage = function(cb, skipWaitingDice){ 44 | var browser = new Browser(); 45 | browser.debug = true; 46 | browser.visit("http://0.0.0.0:5000") 47 | waitFor( 48 | browser, 49 | function(b){ 50 | var piecesOK = false; 51 | var diceOK = false; 52 | var pageLoaded = b.success 53 | 54 | if (pageLoaded){ 55 | piecesOK = (b.queryAll("circle")|| []).length > 0; 56 | diceOK = (b.queryAll("#dice a") || []).length > 0; 57 | if (skipWaitingDice){ 58 | diceOK = true; 59 | } 60 | } 61 | return pageLoaded && diceOK && piecesOK 62 | }, function(){ 63 | $ = jQuery(browser.window) 64 | $.fn.d3Click = function () { 65 | this.each(function (i, e) { 66 | var evt = browser.window.document.createEvent("MouseEvents"); 67 | evt.initMouseEvent("click", true, true, browser.window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 68 | 69 | e.dispatchEvent(evt); 70 | }); 71 | }; 72 | $.fn.pieceAt = function(pos, index){ 73 | return $('circle[pos="'+pos+'"][index="'+index+'"]').first() 74 | }; 75 | $.fn.diceAt = function(pos){ 76 | return $('#dice a').eq(pos) 77 | } 78 | if (cb){ 79 | cb(); 80 | } 81 | } 82 | ) 83 | return browser 84 | } 85 | describe('Game', function(){ 86 | describe('#launchapp', function(){ 87 | it.skip("Should be possible to restart and have a different intial dice", function(done){ 88 | require('../app.js') 89 | t = chance.seed === parseInt(chance.seed) 90 | t.should.equal.true; 91 | app_module.stop(done); 92 | }) 93 | }), 94 | describe('#spacedicetrigger', function(){ 95 | before(function(){ 96 | app_module.start(5000); 97 | app_module.io().set('log level', 0); 98 | }), 99 | beforeEach(function(done){ 100 | this.timeout(5000); 101 | app_module.resetServer(seed=5, autodiceroll=false); 102 | // setup the browser and jQuery 103 | browser = loadPage(done, skipWaitingDice=true); 104 | }), 105 | after(function(done){ 106 | app_module.stop(done); 107 | }), 108 | it("Should be possible to force a player to roll the dice, clicking a link", function(done){ 109 | $('#dice a').length.should.equal(1); 110 | $('#dice a').text().should.equal("Perform roll"); 111 | $('#playable a').length.should.equal(0) 112 | browser.clickLink("#diceroll"); 113 | 114 | // roll should have been performed 115 | waitFor( 116 | browser, 117 | function(){ 118 | return $('#dice a').text() == '21' 119 | }, 120 | function(){ 121 | client = ioClient.connect(socketURL, options); 122 | client.emit("move", 1, 0); 123 | client.emit("move", 1, 1); 124 | waitFor( 125 | browser, 126 | function(){ 127 | return $('#dice a').text() == "Perform roll"; 128 | }, 129 | function(){done();} 130 | ) 131 | } 132 | ) 133 | }), 134 | it("Cannot pass if you haven't rolled", function(){ 135 | app_module.performPass().should.equal.false 136 | 137 | }), 138 | it("Should be possible to force a player to roll the dice, using spacebar", function(done){ 139 | browser.keydown(browser.window, ' '); 140 | waitFor( 141 | browser, 142 | function(){ 143 | return $('#dice a').text() == '21' 144 | }, 145 | function(){done()} 146 | ) 147 | 148 | }), 149 | it("Should be able to see a notification that a roll is neccessary", function(){ 150 | $('#dice a').text().should.equal("Perform roll") 151 | }) 152 | }), 153 | describe('#diceselect', function(){ 154 | before(function(){ 155 | app_module.start(5000); 156 | app_module.io().set('log level', 0); 157 | }), 158 | beforeEach(function(done){ 159 | this.timeout(5000); 160 | app_module.resetServer(seed=5); 161 | // setup the browser and jQuery 162 | browser = loadPage(done); 163 | }), 164 | after(function(done){ 165 | app_module.stop(done); 166 | }), 167 | it("should be possible to trigger a roll with a key press", function(done){ 168 | $('body').pieceAt(1, 1).d3Click() 169 | browser.keydown(browser.window, '2'); 170 | 171 | firstDiceSelected = function(){ 172 | return $('body').diceAt(0).hasClass('used') 173 | } 174 | firstDiceSelected().should.be.false; 175 | waitFor(browser, firstDiceSelected, function(){done()}); 176 | }), 177 | it("should be possible to trigger a roll with a key press, for none dupe dice", function(done){ 178 | $('body').pieceAt(1, 1).d3Click() 179 | browser.keydown(browser.window, '1'); 180 | secondDiceSelected = function(){ 181 | return $('body').diceAt(1).hasClass('used') 182 | } 183 | waitFor(browser, secondDiceSelected, function(){done()}); 184 | }), 185 | it("Shouldn't display a notification for rolling when a roll isn't neccessary", function(){ 186 | $('#dice a').text().should.not.equal("Perform roll") 187 | }) 188 | }), 189 | describe('#play', function(){ 190 | before(function(){ 191 | app_module.start(5000); 192 | app_module.io().set('log level', 0); 193 | }), 194 | beforeEach(function(done){ 195 | this.timeout(5000); 196 | app_module.resetServer(seed=4); 197 | // setup the browser and jQuery 198 | browser = loadPage(done); 199 | }), 200 | after(function(done){ 201 | app_module.stop(done); 202 | }), 203 | it('should be able to access main page', function(){ 204 | browser.success.should.be.ok; 205 | }), 206 | it('should be able to see main board', function(){ 207 | browser.queryAll("circle").length.should.be.equal(30); 208 | browser.queryAll("#dice a").length.should.be.above(0); 209 | }), 210 | it('should be able to select a selectable piece on the board', function(){ 211 | var circle= $('circle[pos="1"][index="1"]').first(); 212 | circle.attr('class').should.equal('red'); 213 | circle.d3Click(); 214 | circle.attr('class').should.equal('red selected'); 215 | }), 216 | it('should not be possible to select a piece which is not on the top of its stack', function(){ 217 | var circle= $('circle[pos="1"][index="0"]').first(); 218 | circle.attr('class').should.equal('red'); 219 | circle.d3Click(); 220 | circle.attr('class').should.equal('red'); 221 | }), 222 | it("should be possible to read who's move it is", function(){ 223 | $('#player').text().should.equal('Red'); 224 | $('#playable a').length.should.equal(0); 225 | }), 226 | it.skip('should be possible to select any piece on the bar', function(){ 227 | }), 228 | it.skip("shouldn't be able to select a piece which isn't yours", function(){ 229 | }), 230 | it("should be possible to skip a turn if no piece can be moved", function(done){ 231 | this.timeout(5000); 232 | var c = app_module.board(); 233 | c.redState = {1:15}; 234 | c.blackState = {7:15}; 235 | app_module.canMove().should.be.false; 236 | browser = loadPage(function(){ 237 | $('#playable a').length.should.equal(1); 238 | $('#playable').text().should.equal('Cannot move - skip turn'); 239 | browser.click('#playlink'); 240 | browser = loadPage(function(){ 241 | $('#playable a').length.should.equal(0); 242 | $('#player').text().should.equal('Black'); 243 | $('#dice a').text().should.equal('42'); 244 | done(); 245 | }); 246 | }); 247 | }), 248 | it("should be possible to skip a turn if no - server direct", function(done){ 249 | this.timeout(5000); 250 | var c = app_module.board(); 251 | c.redState = {1:15}; 252 | c.blackState = {7:15}; 253 | app_module.performPass().should.equal.true 254 | browser = loadPage(function(){ 255 | $('#dice a').text().should.equal('42'); 256 | $('#playable').text().should.equal('') 257 | app_module.performPass().should.equal.false; 258 | done(); 259 | }); 260 | }), 261 | it("should be possible to skip a turn if no piece can be moved but only one dice is left", function(done){ 262 | this.timeout(5000); 263 | var c = app_module.board(); 264 | c.redState = {1:15}; 265 | c.blackState = {7:15}; 266 | app_module.setDice([{val:3, rolled:true}, {val:6, rolled:false}]) 267 | app_module.canMove().should.be.false; 268 | browser = loadPage(function(){ 269 | $('#playable a').length.should.equal(1); 270 | $('#playable').text().should.equal('Cannot move - skip turn'); 271 | done(); 272 | }); 273 | }), 274 | it('should be possible to watch a full move complete and have the UI update the the next player', function(done){ 275 | client = ioClient.connect(socketURL, options); 276 | checkGameState = function(){ 277 | var diceStr = '#dice a'; 278 | waitFor( 279 | browser, 280 | function(b){ 281 | return $(diceStr).text() == '42'; // dice have updated 282 | }, 283 | function(){ 284 | // confirm that the UI has updated, new player 285 | 286 | $('#player').text().should.equal('Black') 287 | done(); 288 | } 289 | ); 290 | } 291 | 292 | checkAfter4Moves = _.after(4, checkGameState); 293 | client.on('status', checkAfter4Moves); 294 | client.emit("move", 1, 0); 295 | client.emit("move", 1, 1); 296 | client.emit("move", 12, 2); 297 | client.emit("move", 12, 3); 298 | 299 | // check the dice have updated to the next set 300 | // check that the player has updated 301 | }), 302 | it('should be possible to move a piece', function(done){ 303 | this.timeout(3000) 304 | locationToMoveFrom = 'circle[pos="1"][index="1"]' 305 | locationToMoveTo = 'circle[pos="7"][index="0"]' 306 | diceStr = '#dice a' 307 | 308 | // target should be empty 309 | $(locationToMoveTo).length.should.equal(0) 310 | 311 | var piece = $(locationToMoveFrom).first().d3Click() 312 | var die = $(diceStr).first().d3Click() 313 | $(diceStr).text().should.equal('6666') 314 | 315 | waitFor( 316 | browser, 317 | function(b){return $(locationToMoveFrom).length === 0}, // wait for the refresh 318 | function(){ 319 | $(locationToMoveTo).length.should.equal(1) // assert the correct move has happened 320 | $(diceStr).text().should.equal('6666') 321 | $(diceStr).eq(0).hasClass('used').should.be.true 322 | // should not be actionable 323 | $(diceStr).eq(1).hasClass('used').should.be.false 324 | $(diceStr).eq(2).hasClass('used').should.be.false 325 | $(diceStr).eq(3).hasClass('used').should.be.false 326 | done() 327 | } 328 | ); 329 | }), 330 | it("should be possible to trigger a roll with a key press, for dupe dice", function(done){ 331 | this.timeout(5000); 332 | $('body').pieceAt(1, 1).d3Click() 333 | diceStr = '#dice a'; 334 | browser.keydown(browser.window, '6'); 335 | 336 | firstDiceSelected = function(){ 337 | return $('body').diceAt(0).hasClass('used') 338 | } 339 | secondDiceSelected = function(){ 340 | return $('body').diceAt(1).hasClass('used') 341 | } 342 | firstDiceSelected().should.be.false; 343 | waitFor(browser, firstDiceSelected, function(){ 344 | $('body').pieceAt(1, 0).d3Click() 345 | browser.keydown(browser.window, '6'); 346 | waitFor(browser, secondDiceSelected, function(){done()}) 347 | }); 348 | }) 349 | }), 350 | describe('#view', function(){ 351 | before(function(done){ 352 | app_module.start(5000); 353 | app_module.io().set('log level', 0); 354 | done(); 355 | }), 356 | beforeEach(function(){ 357 | app_module.resetServer(seed=4); 358 | }), 359 | after(function(done){ 360 | app_module.stop(done); 361 | }), 362 | it('should be possible to connect to a server', function(done){ 363 | var client = ioClient.connect(socketURL, options); 364 | client.on("connect", done); 365 | }) 366 | it('should be possible to retreive an initial game state', function(done){ 367 | var client = ioClient.connect(socketURL, options); 368 | game = require('../src/game.js'); 369 | 370 | client.on("connect", function(data){ 371 | client.emit("status"); 372 | }); 373 | client.on("status", function(data){ 374 | var positionSummary = _.countBy(data, _.values) 375 | assert.deepEqual( 376 | data, 377 | game.initialBoard().state() 378 | ); 379 | positionSummary['1,red'].should.equal(2); 380 | done(); 381 | }); 382 | }), 383 | it('should be possible to retrieve the current dice roll', function(done){ 384 | 385 | var client = ioClient.connect(socketURL, options); 386 | client.on("connect", function(data){ 387 | client.emit("dice"); 388 | client.on("dice", function(dice){ 389 | dice.should.eql( 390 | {dice:[ 391 | {"val":6,"rolled":false}, 392 | {"val":6,"rolled":false}, 393 | {"val":6,"rolled":false}, 394 | {"val":6,"rolled":false} 395 | ], playable: true} 396 | ) 397 | done(); 398 | }); 399 | }); 400 | }), 401 | it('should be the case that all players are notified about a status change', function(done){ 402 | this.timeout(10000); 403 | var client1 = ioClient.connect(socketURL, options); 404 | 405 | client1.on("connect", function(data){ 406 | var client2 = ioClient.connect(socketURL, options); 407 | 408 | client2.on("connect", function(data){ 409 | // this test will only complete when done has been called twice 410 | success = _.after(2, done) 411 | client2.on("status", function(data){ 412 | success(); 413 | }); 414 | client1.on("status", function(data){ 415 | success(); 416 | }); 417 | client1.emit("move", 1, 2) 418 | }); 419 | }); 420 | }), 421 | it('should be Red to play first', function(done){ 422 | var client = ioClient.connect(socketURL, options); 423 | client.on("connect", function(data){ 424 | client.emit("player"); 425 | client.on("player", function(player){ 426 | player.should.equal('red') 427 | done(); 428 | }); 429 | }); 430 | }), 431 | it('should not be able to use the same die twice', function(done){ 432 | var client = ioClient.connect(socketURL, options); 433 | client.on("connect", function(){ 434 | client.emit("move", 1, 0); 435 | client.once("status", function(data1){ 436 | var pos1 = _.countBy(data1, _.values); 437 | pos1['1,red'].should.equal(1); 438 | pos1['7,red'].should.equal(1); 439 | client.emit("move", 1, 0); 440 | 441 | client.once("status", function(data2){ 442 | var pos2 = _.countBy(data2, _.values); 443 | pos2['1,red'].should.equal(1); 444 | pos2['7,red'].should.equal(1); 445 | done(); 446 | }) 447 | }) 448 | }) 449 | }), 450 | it('should not be able to move a black piece when it is red to move', function(done){ 451 | var client = ioClient.connect(socketURL, options); 452 | client.on("connect", function(){ 453 | client.emit("move", 24, 0); 454 | client.on("status", function(board){ 455 | var pos = _.countBy(board, _.values); 456 | pos.should.eql( 457 | { 458 | '1,red': 2, 459 | '12,red': 5, 460 | '17,red': 3, 461 | '19,red': 5, 462 | '6,black': 5, 463 | '8,black': 3, 464 | '13,black': 5, 465 | '24,black': 2 466 | } 467 | ) 468 | done(); 469 | }); 470 | 471 | }) 472 | }), 473 | it('should be Black to play after a move', function(done){ 474 | var client = ioClient.connect(socketURL, options); 475 | client.on("connect", function(data){ 476 | client.emit("move", 1, 0) 477 | client.once("status", function(data){ 478 | // check the update of the last move 479 | var positionSummary = _.countBy(data, _.values); 480 | positionSummary['1,red'].should.equal(1); 481 | positionSummary['7,red'].should.equal(1); 482 | 483 | // make some more moves 484 | client.emit("move", 1, 1) 485 | client.emit("move", 12, 2) 486 | client.emit("move", 12, 3) 487 | 488 | // check if the player changed 489 | client.on("player", function(player){ 490 | player.should.equal('black'); 491 | done(); 492 | }); 493 | 494 | 495 | }); 496 | }); 497 | }), 498 | it('should roll dice between moves completed', function(done){ 499 | var client = ioClient.connect(socketURL, options); 500 | // 6,6,6,6 - move(1,6) RED 501 | // 4,2 - move(13,4) BLACK 502 | // 6,6,6,6 - move(24,6) RED 503 | // 5,4 - move(9, 5) BLACK 504 | // 5,4 - move(7, 4) RED 505 | // 2,1 - move(2, 1) BLACK 506 | // 6,2 -> 507 | this.timeout(10000); 508 | var i = 0 509 | doneAfter5 = _.after(5, done) 510 | diceCallback = function(dice){ 511 | var targets = [ 512 | {dice:[{'val':6, 'rolled':true}, {'val':6, 'rolled':false}, {'val':6, 'rolled':false}, {'val':6, 'rolled':false}], playable:true}, 513 | {dice:[{'val':6, 'rolled':true}, {'val':6, 'rolled':true}, {'val':6, 'rolled':false}, {'val':6, 'rolled':false}], playable:true}, 514 | {dice:[{'val':6, 'rolled':true}, {'val':6, 'rolled':true}, {'val':6, 'rolled':true}, {'val':6, 'rolled':false}], playable:true}, 515 | {dice:[{'val':4, 'rolled':false},{'val':2, 'rolled':false}], playable:true }, 516 | {dice:[{'val':4, 'rolled':false},{'val':2, 'rolled':true} ], playable:true } 517 | ] 518 | dice.should.eql(targets[i]) 519 | i += 1; 520 | doneAfter5() 521 | } 522 | client.on("dice", diceCallback) 523 | client.on("connect", function(data){ 524 | client.emit("move", 1, 0); 525 | client.emit("move", 1, 1); 526 | client.emit("move", 12, 2); 527 | client.emit("move", 12, 3); 528 | client.emit("move", 13, 1); 529 | }); 530 | }), 531 | it('should make no dice change if the move was invalid', function(done){ 532 | var client = ioClient.connect(socketURL, options); 533 | client.on("connect", function(){ 534 | client.emit("move", 19, 0); 535 | client.on("dice", function(dice){ 536 | dice.should.eql( 537 | { 538 | dice:[ 539 | {'val':6, 'rolled':false}, 540 | {'val':6, 'rolled':false}, 541 | {'val':6, 'rolled':false}, 542 | {'val':6, 'rolled':false} 543 | ], 544 | playable: true 545 | } 546 | ) 547 | done(); 548 | }) 549 | }) 550 | }), 551 | it('should be possible determine that a move can be made', function(){ 552 | var c = app_module.board(); 553 | c.redState = {2:15}; 554 | c.blackState = {7:15}; 555 | console.log(app_module.board()); 556 | console.log(app_module.dice()); 557 | app_module.canMove().should.be.true; 558 | }) 559 | it('should be possible determine that no move can be made', function(){ 560 | var c = app_module.board(); 561 | c.redState = {1:15}; 562 | c.blackState = {7:15}; 563 | console.log(app_module.board()); 564 | console.log(app_module.dice()); 565 | app_module.canMove().should.be.false; 566 | }) 567 | it.skip('should be possible to capture Black piece', function(done){ 568 | var client = ioClient.connect(socketURL, options); 569 | display = function(data){console.log(data)} 570 | client.on('connect', function(){ 571 | client.emit("move", 1, 6) 572 | client.once("status", function(data){ 573 | client.emit("move", 13, 6) 574 | client.once("status", function(data){ 575 | client.emit("move", 1, 6) 576 | client.once("status", function(data){ 577 | done(); 578 | }) 579 | }) 580 | }) 581 | }) 582 | }) 583 | }) 584 | }) 585 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 |

Backgammon-js

2 |

Feel free to stay a while and play a game

3 |
to play next
4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 1 60 | 2 61 | 3 62 | 4 63 | 5 64 | 6 65 | 7 66 | 8 67 | 9 68 | 10 69 | 11 70 | 12 71 | 24 72 | 23 73 | 22 74 | 21 75 | 20 76 | 19 77 | 18 78 | 17 79 | 16 80 | 15 81 | 14 82 | 13 83 | 84 | 85 | 86 |
87 |
88 |
89 | 90 | -------------------------------------------------------------------------------- /views/layouts/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello everyone 7 | 8 | 9 | 10 | 11 | {{{body}}} 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /views/layouts/main.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example App 6 | 7 | 8 | 9 | {{{body}}} 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------