updates before renaming to color-thief

This commit is contained in:
Lokesh Dhakar
2011-11-03 15:36:42 -04:00
parent 69f3e21a83
commit 16894c23f4
10 changed files with 1038 additions and 242 deletions

View File

@@ -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 = $('<div>', {
'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 = $('<div>', {
'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 = $('<div>', {
'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 = $('<div>', {
'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 = $('<div>', {
'class': 'swatch'
}).css('background-color','rgba('+value[0]+','+value[1]+ ','+value[2]+', 1)');
medianCutPalette.append(swatchEl);
});
});
});
});

196
js/color-thief.js Normal file
View File

@@ -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<rowCount; i++){
vertOffset = i * rowHeight * image.width * 4;
for(var j=0; j<colCount; j++){
horizOffset = j * colWidth * 4;
for( var k = 0; k < rowHeight; k++){
rowOffset = k * image.width * 4;
for( var l = 0; l < colWidth; l++){
offset = vertOffset + horizOffset + rowOffset + (l * 4);
rgb.r += pixels[offset];
rgb.g += pixels[offset+1];
rgb.b += pixels[offset+2];
count++;
}
}
rgb.r = Math.floor(rgb.r/count);
rgb.g = Math.floor(rgb.g/count);
rgb.b = Math.floor(rgb.b/count);
palette.push(rgb);
// reset before next section
rgb = {r:0,g:0,b:0};
count = 0;
}
}
return palette;
}

View File

@@ -1,153 +0,0 @@
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;
}
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<rowCount; i++){
vertOffset = i * rowHeight * image.width * 4;
for(var j=0; j<colCount; j++){
horizOffset = j * colWidth * 4;
for( var k = 0; k < rowHeight; k++){
rowOffset = k * image.width * 4;
for( var l = 0; l < colWidth; l++){
offset = vertOffset + horizOffset + rowOffset + (l * 4);
rgb.r += pixels[offset];
rgb.g += pixels[offset+1];
rgb.b += pixels[offset+2];
count++;
}
}
rgb.r = Math.floor(rgb.r/count);
rgb.g = Math.floor(rgb.g/count);
rgb.b = Math.floor(rgb.b/count);
palette.push(rgb);
// reset before next section
rgb = {r:0,g:0,b:0};
count = 0;
}
}
return palette;
}
function createMedianCutPalette(sourceImage, colorCount){
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, 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);
*/

View File

@@ -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);

419
js/libs/mustache.js Normal file
View File

@@ -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 "&amp;";
case '"': return '&quot;';
case "'": return '&#39;';
case "<": return "&lt;";
case ">": return "&gt;";
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");
}
}
});
}();