// =================== // Mondrian Art Puzzle // =================== // // A Pen By Ben Lorantfy // Website: benlorantfy.com // Twitter: @benlorantfy var App = function() { var blockSize = 50; var width = 0; var height = 0; var area = 0; var debug = true; var mouseX = 0; var mouseY = 0; var localX = 0; var localY = 0; var blockX = 0; var blockY = 0; var firstBlockX = 0; var firstBlockY = 0; var rectangles = []; var score = 0; var author = null; var submitUrl = "https://us-central1-mondrian-9afdb.cloudfunctions.net/submit"; var highscores = {}; function listenForHighscores(){ // [ Initialize Firebase ] // Firebase is used for highscroes var config = { apiKey: "AIzaSyAjrY5X0svyujcv1V11vZZoQ-40h43UzXY", authDomain: "mondrian-9afdb.firebaseapp.com", databaseURL: "https://mondrian-9afdb.firebaseio.com", projectId: "mondrian-9afdb", storageBucket: "mondrian-9afdb.appspot.com", messagingSenderId: "921671287404" }; firebase.initializeApp(config); // [ Highscore List ] // Populates the highscore list firebase.database().ref("grids").limitToFirst(200).on("value", function(snapshot) { $("#highscoresData").empty(); highscores = snapshot.val(); for (var key in highscores) { var data = highscores[key]; var classes = ""; if(data.width != data.height){ classes = "rectangleGrid"; } var row = $("<tr class='" + classes + "'></tr>"); var date = moment(data.timestamp).format("MMM Do YYYY"); row.append($("<td></td>").text(data.name)); row.append($("<td></td>").text(data.score)); row.append($("<td></td>").text(data.author)); row.append($("<td></td>").text(date)); row.append($("<td><button class='viewSolution' data-grid='" + key + "'>View</button></td>")); $("#highscoresData").append(row); } }) } function attachEvents() { $("#squareGridToggle").click(function(){ $(".rectangleGrid").toggleClass("hide"); }) $("#width").on("change", function() { updateGrid(); }); $("#height").on("change", function() { updateGrid(); }); var dragging = false; $("#holder").on("mousedown", ".gridBlock", function(e) { e.preventDefault(); dragging = true; firstBlockX = blockX; firstBlockY = blockY; colorGridSelection(); }) $(document).on("mousemove", function(e) { var offset = $("#holder").offset(); mouseX = e.pageX; mouseY = e.pageY; localX = mouseX - offset.left; localY = mouseY - offset.top; blockX = Math.floor(localX / blockSize); blockY = Math.floor(localY / blockSize); if (dragging) { colorGridSelection(); } }); $(document).on("mouseup", function() { if (!dragging) return; dragging = false; if ($(".selected").length > 0 && $(".blocked").length == 0) { var x = Math.min(blockX, firstBlockX) var y = Math.min(blockY, firstBlockY) var maxX = Math.max(blockX, firstBlockX) var maxY = Math.max(blockY, firstBlockY) // Make sure x/y/etc. are within bounds of rectangle if (x < 0) x = 0; if (y < 0) y = 0; if (x >= width) x = width - 1; if (y >= height) y = height - 1; if (maxX < 0) maxX = 0; if (maxY < 0) maxY = 0; if (maxX >= width) maxX = width - 1; if (maxY >= height) maxY = height - 1; rectangles.push({ x: x, y: y, maxX: maxX, maxY: maxY }); var c = generateColor(); $(".selected").addClass("done"); $(".selected").css("background-color", "rgb(" + c[0] + "," + c[1] + "," + c[2] + ")"); } $(".selected").removeClass("selected"); $(".blocked").removeClass("blocked"); checkIfFull(); }); $("#submit").click(function() { author = prompt("What name would you like beside your highscore?"); if (!author) { author = "Anonymous"; } submitScore(); }); $("#reset").click(function() { updateGrid(); }) $("#gridToggle").click(function() { $("#holder").toggleClass("hideGrid"); }) $("#highscores").on("click",".viewSolution",function(){ var key = $(this).data("grid"); var highscore = highscores[key]; width = highscore.width; height = highscore.height; $("#width").val(width); $("#height").val(height); updateGrid(); rectangles = highscore.rectangles; checkIfFull(); for(var i = 0; i < rectangles.length; i++){ var r = rectangles[i]; var c = generateColor(); for(var x = r.x; x <= r.maxX; x++){ for(var y = r.y; y <= r.maxY; y++){ $(".block-" + x + "-" + y).css("background-color", "rgb(" + c[0] + "," + c[1] + "," + c[2] + ")"); } } } $(".gridBlock").addClass("done"); }) } function updateGrid() { $("#holder").removeClass("hideGrid"); width = $("#width").val(); height = $("#height").val(); $("#holder").height(height * blockSize); $("#holder").width(width * blockSize); area = width * height; $("#score").text("Incomplete"); $("#scoreDetails").text(""); $("#holder").empty(); rectangles = []; var gridBlock = $("<div class = 'gridBlock'></div>").css("width",blockSize + "px").css("height",blockSize + "px"); for (var i = 0; i < area; i++) { var y = Math.floor(i / width); var x = i - y * width; var clone = gridBlock.clone().addClass("block-" + x + "-" + y); $("#holder").append(clone); } } function colorGridSelection() { $(".gridBlock").removeClass("selected"); $(".gridBlock").removeClass("blocked"); // Check all existing rectangles to see if any are the same dimensions var blockAll = false; var dragWidth = Math.max(firstBlockX, blockX) - Math.min(firstBlockX, blockX) + 1; var dragHeight = Math.max(firstBlockY, blockY) - Math.min(firstBlockY, blockY) + 1; if (dragWidth >= width && dragHeight >= height) { blockAll = true; } if (!blockAll) { for (var i = 0; i < rectangles.length; i++) { var a = rectangles[i]; var b = { x: Math.min(firstBlockX, blockX), y: Math.min(firstBlockY, blockY), maxX: Math.max(firstBlockX, blockX), maxY: Math.max(firstBlockY, blockY) } var widthA = a.maxX - a.x; var heightA = a.maxY - a.y; var widthB = b.maxX - b.x; var heightB = b.maxY - b.y; if (widthA == widthB && heightA == heightB) blockAll = true; if (widthA == heightB && heightA == widthB) blockAll = true; } } for (var x = Math.min(firstBlockX, blockX); x <= Math.max(firstBlockX, blockX); x++) { for (var y = Math.min(firstBlockY, blockY); y <= Math.max(firstBlockY, blockY); y++) { var block = $(".block-" + x + "-" + y); if (!block.hasClass("done") && !blockAll) { block.addClass("selected"); } else { block.addClass("blocked"); } } } } function checkIfFull() { var request = {}; var valid = validateRectangles(rectangles, width, height); if (!valid) { return; } score = calculateScore(rectangles); $("#score").text(score.score); // Shows Score Breakdown $("#scoreDetails").text( "Biggest: " + score.max.w + "x" + score.max.h + " = " + score.max.area + "

" + "Smallest: " + score.min.w + "x" + score.min.h + " = " + score.min.area + "

" + "Score: " + score.max.area + " - " + score.min.area + " = " + score.score ); } function submitScore() { var request = {}; request.rectangles = rectangles; request.width = width * 1; request.height = height * 1; request.author = author; $.ajax({ type: "POST", url: submitUrl, contentType: "application/json; charset=utf-8", dataType: "json", data: JSON.stringify(request) }).done(function(data) { }).fail(function(data) { }); } function validateRectangles(rectangles, width, height) { // // Criteria for rectangles to be valid // ----------------------------------- // 1. Proper format // 2. Have to be within width/height // 3. Can't overlap // 4. Can't be any rectangles with same dimensions // 5. Can't be any leftover space // 6. Can't have one or zero rectangles if (rectangles.length <= 1) return false; // O(n) for (var i = 0; i < rectangles.length; i++) { var rectangle = rectangles[i]; // [ Check proper format ] if (typeof rectangle.x !== "number") return false; if (typeof rectangle.y !== "number") return false; if (typeof rectangle.maxX !== "number") return false; if (typeof rectangle.maxY !== "number") return false; if (rectangle.x < 0) return false; if (rectangle.y < 0) return false; if (rectangle.maxX < 0) return false; if (rectangle.maxY < 0) return false; if (!isFinite(rectangle.x)) return false; if (!isFinite(rectangle.y)) return false; if (!isFinite(rectangle.maxX)) return false; if (!isFinite(rectangle.maxY)) return false; if (rectangle.x > rectangle.maxX) return false; if (rectangle.y > rectangle.maxY) return false; // [ Check within width/height ] if (rectangle.x >= width) return false; if (rectangle.maxX >= width) return false; if (rectangle.y >= height) return false; if (rectangle.maxY >= height) return false; } // O(n^2) for (var i = 0; i < rectangles.length; i++) { var rectangle = rectangles[i]; for (var j = 0; j < rectangles.length; j++) { if (i == j) continue; var a = rectangle; var b = rectangles[j]; // [ Check Overlap ] var intersects = a.x <= b.maxX && b.x <= a.maxX && a.y <= b.maxY && b.y <= a.maxY; if (intersects) return false; // [ Check if dimensions are same ] var widthA = a.maxX - a.x; var heightA = a.maxY - a.y; var widthB = b.maxX - b.x; var heightB = b.maxY - b.y; if (widthA == widthB && heightA == heightB) return false; if (widthA == heightB && heightA == widthB) return false; } } // [ Check for left over space ] var blocks = {}; for (var i = 0; i < rectangles.length; i++) { var rectangle = rectangles[i]; for (var x = rectangle.x; x <= rectangle.maxX; x++) { for (var y = rectangle.y; y <= rectangle.maxY; y++) { blocks[x + "-" + y] = true; } } } // Count number of blocks var blockCount = 0; for (var key in blocks) { blockCount++; } if (blockCount != (width * height)) return false; return true; } function calculateScore(rectangles) { var min = Number.MAX_VALUE; var max = Number.MAX_VALUE * -1; var minDimensions = {}; var maxDimensions = {}; for (var i = 0; i < rectangles.length; i++) { var rectangle = rectangles[i]; var w = (rectangle.maxX - rectangle.x + 1); var h = (rectangle.maxY - rectangle.y + 1); var area = w * h; min = Math.min(min, area); max = Math.max(max, area); if(min == area){ minDimensions = { w:w ,h:h } } if(max == area){ maxDimensions = { w:w ,h:h } } } minDimensions.area = minDimensions.w*minDimensions.h; maxDimensions.area = maxDimensions.w*maxDimensions.h; return { score:max - min ,min:minDimensions ,max:maxDimensions } } var generateColor = (function() { var i = 4; var hueStep = 40; var satStep = 30; var lightStep = 15; /** * Converts an HSL color value to RGB. Conversion formula * adapted from http://en.wikipedia.org/wiki/HSL_color_space. * Assumes h, s, and l are contained in the set [0, 1] and * returns r, g, and b in the set [0, 255]. * Taken from: http://stackoverflow.com/a/9493060 * * @param {number} h The hue * @param {number} s The saturation * @param {number} l The lightness * @return {Array} The RGB representation */ function hslToRgb(h, s, l) { var r, g, b; if (s == 0) { r = g = b = l; // achromatic } else { var hue2rgb = function hue2rgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; } var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } return function() { var hue = hueStep * i; var numHueCycles = Math.floor(hue / 360); var light = lightStep * numHueCycles; var numLightCycles = Math.floor(light / 50); var sat = satStep * numLightCycles; hue = hue - numHueCycles * 360; light = light - numLightCycles * 60; light = 40 + light; sat = sat - Math.floor(sat / 50) * 50; sat = 50 + sat; var rgb = hslToRgb(hue / 360, sat / 100, light / 100); i++; return rgb; } })(); this.start = function() { listenForHighscores(); attachEvents(); updateGrid(); } } var app = new App(); app.start();

!