UNPKG

@discord-user-card/core

Version:

The core behind the Discord User Card project.

401 lines 13.3 kB
/** * Modified version of the MMCQ (modified median cut quantization) algorithm * from the Leptonica library (http://www.leptonica.com/). */ const protovis = { naturalOrder(a, b) { return a < b ? -1 : a > b ? 1 : 0; }, max(array) { return Math.max.apply(null, array); }, }; const MMCQ = (() => { // private constants const sigbits = 5; const rshift = 8 - sigbits; const maxIterations = 1000; const fractByPopulations = 0.75; // get the color index for a pixel function getColorIndex(r, g, b) { return (r << (2 * sigbits)) + (g << sigbits) + b; } // Simple priority queue class PQueue { comparator; contents = []; sorted = false; constructor(comparator) { this.comparator = comparator; } sort() { this.contents.sort(this.comparator); this.sorted = true; } push(o) { this.contents.push(o); this.sorted = false; } peek(index) { if (!this.sorted) this.sort(); if (index === undefined) index = this.contents.length - 1; return this.contents[index]; } pop() { if (!this.sorted) this.sort(); return this.contents.pop(); } size() { return this.contents.length; } map(f) { return this.contents.map(f); } debug() { if (!this.sorted) this.sort(); return this.contents; } } // 3d color space box class VBox { r1; r2; g1; g2; b1; b2; histo; _avg; _count_set = false; _count; _volume; constructor(r1, r2, g1, g2, b1, b2, histo) { this.r1 = r1; this.r2 = r2; this.g1 = g1; this.g2 = g2; this.b1 = b1; this.b2 = b2; this.histo = histo; } volume(force) { if (!this._volume || force) { this._volume = (this.r2 - this.r1 + 1) * (this.g2 - this.g1 + 1) * (this.b2 - this.b1 + 1); } return this._volume; } count(force) { if (!this._count_set || force) { let npix = 0; let i; let j; let k; for (i = this.r1; i <= this.r2; i++) { for (j = this.g1; j <= this.g2; j++) { for (k = this.b1; k <= this.b2; k++) { const index = getColorIndex(i, j, k); npix += this.histo[index] || 0; } } } this._count = npix; this._count_set = true; } return this._count; } copy() { return new VBox(this.r1, this.r2, this.g1, this.g2, this.b1, this.b2, this.histo); } avg(force) { if (!this._avg || force) { const mult = 1 << (8 - sigbits); let ntot = 0; let rsum = 0; let gsum = 0; let bsum = 0; for (let i = this.r1; i <= this.r2; i++) { for (let j = this.g1; j <= this.g2; j++) { for (let k = this.b1; k <= this.b2; k++) { const histoindex = getColorIndex(i, j, k); const hval = this.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) { this._avg = [~~(rsum / ntot), ~~(gsum / ntot), ~~(bsum / ntot)]; } else { this._avg = [ ~~((mult * (this.r1 + this.r2 + 1)) / 2), ~~((mult * (this.g1 + this.g2 + 1)) / 2), ~~((mult * (this.b1 + this.b2 + 1)) / 2), ]; } } return this._avg; } contains(pixel) { const rval = pixel[0] >> rshift; const gval = pixel[1] >> rshift; const bval = pixel[2] >> rshift; return (rval >= this.r1 && rval <= this.r2 && gval >= this.g1 && gval <= this.g2 && bval >= this.b1 && bval <= this.b2); } } // Color map class CMap { vboxes; constructor() { this.vboxes = new PQueue((a, b) => protovis.naturalOrder(a.vbox.count() * a.vbox.volume(), b.vbox.count() * b.vbox.volume())); } push(vbox) { this.vboxes.push({ vbox, color: vbox.avg(), }); } palette() { return this.vboxes.map(vb => vb.color); } size() { return this.vboxes.size(); } map(color) { const vboxes = this.vboxes; for (let i = 0; i < vboxes.size(); i++) { if (vboxes.peek(i).vbox.contains(color)) { return vboxes.peek(i).color; } } return this.nearest(color); } nearest(color) { const vboxes = this.vboxes; let d1; let d2; let pColor; for (let i = 0; i < vboxes.size(); i++) { d2 = Math.sqrt((color[0] - vboxes.peek(i).color[0]) ** 2 + (color[1] - vboxes.peek(i).color[1]) ** 2 + (color[2] - vboxes.peek(i).color[2]) ** 2); if (d1 === undefined || d2 < d1) { d1 = d2; pColor = vboxes.peek(i).color; } } return pColor; } } // histo (1-d array, giving the number of pixels in // each quantized region of color space), or null on error function getHisto(pixels) { const histo = Array.from({ length: 1 << (3 * sigbits) }); pixels.forEach((pixel) => { const rval = pixel[0] >> rshift; const gval = pixel[1] >> rshift; const bval = pixel[2] >> rshift; const index = getColorIndex(rval, gval, bval); histo[index] = (histo[index] || 0) + 1; }); return histo; } function vboxFromPixels(pixels, histo) { let rmin = 1e6; let rmax = 0; let gmin = 1e6; let gmax = 0; let bmin = 1e6; let bmax = 0; // find min/max pixels.forEach((pixel) => { const rval = pixel[0] >> rshift; const gval = pixel[1] >> rshift; const 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; const rw = vbox.r2 - vbox.r1 + 1; const gw = vbox.g2 - vbox.g1 + 1; const bw = vbox.b2 - vbox.b1 + 1; const maxw = protovis.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. */ let total = 0; const partialsum = []; const lookaheadsum = []; let i; let j; let k; let sum; let 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((d, i) => { lookaheadsum[i] = total - d; }); function doCut(color) { const dim1 = `${color}1`; const dim2 = `${color}2`; let left; let right; let vbox1; let vbox2; let d2; let 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; 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) return false; const histo = getHisto(pixels); // get the beginning vbox from the colors const vbox = vboxFromPixels(pixels, histo); const pq = new PQueue((a, b) => protovis.naturalOrder(a.count(), b.count())); pq.push(vbox); // inner function to do the iteration function iter(lh, target) { let ncolors = 1; let niters = 0; let vbox; while (niters < maxIterations) { vbox = lh.pop(); if (!vbox.count()) { /* just put it back */ lh.push(vbox); niters++; continue; } // do the cut const vboxes = medianCutApply(histo, vbox); const vbox1 = vboxes?.[0]; const vbox2 = vboxes?.[1]; if (!vbox1) return; lh.push(vbox1); if (vbox2) { /* vbox2 can be null */ lh.push(vbox2); ncolors++; } if (ncolors >= target) return; if (niters++ > maxIterations) return; } } // first set of colors, sorted by population iter(pq, fractByPopulations * maxcolors); // Re-sort by the product of pixel occupancy times the size in color space. const pq2 = new PQueue((a, b) => protovis.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 const cmap = new CMap(); while (pq2.size()) cmap.push(pq2.pop()); return cmap; } return { quantize, }; })(); export default MMCQ.quantize; //# sourceMappingURL=quantize.js.map