├── .babelrc ├── .gitignore ├── README.md ├── docs ├── dist │ └── grade.js ├── index.html └── samples │ ├── drive.jpg │ ├── finding-dory.jpg │ ├── good-dinosaur.jpg │ ├── inside-out.jpg │ ├── only-god-forgives.jpg │ ├── stanger-things.jpg │ ├── true-detective.jpg │ ├── up.jpg │ └── wall-e.jpg ├── package.json └── src └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grade 2 | 3 | [![](https://img.shields.io/npm/dt/grade-js.svg)](https://www.npmjs.com/package/grade-js) 4 | 5 | ## Demo 6 | 7 | [Check it out](https://benhowdle89.github.io/grade/) 8 | 9 | ## Install 10 | 11 | Download this repo and grab the `grade.js` file from the `/docs/dist` folder. 12 | 13 | Or install from npm: `npm install grade-js` 14 | 15 | Use the CDN link: 16 | 17 | `https://unpkg.com/grade-js/docs/dist/grade.js` 18 | 19 | ## Usage 20 | 21 | Recommended HTML structure: 22 | 23 | ```html 24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 | ``` 32 | 33 | If you have the `grade.js` in your project, you can include it with a script tag and initialise it like so: 34 | 35 | ```html 36 | 37 | 47 | ``` 48 | 49 | If you pass in a 3rd parameter and it's a function, the HTML element(s) you passed in as the 1st parameter will **not** be manipulated, but an array will be returned to you, for you to do as you please with, ie. 50 | ```javascript 51 | Grade(document.querySelectorAll('.gradient-wrap'), null, function(gradientData){ 52 | // sample contents of `gradientData` can be inspected here https://jsonblob.com/57c4601ee4b0dc55a4f180f1 53 | }) 54 | ``` 55 | 56 | If you've installed from npm, you can use the library like so: 57 | 58 | ```javascript 59 | import Grade from 'grade-js' 60 | // initialise as above 61 | ``` 62 | 63 | The module this imports will be using ES2015 syntax, so it will need to be transpiled by a build tool, like [Babel](https://babeljs.io/), and if you are importing the module in this fashion (and using npm), I imagine you're already using a bundling tool, like Webpack or Browserify! 64 | 65 | ## Running locally 66 | 67 | If you want to run this locally, just to test it, you need to serve `index.html` via a webserver, not just by opening it in a browser, else the browser will throw a security error. I would recommend either [live-server](https://www.npmjs.com/package/live-server) (requires Node.js installed on your machine) or if you have Python installed, just run `python -m SimpleHTTPServer` inside the project root. If you're on Windows, I believe WAMP/Apache is the best way to go. 68 | 69 | ## Remote images 70 | 71 | This plugin utilises the `` element and the `ImageData` object, and due to cross-site security limitations, the script will fail if one tries to extract the colors from an image not hosted on the current domain, *unless* the image allows for [Cross Origin Resource Sharing](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing). 72 | 73 | __Enabling CORS on S3__ 74 | 75 | To enable CORS for images hosted on S3 buckets, follow the Amazon guide [here](http://docs.aws.amazon.com/AmazonS3/latest/UG/EditingBucketPermissions.html); adding the following to the bucket's CORS configuration: 76 | 77 | ```xml 78 | 79 | * 80 | GET 81 | 82 | ``` 83 | 84 | For all images, you can optionally also include a cross-origin attribute in your image. 85 | 86 | ```html 87 | 88 | ``` 89 | 90 | ## License 91 | 92 | MIT License 93 | 94 | Copyright (c) 2016 Ben Howdle 95 | 96 | Permission is hereby granted, free of charge, to any person obtaining a copy 97 | of this software and associated documentation files (the "Software"), to deal 98 | in the Software without restriction, including without limitation the rights 99 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 100 | copies of the Software, and to permit persons to whom the Software is 101 | furnished to do so, subject to the following conditions: 102 | 103 | The above copyright notice and this permission notice shall be included in all 104 | copies or substantial portions of the Software. 105 | 106 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 107 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 108 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 109 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 110 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 111 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 112 | SOFTWARE. 113 | -------------------------------------------------------------------------------- /docs/dist/grade.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Grade = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0; 63 | }); 64 | }); 65 | 66 | return filtered; 67 | } 68 | }, { 69 | key: 'getRGBAGradientValues', 70 | value: function getRGBAGradientValues(top) { 71 | return top.map(function (color, index) { 72 | return 'rgb(' + color.rgba.slice(0, 3).join(',') + ') ' + (index == 0 ? '0%' : '75%'); 73 | }).join(','); 74 | } 75 | }, { 76 | key: 'getCSSGradientProperty', 77 | value: function getCSSGradientProperty(top) { 78 | var val = this.getRGBAGradientValues(top); 79 | return prefixes.map(function (prefix) { 80 | return 'background-image: -' + prefix + '-linear-gradient(\n 135deg,\n ' + val + '\n )'; 81 | }).concat(['background-image: linear-gradient(\n 135deg,\n ' + val + '\n )']).join(';'); 82 | } 83 | }, { 84 | key: 'getMiddleRGB', 85 | value: function getMiddleRGB(start, end) { 86 | var w = 0.5 * 2 - 1; 87 | var w1 = (w + 1) / 2.0; 88 | var w2 = 1 - w1; 89 | var rgb = [parseInt(start[0] * w1 + end[0] * w2), parseInt(start[1] * w1 + end[1] * w2), parseInt(start[2] * w1 + end[2] * w2)]; 90 | return rgb; 91 | } 92 | }, { 93 | key: 'getSortedValues', 94 | value: function getSortedValues(uniq) { 95 | var occurs = Object.keys(uniq).map(function (key) { 96 | var rgbaKey = key; 97 | var components = key.split('|'), 98 | brightness = (components[0] * 299 + components[1] * 587 + components[2] * 114) / 1000; 99 | return { 100 | rgba: rgbaKey.split('|'), 101 | occurs: uniq[key], 102 | brightness: brightness 103 | }; 104 | }).sort(function (a, b) { 105 | return a.occurs - b.occurs; 106 | }).reverse().slice(0, 10); 107 | return occurs.sort(function (a, b) { 108 | return a.brightness - b.brightness; 109 | }).reverse(); 110 | } 111 | }, { 112 | key: 'getTextProperty', 113 | value: function getTextProperty(top) { 114 | var rgb = this.getMiddleRGB(top[0].rgba.slice(0, 3), top[1].rgba.slice(0, 3)); 115 | var o = Math.round((parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000); 116 | if (o > 125) { 117 | return 'color: #000'; 118 | } else { 119 | return 'color: #fff'; 120 | } 121 | } 122 | }, { 123 | key: 'getTopValues', 124 | value: function getTopValues(uniq) { 125 | var sorted = this.getSortedValues(uniq); 126 | return [sorted[0], sorted[sorted.length - 1]]; 127 | } 128 | }, { 129 | key: 'getUniqValues', 130 | value: function getUniqValues(chunked) { 131 | return chunked.reduce(function (accum, current) { 132 | var key = current.join('|'); 133 | if (!accum[key]) { 134 | accum[key] = 1; 135 | return accum; 136 | } 137 | accum[key] = ++accum[key]; 138 | return accum; 139 | }, {}); 140 | } 141 | }, { 142 | key: 'renderGradient', 143 | value: function renderGradient() { 144 | var ls = window.localStorage; 145 | var item_name = 'grade-' + this.image.getAttribute('src'); 146 | var top = null; 147 | 148 | if (ls && ls.getItem(item_name)) { 149 | top = JSON.parse(ls.getItem(item_name)); 150 | } else { 151 | var chunked = this.getChunkedImageData(); 152 | top = this.getTopValues(this.getUniqValues(chunked)); 153 | 154 | if (ls) { 155 | ls.setItem(item_name, JSON.stringify(top)); 156 | } 157 | } 158 | 159 | if (this.callback) { 160 | this.gradientData = top; 161 | return; 162 | } 163 | 164 | var gradientProperty = this.getCSSGradientProperty(top); 165 | 166 | var textProperty = this.getTextProperty(top); 167 | 168 | var style = (this.container.getAttribute('style') || '') + '; ' + gradientProperty + '; ' + textProperty; 169 | this.container.setAttribute('style', style); 170 | } 171 | }, { 172 | key: 'render', 173 | value: function render() { 174 | this.canvas.width = this.imageDimensions.width; 175 | this.canvas.height = this.imageDimensions.height; 176 | this.ctx.drawImage(this.image, 0, 0, this.imageDimensions.width, this.imageDimensions.height); 177 | this.getImageData(); 178 | this.renderGradient(); 179 | } 180 | }]); 181 | 182 | return Grade; 183 | }(); 184 | 185 | module.exports = function (containers, img_selector, callback) { 186 | var init = function init(container, img_selector, callback) { 187 | var grade = new Grade(container, img_selector, callback), 188 | gradientData = grade.gradientData; 189 | if (!gradientData.length) { 190 | return null; 191 | } 192 | return { 193 | element: container, 194 | gradientData: gradientData 195 | }; 196 | }; 197 | var results = (NodeList.prototype.isPrototypeOf(containers) ? Array.from(containers).map(function (container) { 198 | return init(container, img_selector, callback); 199 | }) : [init(containers, img_selector, callback)]).filter(Boolean); 200 | 201 | if (results.length) { 202 | return callback(results); 203 | } 204 | }; 205 | 206 | },{}]},{},[1])(1) 207 | }); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Grade.js 8 | 9 | 44 | 45 | 46 | 47 |
48 |

Grade.js

49 |

This JavaScript library produces complementary gradients generated from the top 2 dominant colours in supplied images.

50 |
51 | 52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 | 67 |
68 |
69 | 70 |
71 |
72 | 73 |
74 |
75 |

Download

76 |
77 | GitHub 78 | npm 79 |
80 |

81 | npm install grade-js 82 |

83 |
84 |
85 |

Usage

86 |
<!--the gradient is applied to this div as a background-image-->
 87 | <div class="gradient-wrap">
 88 |     <!--this inner image is used to create the gradient-->
 89 |     <img src="./samples/good-dinosaur.jpg" />
 90 | </div>
 91 | 
 92 | <script src="../dist/grade.js"></script>
 93 | <script type="text/javascript">
 94 |     window.addEventListener('load', function(){
 95 |         Grade(document.querySelectorAll('.gradient-wrap'))
 96 |     })
 97 | </script>
 98 | 
99 |
// If you pass in a 3rd parameter and it's a function,
100 | // the HTML element(s) you passed in as the
101 | // 1st parameter will not be manipulated, but an array will be
102 | // returned to you, for you to do as you please with, ie.
103 | Grade(document.querySelectorAll('.gradient-wrap'), null, function(gradientData){
104 |     // sample contents of `gradientData` can be inspected here https://jsonblob.com/57c4601ee4b0dc55a4f180f1
105 | })
106 | 
107 | 108 |
109 | 114 |
115 | 116 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/samples/drive.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowdle89/grade/0be2020ffeeba701625f65cd067c8ae0f1d4cc53/docs/samples/drive.jpg -------------------------------------------------------------------------------- /docs/samples/finding-dory.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowdle89/grade/0be2020ffeeba701625f65cd067c8ae0f1d4cc53/docs/samples/finding-dory.jpg -------------------------------------------------------------------------------- /docs/samples/good-dinosaur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowdle89/grade/0be2020ffeeba701625f65cd067c8ae0f1d4cc53/docs/samples/good-dinosaur.jpg -------------------------------------------------------------------------------- /docs/samples/inside-out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowdle89/grade/0be2020ffeeba701625f65cd067c8ae0f1d4cc53/docs/samples/inside-out.jpg -------------------------------------------------------------------------------- /docs/samples/only-god-forgives.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowdle89/grade/0be2020ffeeba701625f65cd067c8ae0f1d4cc53/docs/samples/only-god-forgives.jpg -------------------------------------------------------------------------------- /docs/samples/stanger-things.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowdle89/grade/0be2020ffeeba701625f65cd067c8ae0f1d4cc53/docs/samples/stanger-things.jpg -------------------------------------------------------------------------------- /docs/samples/true-detective.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowdle89/grade/0be2020ffeeba701625f65cd067c8ae0f1d4cc53/docs/samples/true-detective.jpg -------------------------------------------------------------------------------- /docs/samples/up.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowdle89/grade/0be2020ffeeba701625f65cd067c8ae0f1d4cc53/docs/samples/up.jpg -------------------------------------------------------------------------------- /docs/samples/wall-e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowdle89/grade/0be2020ffeeba701625f65cd067c8ae0f1d4cc53/docs/samples/wall-e.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grade-js", 3 | "version": "1.0.10", 4 | "description": "This JavaScript library produces complimentary gradients generated from the top 2 dominant colours in supplied images", 5 | "main": "src/index.js", 6 | "files": [ 7 | "src/index.js", 8 | "docs/dist/grade.js" 9 | ], 10 | "scripts": { 11 | "build": "browserify src/index.js -t babelify --standalone Grade > docs/dist/grade.js", 12 | "dev": "watchify src/index.js -t babelify --standalone Grade -o docs/dist/grade.js", 13 | "prepublish": "npm run build" 14 | }, 15 | "keywords": [ 16 | "complimentary", 17 | "gradients", 18 | "image", 19 | "color", 20 | "complementary" 21 | ], 22 | "homepage": "https://benhowdle89.github.io/grade/", 23 | "author": "Ben Howdle (http://benhowdle.im/)", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "babel-cli": "^6.11.4", 27 | "babel-preset-es2015": "^6.13.2", 28 | "babelify": "^7.3.0", 29 | "browserify": "^13.1.0", 30 | "watchify": "^3.7.0" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/benhowdle89/grade.git" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const prefixes = ['webkit']; 2 | 3 | class Grade { 4 | constructor(container, img_selector, callback) { 5 | this.callback = callback || null 6 | this.container = container; 7 | this.imageContainer = this.container.querySelector(img_selector) || this.container.querySelector('img') 8 | this.gradientData = [] 9 | if(!this.imageContainer || !this.container){ 10 | return 11 | } 12 | this.canvas = document.createElement('canvas'); 13 | this.ctx = this.canvas.getContext('2d'); 14 | this.imageDimensions = { 15 | width: 0, 16 | height: 0 17 | }; 18 | this.imageData = []; 19 | this.image = new Image(); 20 | this.image.src = this.imageContainer.src; 21 | this.image.crossOrigin = "anonymous"; 22 | this.image.onload = () => { 23 | this.readImage(); 24 | }; 25 | } 26 | 27 | readImage() { 28 | this.imageDimensions.width = this.image.width * 0.1; 29 | this.imageDimensions.height = this.image.height * 0.1; 30 | this.render() 31 | } 32 | 33 | getImageData() { 34 | let imageData = this.ctx.getImageData( 35 | 0, 0, this.imageDimensions.width, this.imageDimensions.height 36 | ).data; 37 | this.imageData = Array.from(imageData) 38 | } 39 | 40 | getChunkedImageData() { 41 | const perChunk = 4; 42 | 43 | let chunked = this.imageData.reduce((ar, it, i) => { 44 | const ix = Math.floor(i / perChunk) 45 | if (!ar[ix]) { 46 | ar[ix] = [] 47 | } 48 | ar[ix].push(it); 49 | return ar 50 | }, []); 51 | 52 | let filtered = chunked.filter(rgba => { 53 | return rgba.slice(0, 2).every(val => val < 250) && rgba.slice(0, 2).every(val => val > 0) 54 | }); 55 | 56 | return filtered 57 | } 58 | 59 | getRGBAGradientValues(top) { 60 | return top.map((color, index) => { 61 | return `rgb(${color.rgba.slice(0, 3).join(',')}) ${index == 0 ? '0%' : '75%'}` 62 | }).join(',') 63 | } 64 | 65 | getCSSGradientProperty(top) { 66 | const val = this.getRGBAGradientValues(top); 67 | return prefixes.map(prefix => { 68 | return `background-image: -${prefix}-linear-gradient( 69 | 135deg, 70 | ${val} 71 | )` 72 | }).concat([`background-image: linear-gradient( 73 | 135deg, 74 | ${val} 75 | )`]).join(';') 76 | } 77 | 78 | getMiddleRGB(start, end) { 79 | let w = 0.5 * 2 - 1; 80 | let w1 = (w + 1) / 2.0; 81 | let w2 = 1 - w1; 82 | let rgb = [parseInt(start[0] * w1 + end[0] * w2), parseInt(start[1] * w1 + end[1] * w2), parseInt(start[2] * w1 + end[2] * w2)]; 83 | return rgb; 84 | } 85 | 86 | getSortedValues(uniq) { 87 | const occurs = Object.keys(uniq).map(key => { 88 | const rgbaKey = key; 89 | let components = key.split('|'), 90 | brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 91 | return { 92 | rgba: rgbaKey.split('|'), 93 | occurs: uniq[key], 94 | brightness 95 | } 96 | }).sort((a, b) => a.occurs - b.occurs).reverse().slice(0, 10); 97 | return occurs.sort((a, b) => a.brightness - b.brightness).reverse() 98 | } 99 | 100 | getTextProperty(top) { 101 | let rgb = this.getMiddleRGB(top[0].rgba.slice(0,3), top[1].rgba.slice(0,3)); 102 | let o = Math.round(((parseInt(rgb[0]) * 299) + (parseInt(rgb[1]) * 587) + (parseInt(rgb[2]) * 114)) /1000); 103 | if (o > 125) { 104 | return 'color: #000'; 105 | } else { 106 | return 'color: #fff'; 107 | } 108 | } 109 | 110 | getTopValues(uniq) { 111 | let sorted = this.getSortedValues(uniq); 112 | return [sorted[0], sorted[sorted.length - 1]] 113 | } 114 | 115 | getUniqValues(chunked) { 116 | return chunked.reduce((accum, current) => { 117 | let key = current.join('|'); 118 | if (!accum[key]) { 119 | accum[key] = 1; 120 | return accum 121 | } 122 | accum[key] = ++(accum[key]); 123 | return accum 124 | }, {}) 125 | } 126 | 127 | renderGradient() { 128 | const ls = window.localStorage; 129 | const item_name = `grade-${this.image.getAttribute('src')}`; 130 | let top = null; 131 | 132 | if (ls && ls.getItem(item_name)) { 133 | top = JSON.parse(ls.getItem(item_name)); 134 | } else { 135 | let chunked = this.getChunkedImageData(); 136 | top = this.getTopValues(this.getUniqValues(chunked)); 137 | 138 | if (ls) { 139 | ls.setItem(item_name, JSON.stringify(top)); 140 | } 141 | } 142 | 143 | if(this.callback){ 144 | this.gradientData = top 145 | return 146 | } 147 | 148 | let gradientProperty = this.getCSSGradientProperty(top); 149 | 150 | let textProperty = this.getTextProperty(top); 151 | 152 | let style = `${this.container.getAttribute('style') || ''}; ${gradientProperty}; ${textProperty}`; 153 | this.container.setAttribute('style', style) 154 | } 155 | 156 | render() { 157 | this.canvas.width = this.imageDimensions.width; 158 | this.canvas.height = this.imageDimensions.height; 159 | this.ctx.drawImage(this.image, 0, 0, this.imageDimensions.width, this.imageDimensions.height); 160 | this.getImageData(); 161 | this.renderGradient(); 162 | } 163 | } 164 | 165 | module.exports = (containers, img_selector, callback) => { 166 | const init = (container, img_selector, callback) => { 167 | let grade = new Grade(container, img_selector, callback), 168 | gradientData = grade.gradientData 169 | if(!gradientData.length){ 170 | return null 171 | } 172 | return { 173 | element: container, 174 | gradientData 175 | } 176 | } 177 | let results = (NodeList.prototype.isPrototypeOf(containers) 178 | ? Array.from(containers).map(container => init(container, img_selector, callback)) 179 | : [init(containers, img_selector, callback)]).filter(Boolean) 180 | 181 | if(results.length){ 182 | return callback(results) 183 | } 184 | }; 185 | --------------------------------------------------------------------------------