From 16894c23f4a7e4116c6e7e7448f01aa5d2f7c4e2 Mon Sep 17 00:00:00 2001 From: Lokesh Dhakar Date: Thu, 3 Nov 2011 15:36:42 -0400 Subject: [PATCH] updates before renaming to color-thief --- css/app.css | 197 +++++++++++++++- index.html | 48 +--- js/app.js | 66 +++--- js/color-thief.js | 196 +++++++++++++++ js/functions.js | 153 ------------ js/libs/jquery.imagesloaded.js | 54 +++++ js/libs/mustache.js | 419 +++++++++++++++++++++++++++++++++ js/{ => libs}/quantize.js | 0 sass/_base.sass | 1 + sass/app.sass | 146 +++++++++++- 10 files changed, 1038 insertions(+), 242 deletions(-) create mode 100644 js/color-thief.js delete mode 100644 js/functions.js create mode 100644 js/libs/jquery.imagesloaded.js create mode 100644 js/libs/mustache.js rename js/{ => libs}/quantize.js (100%) diff --git a/css/app.css b/css/app.css index a165992..c7e8389 100644 --- a/css/app.css +++ b/css/app.css @@ -63,43 +63,214 @@ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, display: block; } -/* line 3, ../sass/app.sass */ -body { - color: #444444; - font-family: "lucida grande", sans-serif; - line-height: 1.625em; - padding: 40px; +/* =Global */ +/* line 9, ../sass/app.sass */ +body, input, textarea { + margin: 40px; + color: #555555; + font: 16px/1.625em "Varela Round", "lucida grande", tahoma, sans-serif; + font-weight: 400; + -webkit-font-smoothing: antialiased; } -/* line 9, ../sass/app.sass */ +/* line 17, ../sass/app.sass */ +h1, h2, h3, h4, h5, h6 { + font-family: "Terminal Dosis", "lucida grande", tahoma, sans-serif; + line-height: 1em; + font-weight: 600; +} + +/* line 23, ../sass/app.sass */ +h1 { + font-size: 60px; +} + +/* line 26, ../sass/app.sass */ +h2 { + font-size: 24px; + line-height: 1.2em; +} + +/* line 30, ../sass/app.sass */ +h3 { + font-size: 18px; + letter-spacing: 0.1em; +} + +/* line 34, ../sass/app.sass */ +p { + margin-bottom: 1.25em; +} + +/* line 38, ../sass/app.sass */ +strong { + font-weight: bold; +} + +/* Forms */ +/* line 44, ../sass/app.sass */ +input[type=text], +input[type=password], +textarea { + background: #fafafa; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + -o-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + border: 1px solid #dddddd; + color: #888888; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -o-border-radius: 4px; + -ms-border-radius: 4px; + -khtml-border-radius: 4px; + border-radius: 4px; +} + +/* line 55, ../sass/app.sass */ +input[type=text]:focus, +textarea:focus { + color: #373737; +} + +/* line 59, ../sass/app.sass */ +textarea { + padding-left: 3px; + width: 98%; +} + +/* line 63, ../sass/app.sass */ +input[type=text] { + padding: 3px; +} + +/* Links */ +/* line 67, ../sass/app.sass */ +a { + color: #555555; + text-decoration: none; +} +/* line 70, ../sass/app.sass */ +a:hover { + color: #09a1ec; +} + +/* line 75, ../sass/app.sass */ +.button { + text-transform: uppercase; + font-family: "Terminal Dosis", "lucida grande", tahoma, sans-serif; + font-weight: 700; + letter-spacing: 0.1em; + olor: #555555; + display: block; + float: left; + position: relative; + line-height: 1; + padding: 0.6em 1.5em 0.5em 1.5em; + border: 1px solid transparent; + -moz-box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.1), 0 2px 5px 0 rgba(0, 0, 0, 0.25); + -webkit-box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.1), 0 2px 5px 0 rgba(0, 0, 0, 0.25); + -o-box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.1), 0 2px 5px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.1), 0 2px 5px 0 rgba(0, 0, 0, 0.25); + background-color: #eeeeee; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -o-border-radius: 4px; + -ms-border-radius: 4px; + -khtml-border-radius: 4px; + border-radius: 4px; + border: 1px solid #d6d6d6; + margin-bottom: 6px; +} +/* line 92, ../sass/app.sass */ +.button:hover { + background-color: #f1f1f1; +} +/* line 94, ../sass/app.sass */ +.button:active { + -moz-box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.1); + -webkit-box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.1); + -o-box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.1); + box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.1); + top: 2px; + margin-bottom: 4px; + border-bottom-width: 0; +} + +/* Buttons with dark backgrounds */ +/* line 103, ../sass/app.sass */ +.button.nav, +.button.warn, +.button.primary, +.nav-previous .button, +.nav-next .button { + color: white; + text-shadow: rgba(0, 0, 0, 0.2) 1px 1px 0; +} + +/* line 112, ../sass/app.sass */ +.button.nav, +.nav-previous .button, +.nav-next .button { + background-color: #09a1ec; + border-color: #0891d4; +} +/* line 117, ../sass/app.sass */ +.button.nav:hover, +.nav-previous .button:hover, +.nav-next .button:hover { + background-color: #33b6f7; +} + +/* line 123, ../sass/app.sass */ +.button.warn { + background-color: #ee8833; + border-color: #ec7818; +} +/* line 126, ../sass/app.sass */ +.button.warn:hover { + background-color: #f1a05c; +} + +/* line 131, ../sass/app.sass */ +.button.primary { + background-color: #6bb445; + border-color: #6bb445; +} +/* line 134, ../sass/app.sass */ +.button.primary:hover { + background-color: #88c568; +} + +/* line 143, ../sass/app.sass */ h1 { font-size: 36px; margin-bottom: 0.5em; } -/* line 16, ../sass/app.sass */ +/* line 150, ../sass/app.sass */ .imageSection { overflow: hidden; *zoom: 1; margin-bottom: 40px; } -/* line 19, ../sass/app.sass */ +/* line 153, ../sass/app.sass */ .imageSection img { float: left; margin-right: 20px; } -/* line 22, ../sass/app.sass */ +/* line 156, ../sass/app.sass */ .imageSection .colors { width: 400px; float: left; } -/* line 25, ../sass/app.sass */ +/* line 159, ../sass/app.sass */ .imageSection .colors .function { overflow: hidden; *zoom: 1; margin-bottom: 10px; } -/* line 28, ../sass/app.sass */ +/* line 162, ../sass/app.sass */ .imageSection .colors .function .swatch { width: 40px; height: 20px; @@ -108,7 +279,7 @@ h1 { float: left; } -/* line 35, ../sass/app.sass */ +/* line 169, ../sass/app.sass */ canvas { display: none; } diff --git a/index.html b/index.html index 2117fb6..0a5d3cf 100755 --- a/index.html +++ b/index.html @@ -4,6 +4,8 @@ + + Image Palette @@ -26,15 +28,11 @@
-
-

getAverageRGB()

-
+

getDominantColor()

-
-

createAreaBasedPalette()

-
+

createMedianCutPalette()

@@ -44,15 +42,11 @@
-
-

getAverageRGB()

-
+

getDominantColor()

-
-

createAreaBasedPalette()

-
+

createMedianCutPalette()

@@ -62,15 +56,11 @@
-
-

getAverageRGB()

-
+

getDominantColor()

-
-

createAreaBasedPalette()

-
+

createMedianCutPalette()

@@ -80,15 +70,9 @@
-
-

getAverageRGB()

-

getDominantColor()

-
-

createAreaBasedPalette()

-

createMedianCutPalette()

@@ -98,15 +82,9 @@
-
-

getAverageRGB()

-

getDominantColor()

-
-

createAreaBasedPalette()

-

createMedianCutPalette()

@@ -116,15 +94,9 @@
-
-

getAverageRGB()

-

getDominantColor()

-
-

createAreaBasedPalette()

-

createMedianCutPalette()

@@ -139,7 +111,9 @@ - + + + diff --git a/js/app.js b/js/app.js index 7ee147c..21e92f5 100644 --- a/js/app.js +++ b/js/app.js @@ -1,46 +1,46 @@ $(document).ready(function(){ + + var view = { + title: "Joe", + calc: function() { + return 2 + 4; + } + } + + var template = "{{title}} spends {{calc}}"; + + var html = Mustache.to_html(template, view); + + $('body').prepend(html); - $('img').each(function(index){ + $('img').imagesLoaded(function(){ + + $('img').each(function(index){ - var averageRGB = getAverageRGB(this); - var dominantColor = getDominantColor(this); - var areaPalette = createAreaBasedPalette(this, 9); - var medianPalette = createMedianCutPalette(this, 10); + var dominantColor = getDominantColor(this); + var medianPalette = createPalette(this, 10); - var imageSection = $(this).closest('.imageSection'); - - var swatchEl = $('
', { - 'class': 'swatch' - }).css('background-color','rgba('+averageRGB.r+','+averageRGB.g+ ','+averageRGB.b+', 1)'); + var imageSection = $(this).closest('.imageSection'); - imageSection.find('.averageRGB').append(swatchEl); + var switchEl; - swatchEl = $('
', { - 'class': 'swatch' - }).css('background-color','rgba('+dominantColor.r+','+dominantColor.g+ ','+dominantColor.b+', 1)'); - - imageSection.find('.dominantColor').append(swatchEl); - - - var areaBasedPalette = imageSection.find('.areaBasedPalette'); - - $.each(areaPalette, function(index, value){ swatchEl = $('
', { 'class': 'swatch' - }).css('background-color','rgba('+value.r+','+value.g+ ','+value.b+', 1)'); - areaBasedPalette.append(swatchEl); - }); - - var medianCutPalette = imageSection.find('.medianCutPalette'); + }).css('background-color','rgba('+dominantColor.r+','+dominantColor.g+ ','+dominantColor.b+', 1)'); - $.each(medianPalette, function(index, value){ - swatchEl = $('
', { - 'class': 'swatch' - }).css('background-color','rgba('+value[0]+','+value[1]+ ','+value[2]+', 1)'); - medianCutPalette.append(swatchEl); - }); + imageSection.find('.dominantColor').append(swatchEl); + var medianCutPalette = imageSection.find('.medianCutPalette'); + + $.each(medianPalette, function(index, value){ + swatchEl = $('
', { + 'class': 'swatch' + }).css('background-color','rgba('+value[0]+','+value[1]+ ','+value[2]+', 1)'); + medianCutPalette.append(swatchEl); + }); + + }); }); - + }); \ No newline at end of file diff --git a/js/color-thief.js b/js/color-thief.js new file mode 100644 index 0000000..005627e --- /dev/null +++ b/js/color-thief.js @@ -0,0 +1,196 @@ +/* + * Image Palette v0.1 + * by Lokesh Dhakar - http://www.lokeshdhakar.com + * + * Licensed under the Creative Commons Attribution 2.5 License - http://creativecommons.org/licenses/by/2.5/ + * + * The median cut palette function uses quantize.js which is written by Nick Rabinowitz + * and licensed under the MIT license. Big props to Nick as this is where the magic happens. + * + * == Functions + * getDominantColor() + * createPalette() + * getAverageRGB() + * createAreaBasedPalette() +*/ + + +/* + * getDominantColor(sourceImage) + * returns {r: num, g: num, b: num} + * + * Use the median cut algorithm provided by quantize.js to cluster similar + * colors and return the base color from the largest cluster. +*/ +function getDominantColor(sourceImage){ + + var palette = []; + + // Create custom CanvasImage object + var image = new CanvasImage(sourceImage), + imageData = image.getImageData(), + pixels = imageData.data, + pixelCount = image.getPixelCount(); + + // Store the RGB values in an array format suitable for quantize function + var pixelArray = []; + for (var i = 0; i < pixelCount; i++) { + // If pixel is mostly opaque + if(pixels[i*4+3] >= 125){ + pixelArray.push( [pixels[i*4], pixels[i*4+1], pixels[i*4+2]]); + } + }; + + // Send array to quantize function which clusters values + // using median cut algorithm + var cmap = MMCQ.quantize(pixelArray, 5); + var newPalette = cmap.palette(); + + return {r: newPalette[0][0], g: newPalette[0][1], b: newPalette[0][2]}; +} + + + +/* + * createPalette(sourceImage) + * returns array[ {r: num, g: num, b: num}, {r: num, g: num, b: num}, ...] + * + * Use the median cut algorithm provided by quantize.js to cluster similar + * colors. + * + * BUGGY: Function does not always return the requested amount of colors. It can be +/- 2. +*/ +function createPalette(sourceImage, colorCount){ + + var palette = []; + + // Create custom CanvasImage object + var image = new CanvasImage(sourceImage), + imageData = image.getImageData(), + pixels = imageData.data, + pixelCount = image.getPixelCount(); + + // Store the RGB values in an array format suitable for quantize function + var pixelArray = []; + for (var i = 0; i < pixelCount; i++) { + // If pixel is mostly opaque + if(pixels[i*4+3] >= 125){ + pixelArray.push( [pixels[i*4], pixels[i*4+1], pixels[i*4+2]]); + } + }; + + // Send array to quantize function which clusters values + // using median cut algorithm + var cmap = MMCQ.quantize(pixelArray, colorCount); + var newPalette = cmap.palette(); + + return newPalette; +} + + +/* + * getAverageRGB(sourceImage) + * returns {r: num, g: num, b: num} + * + * Add up all pixels RGB values and return average. + * Tends to return muddy gray/brown color. Most likely, you'll be better + * off using getDominantColor() instead. +*/ +function getAverageRGB(sourceImage) { + // Config + var sampleSize = 10; + + // Create custom CanvasImage object + var image = new CanvasImage(sourceImage), + imageData = image.getImageData(), + pixels = imageData.data, + pixelCount = image.getPixelCount(); + + // Reset vars + var i = 0, + count = 0, + rgb = {r:0,g:0,b:0}; + + // Loop through every # pixels. (# is set in Config above via the blockSize var) + // Add all the red values together, repeat for blue and green. + // Last step, divide by the number of pixels checked to get average. + while ( (i += sampleSize * 4) < pixelCount ) { + // if pixel is mostly opaque + if(pixels[i+3] > 125){ + ++count; + rgb.r += pixels[i]; + rgb.g += pixels[i+1]; + rgb.b += pixels[i+2]; + } + } + + rgb.r = Math.floor(rgb.r/count); + rgb.g = Math.floor(rgb.g/count); + rgb.b = Math.floor(rgb.b/count); + + return rgb; +} + + +/* + * createAreaBasedPalette(sourceImage, colorCount) + * returns array[ {r: num, g: num, b: num}, {r: num, g: num, b: num}, ...] + * + * Break the image into sections. Loops through pixel RGBS in the section and average color. + * Tends to return muddy gray/brown color. You're most likely better off using createPalette(). + * + * BUGGY: Function does not always return the requested amount of colors. It can be +/- 2. + * +*/ +function createAreaBasedPalette(sourceImage, colorCount){ + + var palette = []; + + // Create custom CanvasImage object + var image = new CanvasImage(sourceImage), + imageData = image.getImageData(), + pixels = imageData.data, + pixelCount = image.getPixelCount(); + + + // How big a pixel area does each palette color get + var rowCount = colCount = Math.round(Math.sqrt(colorCount)), + colWidth = Math.round(image.width / colCount), + rowHeight = Math.round(image.height / rowCount); + + var count = offset = rowOffset = vertOffset = horizOffset = 0, + rgb = {r:0,g:0,b:0}; + + // Loop through pixels section by section. + // At the end of each section, push the average rgb color to palette array. + for(var i=0; i 125){ - ++count; - rgb.r += pixels[i]; - rgb.g += pixels[i+1]; - rgb.b += pixels[i+2]; - } - } - - rgb.r = Math.floor(rgb.r/count); - rgb.g = Math.floor(rgb.g/count); - rgb.b = Math.floor(rgb.b/count); - - return rgb; -} - - - -function getDominantColor(sourceImage){ - - var palette = []; - - // Create custom CanvasImage object - var image = new CanvasImage(sourceImage), - imageData = image.getImageData(), - pixels = imageData.data, - pixelCount = image.getPixelCount(); - - var pixelArray = []; - - for (var i = 0; i < pixelCount; i++) { - // If pixel is mostly opaque - if(pixels[i*4+3] >= 125){ - pixelArray.push( [pixels[i*4], pixels[i*4+1], pixels[i*4+2]]); - } - }; - - var cmap = MMCQ.quantize(pixelArray, 5); - var newPalette = cmap.palette(); - - return {r: newPalette[0][0], g: newPalette[0][1], b: newPalette[0][2]}; -} - - - -function createAreaBasedPalette(sourceImage, colorCount){ - - var palette = []; - - // Create custom CanvasImage object - var image = new CanvasImage(sourceImage), - imageData = image.getImageData(), - pixels = imageData.data, - pixelCount = image.getPixelCount(); - - - // How big a pixel area does each palette color get - var rowCount = colCount = Math.round(Math.sqrt(colorCount)), - colWidth = Math.round(image.width / colCount), - rowHeight = Math.round(image.height / rowCount); - - var count = offset = rowOffset = vertOffset = horizOffset = 0, - rgb = {r:0,g:0,b:0}; - - for(var i=0; i= 125){ - pixelArray.push( [pixels[i*4], pixels[i*4+1], pixels[i*4+2]]); - } - }; - - var cmap = MMCQ.quantize(pixelArray, colorCount); - var newPalette = cmap.palette(); - - return newPalette; -} - -/* - for (var i = 0; i < pixelCount; i++) { - pixels[i*4] = pixels[i*4]; // Red - pixels[i*4+1] = pixels[i*4+1]; // Green - pixels[i*4+2] = pixels[i*4+2]; // Blue - }; - - image.clear(); - imageData.data = pixels; - image.update(imageData); -*/ diff --git a/js/libs/jquery.imagesloaded.js b/js/libs/jquery.imagesloaded.js new file mode 100644 index 0000000..48f0ea0 --- /dev/null +++ b/js/libs/jquery.imagesloaded.js @@ -0,0 +1,54 @@ +/*! + * jQuery imagesLoaded plugin v1.0.4 + * http://github.com/desandro/imagesloaded + * + * MIT License. by Paul Irish et al. + */ + +(function($, undefined) { + + // $('#my-container').imagesLoaded(myFunction) + // or + // $('img').imagesLoaded(myFunction) + + // execute a callback when all images have loaded. + // needed because .load() doesn't work on cached images + + // callback function gets image collection as argument + // `this` is the container + + $.fn.imagesLoaded = function( callback ) { + var $this = this, + $images = $this.find('img').add( $this.filter('img') ), + len = $images.length, + blank = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; + + function triggerCallback() { + callback.call( $this, $images ); + } + + function imgLoaded( event ) { + if ( --len <= 0 && event.target.src !== blank ){ + setTimeout( triggerCallback ); + $images.unbind( 'load error', imgLoaded ); + } + } + + if ( !len ) { + triggerCallback(); + } + + $images.bind( 'load error', imgLoaded ).each( function() { + // cached images don't fire load sometimes, so we reset src. + if (this.complete || this.complete === undefined){ + var src = this.src; + // webkit hack from http://groups.google.com/group/jquery-dev/browse_thread/thread/eee6ab7b2da50e1f + // data uri bypasses webkit log warning (thx doug jones) + this.src = blank; + this.src = src; + } + }); + + return $this; + }; +})(jQuery); \ No newline at end of file diff --git a/js/libs/mustache.js b/js/libs/mustache.js new file mode 100644 index 0000000..e291f05 --- /dev/null +++ b/js/libs/mustache.js @@ -0,0 +1,419 @@ +/* + mustache.js — Logic-less templates in JavaScript + + See http://mustache.github.com/ for more info. +*/ + +var Mustache = function() { + var regexCache = {}; + var Renderer = function() {}; + + Renderer.prototype = { + otag: "{{", + ctag: "}}", + pragmas: {}, + buffer: [], + pragmas_implemented: { + "IMPLICIT-ITERATOR": true + }, + context: {}, + + render: function(template, context, partials, in_recursion) { + // reset buffer & set context + if(!in_recursion) { + this.context = context; + this.buffer = []; // TODO: make this non-lazy + } + + // fail fast + if(!this.includes("", template)) { + if(in_recursion) { + return template; + } else { + this.send(template); + return; + } + } + + // get the pragmas together + template = this.render_pragmas(template); + + // render the template + var html = this.render_section(template, context, partials); + + // render_section did not find any sections, we still need to render the tags + if (html === false) { + html = this.render_tags(template, context, partials, in_recursion); + } + + if (in_recursion) { + return html; + } else { + this.sendLines(html); + } + }, + + /* + Sends parsed lines + */ + send: function(line) { + if(line !== "") { + this.buffer.push(line); + } + }, + + sendLines: function(text) { + if (text) { + var lines = text.split("\n"); + for (var i = 0; i < lines.length; i++) { + this.send(lines[i]); + } + } + }, + + /* + Looks for %PRAGMAS + */ + render_pragmas: function(template) { + // no pragmas + if(!this.includes("%", template)) { + return template; + } + + var that = this; + var regex = this.getCachedRegex("render_pragmas", function(otag, ctag) { + return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g"); + }); + + return template.replace(regex, function(match, pragma, options) { + if(!that.pragmas_implemented[pragma]) { + throw({message: + "This implementation of mustache doesn't understand the '" + + pragma + "' pragma"}); + } + that.pragmas[pragma] = {}; + if(options) { + var opts = options.split("="); + that.pragmas[pragma][opts[0]] = opts[1]; + } + return ""; + // ignore unknown pragmas silently + }); + }, + + /* + Tries to find a partial in the curent scope and render it + */ + render_partial: function(name, context, partials) { + name = this.trim(name); + if(!partials || partials[name] === undefined) { + throw({message: "unknown_partial '" + name + "'"}); + } + if(typeof(context[name]) != "object") { + return this.render(partials[name], context, partials, true); + } + return this.render(partials[name], context[name], partials, true); + }, + + /* + Renders inverted (^) and normal (#) sections + */ + render_section: function(template, context, partials) { + if(!this.includes("#", template) && !this.includes("^", template)) { + // did not render anything, there were no sections + return false; + } + + var that = this; + + var regex = this.getCachedRegex("render_section", function(otag, ctag) { + // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder + return new RegExp( + "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) + + otag + // {{ + "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) + ctag + // }} + + "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped + + otag + // {{ + "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). + ctag + // }} + + "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. + + "g"); + }); + + + // for each {{#foo}}{{/foo}} section do... + return template.replace(regex, function(match, before, type, name, content, after) { + // before contains only tags, no sections + var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", + + // after may contain both sections and tags, so use full rendering function + renderedAfter = after ? that.render(after, context, partials, true) : "", + + // will be computed below + renderedContent, + + value = that.find(name, context); + + if (type === "^") { // inverted section + if (!value || that.is_array(value) && value.length === 0) { + // false or empty list, render it + renderedContent = that.render(content, context, partials, true); + } else { + renderedContent = ""; + } + } else if (type === "#") { // normal section + if (that.is_array(value)) { // Enumerable, Let's loop! + renderedContent = that.map(value, function(row) { + return that.render(content, that.create_context(row), partials, true); + }).join(""); + } else if (that.is_object(value)) { // Object, Use it as subcontext! + renderedContent = that.render(content, that.create_context(value), + partials, true); + } else if (typeof value === "function") { + // higher order section + renderedContent = value.call(context, content, function(text) { + return that.render(text, context, partials, true); + }); + } else if (value) { // boolean section + renderedContent = that.render(content, context, partials, true); + } else { + renderedContent = ""; + } + } + + return renderedBefore + renderedContent + renderedAfter; + }); + }, + + /* + Replace {{foo}} and friends with values from our view + */ + render_tags: function(template, context, partials, in_recursion) { + // tit for tat + var that = this; + + + + var new_regex = function() { + return that.getCachedRegex("render_tags", function(otag, ctag) { + return new RegExp(otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + ctag + "+", "g"); + }); + }; + + var regex = new_regex(); + var tag_replace_callback = function(match, operator, name) { + switch(operator) { + case "!": // ignore comments + return ""; + case "=": // set new delimiters, rebuild the replace regexp + that.set_delimiters(name); + regex = new_regex(); + return ""; + case ">": // render partial + return that.render_partial(name, context, partials); + case "{": // the triple mustache is unescaped + return that.find(name, context); + default: // escape the value + return that.escape(that.find(name, context)); + } + }; + var lines = template.split("\n"); + for(var i = 0; i < lines.length; i++) { + lines[i] = lines[i].replace(regex, tag_replace_callback, this); + if(!in_recursion) { + this.send(lines[i]); + } + } + + if(in_recursion) { + return lines.join("\n"); + } + }, + + set_delimiters: function(delimiters) { + var dels = delimiters.split(" "); + this.otag = this.escape_regex(dels[0]); + this.ctag = this.escape_regex(dels[1]); + }, + + escape_regex: function(text) { + // thank you Simon Willison + if(!arguments.callee.sRE) { + var specials = [ + '/', '.', '*', '+', '?', '|', + '(', ')', '[', ']', '{', '}', '\\' + ]; + arguments.callee.sRE = new RegExp( + '(\\' + specials.join('|\\') + ')', 'g' + ); + } + return text.replace(arguments.callee.sRE, '\\$1'); + }, + + /* + find `name` in current `context`. That is find me a value + from the view object + */ + find: function(name, context) { + name = this.trim(name); + + // Checks whether a value is thruthy or false or 0 + function is_kinda_truthy(bool) { + return bool === false || bool === 0 || bool; + } + + var value; + + // check for dot notation eg. foo.bar + if(name.match(/([a-z_]+)\./ig)){ + value = is_kinda_truthy(this.walk_context(name, context)); + } + else{ + if(is_kinda_truthy(context[name])) { + value = context[name]; + } else if(is_kinda_truthy(this.context[name])) { + value = this.context[name]; + } + } + + if(typeof value === "function") { + return value.apply(context); + } + if(value !== undefined) { + return value; + } + // silently ignore unkown variables + return ""; + }, + + walk_context: function(name, context){ + var path = name.split('.'); + // if the var doesn't exist in current context, check the top level context + var value_context = (context[path[0]] != undefined) ? context : this.context; + var value = value_context[path.shift()]; + while(value != undefined && path.length > 0){ + value_context = value; + value = value[path.shift()]; + } + // if the value is a function, call it, binding the correct context + if(typeof value === "function") { + return value.apply(value_context); + } + return value; + }, + + // Utility methods + + /* includes tag */ + includes: function(needle, haystack) { + return haystack.indexOf(this.otag + needle) != -1; + }, + + /* + Does away with nasty characters + */ + escape: function(s) { + s = String(s === null ? "" : s); + return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) { + switch(s) { + case "&": return "&"; + case '"': return '"'; + case "'": return '''; + case "<": return "<"; + case ">": return ">"; + default: return s; + } + }); + }, + + // by @langalex, support for arrays of strings + create_context: function(_context) { + if(this.is_object(_context)) { + return _context; + } else { + var iterator = "."; + if(this.pragmas["IMPLICIT-ITERATOR"]) { + iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; + } + var ctx = {}; + ctx[iterator] = _context; + return ctx; + } + }, + + is_object: function(a) { + return a && typeof a == "object"; + }, + + is_array: function(a) { + return Object.prototype.toString.call(a) === '[object Array]'; + }, + + /* + Gets rid of leading and trailing whitespace + */ + trim: function(s) { + return s.replace(/^\s*|\s*$/g, ""); + }, + + /* + Why, why, why? Because IE. Cry, cry cry. + */ + map: function(array, fn) { + if (typeof array.map == "function") { + return array.map(fn); + } else { + var r = []; + var l = array.length; + for(var i = 0; i < l; i++) { + r.push(fn(array[i])); + } + return r; + } + }, + + getCachedRegex: function(name, generator) { + var byOtag = regexCache[this.otag]; + if (!byOtag) { + byOtag = regexCache[this.otag] = {}; + } + + var byCtag = byOtag[this.ctag]; + if (!byCtag) { + byCtag = byOtag[this.ctag] = {}; + } + + var regex = byCtag[name]; + if (!regex) { + regex = byCtag[name] = generator(this.otag, this.ctag); + } + + return regex; + } + }; + + return({ + name: "mustache.js", + version: "0.4.0-dev", + + /* + Turns a template and view into HTML + */ + to_html: function(template, view, partials, send_fun) { + var renderer = new Renderer(); + if(send_fun) { + renderer.send = send_fun; + } + renderer.render(template, view || {}, partials); + if(!send_fun) { + return renderer.buffer.join("\n"); + } + } + }); +}(); diff --git a/js/quantize.js b/js/libs/quantize.js similarity index 100% rename from js/quantize.js rename to js/libs/quantize.js diff --git a/sass/_base.sass b/sass/_base.sass index 4e03414..3cc2c52 100644 --- a/sass/_base.sass +++ b/sass/_base.sass @@ -1,6 +1,7 @@ $color: #555 $headingColor: #222 $altColor: scale-color($color, $lightness: 50%) +$lightGray: #eee $blue: #09a1ec $orange: #e83 $green: #6bb445 diff --git a/sass/app.sass b/sass/app.sass index c5ddba4..2952552 100644 --- a/sass/app.sass +++ b/sass/app.sass @@ -1,11 +1,145 @@ @import "base" -body - color: #444 - font-family: 'lucida grande', sans-serif - line-height: 1.625em - padding: 40px - + + + +/* =Global */ + + +body, input, textarea + margin: 40px + color: $color + font: 16px/1.625em 'Varela Round',"lucida grande",tahoma,sans-serif + font-weight: 400 + -webkit-font-smoothing: antialiased + + +h1,h2,h3,h4,h5,h6 + font-family: 'Terminal Dosis', 'lucida grande', tahoma, sans-serif + line-height: 1em + font-weight: 600 + + +h1 + font-size: 60px + +h2 + font-size: 24px + line-height: 1.2em + +h3 + font-size: 18px + letter-spacing: 0.1em + +p + margin-bottom: 1.25em + + +strong + font-weight: bold + + + +/* Forms */ +input[type=text], +input[type=password], +textarea + background: #fafafa + +box-shadow(inset 0 1px 1px rgba(0,0,0,0.1)) + border: 1px solid #ddd + color: #888 + +border-radius($radius) + + + +input[type=text]:focus, +textarea:focus + color: #373737 + +textarea + padding-left: 3px + width: 98% + +input[type=text] + padding: 3px + +/* Links */ +a + color: $color + text-decoration: none + &:hover + color: $blue + + + +.button + text-transform: uppercase + font-family: 'Terminal Dosis', 'lucida grande', tahoma, sans-serif + font-weight: 700 + letter-spacing: 0.1em + olor: #555 + display: block + float: left + position: relative + line-height: 1 + padding: .6em 1.5em .5em 1.5em + border: 1px solid transparent + +box-shadow( inset 0 2px 0 rgba(255, 255, 255, 0.1), 0 2px 5px 0 rgba(0,0,0, .25)) + background-color: $lightGray + +border-radius($radius) + border: 1px solid scale-color($lightGray, $lightness: -10%) + margin-bottom: 6px + &:hover + background-color: scale-color($lightGray, $lightness: 20%) + &:active + +box-shadow( inset 0 2px 0 rgba(0, 0, 0, 0.1)) + top: 2px + margin-bottom: 4px + border-bottom-width: 0 + + + +/* Buttons with dark backgrounds */ +.button.nav, +.button.warn, +.button.primary, +.nav-previous .button, +.nav-next .button + color: #fff + @include text-shadow(rgba(0,0,0, 0.2) 1px 1px 0) + + +.button.nav, +.nav-previous .button, +.nav-next .button + background-color: $blue + border-color: scale-color($blue, $lightness: -10%) + &:hover + background-color: scale-color($blue, $lightness: 20%) + + + + +.button.warn + background-color: $orange + border-color: scale-color($orange, $lightness: -10%) + &:hover + background-color: scale-color($orange, $lightness: 20%) + + + +.button.primary + background-color: $green + border-color: $green + &:hover + background-color: scale-color($green, $lightness: 20%) + + + + + + + h1 font-size: 36px margin-bottom: .5em