Repository: FormidableLabs/react-image-palette Branch: master Commit: 4b496e67cb9d Files: 37 Total size: 54.5 KB Directory structure: gitextract_w4jasbnq/ ├── .babelrc ├── .flowconfig ├── .gitignore ├── .prettierignore ├── .travis.yml ├── .yarnclean ├── LICENSE.txt ├── README.md ├── demo/ │ ├── .gitignore │ ├── package.json │ ├── public/ │ │ ├── index.html │ │ └── manifest.json │ └── src/ │ ├── App.js │ ├── index.js │ └── main.css ├── karma.conf.js ├── lerna.json ├── package.json └── packages/ ├── image-palette-core/ │ ├── .npmignore │ ├── README.md │ ├── __tests__/ │ │ └── test.js │ ├── package.json │ └── src/ │ ├── color-thief.js │ └── index.js ├── preact-image-palette/ │ ├── .babelrc │ ├── .npmignore │ ├── README.md │ ├── __tests__/ │ │ └── test.js │ ├── package.json │ └── src/ │ ├── index.js │ └── provider.js └── react-image-palette/ ├── .npmignore ├── README.md ├── __tests__/ │ └── test.js ├── package.json └── src/ ├── index.js └── provider.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ ["env", { "modules": false }], "react" ], "env": { "commonjs": { "presets": [ ["env", { "modules": "commonjs" }] ] } } } ================================================ FILE: .flowconfig ================================================ [ignore] [include] [libs] [lints] [options] ================================================ FILE: .gitignore ================================================ node_modules lib es .DS_Store yarn-error.log package-lock.json lerna-debug.log ================================================ FILE: .prettierignore ================================================ src/color-thief.js ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "8" - "6" - "4" dist: trusty # needs Ubuntu Trusty sudo: false # no need for virtualization. addons: chrome: stable # have Travis install chrome stable. script: - yarn test ================================================ FILE: .yarnclean ================================================ # test directories __tests__ test tests powered-test # asset directories docs doc website images assets # examples example examples # code coverage directories coverage .nyc_output # build scripts Makefile Gulpfile.js Gruntfile.js # configs .tern-project .gitattributes .editorconfig .*ignore .eslintrc .jshintrc .flowconfig .documentup.json .yarn-metadata.json .*.yml *.yml # misc *.gz *.md ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2017 Formidable Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Build Status](https://travis-ci.com/FormidableLabs/react-image-palette.svg?token=ycKCGETrX5nV3P6ePUdx&branch=master)](https://travis-ci.com/FormidableLabs/react-image-palette)

image-palette

Dynamically generate accessible color palettes from images

![image-palette demo](./screenshot.jpg) Implement adaptive UIs dynamically from any image in right in the browser. Every palette is parsed from the most dominant and vibrant colors in the source image, and guaranteed to meet the [WCAG contrast standard](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) for accessible color pairings. ### Packages This repository is setup as a monorepo with the following packages: * [`image-palette-core`](https://github.com/FormidableLabs/image-palette/tree/master/packages/image-palette-core) - core logic for parsing palettes from images * [`react-image-palette`](https://github.com/FormidableLabs/image-palette/tree/master/packages/react-image-palette) - A React adapter for `image-palette-core` * [`preact-image-palette`](https://github.com/FormidableLabs/image-palette/tree/master/packages/preact-image-palette) - A Preact adapter for `image-palette-core` ## Maintenance Status **Archived:** This project is no longer maintained by Formidable. We are no longer responding to issues or pull requests unless they relate to security concerns. We encourage interested developers to fork this project and make it their own! ================================================ FILE: demo/.gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: demo/package.json ================================================ { "name": "demo", "version": "0.1.0", "private": true, "dependencies": { "react": "^16.0.0", "react-dom": "^16.0.0", "react-image-palette": "^0.1.3", "react-scripts": "1.0.13", "react-spinkit": "^3.0.0" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", "deploy": "yarn build && surge ./build -d react-image-palette-demo.surge.sh" } } ================================================ FILE: demo/public/index.html ================================================ React App
================================================ FILE: demo/public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "192x192", "type": "image/png" } ], "start_url": "./index.html", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: demo/src/App.js ================================================ import React, { Component } from "react"; import ImagePaletteProvider from "react-image-palette"; import Spinner from "react-spinkit"; import "./main.css"; const getAPIURL = artist => `http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=${encodeURIComponent( artist.toLowerCase() )}&api_key=ff4b76a7c5f5029f2196a4ae65468679&format=json`; class App extends Component { state = { albums: [], artist: "Com Truise", loading: true }; componentWillMount() { this.mounted = true; this.updateSearch(); } updateArist = event => this.setState({ artist: event.target.value }); updateSearch = () => { const { artist } = this.state; this.setState({ albums: [] }); fetch(getAPIURL(this.state.artist)) .then(res => res.json()) .then(json => this.setState({ albums: json.topalbums ? json.topalbums.album : [], loading: false }) ); }; getLargestImageUrl(images) { const image = images && (images[3] || images[2] || images[1] || images[0]); return image ? image["#text"] : null; } handleKeyDown = event => { if (event.key === "Enter") { this.updateSearch(); } }; render() { const { albums, loading } = this.state; const renderedAlbums = albums.map(album => { const image = this.getLargestImageUrl(album.image); if (!image) { return null; } return ( {({ backgroundColor, color, alternativeColor }) => (

{album.name}

Learn More
)}
); }); return (
Fork me on GitHub

react-image-palette

Search for an artist to see generated color palettes for all the album art in their discog.


{loading && (
)} {!loading && renderedAlbums}
); } } export default App; ================================================ FILE: demo/src/index.js ================================================ import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; ReactDOM.render(, document.getElementById("root")); ================================================ FILE: demo/src/main.css ================================================ .container { font-family: 'Nunito', sans-serif; text-align: center; padding: 5px; } .title { font-size: 20px; margin-bottom: 0px; } .search--container { display: flex; justify-content: center; margin-bottom: 20px; max-width: 362px; margin: 10px auto; } .search--input { box-sizing: border-box; height: 40px; font-size: 16px; padding: 10px; border: 1px solid #efefef; outline: none; width: 100%; } .search--button { background-color: #37474f; color: #ffffff; font-size: 14px; border: none; width: 20%; } .album--container { display: flex; flex-wrap: wrap; justify-content: center; } .album { box-sizing: border-box; width: 100%; max-width: 400px; padding: 30px; margin: 10px 0px; display: flex; flex-direction: column; align-items: center; } .album img { width: 280px; } .spinner--container { display: flex; align-items: center; height: 200px; } .github-ribbon { display: none; } @media screen and (min-width: 350px) { .title { font-size: 25px; } } @media screen and (min-width: 768px) { .title { font-size: 35px; } .album { width: 400px; margin: 20px; } .album img { width: 300px; } .github-ribbon { display: block; } } @media screen and (min-width: 1024px) { .title { font-size: 40px; } } ================================================ FILE: karma.conf.js ================================================ module.exports = config => { config.set({ // client: { // mocha: { // timeout: 6000 // } // }, frameworks: ['mocha', 'chai'], files: ['./packages/**/__tests__/**/*.js'], preprocessors: { './packages/**/__tests__/**/*.js': ['webpack'] }, webpack: { module: { loaders: [ { test: /\.png$/, loader: 'url-loader' } ] } }, webpackMiddleware: { quiet: true }, reporters: ['mocha'], port: 9876, colors: true, logLevel: config.LOG_INFO, browsers: ['ChromeHeadless'], autoWatch: false, concurrency: Infinity }); }; ================================================ FILE: lerna.json ================================================ { "lerna": "2.1.2", "packages": [ "packages/*" ], "version": "independent" } ================================================ FILE: package.json ================================================ { "name": "image-palette", "private": true, "scripts": { "test": "lerna run test", "build": "lerna run build" }, "devDependencies": { "babel-cli": "^6.26.0", "babel-preset-env": "^1.6.0", "babel-preset-react": "^6.24.1", "chai": "^4.1.2", "flow-bin": "^0.55.0", "karma": "^1.7.1", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^2.2.0", "karma-mocha": "^1.3.0", "karma-mocha-reporter": "^2.2.4", "karma-webpack": "^2.0.4", "lerna": "^2.1.2", "mocha": "^3.5.3", "prettier": "^1.7.2", "rimraf": "^2.6.2", "url-loader": "^0.6.2", "webpack": "^3.8.1" } } ================================================ FILE: packages/image-palette-core/.npmignore ================================================ /* !/lib !/es !/src !LICENSE.txt !README.md !package.json ================================================ FILE: packages/image-palette-core/README.md ================================================ # image-palette-core The core logic for parsing a palette from image data. You can use this if you want an imperative API for generating a palette. If you want to use it with React you can use [`react-image-palette`](https://github.com/FormidableLabs/image-palette/tree/master/packages/react-image-palette) ### Install ``` npm install --save image-palette-core ``` ## Usage The main export of the package is a `getImagePalette` function which takes an image and returns an accessible color palette representing the most dominant colors in the image. ```js import getImagePalette from 'image-palette-core' const img = new Image(); img.src = 'foo.jpg'; img.onload = function() { // The image *must* be loaded before calling `getImagePalette` const palette = getImagePalette(img); } ``` > ⚠️ Keep in mind that the image will be loaded into a canvas and parsed as data, so you should only use images from trusted origins. ### The Palette The parsed palette will have the following shape: ``` type Palette = { backgroundColor: String, color: String, alternativeColor: String } ``` * `backgroundColor` will be the most dominant color in the image. * `color` will be the color that looks the best overlayed over `backgroundColor`. * `alternativeColor` will be the second best color. If there are only two colors parsed, it will default to `color`. Both `alternativeColor` and `color` are guaranteed to meet the minimum contrast ratio requirements when overlayed with `backgroundColor`, but overlaying `color` on `alternativeColor` (or vice-versa) is a bad idea as they will often have very similar contrast levels. ================================================ FILE: packages/image-palette-core/__tests__/test.js ================================================ import getImagePalette from "../lib"; // Album cover for Com Truise - Fairlight import testImage from "./fairlight.png"; describe("image-palette-core", () => { describe("provider", () => { it("should parse a palette from an image", done => { const img = new Image(); img.src = testImage; img.onload = () => { const palette = getImagePalette(img); expect(palette.backgroundColor).to.equal("rgb(60, 16, 32)"); expect(palette.color).to.equal("#EF4E2E"); expect(palette.alternativeColor).to.equal("#D17872"); done(); }; }); }); }); ================================================ FILE: packages/image-palette-core/package.json ================================================ { "name": "image-palette-core", "version": "0.2.2", "description": "Create ARIA-compliant color themes based on any image.", "main": "lib/index.js", "module": "es/index.js", "jsnext:main": "es/index.js", "author": "Brandon Dail", "license": "MIT", "scripts": { "build": "npm run clean && npm run build:es && npm run build:lib", "build:es": "BABEL_ENV=es babel src --out-dir es", "build:lib": "BABEL_ENV=commonjs babel src --out-dir lib", "clean": "rimraf es lib", "preversion": "npm run test", "test:only": "karma start --single-run --browsers ChromeHeadless ../../karma.conf.js", "test": "npm run build && npm run test:only" }, "dependencies": { "color": "^2.0.0", "lodash.isequal": "^4.5.0", "lodash.sortby": "^4.7.0", "lodash.uniq": "^4.5.0", "lodash.uniqby": "^4.7.0" } } ================================================ FILE: packages/image-palette-core/src/color-thief.js ================================================ /* * Color Thief v2.0 * by Lokesh Dhakar - http://www.lokeshdhakar.com * * License * ------- * Creative Commons Attribution 2.5 License: * http://creativecommons.org/licenses/by/2.5/ * * Thanks * ------ * Nick Rabinowitz - For creating quantize.js. * John Schulz - For clean up and optimization. @JFSIII * Nathan Spady - For adding drag and drop support to the demo page. * */ /* CanvasImage Class Class that wraps the html image element and canvas. It also simplifies some of the canvas context manipulation with a set of helper functions. */ var iAmOnNode = false; var Canvas; var Image; var fs; if ( !!process && process.execPath ) { iAmOnNode = true; } // if (iAmOnNode) { // Canvas = require('canvas'); // Image = Canvas.Image; // fs = require('fs'); // } var vboxColorMap = {}; var CanvasImage = function (image) { // in node we use strings as path to an image // whereas in the browser we use an image element if (iAmOnNode) { this.canvas = new Canvas() var img = new Image; if(image instanceof Buffer) { img.src = image }else{ img.src = fs.readFileSync(image); } } else { this.canvas = document.createElement('canvas'); document.body.appendChild(this.canvas); var img = image; } this.context = this.canvas.getContext('2d'); this.width = this.canvas.width = img.width; this.height = this.canvas.height = img.height; this.context.drawImage(img, 0, 0, this.width, this.height); }; CanvasImage.prototype.clear = function () { this.context.clearRect(0, 0, this.width, this.height); }; CanvasImage.prototype.update = function (imageData) { this.context.putImageData(imageData, 0, 0); }; CanvasImage.prototype.getPixelCount = function () { return this.width * this.height; }; CanvasImage.prototype.getImageData = function () { return this.context.getImageData(0, 0, this.width, this.height); }; CanvasImage.prototype.removeCanvas = function () { if (this.canvas.parentNode) { this.canvas.parentNode.removeChild(this.canvas); } }; var ColorThief = function () {}; /* * getColor(sourceImage[, quality]) * 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. * * Quality is an optional argument. It needs to be an integer. 0 is the highest quality settings. * 10 is the default. There is a trade-off between quality and speed. The bigger the number, the * faster a color will be returned but the greater the likelihood that it will not be the visually * most dominant color. * * */ ColorThief.prototype.getColor = function(sourceImage, quality, allowWhite) { // control if second parameter is allowWhite if (quality === true || quality === false) { allowWhite = quality; quality = undefined; } var palette = this.getPalette(sourceImage, 5, quality, allowWhite); var dominantColor = palette[0]; return dominantColor; }; /* * getPalette(sourceImage[, colorCount, quality]) * 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. * * colorCount determines the size of the palette; the number of colors returned. If not set, it * defaults to 10. * * BUGGY: Function does not always return the requested amount of colors. It can be +/- 2. * * quality is an optional argument. It needs to be an integer. 0 is the highest quality settings. * 10 is the default. There is a trade-off between quality and speed. The bigger the number, the * faster the palette generation but the greater the likelihood that colors will be missed. * * */ ColorThief.prototype.getPalette = function(sourceImage, colorCount, quality, allowWhite) { if (typeof colorCount === 'undefined') { colorCount = 10; }; if (typeof quality === 'undefined') { quality = 10; }; // Create custom CanvasImage object var image = new CanvasImage(sourceImage); var imageData = image.getImageData(); var pixels = imageData.data; var pixelCount = image.getPixelCount(); var palette = this.getPaletteFromPixels(pixels, pixelCount, colorCount, quality, allowWhite); // Clean up image.removeCanvas(); return palette; }; /* * getPaletteFromPixels(pixels, pixelCount, colorCount, quality) * returns array[ {r: num, g: num, b: num}, {r: num, g: num, b: num}, ...] * * Low-level function that takes pixels and computes color palette. * Used by getPalette() and getColor() * */ ColorThief.prototype.getPaletteFromPixels = function(pixels, pixelCount, colorCount, quality) { var allowWhite = true; // Store the RGB values in an array format suitable for quantize function var pixelArray = []; for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) { offset = i * 4; r = pixels[offset + 0]; g = pixels[offset + 1]; b = pixels[offset + 2]; a = pixels[offset + 3]; // If pixel is mostly opaque and not white if (a >= 125) { if (!(r > 255 && g > 255 && b > 255)) { pixelArray.push([r, g, b]); } } } // Send array to quantize function which clusters values // using median cut algorithm var cmap = MMCQ.quantize(pixelArray, colorCount); var palette = cmap.palette(); return palette; } /*! * 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); }); */ var 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++) { var 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 { 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 && gval <= vbox.g2 && bval >= vbox.b1 && bval <= 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, rgb: vbox.avg(), count: vbox.count() }); }, palette: function() { return this.vboxes.map(function(vb) { return { rgb: vb.rgb, count: vb.count } }); }, size: function() { return this.vboxes.size(); }, map: function(color) { var vboxes = this.vboxes; for (var i=0; i 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; 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 new CMap(); } // 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) { 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. 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 } })(); module.exports = ColorThief; ================================================ FILE: packages/image-palette-core/src/index.js ================================================ // @flow import uniqBy from "lodash.uniqby"; import isEqual from "lodash.isequal"; import sortBy from "lodash.sortby"; import ColorThief from "./color-thief"; import Color from "color"; type ColorDescriptor = { color: Color, score: number, contrast: number }; type ColorPairings = Array; type ColorPairingMap = { [key: string]: ColorPairings }; const THRESHOLD_CONTRAST_RATIO = 1.0; // This is the minimum required for "AA" certification const MINIMUM_CONTRAST_RATIO = 4.5; const IDEAL_CONTRAST_RATIO = 7.5; // Track the total number of pixels, reset everytime // a palette is parsed. let totalPixelCount = 0; // Track pixel count by RGB values, reset everytime // a palette is parsed. let RGBToPixelCountMap = {}; /** * The "range" is a metric used to determine how * vibrant a color is. It checks the delta between * the highest and lowest channel values, giving us an indiciation * of how dominant one range might be. * * @example * For the RGB value [250, 30, 10] we can see * that the red channel dominates, meaning it will be * primarily red. */ function getRGBRange(color: Color) { var rgb = sortBy(color.rgb().array()).reverse(); var [max, med, min] = rgb; return (max - min) / 10; } /** * Returns a value between 0 and 1 representing how * many pixels in the original image are represented * by this color, 0 meaning none and 1 meaning all. * @param {*} color * @returns {number} */ function getPixelDominance(color: Color) { const pixelCount: number = RGBToPixelCountMap[color]; return pixelCount / totalPixelCount; } /** * Calculates the total score for each color * in an array of pairs which are matches for some * dominant color. Score represents viability, so higher is better. * @param {*} pairs * @returns {number} */ function calculateTotalPairScore(pairs) { return pairs.reduce((score, color) => score + color.score, 0); } /** * Return a new array of pairs, sorted by score. * @param {*} pairs */ function sortPairsByScore(pairs: ColorPairings) { pairs.sort((a, b) => { if (a.score === b.score) { return 0; } return a.score > b.score ? -1 : 1; }); } /** * Dominance is ranked using a system that weighs * each dominant color by three factors: * - The true dominance of the color in the original image. * This is determined by tracking the total number of pixels * in the image, and the total pixels found for an image, * and dividing the color pixels by the total. * * - The number of valid color pairs. If a color has no matching * pairs it is given a score of zero, since we can't build * a color palette with a single color. * * - The total score of each matching color. Each color is scored * based on the contrast with the dominant color, the dominance * in the original image, and its "range", which is the difference * between the highest channel value and lowest, which sort of measures * vibrance. * @param {*} WCAGCompliantColorPairs */ function getMostDominantPrimaryColor(WCAGCompliantColorPairs: ColorPairingMap) { var highestDominanceScore = 0; var mostDominantColor = ""; for (var dominantColor in WCAGCompliantColorPairs) { var pairs = WCAGCompliantColorPairs[dominantColor]; var dominance = getPixelDominance(dominantColor); var totalPairScore = calculateTotalPairScore(pairs); var score = pairs.length ? (pairs.length + totalPairScore) * dominance : 0; if (score > highestDominanceScore) { highestDominanceScore = score; mostDominantColor = dominantColor; } } sortPairsByScore(WCAGCompliantColorPairs[mostDominantColor]); return mostDominantColor; } /** * Gets the palette for an image from color-thief and maps * the colors to Color instances. It also re-initializes * and updates the RGBToPixelCountMap so we get a fresh * dataset for pixel dominance * @param {string} image * @param {ColorThief} colorThief */ function getColorPalette(image, colorThief: ColorThief): Array { totalPixelCount = 0; RGBToPixelCountMap = {}; return colorThief.getPalette(image).map(color => { var colorWrapper = new Color(color.rgb); RGBToPixelCountMap[colorWrapper] = color.count; totalPixelCount += color.count; return colorWrapper; }); } /** * The main export that takes an image URI and optional instance * of color-thief and generates the palettes. Responsible for * building the WCAGCompliantColorPairs map and generating * accessible color pairings. * * Returns an object with a backgroundColor, color, and alternativeColor. * If there's only one potential color pairing, alternativeColor will * just be color. * @param {*} image * @param {*} colorThief */ export default function getImagePalette(image: string, colorThief: ColorThief) { colorThief = colorThief || new ColorThief(); var palettes = getColorPalette(image, colorThief); var highestMatchCount = 0; var WCAGCompliantColorPairs: ColorPairingMap = {}; palettes.forEach((dominantColor, index) => { var pairs = (WCAGCompliantColorPairs[dominantColor] = []); palettes.forEach(color => { var contrast = dominantColor.contrast(color); if (contrast > THRESHOLD_CONTRAST_RATIO) { /** * The score is determined based three things: * * contrast: * how well contrasted the color is with the dominant color. * dominance: * the level of dominance of the dominant color * which is based on the index of the color in * the palette array. * range/vibrance: * we want some vibrant colors */ var range = getRGBRange(color); /**0 * If the contrast isn't high enough, lighten/darken * the color so that we get a more accessible * version of the color. */ if (contrast < MINIMUM_CONTRAST_RATIO) { var delta = (MINIMUM_CONTRAST_RATIO - contrast) / 20; var lighten = dominantColor.dark(); while (contrast < MINIMUM_CONTRAST_RATIO) { var newColor = lighten ? color.lighten(delta) : color.darken(delta); // If the new color is the same as the old one, we're not getting any // lighter or darker so we need to stop. if (newColor.hex() === color.hex()) { break; } var newContrast = dominantColor.contrast(newColor); // If the new contrast is lower than the old contrast // then we need to start moving the other direction in the spectrum if (newContrast < contrast) { lighten = !lighten; } color = newColor; contrast = newContrast; } } var score = contrast + range; pairs.push({ color, score, contrast }); } }); }); var backgroundColor = getMostDominantPrimaryColor(WCAGCompliantColorPairs); var [color, alternativeColor, accentColor] = WCAGCompliantColorPairs[ backgroundColor ]; if (!alternativeColor) { alternativeColor = color; } if (!accentColor) { accentColor = alternativeColor; } return { backgroundColor, color: color.color.hex(), alternativeColor: alternativeColor.color.hex() }; } ================================================ FILE: packages/preact-image-palette/.babelrc ================================================ { "presets": [ ["env", { "modules": false }] ], "env": { "commonjs": { "presets": [ ["env", { "modules": "commonjs" }] ] } } } ================================================ FILE: packages/preact-image-palette/.npmignore ================================================ /* !/lib !/es !/src !LICENSE.txt !README.md !package.json ================================================ FILE: packages/preact-image-palette/README.md ================================================ # preact-image-palette A Preact adpater for [`image-palette-core`](https://github.com/FormidableLabs/image-palette/tree/master/packages/image-palette-core) ### Install ``` npm install --save preact-image-palette ``` ## Usage The main export of the package is the `ImagePalette` component, which uses a [render callback](https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce) to provide the color palette after the image is parsed. ```jsx import ImagePalette from 'preact-image-palette' const SomeComponent = ({ image }) => ( {({ backgroundColor, color, alternativeColor }) => (
This div has been themed based on {image}
)}
) ``` ## API ### Palette See the [`image-palette-core` documentation](https://github.com/FormidableLabs/tree/master/packages/image-palette-core#the-palette) ### Props Property | Type | Description :-----------------------|:--------------|:-------------------------------- `image` | `String!` | The URL for the image to parse. `crossOrigin` | `Boolean` | Sets the `crossOrigin` property on the `Image` instance that loads the source image 1 `render` | `Palette => ReactElement` | If you prefer to use a `render` prop over a function child, go for it! `react-image-palette` supports both. `defaults` | `Palette` | A default palette to render if a palette cannot be parsed. This would typically occur when the source image fails to load > 1 ⚠️ Keep in mind that the image will be loaded into a canvas and parsed as data, so you should only use images from trusted origins. ================================================ FILE: packages/preact-image-palette/__tests__/test.js ================================================ import {h, render, Component} from 'preact' import PreactImagePalette from "../lib"; // Album cover for Com Truise - Fairlight import testImage from "./fairlight.png"; class TestComponent extends Component { render() { const { render } = this.props; return h(ReactImagePalette, { image: testImage }, render); } } let container; const renderWithExpect = (done, palette) => { expect(palette.backgroundColor).to.equal("rgb(60, 16, 32)"); expect(palette.color).to.equal("#EF4E2E"); expect(palette.alternativeColor).to.equal("#D17872"); done(); // Return null so React doesn't throw an error // about an empty return return null; }; describe("preact-image-palette", () => { beforeEach(() => { container = document.createElement("div"); }); describe("provider", () => { it("should parse a palette from an image", done => { render( h(PreactImagePalette, { image: testImage, render: renderWithExpect.bind(this, done) }), container ); }); it("should render defaults if the image fails to load", done => { const defaults = { backgroundColor: "red", color: "white", alternativeColor: "blue" }; render( h(PreactImagePalette, { image: "unknown-image.gif", defaults, render: palette => { expect(palette).to.equal(defaults); done(); return null; } }), container ); }); }); }); ================================================ FILE: packages/preact-image-palette/package.json ================================================ { "name": "preact-image-palette", "version": "0.1.1", "description": "Create ARIA-compliant color themes based on any image.", "main": "lib/index.js", "module": "es/index.js", "jsnext:main": "es/index.js", "author": "Brandon Dail", "license": "MIT", "scripts": { "build": "npm run clean && npm run build:es && npm run build:lib", "build:es": "BABEL_ENV=es babel src --out-dir es", "build:lib": "BABEL_ENV=commonjs babel src --out-dir lib", "clean": "rimraf es lib", "preversion": "npm run test", "test:only": "karma start --single-run --browsers ChromeHeadless ../../karma.conf.js", "test": "npm run build && npm run test:only" }, "dependencies": { "image-palette-core": "^0.2.2" }, "devDependencies": { "preact": "^8.0.0" }, "peerDependencies": { "preact": "^8.0.0" } } ================================================ FILE: packages/preact-image-palette/src/index.js ================================================ import ImagePaletteProvider from "./provider"; export default ImagePaletteProvider; ================================================ FILE: packages/preact-image-palette/src/provider.js ================================================ import {h, Component} from "preact"; import getImagePalette from "image-palette-core"; export default class ImagePaletteProvider extends Component { constructor(...args) { super(...args); this.state = { colors: null }; this.onImageload = this.onImageload.bind(this); this.onImageError = this.onImageError.bind(this); } componentDidMount() { const image = (this.image = new Image()); image.crossOrigin = this.props.crossOrigin; image.src = this.props.image; image.onload = this.onImageload; image.onerror = this.onImageError; } componentWillUnmount() { this.image.onload = null; this.image.onerror = null; } onImageload() { var image = this.image; var colors = getImagePalette(this.image); this.setState({ colors }); } onImageError() { if (this.props.defaults) { this.setState({ colors: this.props.defaults }); } } render() { const { colors } = this.state; const { children, render } = this.props; const callback = render || children; if (!callback) { throw new Error( "ImagePaletteProvider expects a render callback either as a child or via the `render` prop" ); } return colors ? callback(colors) : null; } } ================================================ FILE: packages/react-image-palette/.npmignore ================================================ /* !/lib !/es !/src !LICENSE.txt !README.md !package.json ================================================ FILE: packages/react-image-palette/README.md ================================================ # react-image-palette A React adpater for [`image-palette-core`](https://github.com/FormidableLabs/image-palette/tree/master/packages/image-palette-core) ### Install ``` npm install --save react-image-palette ``` ## Usage The main export of the package is the `ImagePalette` component, which uses a [render callback](https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce) to provide the color palette after the image is parsed. ```jsx import ImagePalette from 'react-image-palette' const SomeComponent = ({ image }) => ( {({ backgroundColor, color, alternativeColor }) => (
This div has been themed based on {image}
)}
) ``` ## API ### Palette See the [`image-palette-core` documentation](https://github.com/FormidableLabs/image-palette/tree/master/packages/image-palette-core#the-palette) ### Props Property | Type | Description :-----------------------|:--------------|:-------------------------------- `image` | `String!` | The URL for the image to parse. `crossOrigin` | `Boolean` | Sets the `crossOrigin` property on the `Image` instance that loads the source image 1 `render` | `Palette => ReactElement` | If you prefer to use a `render` prop over a function child, go for it! `react-image-palette` supports both. `defaults` | `Palette` | A default palette to render if a palette cannot be parsed. This would typically occur when the source image fails to load > 1 ⚠️ Keep in mind that the image will be loaded into a canvas and parsed as data, so you should only use images from trusted origins. ================================================ FILE: packages/react-image-palette/__tests__/test.js ================================================ import React from "react"; import ReactDOM from "react-dom"; import ReactImagePalette from "../lib"; // Album cover for Com Truise - Fairlight import testImage from "./fairlight.png"; class TestComponent extends React.Component { render() { const { render } = this.props; return React.createElement(ReactImagePalette, { image: testImage }, render); } } let container; const renderWithExpect = (done, palette) => { expect(palette.backgroundColor).to.equal("rgb(60, 16, 32)"); expect(palette.color).to.equal("#EF4E2E"); expect(palette.alternativeColor).to.equal("#D17872"); done(); // Return null so React doesn't throw an error // about an empty return return null; }; describe("react-image-palette", () => { beforeEach(() => { container = document.createElement("div"); }); describe("provider", () => { it("should parse a palette from an image", done => { ReactDOM.render( React.createElement(ReactImagePalette, { image: testImage, render: renderWithExpect.bind(this, done) }), container ); }); it("should render defaults if the image fails to load", done => { const defaults = { backgroundColor: "red", color: "white", alternativeColor: "blue" }; ReactDOM.render( React.createElement(ReactImagePalette, { image: "unknown-image.gif", defaults, render: palette => { expect(palette).to.equal(defaults); done(); return null; } }), container ); }); }); }); ================================================ FILE: packages/react-image-palette/package.json ================================================ { "name": "react-image-palette", "version": "0.2.4", "description": "Create ARIA-compliant color themes based on any image.", "main": "lib/index.js", "module": "es/index.js", "jsnext:main": "es/index.js", "author": "Brandon Dail", "license": "MIT", "scripts": { "build": "npm run clean && npm run build:es && npm run build:lib", "build:es": "BABEL_ENV=es babel src --out-dir es", "build:lib": "BABEL_ENV=commonjs babel src --out-dir lib", "clean": "rimraf es lib", "preversion": "npm run test", "test:only": "karma start --single-run --browsers ChromeHeadless ../../karma.conf.js", "test": "npm run build && npm run test:only" }, "dependencies": { "image-palette-core": "^0.2.2" }, "devDependencies": { "react-dom": "^16.1.0" }, "peerDependencies": { "react": "^15.0.0-0 || ^16.0.0-0" } } ================================================ FILE: packages/react-image-palette/src/index.js ================================================ import ImagePaletteProvider from "./provider"; export default ImagePaletteProvider; ================================================ FILE: packages/react-image-palette/src/provider.js ================================================ import React from "react"; import getImagePalette from "image-palette-core"; export default class ImagePaletteProvider extends React.Component { constructor(...args) { super(...args); this.state = { colors: null }; this.onImageload = this.onImageload.bind(this); this.onImageError = this.onImageError.bind(this); } componentDidMount() { const image = (this.image = new Image()); image.crossOrigin = this.props.crossOrigin; image.src = this.props.image; image.onload = this.onImageload; image.onerror = this.onImageError; } componentWillUnmount() { this.image.onload = null; this.image.onerror = null; } onImageload() { var image = this.image; var colors = getImagePalette(this.image); this.setState({ colors }); } onImageError() { if (this.props.defaults) { this.setState({ colors: this.props.defaults }); } } render() { const { colors } = this.state; const { children, render } = this.props; const callback = render || children; if (!callback) { throw new Error( "ImagePaletteProvider expects a render callback either as a child or via the `render` prop" ); } return colors ? callback(colors) : null; } }