summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
authorYuval Adam <yuval@y3xz.com>2015-06-13 15:46:40 +0300
committerYuval Adam <yuval@y3xz.com>2015-06-13 15:46:40 +0300
commit21eb0e3b59966a469cd2b9f09722642bf1b8c661 (patch)
treee969cc5368e16febfdca79a3c8ef139522662fd8 /client
parent9ae25440f70d97ce33dc2b139b381a098f761349 (diff)
Switch to rgbquant
Diffstat (limited to 'client')
-rw-r--r--client/button.js7
-rw-r--r--client/effects.js78
-rw-r--r--client/quantize.js471
-rw-r--r--client/rgbquant.js935
-rw-r--r--client/templates/photos.html1
5 files changed, 1002 insertions, 490 deletions
diff --git a/client/button.js b/client/button.js
index 2fabf31..786091d 100644
--- a/client/button.js
+++ b/client/button.js
@@ -47,6 +47,13 @@ Template.photos.events({
Effects.pixelate(x.id);
$('#canvas-'+x.id).show()
})
+ },
+ 'click button.color': function (event) {
+ _.each(Session.get('photos'), function(x) {
+ $('#img-'+x.id).hide()
+ Effects.colorize(x.id);
+ $('#canvas-'+x.id).show()
+ })
}
})
diff --git a/client/effects.js b/client/effects.js
index 3743b9e..9e095da 100644
--- a/client/effects.js
+++ b/client/effects.js
@@ -14,11 +14,12 @@ Effects = (function() {
for (var y = 0; y < raster.height; y++) {
for(var x = 0; x < raster.width; x++) {
var color = raster.getPixel(x, y);
- colors.push([
+ colors.push(
Math.round(256 * color.red),
Math.round(256 * color.green),
- Math.round(256 * color.blue)
- ]);
+ Math.round(256 * color.blue),
+ 1 // alpha
+ );
var rec = new paper.Path.Rectangle({
point: [x * gridSize, y * gridSize],
size: [gridSize, gridSize],
@@ -29,33 +30,72 @@ Effects = (function() {
}
paper.view.draw();
- return;
- var quant = MMCQ.quantize(colors, 2);
- var palette = quant.palette();
+ // quantize now
+ var quant = new RgbQuant({
+ colors: 2,
+ minHueCols: 2
+ });
+ quant.sample(colors);
+ var palette = quant.palette(true);
+ var newcolors = quant.reduce(colors);
+
+ var newcolors2 = [];
+ for (var x=0; x < gridDim * gridDim; x++) {
+ newcolors2.push([newcolors[(x*4)], newcolors[(x*4)+1], newcolors[(x*4)+2]]);
+ // skip dummy alpha value (x*4)+3
+ }
+ // two primary colors
var pc1 = new paper.Color(palette[0][0] / 256, palette[0][1] / 256, palette[0][2] / 256);
var pc2 = new paper.Color(palette[1][0] / 256, palette[1][1] / 256, palette[1][2] / 256);
- var c1 = new paper.Path.Rectangle({
- point: [0, gridDim * gridSize],
- size: [gridSize * gridDim / 2, 50],
- strokeColor: pc1,
- fillColor: pc1
- });
+ // save for later
+ Session.set(id + ':pc1', pc1);
+ Session.set(id + ':pc2', pc2);
+ Session.set(id + ':colors', colors);
+ Session.set(id + ':newcolors', newcolors2);
+ console.log(newcolors2);
+ }
- var c2 = new paper.Path.Rectangle({
- point: [gridSize * gridDim / 2, gridDim * gridSize],
- size: [gridSize * gridDim / 2, 50],
- strokeColor: pc2,
- fillColor: pc2
- });
+ function colorize(id) {
+ var canvas = document.getElementById('canvas-' + id);
+ paper.setup(canvas);
+
+ // params
+ var gridSize = 6;
+ var gridDim = 9;
+
+ // fetch the new colors
+ var pc1 = new paper.Color(Session.get(id + ':pc1'));
+ var pc2 = new paper.Color(Session.get(id + ':pc2'));
+ var colors = Session.get(id + ':newcolors');
+
+ console.log(colors);
+
+ // sample each color, diff and color new pixels
+ for (var y = 0; y < gridDim; y++) {
+ for(var x = 0; x < gridDim; x++) {
+ var c = colors[(y * gridDim) + x];
+ var nc = new paper.Color(c[0] / 256, c[1] / 256, c[2] / 256);
+
+ // console.log(x * gridSize, y * gridSize, nc);
+
+ var rec = new paper.Path.Rectangle({
+ point: [x * gridSize, y * gridSize],
+ size: [gridSize, gridSize],
+ strokeColor: nc,
+ fillColor: nc
+ });
+ }
+ }
paper.view.draw();
}
return {
- pixelate: pixelate
+ pixelate: pixelate,
+ colorize: colorize
}
})();
diff --git a/client/quantize.js b/client/quantize.js
deleted file mode 100644
index 5a99945..0000000
--- a/client/quantize.js
+++ /dev/null
@@ -1,471 +0,0 @@
-/*!
- * quantize.js Copyright 2008 Nick Rabinowitz.
- * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
- */
-
-// fill out a couple protovis dependencies
-/*!
- * Block below copied from Protovis: http://mbostock.github.com/protovis/
- * Copyright 2010 Stanford Visualization Group
- * Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php
- */
-if (!pv) {
- var pv = {
- map: function(array, f) {
- var o = {};
- return f
- ? array.map(function(d, i) { o.index = i; return f.call(o, d); })
- : array.slice();
- },
- naturalOrder: function(a, b) {
- return (a < b) ? -1 : ((a > b) ? 1 : 0);
- },
- sum: function(array, f) {
- var o = {};
- return array.reduce(f
- ? function(p, d, i) { o.index = i; return p + f.call(o, d); }
- : function(p, d) { return p + d; }, 0);
- },
- max: function(array, f) {
- return Math.max.apply(null, f ? pv.map(array, f) : array);
- }
- }
-}
-
-/**
- * Basic Javascript port of the MMCQ (modified median cut quantization)
- * algorithm from the Leptonica library (http://www.leptonica.com/).
- * Returns a color map you can use to map original pixels to the reduced
- * palette. Still a work in progress.
- *
- * @author Nick Rabinowitz
- * @example
-
-// array of pixels as [R,G,B] arrays
-var myPixels = [[190,197,190], [202,204,200], [207,214,210], [211,214,211], [205,207,207]
- // etc
- ];
-var maxColors = 4;
-
-var cmap = MMCQ.quantize(myPixels, maxColors);
-var newPalette = cmap.palette();
-var newPixels = myPixels.map(function(p) {
- return cmap.map(p);
-});
-
- */
-MMCQ = (function() {
- // private constants
- var sigbits = 5,
- rshift = 8 - sigbits,
- maxIterations = 1000,
- fractByPopulations = 0.75;
-
- // get reduced-space color index for a pixel
- function getColorIndex(r, g, b) {
- return (r << (2 * sigbits)) + (g << sigbits) + b;
- }
-
- // Simple priority queue
- function PQueue(comparator) {
- var contents = [],
- sorted = false;
-
- function sort() {
- contents.sort(comparator);
- sorted = true;
- }
-
- return {
- push: function(o) {
- contents.push(o);
- sorted = false;
- },
- peek: function(index) {
- if (!sorted) sort();
- if (index===undefined) index = contents.length - 1;
- return contents[index];
- },
- pop: function() {
- if (!sorted) sort();
- return contents.pop();
- },
- size: function() {
- return contents.length;
- },
- map: function(f) {
- return contents.map(f);
- },
- debug: function() {
- if (!sorted) sort();
- return contents;
- }
- };
- }
-
- // 3d color space box
- function VBox(r1, r2, g1, g2, b1, b2, histo) {
- var vbox = this;
- vbox.r1 = r1;
- vbox.r2 = r2;
- vbox.g1 = g1;
- vbox.g2 = g2;
- vbox.b1 = b1;
- vbox.b2 = b2;
- vbox.histo = histo;
- }
- VBox.prototype = {
- volume: function(force) {
- var vbox = this;
- if (!vbox._volume || force) {
- vbox._volume = ((vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1));
- }
- return vbox._volume;
- },
- count: function(force) {
- var vbox = this,
- histo = vbox.histo;
- if (!vbox._count_set || force) {
- var npix = 0,
- i, j, k;
- for (i = vbox.r1; i <= vbox.r2; i++) {
- for (j = vbox.g1; j <= vbox.g2; j++) {
- for (k = vbox.b1; k <= vbox.b2; k++) {
- index = getColorIndex(i,j,k);
- npix += (histo[index] || 0);
- }
- }
- }
- vbox._count = npix;
- vbox._count_set = true;
- }
- return vbox._count;
- },
- copy: function() {
- var vbox = this;
- return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo);
- },
- avg: function(force) {
- var vbox = this,
- histo = vbox.histo;
- if (!vbox._avg || force) {
- var ntot = 0,
- mult = 1 << (8 - sigbits),
- rsum = 0,
- gsum = 0,
- bsum = 0,
- hval,
- i, j, k, histoindex;
- for (i = vbox.r1; i <= vbox.r2; i++) {
- for (j = vbox.g1; j <= vbox.g2; j++) {
- for (k = vbox.b1; k <= vbox.b2; k++) {
- histoindex = getColorIndex(i,j,k);
- hval = histo[histoindex] || 0;
- ntot += hval;
- rsum += (hval * (i + 0.5) * mult);
- gsum += (hval * (j + 0.5) * mult);
- bsum += (hval * (k + 0.5) * mult);
- }
- }
- }
- if (ntot) {
- vbox._avg = [~~(rsum/ntot), ~~(gsum/ntot), ~~(bsum/ntot)];
- } else {
- console.log('empty box');
- vbox._avg = [
- ~~(mult * (vbox.r1 + vbox.r2 + 1) / 2),
- ~~(mult * (vbox.g1 + vbox.g2 + 1) / 2),
- ~~(mult * (vbox.b1 + vbox.b2 + 1) / 2)
- ];
- }
- }
- return vbox._avg;
- },
- contains: function(pixel) {
- var vbox = this,
- rval = pixel[0] >> rshift;
- gval = pixel[1] >> rshift;
- bval = pixel[2] >> rshift;
- return (rval >= vbox.r1 && rval <= vbox.r2 &&
- gval >= vbox.g1 && rval <= vbox.g2 &&
- bval >= vbox.b1 && rval <= vbox.b2);
- }
- };
-
- // Color map
- function CMap() {
- this.vboxes = new PQueue(function(a,b) {
- return pv.naturalOrder(
- a.vbox.count()*a.vbox.volume(),
- b.vbox.count()*b.vbox.volume()
- )
- });;
- }
- CMap.prototype = {
- push: function(vbox) {
- this.vboxes.push({
- vbox: vbox,
- color: vbox.avg()
- });
- },
- palette: function() {
- return this.vboxes.map(function(vb) { return vb.color });
- },
- size: function() {
- return this.vboxes.size();
- },
- map: function(color) {
- var vboxes = this.vboxes;
- for (var i=0; i<vboxes.size(); i++) {
- if (vboxes.peek(i).vbox.contains(color)) {
- return vboxes.peek(i).color;
- }
- }
- return this.nearest(color);
- },
- nearest: function(color) {
- var vboxes = this.vboxes,
- d1, d2, pColor;
- for (var i=0; i<vboxes.size(); i++) {
- d2 = Math.sqrt(
- Math.pow(color[0] - vboxes.peek(i).color[0], 2) +
- Math.pow(color[1] - vboxes.peek(i).color[1], 2) +
- Math.pow(color[1] - vboxes.peek(i).color[1], 2)
- );
- if (d2 < d1 || d1 === undefined) {
- d1 = d2;
- pColor = vboxes.peek(i).color;
- }
- }
- return pColor;
- },
- forcebw: function() {
- // XXX: won't work yet
- var vboxes = this.vboxes;
- vboxes.sort(function(a,b) { return pv.naturalOrder(pv.sum(a.color), pv.sum(b.color) )});
-
- // force darkest color to black if everything < 5
- var lowest = vboxes[0].color;
- if (lowest[0] < 5 && lowest[1] < 5 && lowest[2] < 5)
- vboxes[0].color = [0,0,0];
-
- // force lightest color to white if everything > 251
- var idx = vboxes.length-1,
- highest = vboxes[idx].color;
- if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251)
- vboxes[idx].color = [255,255,255];
- }
- };
-
- // histo (1-d array, giving the number of pixels in
- // each quantized region of color space), or null on error
- function getHisto(pixels) {
- var histosize = 1 << (3 * sigbits),
- histo = new Array(histosize),
- index, rval, gval, bval;
- pixels.forEach(function(pixel) {
- rval = pixel[0] >> rshift;
- gval = pixel[1] >> rshift;
- bval = pixel[2] >> rshift;
- index = getColorIndex(rval, gval, bval);
- histo[index] = (histo[index] || 0) + 1;
- });
- return histo;
- }
-
- function vboxFromPixels(pixels, histo) {
- var rmin=1000000, rmax=0,
- gmin=1000000, gmax=0,
- bmin=1000000, bmax=0,
- rval, gval, bval;
- // find min/max
- pixels.forEach(function(pixel) {
- rval = pixel[0] >> rshift;
- gval = pixel[1] >> rshift;
- bval = pixel[2] >> rshift;
- if (rval < rmin) rmin = rval;
- else if (rval > rmax) rmax = rval;
- if (gval < gmin) gmin = gval;
- else if (gval > gmax) gmax = gval;
- if (bval < bmin) bmin = bval;
- else if (bval > bmax) bmax = bval;
- });
- return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
- }
-
- function medianCutApply(histo, vbox) {
- if (!vbox.count()) return;
-
- var rw = vbox.r2 - vbox.r1 + 1,
- gw = vbox.g2 - vbox.g1 + 1,
- bw = vbox.b2 - vbox.b1 + 1,
- maxw = pv.max([rw, gw, bw]);
- // only one pixel, no split
- if (vbox.count() == 1) {
- return [vbox.copy()]
- }
- /* Find the partial sum arrays along the selected axis. */
- var total = 0,
- partialsum = [],
- lookaheadsum = [],
- i, j, k, sum, index;
- if (maxw == rw) {
- for (i = vbox.r1; i <= vbox.r2; i++) {
- sum = 0;
- for (j = vbox.g1; j <= vbox.g2; j++) {
- for (k = vbox.b1; k <= vbox.b2; k++) {
- index = getColorIndex(i,j,k);
- sum += (histo[index] || 0);
- }
- }
- total += sum;
- partialsum[i] = total;
- }
- }
- else if (maxw == gw) {
- for (i = vbox.g1; i <= vbox.g2; i++) {
- sum = 0;
- for (j = vbox.r1; j <= vbox.r2; j++) {
- for (k = vbox.b1; k <= vbox.b2; k++) {
- index = getColorIndex(j,i,k);
- sum += (histo[index] || 0);
- }
- }
- total += sum;
- partialsum[i] = total;
- }
- }
- else { /* maxw == bw */
- for (i = vbox.b1; i <= vbox.b2; i++) {
- sum = 0;
- for (j = vbox.r1; j <= vbox.r2; j++) {
- for (k = vbox.g1; k <= vbox.g2; k++) {
- index = getColorIndex(j,k,i);
- sum += (histo[index] || 0);
- }
- }
- total += sum;
- partialsum[i] = total;
- }
- }
- partialsum.forEach(function(d,i) {
- lookaheadsum[i] = total-d
- });
- function doCut(color) {
- var dim1 = color + '1',
- dim2 = color + '2',
- left, right, vbox1, vbox2, d2, count2=0;
- for (i = vbox[dim1]; i <= vbox[dim2]; i++) {
- if (partialsum[i] > total / 2) {
- vbox1 = vbox.copy();
- vbox2 = vbox.copy();
- left = i - vbox[dim1];
- right = vbox[dim2] - i;
- if (left <= right)
- d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2));
- else d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2));
- // avoid 0-count boxes
- while (!partialsum[d2]) d2++;
- count2 = lookaheadsum[d2];
- while (!count2 && partialsum[d2-1]) count2 = lookaheadsum[--d2];
- // set dimensions
- vbox1[dim2] = d2;
- vbox2[dim1] = vbox1[dim2] + 1;
- console.log('vbox counts:', vbox.count(), vbox1.count(), vbox2.count());
- return [vbox1, vbox2];
- }
- }
-
- }
- // determine the cut planes
- return maxw == rw ? doCut('r') :
- maxw == gw ? doCut('g') :
- doCut('b');
- }
-
- function quantize(pixels, maxcolors) {
- // short-circuit
- if (!pixels.length || maxcolors < 2 || maxcolors > 256) {
- console.log('wrong number of maxcolors');
- return false;
- }
-
- // XXX: check color content and convert to grayscale if insufficient
-
- var histo = getHisto(pixels),
- histosize = 1 << (3 * sigbits);
-
- // check that we aren't below maxcolors already
- var nColors = 0;
- histo.forEach(function() { nColors++ });
- if (nColors <= maxcolors) {
- // XXX: generate the new colors from the histo and return
- }
-
- // get the beginning vbox from the colors
- var vbox = vboxFromPixels(pixels, histo),
- pq = new PQueue(function(a,b) { return pv.naturalOrder(a.count(), b.count()) });
- pq.push(vbox);
-
- // inner function to do the iteration
- function iter(lh, target) {
- var ncolors = 1,
- niters = 0,
- vbox;
- while (niters < maxIterations) {
- vbox = lh.pop();
- if (!vbox.count()) { /* just put it back */
- lh.push(vbox);
- niters++;
- continue;
- }
- // do the cut
- var vboxes = medianCutApply(histo, vbox),
- vbox1 = vboxes[0],
- vbox2 = vboxes[1];
-
- if (!vbox1) {
- console.log("vbox1 not defined; shouldn't happen!");
- return;
- }
- lh.push(vbox1);
- if (vbox2) { /* vbox2 can be null */
- lh.push(vbox2);
- ncolors++;
- }
- if (ncolors >= target) return;
- if (niters++ > maxIterations) {
- console.log("infinite loop; perhaps too few pixels!");
- return;
- }
- }
- }
-
- // first set of colors, sorted by population
- iter(pq, fractByPopulations * maxcolors);
- // console.log(pq.size(), pq.debug().length, pq.debug().slice());
-
- // Re-sort by the product of pixel occupancy times the size in color space.
- var pq2 = new PQueue(function(a,b) {
- return pv.naturalOrder(a.count()*a.volume(), b.count()*b.volume())
- });
- while (pq.size()) {
- pq2.push(pq.pop());
- }
-
- // next set - generate the median cuts using the (npix * vol) sorting.
- iter(pq2, maxcolors - pq2.size());
-
- // calculate the actual colors
- var cmap = new CMap();
- while (pq2.size()) {
- cmap.push(pq2.pop());
- }
-
- return cmap;
- }
-
- return {
- quantize: quantize
- }
-})();
diff --git a/client/rgbquant.js b/client/rgbquant.js
new file mode 100644
index 0000000..953e6f4
--- /dev/null
+++ b/client/rgbquant.js
@@ -0,0 +1,935 @@
+/*
+* Copyright (c) 2015, Leon Sorokin
+* All rights reserved. (MIT Licensed)
+*
+* RgbQuant.js - an image quantization lib
+*/
+
+(function(){
+ function RgbQuant(opts) {
+ opts = opts || {};
+
+ // 1 = by global population, 2 = subregion population threshold
+ this.method = opts.method || 2;
+ // desired final palette size
+ this.colors = opts.colors || 256;
+ // # of highest-frequency colors to start with for palette reduction
+ this.initColors = opts.initColors || 4096;
+ // color-distance threshold for initial reduction pass
+ this.initDist = opts.initDist || 0.01;
+ // subsequent passes threshold
+ this.distIncr = opts.distIncr || 0.005;
+ // palette grouping
+ this.hueGroups = opts.hueGroups || 10;
+ this.satGroups = opts.satGroups || 10;
+ this.lumGroups = opts.lumGroups || 10;
+ // if > 0, enables hues stats and min-color retention per group
+ this.minHueCols = opts.minHueCols || 0;
+ // HueStats instance
+ this.hueStats = this.minHueCols ? new HueStats(this.hueGroups, this.minHueCols) : null;
+
+ // subregion partitioning box size
+ this.boxSize = opts.boxSize || [64,64];
+ // number of same pixels required within box for histogram inclusion
+ this.boxPxls = opts.boxPxls || 2;
+ // palette locked indicator
+ this.palLocked = false;
+ // palette sort order
+// this.sortPal = ['hue-','lum-','sat-'];
+
+ // dithering/error diffusion kernel name
+ this.dithKern = opts.dithKern || null;
+ // dither serpentine pattern
+ this.dithSerp = opts.dithSerp || false;
+ // minimum color difference (0-1) needed to dither
+ this.dithDelta = opts.dithDelta || 0;
+
+ // accumulated histogram
+ this.histogram = {};
+ // palette - rgb triplets
+ this.idxrgb = opts.palette ? opts.palette.slice(0) : [];
+ // palette - int32 vals
+ this.idxi32 = [];
+ // reverse lookup {i32:idx}
+ this.i32idx = {};
+ // {i32:rgb}
+ this.i32rgb = {};
+ // enable color caching (also incurs overhead of cache misses and cache building)
+ this.useCache = opts.useCache !== false;
+ // min color occurance count needed to qualify for caching
+ this.cacheFreq = opts.cacheFreq || 10;
+ // allows pre-defined palettes to be re-indexed (enabling palette compacting and sorting)
+ this.reIndex = opts.reIndex || this.idxrgb.length == 0;
+ // selection of color-distance equation
+ this.colorDist = opts.colorDist == "manhattan" ? distManhattan : distEuclidean;
+
+ // if pre-defined palette, build lookups
+ if (this.idxrgb.length > 0) {
+ var self = this;
+ this.idxrgb.forEach(function(rgb, i) {
+ var i32 = (
+ (255 << 24) | // alpha
+ (rgb[2] << 16) | // blue
+ (rgb[1] << 8) | // green
+ rgb[0] // red
+ ) >>> 0;
+
+ self.idxi32[i] = i32;
+ self.i32idx[i32] = i;
+ self.i32rgb[i32] = rgb;
+ });
+ }
+ }
+
+ // gathers histogram info
+ RgbQuant.prototype.sample = function sample(img, width) {
+ if (this.palLocked)
+ throw "Cannot sample additional images, palette already assembled.";
+
+ var data = getImageData(img, width);
+
+ switch (this.method) {
+ case 1: this.colorStats1D(data.buf32); break;
+ case 2: this.colorStats2D(data.buf32, data.width); break;
+ }
+ };
+
+ // image quantizer
+ // todo: memoize colors here also
+ // @retType: 1 - Uint8Array (default), 2 - Indexed array, 3 - Match @img type (unimplemented, todo)
+ RgbQuant.prototype.reduce = function reduce(img, retType, dithKern, dithSerp) {
+ if (!this.palLocked)
+ this.buildPal();
+
+ dithKern = dithKern || this.dithKern;
+ dithSerp = typeof dithSerp != "undefined" ? dithSerp : this.dithSerp;
+
+ retType = retType || 1;
+
+ // reduce w/dither
+ if (dithKern)
+ var out32 = this.dither(img, dithKern, dithSerp);
+ else {
+ var data = getImageData(img),
+ buf32 = data.buf32,
+ len = buf32.length,
+ out32 = new Uint32Array(len);
+
+ for (var i = 0; i < len; i++) {
+ var i32 = buf32[i];
+ out32[i] = this.nearestColor(i32);
+ }
+ }
+
+ if (retType == 1)
+ return new Uint8Array(out32.buffer);
+
+ if (retType == 2) {
+ var out = [],
+ len = out32.length;
+
+ for (var i = 0; i < len; i++) {
+ var i32 = out32[i];
+ out[i] = this.i32idx[i32];
+ }
+
+ return out;
+ }
+ };
+
+ // adapted from http://jsbin.com/iXofIji/2/edit by PAEz
+ RgbQuant.prototype.dither = function(img, kernel, serpentine) {
+ // http://www.tannerhelland.com/4660/dithering-eleven-algorithms-source-code/
+ var kernels = {
+ FloydSteinberg: [
+ [7 / 16, 1, 0],
+ [3 / 16, -1, 1],
+ [5 / 16, 0, 1],
+ [1 / 16, 1, 1]
+ ],
+ FalseFloydSteinberg: [
+ [3 / 8, 1, 0],
+ [3 / 8, 0, 1],
+ [2 / 8, 1, 1]
+ ],
+ Stucki: [
+ [8 / 42, 1, 0],
+ [4 / 42, 2, 0],
+ [2 / 42, -2, 1],
+ [4 / 42, -1, 1],
+ [8 / 42, 0, 1],
+ [4 / 42, 1, 1],
+ [2 / 42, 2, 1],
+ [1 / 42, -2, 2],
+ [2 / 42, -1, 2],
+ [4 / 42, 0, 2],
+ [2 / 42, 1, 2],
+ [1 / 42, 2, 2]
+ ],
+ Atkinson: [
+ [1 / 8, 1, 0],
+ [1 / 8, 2, 0],
+ [1 / 8, -1, 1],
+ [1 / 8, 0, 1],
+ [1 / 8, 1, 1],
+ [1 / 8, 0, 2]
+ ],
+ Jarvis: [ // Jarvis, Judice, and Ninke / JJN?
+ [7 / 48, 1, 0],
+ [5 / 48, 2, 0],
+ [3 / 48, -2, 1],
+ [5 / 48, -1, 1],
+ [7 / 48, 0, 1],
+ [5 / 48, 1, 1],
+ [3 / 48, 2, 1],
+ [1 / 48, -2, 2],
+ [3 / 48, -1, 2],
+ [5 / 48, 0, 2],
+ [3 / 48, 1, 2],
+ [1 / 48, 2, 2]
+ ],
+ Burkes: [
+ [8 / 32, 1, 0],
+ [4 / 32, 2, 0],
+ [2 / 32, -2, 1],
+ [4 / 32, -1, 1],
+ [8 / 32, 0, 1],
+ [4 / 32, 1, 1],
+ [2 / 32, 2, 1],
+ ],
+ Sierra: [
+ [5 / 32, 1, 0],
+ [3 / 32, 2, 0],
+ [2 / 32, -2, 1],
+ [4 / 32, -1, 1],
+ [5 / 32, 0, 1],
+ [4 / 32, 1, 1],
+ [2 / 32, 2, 1],
+ [2 / 32, -1, 2],
+ [3 / 32, 0, 2],
+ [2 / 32, 1, 2],
+ ],
+ TwoSierra: [
+ [4 / 16, 1, 0],
+ [3 / 16, 2, 0],
+ [1 / 16, -2, 1],
+ [2 / 16, -1, 1],
+ [3 / 16, 0, 1],
+ [2 / 16, 1, 1],
+ [1 / 16, 2, 1],
+ ],
+ SierraLite: [
+ [2 / 4, 1, 0],
+ [1 / 4, -1, 1],
+ [1 / 4, 0, 1],
+ ],
+ };
+
+ if (!kernel || !kernels[kernel]) {
+ throw 'Unknown dithering kernel: ' + kernel;
+ }
+
+ var ds = kernels[kernel];
+
+ var data = getImageData(img),
+// buf8 = data.buf8,
+ buf32 = data.buf32,
+ width = data.width,
+ height = data.height,
+ len = buf32.length;
+
+ var dir = serpentine ? -1 : 1;
+
+ for (var y = 0; y < height; y++) {
+ if (serpentine)
+ dir = dir * -1;
+
+ var lni = y * width;
+
+ for (var x = (dir == 1 ? 0 : width - 1), xend = (dir == 1 ? width : 0); x !== xend; x += dir) {
+ // Image pixel
+ var idx = lni + x,
+ i32 = buf32[idx],
+ r1 = (i32 & 0xff),
+ g1 = (i32 & 0xff00) >> 8,
+ b1 = (i32 & 0xff0000) >> 16;
+
+ // Reduced pixel
+ var i32x = this.nearestColor(i32),
+ r2 = (i32x & 0xff),
+ g2 = (i32x & 0xff00) >> 8,
+ b2 = (i32x & 0xff0000) >> 16;
+
+ buf32[idx] =
+ (255 << 24) | // alpha
+ (b2 << 16) | // blue
+ (g2 << 8) | // green
+ r2;
+
+ // dithering strength
+ if (this.dithDelta) {
+ var dist = this.colorDist([r1, g1, b1], [r2, g2, b2]);
+ if (dist < this.dithDelta)
+ continue;
+ }
+
+ // Component distance
+ var er = r1 - r2,
+ eg = g1 - g2,
+ eb = b1 - b2;
+
+ for (var i = (dir == 1 ? 0 : ds.length - 1), end = (dir == 1 ? ds.length : 0); i !== end; i += dir) {
+ var x1 = ds[i][1] * dir,
+ y1 = ds[i][2];
+
+ var lni2 = y1 * width;
+
+ if (x1 + x >= 0 && x1 + x < width && y1 + y >= 0 && y1 + y < height) {
+ var d = ds[i][0];
+ var idx2 = idx + (lni2 + x1);
+
+ var r3 = (buf32[idx2] & 0xff),
+ g3 = (buf32[idx2] & 0xff00) >> 8,
+ b3 = (buf32[idx2] & 0xff0000) >> 16;
+
+ var r4 = Math.max(0, Math.min(255, r3 + er * d)),
+ g4 = Math.max(0, Math.min(255, g3 + eg * d)),
+ b4 = Math.max(0, Math.min(255, b3 + eb * d));
+
+ buf32[idx2] =
+ (255 << 24) | // alpha
+ (b4 << 16) | // blue
+ (g4 << 8) | // green
+ r4; // red
+ }
+ }
+ }
+ }
+
+ return buf32;
+ };
+
+ // reduces histogram to palette, remaps & memoizes reduced colors
+ RgbQuant.prototype.buildPal = function buildPal(noSort) {
+ if (this.palLocked || this.idxrgb.length > 0 && this.idxrgb.length <= this.colors) return;
+
+ var histG = this.histogram,
+ sorted = sortedHashKeys(histG, true);
+
+ if (sorted.length == 0)
+ throw "Nothing has been sampled, palette cannot be built.";
+
+ switch (this.method) {
+ case 1:
+ var cols = this.initColors,
+ last = sorted[cols - 1],
+ freq = histG[last];
+
+ var idxi32 = sorted.slice(0, cols);
+
+ // add any cut off colors with same freq as last
+ var pos = cols, len = sorted.length;
+ while (pos < len && histG[sorted[pos]] == freq)
+ idxi32.push(sorted[pos++]);
+
+ // inject min huegroup colors
+ if (this.hueStats)
+ this.hueStats.inject(idxi32);
+
+ break;
+ case 2:
+ var idxi32 = sorted;
+ break;
+ }
+
+ // int32-ify values
+ idxi32 = idxi32.map(function(v){return +v;});
+
+ this.reducePal(idxi32);
+
+ if (!noSort && this.reIndex)
+ this.sortPal();
+
+ // build cache of top histogram colors
+ if (this.useCache)
+ this.cacheHistogram(idxi32);
+
+ this.palLocked = true;
+ };
+
+ RgbQuant.prototype.palette = function palette(tuples, noSort) {
+ this.buildPal(noSort);
+ return tuples ? this.idxrgb : new Uint8Array((new Uint32Array(this.idxi32)).buffer);
+ };
+
+ RgbQuant.prototype.prunePal = function prunePal(keep) {
+ var i32;
+
+ for (var j = 0; j < this.idxrgb.length; j++) {
+ if (!keep[j]) {
+ i32 = this.idxi32[j];
+ this.idxrgb[j] = null;
+ this.idxi32[j] = null;
+ delete this.i32idx[i32];
+ }
+ }
+
+ // compact
+ if (this.reIndex) {
+ var idxrgb = [],
+ idxi32 = [],
+ i32idx = {};
+
+ for (var j = 0, i = 0; j < this.idxrgb.length; j++) {
+ if (this.idxrgb[j]) {
+ i32 = this.idxi32[j];
+ idxrgb[i] = this.idxrgb[j];
+ i32idx[i32] = i;
+ idxi32[i] = i32;
+ i++;
+ }
+ }
+
+ this.idxrgb = idxrgb;
+ this.idxi32 = idxi32;
+ this.i32idx = i32idx;
+ }
+ };
+
+ // reduces similar colors from an importance-sorted Uint32 rgba array
+ RgbQuant.prototype.reducePal = function reducePal(idxi32) {
+ // if pre-defined palette's length exceeds target
+ if (this.idxrgb.length > this.colors) {
+ // quantize histogram to existing palette
+ var len = idxi32.length, keep = {}, uniques = 0, idx, pruned = false;
+
+ for (var i = 0; i < len; i++) {
+ // palette length reached, unset all remaining colors (sparse palette)
+ if (uniques == this.colors && !pruned) {
+ this.prunePal(keep);
+ pruned = true;
+ }
+
+ idx = this.nearestIndex(idxi32[i]);
+
+ if (uniques < this.colors && !keep[idx]) {
+ keep[idx] = true;
+ uniques++;
+ }
+ }
+
+ if (!pruned) {
+ this.prunePal(keep);
+ pruned = true;
+ }
+ }
+ // reduce histogram to create initial palette
+ else {
+ // build full rgb palette
+ var idxrgb = idxi32.map(function(i32) {
+ return [
+ (i32 & 0xff),
+ (i32 & 0xff00) >> 8,
+ (i32 & 0xff0000) >> 16,
+ ];
+ });
+
+ var len = idxrgb.length,
+ palLen = len,
+ thold = this.initDist;
+
+ // palette already at or below desired length
+ if (palLen > this.colors) {
+ while (palLen > this.colors) {
+ var memDist = [];
+
+ // iterate palette
+ for (var i = 0; i < len; i++) {
+ var pxi = idxrgb[i], i32i = idxi32[i];
+ if (!pxi) continue;
+
+ for (var j = i + 1; j < len; j++) {
+ var pxj = idxrgb[j], i32j = idxi32[j];
+ if (!pxj) continue;
+
+ var dist = this.colorDist(pxi, pxj);
+
+ if (dist < thold) {
+ // store index,rgb,dist
+ memDist.push([j, pxj, i32j, dist]);
+
+ // kill squashed value
+ delete(idxrgb[j]);
+ palLen--;
+ }
+ }
+ }
+
+ // palette reduction pass
+ // console.log("palette length: " + palLen);
+
+ // if palette is still much larger than target, increment by larger initDist
+ thold += (palLen > this.colors * 3) ? this.initDist : this.distIncr;
+ }
+
+ // if palette is over-reduced, re-add removed colors with largest distances from last round
+ if (palLen < this.colors) {
+ // sort descending
+ sort.call(memDist, function(a,b) {
+ return b[3] - a[3];
+ });
+
+ var k = 0;
+ while (palLen < this.colors) {
+ // re-inject rgb into final palette
+ idxrgb[memDist[k][0]] = memDist[k][1];
+
+ palLen++;
+ k++;
+ }
+ }
+ }
+
+ var len = idxrgb.length;
+ for (var i = 0; i < len; i++) {
+ if (!idxrgb[i]) continue;
+
+ this.idxrgb.push(idxrgb[i]);
+ this.idxi32.push(idxi32[i]);
+
+ this.i32idx[idxi32[i]] = this.idxi32.length - 1;
+ this.i32rgb[idxi32[i]] = idxrgb[i];
+ }
+ }
+ };
+
+ // global top-population
+ RgbQuant.prototype.colorStats1D = function colorStats1D(buf32) {
+ var histG = this.histogram,
+ num = 0, col,
+ len = buf32.length;
+
+ for (var i = 0; i < len; i++) {
+ col = buf32[i];
+
+ // skip transparent
+ if ((col & 0xff000000) >> 24 == 0) continue;
+
+ // collect hue stats
+ if (this.hueStats)
+ this.hueStats.check(col);
+
+ if (col in histG)
+ histG[col]++;
+ else
+ histG[col] = 1;
+ }
+ };
+
+ // population threshold within subregions
+ // FIXME: this can over-reduce (few/no colors same?), need a way to keep
+ // important colors that dont ever reach local thresholds (gradients?)
+ RgbQuant.prototype.colorStats2D = function colorStats2D(buf32, width) {
+ var boxW = this.boxSize[0],
+ boxH = this.boxSize[1],
+ area = boxW * boxH,
+ boxes = makeBoxes(width, buf32.length / width, boxW, boxH),
+ histG = this.histogram,
+ self = this;
+
+ boxes.forEach(function(box) {
+ var effc = Math.max(Math.round((box.w * box.h) / area) * self.boxPxls, 2),
+ histL = {}, col;
+
+ iterBox(box, width, function(i) {
+ col = buf32[i];
+
+ // skip transparent
+ if ((col & 0xff000000) >> 24 == 0) return;
+
+ // collect hue stats
+ if (self.hueStats)
+ self.hueStats.check(col);
+
+ if (col in histG)
+ histG[col]++;
+ else if (col in histL) {
+ if (++histL[col] >= effc)
+ histG[col] = histL[col];
+ }
+ else
+ histL[col] = 1;
+ });
+ });
+
+ if (this.hueStats)
+ this.hueStats.inject(histG);
+ };
+
+ // TODO: group very low lum and very high lum colors
+ // TODO: pass custom sort order
+ RgbQuant.prototype.sortPal = function sortPal() {
+ var self = this;
+
+ this.idxi32.sort(function(a,b) {
+ var idxA = self.i32idx[a],
+ idxB = self.i32idx[b],
+ rgbA = self.idxrgb[idxA],
+ rgbB = self.idxrgb[idxB];
+
+ var hslA = rgb2hsl(rgbA[0],rgbA[1],rgbA[2]),
+ hslB = rgb2hsl(rgbB[0],rgbB[1],rgbB[2]);
+
+ // sort all grays + whites together
+ var hueA = (rgbA[0] == rgbA[1] && rgbA[1] == rgbA[2]) ? -1 : hueGroup(hslA.h, self.hueGroups);
+ var hueB = (rgbB[0] == rgbB[1] && rgbB[1] == rgbB[2]) ? -1 : hueGroup(hslB.h, self.hueGroups);
+
+ var hueDiff = hueB - hueA;
+ if (hueDiff) return -hueDiff;
+
+ var lumDiff = lumGroup(+hslB.l.toFixed(2)) - lumGroup(+hslA.l.toFixed(2));
+ if (lumDiff) return -lumDiff;
+
+ var satDiff = satGroup(+hslB.s.toFixed(2)) - satGroup(+hslA.s.toFixed(2));
+ if (satDiff) return -satDiff;
+ });
+
+ // sync idxrgb & i32idx
+ this.idxi32.forEach(function(i32, i) {
+ self.idxrgb[i] = self.i32rgb[i32];
+ self.i32idx[i32] = i;
+ });
+ };
+
+ // TOTRY: use HUSL - http://boronine.com/husl/
+ RgbQuant.prototype.nearestColor = function nearestColor(i32) {
+ var idx = this.nearestIndex(i32);
+ return idx === null ? 0 : this.idxi32[idx];
+ };
+
+ // TOTRY: use HUSL - http://boronine.com/husl/
+ RgbQuant.prototype.nearestIndex = function nearestIndex(i32) {
+ // alpha 0 returns null index
+ if ((i32 & 0xff000000) >> 24 == 0)
+ return null;
+
+ if (this.useCache && (""+i32) in this.i32idx)
+ return this.i32idx[i32];
+
+ var min = 1000,
+ idx,
+ rgb = [
+ (i32 & 0xff),
+ (i32 & 0xff00) >> 8,
+ (i32 & 0xff0000) >> 16,
+ ],
+ len = this.idxrgb.length;
+
+ for (var i = 0; i < len; i++) {
+ if (!this.idxrgb[i]) continue; // sparse palettes
+
+ var dist = this.colorDist(rgb, this.idxrgb[i]);
+
+ if (dist < min) {
+ min = dist;
+ idx = i;
+ }
+ }
+
+ return idx;
+ };
+
+ RgbQuant.prototype.cacheHistogram = function cacheHistogram(idxi32) {
+ for (var i = 0, i32 = idxi32[i]; i < idxi32.length && this.histogram[i32] >= this.cacheFreq; i32 = idxi32[i++])
+ this.i32idx[i32] = this.nearestIndex(i32);
+ };
+
+ function HueStats(numGroups, minCols) {
+ this.numGroups = numGroups;
+ this.minCols = minCols;
+ this.stats = {};
+
+ for (var i = -1; i < numGroups; i++)
+ this.stats[i] = {num: 0, cols: []};
+
+ this.groupsFull = 0;
+ }
+
+ HueStats.prototype.check = function checkHue(i32) {
+ if (this.groupsFull == this.numGroups + 1)
+ this.check = function() {return;};
+
+ var r = (i32 & 0xff),
+ g = (i32 & 0xff00) >> 8,
+ b = (i32 & 0xff0000) >> 16,
+ hg = (r == g && g == b) ? -1 : hueGroup(rgb2hsl(r,g,b).h, this.numGroups),
+ gr = this.stats[hg],
+ min = this.minCols;
+
+ gr.num++;
+
+ if (gr.num > min)
+ return;
+ if (gr.num == min)
+ this.groupsFull++;
+
+ if (gr.num <= min)
+ this.stats[hg].cols.push(i32);
+ };
+
+ HueStats.prototype.inject = function injectHues(histG) {
+ for (var i = -1; i < this.numGroups; i++) {
+ if (this.stats[i].num <= this.minCols) {
+ switch (typeOf(histG)) {
+ case "Array":
+ this.stats[i].cols.forEach(function(col){
+ if (histG.indexOf(col) == -1)
+ histG.push(col);
+ });
+ break;
+ case "Object":
+ this.stats[i].cols.forEach(function(col){
+ if (!histG[col])
+ histG[col] = 1;
+ else
+ histG[col]++;
+ });
+ break;
+ }
+ }
+ }
+ };
+
+ // Rec. 709 (sRGB) luma coef
+ var Pr = .2126,
+ Pg = .7152,
+ Pb = .0722;
+
+ // http://alienryderflex.com/hsp.html
+ function rgb2lum(r,g,b) {
+ return Math.sqrt(
+ Pr * r*r +
+ Pg * g*g +
+ Pb * b*b
+ );
+ }
+
+ var rd = 255,
+ gd = 255,
+ bd = 255;
+
+ var euclMax = Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd);
+ // perceptual Euclidean color distance
+ function distEuclidean(rgb0, rgb1) {
+ var rd = rgb1[0]-rgb0[0],
+ gd = rgb1[1]-rgb0[1],
+ bd = rgb1[2]-rgb0[2];
+
+ return Math.sqrt(Pr*rd*rd + Pg*gd*gd + Pb*bd*bd) / euclMax;
+ }
+
+ var manhMax = Pr*rd + Pg*gd + Pb*bd;
+ // perceptual Manhattan color distance
+ function distManhattan(rgb0, rgb1) {
+ var rd = Math.abs(rgb1[0]-rgb0[0]),
+ gd = Math.abs(rgb1[1]-rgb0[1]),
+ bd = Math.abs(rgb1[2]-rgb0[2]);
+
+ return (Pr*rd + Pg*gd + Pb*bd) / manhMax;
+ }
+
+ // http://rgb2hsl.nichabi.com/javascript-function.php
+ function rgb2hsl(r, g, b) {
+ var max, min, h, s, l, d;
+ r /= 255;
+ g /= 255;
+ b /= 255;
+ max = Math.max(r, g, b);
+ min = Math.min(r, g, b);
+ l = (max + min) / 2;
+ if (max == min) {
+ h = s = 0;
+ } else {
+ d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch (max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break
+ }
+ h /= 6;
+ }
+// h = Math.floor(h * 360)
+// s = Math.floor(s * 100)
+// l = Math.floor(l * 100)
+ return {
+ h: h,
+ s: s,
+ l: rgb2lum(r,g,b),
+ };
+ }
+
+ function hueGroup(hue, segs) {
+ var seg = 1/segs,
+ haf = seg/2;
+
+ if (hue >= 1 - haf || hue <= haf)
+ return 0;
+
+ for (var i = 1; i < segs; i++) {
+ var mid = i*seg;
+ if (hue >= mid - haf && hue <= mid + haf)
+ return i;
+ }
+ }
+
+ function satGroup(sat) {
+ return sat;
+ }
+
+ function lumGroup(lum) {
+ return lum;
+ }
+
+ function typeOf(val) {
+ return Object.prototype.toString.call(val).slice(8,-1);
+ }
+
+ var sort = isArrSortStable() ? Array.prototype.sort : stableSort;
+
+ // must be used via stableSort.call(arr, fn)
+ function stableSort(fn) {
+ var type = typeOf(this[0]);
+
+ if (type == "Number" || type == "String") {
+ var ord = {}, len = this.length, val;
+
+ for (var i = 0; i < len; i++) {
+ val = this[i];
+ if (ord[val] || ord[val] === 0) continue;
+ ord[val] = i;
+ }
+
+ return this.sort(function(a,b) {
+ return fn(a,b) || ord[a] - ord[b];
+ });
+ }
+ else {
+ var ord = this.map(function(v){return v});
+
+ return this.sort(function(a,b) {
+ return fn(a,b) || ord.indexOf(a) - ord.indexOf(b);
+ });
+ }
+ }
+
+ // test if js engine's Array#sort implementation is stable
+ function isArrSortStable() {
+ var str = "abcdefghijklmnopqrstuvwxyz";
+
+ return "xyzvwtursopqmnklhijfgdeabc" == str.split("").sort(function(a,b) {
+ return ~~(str.indexOf(b)/2.3) - ~~(str.indexOf(a)/2.3);
+ }).join("");
+ }
+
+ // returns uniform pixel data from various img
+ // TODO?: if array is passed, createimagedata, createlement canvas? take a pxlen?
+ function getImageData(img, width) {
+ var can, ctx, imgd, buf8, buf32, height;
+
+ switch (typeOf(img)) {
+ case "HTMLImageElement":
+ can = document.createElement("canvas");
+ can.width = img.naturalWidth;
+ can.height = img.naturalHeight;
+ ctx = can.getContext("2d");
+ ctx.drawImage(img,0,0);
+ case "Canvas":
+ case "HTMLCanvasElement":
+ can = can || img;
+ ctx = ctx || can.getContext("2d");
+ case "CanvasRenderingContext2D":
+ ctx = ctx || img;
+ can = can || ctx.canvas;
+ imgd = ctx.getImageData(0, 0, can.width, can.height);
+ case "ImageData":
+ imgd = imgd || img;
+ width = imgd.width;
+ if (typeOf(imgd.data) == "CanvasPixelArray")
+ buf8 = new Uint8Array(imgd.data);
+ else
+ buf8 = imgd.data;
+ case "Array":
+ case "CanvasPixelArray":
+ buf8 = buf8 || new Uint8Array(img);
+ case "Uint8Array":
+ case "Uint8ClampedArray":
+ buf8 = buf8 || img;
+ buf32 = new Uint32Array(buf8.buffer);
+ case "Uint32Array":
+ buf32 = buf32 || img;
+ buf8 = buf8 || new Uint8Array(buf32.buffer);
+ width = width || buf32.length;
+ height = buf32.length / width;
+ }
+
+ return {
+ can: can,
+ ctx: ctx,
+ imgd: imgd,
+ buf8: buf8,
+ buf32: buf32,
+ width: width,
+ height: height,
+ };
+ }
+
+ // partitions a rect of wid x hgt into
+ // array of bboxes of w0 x h0 (or less)
+ function makeBoxes(wid, hgt, w0, h0) {
+ var wnum = ~~(wid/w0), wrem = wid%w0,
+ hnum = ~~(hgt/h0), hrem = hgt%h0,
+ xend = wid-wrem, yend = hgt-hrem;
+
+ var bxs = [];
+ for (var y = 0; y < hgt; y += h0)
+ for (var x = 0; x < wid; x += w0)
+ bxs.push({x:x, y:y, w:(x==xend?wrem:w0), h:(y==yend?hrem:h0)});
+
+ return bxs;
+ }
+
+ // iterates @bbox within a parent rect of width @wid; calls @fn, passing index within parent
+ function iterBox(bbox, wid, fn) {
+ var b = bbox,
+ i0 = b.y * wid + b.x,
+ i1 = (b.y + b.h - 1) * wid + (b.x + b.w - 1),
+ cnt = 0, incr = wid - b.w + 1, i = i0;
+
+ do {
+ fn.call(this, i);
+ i += (++cnt % b.w == 0) ? incr : 1;
+ } while (i <= i1);
+ }
+
+ // returns array of hash keys sorted by their values
+ function sortedHashKeys(obj, desc) {
+ var keys = [];
+
+ for (var key in obj)
+ keys.push(key);
+
+ return sort.call(keys, function(a,b) {
+ return desc ? obj[b] - obj[a] : obj[a] - obj[b];
+ });
+ }
+
+ // expose
+ this.RgbQuant = RgbQuant;
+
+ // expose to commonJS
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = RgbQuant;
+ }
+
+}).call(this);
diff --git a/client/templates/photos.html b/client/templates/photos.html
index 41873bc..7a8f8c5 100644
--- a/client/templates/photos.html
+++ b/client/templates/photos.html
@@ -8,6 +8,7 @@
{{/each}}
</div>
<button class="pixelate btn btn-default">Pixelate</button>
+ <button class="color btn btn-default">Color</button>
</div>
</template>