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
================================================
[](https://travis-ci.com/FormidableLabs/react-image-palette)
<h1 align="center">image-palette</h1>
<h4 align="center">
Dynamically generate accessible color palettes from images
</h4>

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
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Nunito:400,600" rel="stylesheet">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
================================================
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 (
<ImagePaletteProvider crossOrigin image={image} key={image}>
{({ backgroundColor, color, alternativeColor }) => (
<div className="album" style={{ backgroundColor, color }}>
<h1>{album.name}</h1>
<a href={album.url} style={{ color: alternativeColor }}>
Learn More
</a>
<br />
<img src={image} />
</div>
)}
</ImagePaletteProvider>
);
});
return (
<div className="container">
<a
className="github-ribbon"
href="https://github.com/FormidableLabs/react-image-palette/"
>
<img
style={{ position: "absolute", top: 0, right: 0, border: 0 }}
src="https://camo.githubusercontent.com/38ef81f8aca64bb9a64448d0d70f1308ef5341ab/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6461726b626c75655f3132313632312e706e67"
alt="Fork me on GitHub"
data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png"
/>
</a>
<h1 className="title">
<a href="https://github.com/FormidableLabs/react-image-palette">
<pre>react-image-palette</pre>
</a>
</h1>
<p>
Search for an artist to see generated color palettes for all the album
art in their discog.
</p>
<br />
<div className="search--container">
<input
spellCheck={false}
className="search--input"
value={this.state.artist}
onChange={this.updateArist}
onKeyDown={this.handleKeyDown}
/>
<button className="search--button" onClick={this.updateSearch}>
Search
</button>
</div>
<div className="album--container">
{loading && (
<div className="spinner--container">
<Spinner name="ball-spin-fade-loader" />
</div>
)}
{!loading && renderedAlbums}
</div>
</div>
);
}
}
export default App;
================================================
FILE: demo/src/index.js
================================================
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, 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<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[2] - vboxes.peek(i).color[2], 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;
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<ColorDescriptor>;
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<Color> {
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 }) => (
<ImagePalette image={image}>
{({ backgroundColor, color, alternativeColor }) => (
<div style={{ backgroundColor, color }}>
This div has been themed based on
<span style={{ color: alternativeColor }}>{image}</span>
</div>
)}
</ImagePalette>
)
```
## 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 <sup>1</sup>
`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
> <sup>1</sup> ⚠️ 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 }) => (
<ImagePalette image={image}>
{({ backgroundColor, color, alternativeColor }) => (
<div style={{ backgroundColor, color }}>
This div has been themed based on
<span style={{ color: alternativeColor }}>{image}</span>
</div>
)}
</ImagePalette>
)
```
## 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 <sup>1</sup>
`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
> <sup>1</sup> ⚠️ 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;
}
}
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
SYMBOL INDEX (40 symbols across 7 files)
FILE: demo/src/App.js
class App (line 11) | class App extends Component {
method componentWillMount (line 14) | componentWillMount() {
method getLargestImageUrl (line 34) | getLargestImageUrl(images) {
method render (line 45) | render() {
FILE: packages/image-palette-core/src/color-thief.js
function getColorIndex (line 258) | function getColorIndex(r, g, b) {
function PQueue (line 263) | function PQueue(comparator) {
function VBox (line 300) | function VBox(r1, r2, g1, g2, b1, b2, histo) {
function CMap (line 388) | function CMap() {
function getHisto (line 456) | function getHisto(pixels) {
function vboxFromPixels (line 471) | function vboxFromPixels(pixels, histo) {
function medianCutApply (line 491) | function medianCutApply(histo, vbox) {
function quantize (line 580) | function quantize(pixels, maxcolors) {
FILE: packages/image-palette-core/src/index.js
constant THRESHOLD_CONTRAST_RATIO (line 20) | const THRESHOLD_CONTRAST_RATIO = 1.0;
constant MINIMUM_CONTRAST_RATIO (line 22) | const MINIMUM_CONTRAST_RATIO = 4.5;
constant IDEAL_CONTRAST_RATIO (line 23) | const IDEAL_CONTRAST_RATIO = 7.5;
function getRGBRange (line 42) | function getRGBRange(color: Color) {
function getPixelDominance (line 55) | function getPixelDominance(color: Color) {
function calculateTotalPairScore (line 67) | function calculateTotalPairScore(pairs) {
function sortPairsByScore (line 75) | function sortPairsByScore(pairs: ColorPairings) {
function getMostDominantPrimaryColor (line 103) | function getMostDominantPrimaryColor(WCAGCompliantColorPairs: ColorPairi...
function getColorPalette (line 128) | function getColorPalette(image, colorThief: ColorThief): Array<Color> {
function getImagePalette (line 151) | function getImagePalette(image: string, colorThief: ColorThief) {
FILE: packages/preact-image-palette/__tests__/test.js
class TestComponent (line 6) | class TestComponent extends Component {
method render (line 7) | render() {
FILE: packages/preact-image-palette/src/provider.js
class ImagePaletteProvider (line 4) | class ImagePaletteProvider extends Component {
method constructor (line 5) | constructor(...args) {
method componentDidMount (line 12) | componentDidMount() {
method componentWillUnmount (line 20) | componentWillUnmount() {
method onImageload (line 25) | onImageload() {
method onImageError (line 31) | onImageError() {
method render (line 37) | render() {
FILE: packages/react-image-palette/__tests__/test.js
class TestComponent (line 7) | class TestComponent extends React.Component {
method render (line 8) | render() {
FILE: packages/react-image-palette/src/provider.js
class ImagePaletteProvider (line 4) | class ImagePaletteProvider extends React.Component {
method constructor (line 5) | constructor(...args) {
method componentDidMount (line 12) | componentDidMount() {
method componentWillUnmount (line 20) | componentWillUnmount() {
method onImageload (line 25) | onImageload() {
method onImageError (line 31) | onImageError() {
method render (line 37) | render() {
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (60K chars).
[
{
"path": ".babelrc",
"chars": 207,
"preview": "{\n \"presets\": [\n [\"env\", {\n \"modules\": false\n }],\n \"react\"\n ],\n \"env\": {\n \"commonjs\": {\n \"prese"
},
{
"path": ".flowconfig",
"chars": 48,
"preview": "[ignore]\n\n[include]\n\n[libs]\n\n[lints]\n\n[options]\n"
},
{
"path": ".gitignore",
"chars": 78,
"preview": "node_modules\nlib\nes\n.DS_Store\nyarn-error.log\npackage-lock.json\nlerna-debug.log"
},
{
"path": ".prettierignore",
"chars": 18,
"preview": "src/color-thief.js"
},
{
"path": ".travis.yml",
"chars": 212,
"preview": "language: node_js\nnode_js:\n - \"8\"\n - \"6\"\n - \"4\"\ndist: trusty # needs Ubuntu Trusty\nsudo: false # no need for virtual"
},
{
"path": ".yarnclean",
"chars": 398,
"preview": "# test directories\n__tests__\ntest\ntests\npowered-test\n\n# asset directories\ndocs\ndoc\nwebsite\nimages\nassets\n\n# examples\nexa"
},
{
"path": "LICENSE.txt",
"chars": 1081,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2017 Formidable Labs\n\nPermission is hereby granted, free of charge, to any person o"
},
{
"path": "README.md",
"chars": 1504,
"preview": "[]"
},
{
"path": "demo/.gitignore",
"chars": 285,
"preview": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/cov"
},
{
"path": "demo/package.json",
"chars": 492,
"preview": "{\n \"name\": \"demo\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"dependencies\": {\n \"react\": \"^16.0.0\",\n \"react-dom\""
},
{
"path": "demo/public/index.html",
"chars": 1685,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-wid"
},
{
"path": "demo/public/manifest.json",
"chars": 298,
"preview": "{\n \"short_name\": \"React App\",\n \"name\": \"Create React App Sample\",\n \"icons\": [\n {\n \"src\": \"favicon.ico\",\n "
},
{
"path": "demo/src/App.js",
"chars": 3611,
"preview": "import React, { Component } from \"react\";\nimport ImagePaletteProvider from \"react-image-palette\";\nimport Spinner from \"r"
},
{
"path": "demo/src/index.js",
"chars": 146,
"preview": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport App from \"./App\";\n\nReactDOM.render(<App />, document"
},
{
"path": "demo/src/main.css",
"chars": 1331,
"preview": ".container {\n font-family: 'Nunito', sans-serif;\n text-align: center;\n padding: 5px;\n}\n\n.title {\n font-size: 20px;\n "
},
{
"path": "karma.conf.js",
"chars": 681,
"preview": "module.exports = config => {\n config.set({\n // client: {\n // mocha: {\n // timeout: 6000\n // }\n /"
},
{
"path": "lerna.json",
"chars": 89,
"preview": "{\n \"lerna\": \"2.1.2\",\n \"packages\": [\n \"packages/*\"\n ],\n \"version\": \"independent\"\n}\n"
},
{
"path": "package.json",
"chars": 684,
"preview": "{\n \"name\": \"image-palette\",\n \"private\": true,\n \"scripts\": {\n \"test\": \"lerna run test\",\n \"build\": \"lerna ru"
},
{
"path": "packages/image-palette-core/.npmignore",
"chars": 57,
"preview": "/*\n!/lib\n!/es\n!/src\n!LICENSE.txt\n!README.md\n!package.json"
},
{
"path": "packages/image-palette-core/README.md",
"chars": 1633,
"preview": "# image-palette-core\n\nThe core logic for parsing a palette from image data. You can use this if you want an imperative\nA"
},
{
"path": "packages/image-palette-core/__tests__/test.js",
"chars": 605,
"preview": "import getImagePalette from \"../lib\";\n// Album cover for Com Truise - Fairlight\nimport testImage from \"./fairlight.png\";"
},
{
"path": "packages/image-palette-core/package.json",
"chars": 848,
"preview": "{\n \"name\": \"image-palette-core\",\n \"version\": \"0.2.2\",\n \"description\": \"Create ARIA-compliant color themes based on an"
},
{
"path": "packages/image-palette-core/src/color-thief.js",
"chars": 21201,
"preview": "/*\n * Color Thief v2.0\n * by Lokesh Dhakar - http://www.lokeshdhakar.com\n *\n * License\n * -------\n * Creative Commons At"
},
{
"path": "packages/image-palette-core/src/index.js",
"chars": 7312,
"preview": "// @flow\nimport uniqBy from \"lodash.uniqby\";\nimport isEqual from \"lodash.isequal\";\nimport sortBy from \"lodash.sortby\";\ni"
},
{
"path": "packages/preact-image-palette/.babelrc",
"chars": 194,
"preview": "{\n \"presets\": [\n [\"env\", {\n \"modules\": false\n }]\n ],\n \"env\": {\n \"commonjs\": {\n \"presets\": [\n "
},
{
"path": "packages/preact-image-palette/.npmignore",
"chars": 57,
"preview": "/*\n!/lib\n!/es\n!/src\n!LICENSE.txt\n!README.md\n!package.json"
},
{
"path": "packages/preact-image-palette/README.md",
"chars": 1730,
"preview": "# preact-image-palette\n\n\nA Preact adpater for [`image-palette-core`](https://github.com/FormidableLabs/image-palette/tre"
},
{
"path": "packages/preact-image-palette/__tests__/test.js",
"chars": 1524,
"preview": "import {h, render, Component} from 'preact'\nimport PreactImagePalette from \"../lib\";\n// Album cover for Com Truise - Fai"
},
{
"path": "packages/preact-image-palette/package.json",
"chars": 843,
"preview": "{\n \"name\": \"preact-image-palette\",\n \"version\": \"0.1.1\",\n \"description\": \"Create ARIA-compliant color themes based on "
},
{
"path": "packages/preact-image-palette/src/index.js",
"chars": 84,
"preview": "import ImagePaletteProvider from \"./provider\";\nexport default ImagePaletteProvider;\n"
},
{
"path": "packages/preact-image-palette/src/provider.js",
"chars": 1254,
"preview": "import {h, Component} from \"preact\";\nimport getImagePalette from \"image-palette-core\";\n\nexport default class ImagePalett"
},
{
"path": "packages/react-image-palette/.npmignore",
"chars": 57,
"preview": "/*\n!/lib\n!/es\n!/src\n!LICENSE.txt\n!README.md\n!package.json"
},
{
"path": "packages/react-image-palette/README.md",
"chars": 1740,
"preview": "# react-image-palette\n\n\nA React adpater for [`image-palette-core`](https://github.com/FormidableLabs/image-palette/tree/"
},
{
"path": "packages/react-image-palette/__tests__/test.js",
"chars": 1615,
"preview": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport ReactImagePalette from \"../lib\";\n// Album cover for "
},
{
"path": "packages/react-image-palette/package.json",
"chars": 861,
"preview": "{\n \"name\": \"react-image-palette\",\n \"version\": \"0.2.4\",\n \"description\": \"Create ARIA-compliant color themes based on a"
},
{
"path": "packages/react-image-palette/src/index.js",
"chars": 84,
"preview": "import ImagePaletteProvider from \"./provider\";\nexport default ImagePaletteProvider;\n"
},
{
"path": "packages/react-image-palette/src/provider.js",
"chars": 1250,
"preview": "import React from \"react\";\nimport getImagePalette from \"image-palette-core\";\n\nexport default class ImagePaletteProvider "
}
]
About this extraction
This page contains the full source code of the FormidableLabs/react-image-palette GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (54.5 KB), approximately 15.3k tokens, and a symbol index with 40 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.