├── .babelrc ├── .bithoundrc ├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo ├── App.jsx ├── Demo.jsx ├── images │ └── bgnoise_lg.png ├── index.js └── main.css ├── karma.conf.js ├── lib ├── index_template.ejs ├── post_install.js └── render.jsx ├── package.json ├── screenshot.png ├── src └── index.js ├── style.css ├── tests └── boilerplate_test.js └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "env": { 7 | "start": { 8 | "presets": [ 9 | "react-hmre" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.bithoundrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "dist/*.js" 4 | ] 5 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "jasmine": true 7 | }, 8 | "plugins": [ 9 | "react" 10 | ], 11 | "globals": { 12 | "jest": false 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 6, 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "sourceType": "module" 20 | }, 21 | "rules": { 22 | "no-underscore-dangle": 0, 23 | "no-use-before-define": 0, 24 | "quotes": [2, "single"], 25 | "comma-dangle": 1, 26 | "jsx-quotes": 1, 27 | "react/jsx-boolean-value": 1, 28 | "react/jsx-no-undef": 1, 29 | "react/jsx-sort-props": 1, 30 | "react/jsx-uses-react": 1, 31 | "react/jsx-uses-vars": 1, 32 | "react/no-did-mount-set-state": 1, 33 | "react/no-did-update-set-state": 1, 34 | "react/no-multi-comp": 1, 35 | "react/no-unknown-property": 1, 36 | "react/prop-types": 1, 37 | "react/react-in-jsx-scope": 1, 38 | "react/self-closing-comp": 1, 39 | "react/wrap-multilines": 1 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | gh-pages/ 3 | dist-modules/ 4 | node_modules/ 5 | dist/ 6 | .idea/ 7 | npm-debug.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | dist/ 3 | tests/ 4 | src/ 5 | .* 6 | karma.conf.js 7 | webpack.config.babel.js 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4.0" 5 | - "4.1" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Juho Vepsalainen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-gradient-color-picker 3 | 4 | This is a simple gradient color picker integrated with react. 5 | The reason I decide to develop it since there's no usable gradient color picker on npm so far (2015/12/30). Please join me to make it better and more useful. 6 | Please checkout the example at [here](https://www.npmjs.com/package/react-gradient-color-picker). 7 | 8 | ![Alt text](screenshot.png "Optional title") 9 | 10 | ## Development 11 | 12 | Please checkout the code at [here](https://github.com/javidhsueh/react-gradient-color-picker). 13 | * Linting - **npm run lint** - Runs ESLint. 14 | * Testing - **npm test** and **npm run tdd** - Runs Karma/Mocha/Chai/Phantom. Code coverage report is generated through istanbul/isparta to `build/`. 15 | * Developing - **npm start** - Runs the development server at *localhost:8080* and use Hot Module Replacement. You can override the default host and port through env (`HOST`, `PORT`). 16 | 17 | ## Installation 18 | ```sh 19 | npm install --save react-gradient-color-picker 20 | ``` 21 | 22 | ## Properties 23 | ### stops {array} default: 24 | ```js 25 | [ 26 | {offset: 0.0, color: '#f00', opacity: 1.0}, 27 | {offset: 0.5, color: '#fff', opacity: 1.0}, 28 | {offset: 1.0, color: '#0f0', opacity: 1.0} 29 | ] 30 | ``` 31 | 32 | The color stops of the color map. 33 | 34 | ### onChange {func} 35 | 36 | Callback called on every value change. 37 | The return value is a d3 linear color scale. Input value range is between 0 to 1. 38 | It only triggers when the stop color changes or end of dragging the handlers. 39 | 40 | ### width {number} 41 | The width of the component. 42 | 43 | ## API 44 | ### getColorStops 45 | return an array of color stops 46 | 47 | ### getColorMap 48 | return a D3 color scale function. 49 | 50 | 51 | ## Highlighting Demo 52 | 53 | ```js 54 | render() { 55 | var style = { 56 | width: '300px' 57 | }; 58 | var stops = [ 59 | {offset: 0.0, color: '#f00', opacity: 1.0}, 60 | {offset: 0.5, color: '#fff', opacity: 1.0}, 61 | {offset: 1.0, color: '#0f0', opacity: 1.0} 62 | ]; 63 | var onChangeCallback = function onChangeCallback(colorStops, colorMap) { 64 | // colorStops: an array of color stops 65 | // colorMap: a d3 linear scale function 66 | // how to get the mapped color: 67 | // var mappedColor = colorMap(0.8); 68 | } 69 | return ( 70 |
71 | 72 |
73 | ); 74 | } 75 | ``` 76 | 77 | ## License 78 | 79 | *react-gradient-color-picker* is available under MIT. See LICENSE for more details. 80 | 81 | -------------------------------------------------------------------------------- /demo/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Fork from 'react-ghfork'; 3 | import pkgInfo from '../package.json'; 4 | import Demo from './Demo.jsx'; 5 | 6 | export default class App extends React.Component { 7 | render() { 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactGradientColorPicker from '../src/index' 3 | 4 | export default class Demo extends React.Component { 5 | 6 | render() { 7 | var style = { 8 | width: '800px', 9 | height: '200px' 10 | }; 11 | var stops = [ 12 | {offset: 0.0, color: '#f00', opacity: 1.0}, 13 | {offset: 0.5, color: '#0f0', opacity: 0.5}, 14 | {offset: 1.0, color: '#00f', opacity: 0.1} 15 | ]; 16 | /* eslint-disable no-unused-vars */ 17 | var onChangeCallback = function onChangeCallback(colorStops, colorMap) { 18 | // colorStops: an array of color stops 19 | // colorMap: a d3 linear scale function 20 | // how to get the mapped color: 21 | // var mappedColor = colorMap(0.8); 22 | } 23 | /* eslint-enable no-unused-vars */ 24 | return ( 25 |
26 |
27 |

HSL

28 | 33 |

HCL

34 | 39 | 40 |
41 |
42 |

Lab

43 | 48 |

RGB

49 | 54 |
55 |
56 | ); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /demo/images/bgnoise_lg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javidhsueh/react-gradient-color-picker/2fc8c695b8e74dbbc1328f0729963572df875887/demo/images/bgnoise_lg.png -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App.jsx'; 4 | 5 | const app = document.getElementsByClassName('demonstration')[0]; 6 | 7 | ReactDOM.render(, app); 8 | 9 | -------------------------------------------------------------------------------- /demo/main.css: -------------------------------------------------------------------------------- 1 | /* TODO: insert main styles of your demo here */ 2 | 3 | body { 4 | background: #fefefe; 5 | 6 | font-family: Sans-Serif; 7 | 8 | line-height: 1.5; 9 | } 10 | 11 | form label { 12 | display: block; 13 | } 14 | 15 | header { 16 | position: fixed; 17 | padding: 0.5em; 18 | top: 0; 19 | 20 | text-align: center; 21 | 22 | color: #333; 23 | border: 0.1em solid #bbb; 24 | 25 | background-image: url(./images/bgnoise_lg.png); 26 | } 27 | 28 | header h1 { 29 | margin: 0; 30 | } 31 | 32 | header .description { 33 | color: #888; 34 | } 35 | 36 | .highlight { 37 | background: rgb(255, 255, 175); 38 | } 39 | 40 | .sticky { 41 | position: fixed; 42 | top: 0; 43 | left: 0; 44 | right: 0; 45 | } 46 | 47 | .github-fork-ribbon { 48 | position: fixed; 49 | 50 | letter-spacing: 0.01em; 51 | } 52 | 53 | article { 54 | display: block !important; 55 | 56 | margin-top: 6em; 57 | margin-left: auto; 58 | margin-right: auto; 59 | 60 | max-width: 768px; 61 | } 62 | 63 | article section { 64 | margin-bottom: 2em; 65 | } 66 | 67 | article h2 { 68 | margin-top: 2em; 69 | } 70 | 71 | pre { 72 | background: #fafefe; 73 | padding: 0.5em; 74 | } 75 | 76 | article .description { 77 | margin: 1em; 78 | padding: 1em; 79 | 80 | max-width: 60em; 81 | 82 | background: #fafafa; 83 | 84 | border: 0.1em solid #eee; 85 | } 86 | 87 | article .description h2 { 88 | margin: 0; 89 | } 90 | .left { 91 | float: left; 92 | } 93 | .right { 94 | float: right; 95 | } 96 | 97 | .halfWidth { 98 | width: 40%; 99 | margin: 30px; 100 | } -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Reference: http://karma-runner.github.io/0.13/config/configuration-file.html 2 | require('babel-register'); 3 | 4 | module.exports = function karmaConfig (config) { 5 | config.set({ 6 | frameworks: [ 7 | // Reference: https://github.com/karma-runner/karma-mocha 8 | // Set framework to mocha 9 | 'mocha', 10 | 11 | // Reference: http://chaijs.com/api/bdd/ 12 | // Use chai assertions 13 | 'chai' 14 | ], 15 | 16 | reporters: [ 17 | // Reference: https://github.com/mlex/karma-spec-reporter 18 | // Set reporter to print detailed results to console 19 | 'spec', 20 | 21 | // Reference: https://github.com/karma-runner/karma-coverage 22 | // Output code coverage files 23 | 'coverage' 24 | ], 25 | 26 | files: [ 27 | // Reference: https://www.npmjs.com/package/phantomjs-polyfill 28 | // Needed because React.js requires bind and phantomjs does not support it 29 | 'node_modules/phantomjs-polyfill/bind-polyfill.js', 30 | 31 | // Grab all files in the tests directory that contain _test. 32 | 'tests/**/*_test.*' 33 | ], 34 | 35 | preprocessors: { 36 | // Reference: http://webpack.github.io/docs/testing.html 37 | // Reference: https://github.com/webpack/karma-webpack 38 | // Convert files with webpack and load sourcemaps 39 | 'tests/**/*_test.*': ['webpack', 'sourcemap'] 40 | }, 41 | 42 | browsers: [ 43 | // Run tests using PhantomJS 44 | 'PhantomJS' 45 | ], 46 | 47 | singleRun: true, 48 | 49 | // Configure code coverage reporter 50 | coverageReporter: { 51 | dir: 'build/coverage/', 52 | type: 'html' 53 | }, 54 | 55 | // Test webpack config 56 | webpack: require('./webpack.config.babel'), 57 | 58 | // Hide webpack build information from output 59 | webpackMiddleware: { 60 | noInfo: true 61 | } 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /lib/index_template.ejs: -------------------------------------------------------------------------------- 1 | 2 | manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title || 'Webpack App'%> 6 | 7 | <% if (htmlWebpackPlugin.files.favicon) { %> 8 | 9 | <% } %> 10 | 11 | <% if (htmlWebpackPlugin.options.mobile) { %> 12 | 13 | <% } %> 14 | 15 | <% for (var css in htmlWebpackPlugin.files.css) { %> 16 | 17 | <% } %> 18 | 19 | <% if (htmlWebpackPlugin.options.baseHref) { %> 20 | 21 | <% } %> 22 | 23 | 24 |
25 |
26 | <% if (htmlWebpackPlugin.options.name) { %> 27 |

<%= htmlWebpackPlugin.options.name %>

28 | <% } %> 29 | 30 | <% if (htmlWebpackPlugin.options.description) { %> 31 |
<%= htmlWebpackPlugin.options.description %>
32 | <% } %> 33 |
34 | 35 |
36 |

Demonstration

37 |
38 | <%= htmlWebpackPlugin.options.demonstration %> 39 |
40 | 41 |

Documentation

42 |
43 | <%= htmlWebpackPlugin.options.documentation %> 44 |
45 |
46 |
47 | 48 | <% for (var chunk in htmlWebpackPlugin.files.chunks) { %> 49 | 50 | <% } %> 51 | 52 | 53 | -------------------------------------------------------------------------------- /lib/post_install.js: -------------------------------------------------------------------------------- 1 | // adapted based on rackt/history (MIT) 2 | // Node 0.10+ 3 | var execSync = require('child_process').execSync; 4 | var stat = require('fs').stat; 5 | 6 | // Node 0.10 check 7 | // if (!execSync) { 8 | // execSync = require('sync-exec'); 9 | // } 10 | 11 | function exec(command) { 12 | execSync(command, { 13 | stdio: [0, 1, 2] 14 | }); 15 | } 16 | 17 | stat('dist-modules', function(error, stat) { 18 | // Skip building on Travis 19 | if (process.env.TRAVIS) { 20 | return; 21 | } 22 | 23 | if (error || !stat.isDirectory()) { 24 | exec('npm i babel-cli babel-preset-es2015 babel-preset-react'); 25 | exec('npm run dist:modules'); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /lib/render.jsx: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom/server'; 6 | import remark from 'remark'; 7 | import reactRenderer from 'remark-react'; 8 | 9 | export default function (rootPath, context, demoTemplate) { 10 | demoTemplate = demoTemplate || ''; 11 | 12 | var readme = fs.readFileSync( 13 | path.join(rootPath, 'README.md'), 'utf8' 14 | ); 15 | 16 | return { 17 | name: context.name, 18 | description: context.description, 19 | demonstration: demoTemplate, 20 | documentation: ReactDOM.renderToStaticMarkup( 21 |
22 | {remark().use(reactRenderer).process(readme)} 23 |
24 | ) 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-gradient-color-picker", 3 | "description": "react gradient color picker", 4 | "author": "Javid Hsueh", 5 | "user": "javidhsueh", 6 | "version": "0.1.2", 7 | "scripts": { 8 | "start": "webpack-dev-server", 9 | "test": "karma start", 10 | "test:tdd": "karma start --auto-watch --no-single-run", 11 | "test:lint": "eslint . --ext .js --ext .jsx --ignore-path .gitignore --ignore-pattern dist", 12 | "gh-pages": "webpack", 13 | "gh-pages:deploy": "gh-pages -d gh-pages", 14 | "gh-pages:stats": "webpack --profile --json > stats.json", 15 | "dist": "webpack", 16 | "dist:min": "webpack", 17 | "dist:modules": "babel ./src --out-dir ./dist-modules", 18 | "pretest": "npm run test:lint", 19 | "preversion": "npm run test && npm run dist && npm run dist:min && git commit --allow-empty -am \"Update dist\"", 20 | "prepublish": "npm run dist:modules", 21 | "postpublish": "npm run gh-pages && npm run gh-pages:deploy", 22 | "postinstall": "node lib/post_install.js" 23 | }, 24 | "main": "dist-modules", 25 | "dependencies": { 26 | "d3": "^3.5.12", 27 | "lodash": "^3.10.1", 28 | "react": "^0.14.7", 29 | "react-colors-picker": "^2.3.0", 30 | "react-dom": "^0.14.7" 31 | }, 32 | "devDependencies": { 33 | "babel-cli": "^6.5.1", 34 | "babel-core": "^6.5.2", 35 | "babel-loader": "^6.2.3", 36 | "babel-preset-es2015": "^6.5.0", 37 | "babel-preset-react": "^6.5.0", 38 | "babel-preset-react-hmre": "^1.1.0", 39 | "babel-register": "^6.5.2", 40 | "chai": "^3.5.0", 41 | "clean-webpack-plugin": "^0.1.8", 42 | "css-loader": "^0.23.1", 43 | "eslint": "^2.2.0", 44 | "eslint-loader": "^1.3.0", 45 | "eslint-plugin-react": "^4.0.0", 46 | "extract-text-webpack-plugin": "^1.0.1", 47 | "file-loader": "^0.8.5", 48 | "gh-pages": "^0.10.0", 49 | "git-prepush-hook": "^1.0.1", 50 | "highlight.js": "^9.1.0", 51 | "html-webpack-plugin": "^2.9.0", 52 | "isparta-instrumenter-loader": "^1.0.0", 53 | "json-loader": "^0.5.4", 54 | "karma": "^0.13.21", 55 | "karma-chai": "^0.1.0", 56 | "karma-coverage": "^0.5.3", 57 | "karma-mocha": "^0.2.2", 58 | "karma-phantomjs-launcher": "^1.0.0", 59 | "karma-sourcemap-loader": "^0.3.7", 60 | "karma-spec-reporter": "0.0.24", 61 | "karma-webpack": "^1.7.0", 62 | "mocha": "^2.4.5", 63 | "phantomjs-polyfill": "0.0.1", 64 | "phantomjs-prebuilt": "^2.1.4", 65 | "purecss": "^0.6.0", 66 | "react-addons-test-utils": "^0.14.7", 67 | "react-ghfork": "^0.3.2", 68 | "remark": "^4.1.1", 69 | "remark-react": "^2.0.0", 70 | "style-loader": "^0.13.0", 71 | "sync-exec": "^0.6.2", 72 | "system-bell-webpack-plugin": "^1.0.0", 73 | "url-loader": "^0.5.7", 74 | "webpack": "^1.12.14", 75 | "webpack-dev-server": "^1.14.1", 76 | "webpack-merge": "^0.7.3" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "https://github.com/javidhsueh/react-gradient-color-picker.git" 81 | }, 82 | "homepage": "https://javidhsueh.github.io/react-gradient-color-picker/", 83 | "bugs": { 84 | "url": "https://github.com/javidhsueh/react-gradient-color-picker/issues" 85 | }, 86 | "keywords": [ 87 | "react", 88 | "reactjs", 89 | "color", 90 | "picker", 91 | "gradient" 92 | ], 93 | "license": "MIT", 94 | "pre-push": [ 95 | "test", 96 | "test:lint" 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javidhsueh/react-gradient-color-picker/2fc8c695b8e74dbbc1328f0729963572df875887/screenshot.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash'; 4 | import ColorPicker from 'react-colors-picker'; 5 | import d3 from 'd3'; 6 | import React from 'react'; 7 | 8 | const HandlerWidth = 4; 9 | const ColorPickerWidth = 16; 10 | const Width = 300; 11 | const Height = 15; 12 | const DefaultColorSpace = 'HSL'; 13 | 14 | const ColorSpaces = { 15 | 'HSL': d3.interpolateHsl, 16 | 'HCL': d3.interpolateHcl, 17 | 'Lab': d3.interpolateLab, 18 | 'RGB': d3.interpolateRgb 19 | }; 20 | 21 | const DefaultStops = [ 22 | { offset: 0.0, color: '#00f', opacity: 1.0 }, 23 | { offset: 0.5, color: '#0f0', opacity: 0.5 }, 24 | { offset: 1.0, color: '#f00', opacity: 1.0 } 25 | ]; 26 | 27 | const CompareOffset = function CompareOffset(a, b) { 28 | return a.offset - b.offset; 29 | }; 30 | const ColorPickerID = function ColorPickerID(containerID, idx) { 31 | return containerID + '_gc-cp_' + idx; 32 | } 33 | 34 | class ReactGradientColorPicker extends React.Component { 35 | 36 | constructor(props) { 37 | super(props); 38 | 39 | // TODO: how to get auto-expanded width 40 | var rootHeight = Height; 41 | var rootWidth = Width; 42 | if (this.props.width) { 43 | rootWidth = this.props.width; 44 | } 45 | 46 | this.containerID = _.uniqueId('gc-canvas_'); 47 | this.svg = null; 48 | var defaultStops = this.props.stops || DefaultStops; 49 | var stops = defaultStops.map((stop, idx) => { 50 | return { 51 | idx: idx, 52 | x: rootWidth * stop.offset, 53 | offset: stop.offset, 54 | color: stop.color, 55 | opacity: stop.opacity 56 | } 57 | }); 58 | 59 | var initColorSpace = DefaultColorSpace; 60 | if (this.props.colorSpace) { 61 | if (ColorSpaces.hasOwnProperty(this.props.colorSpace)) { 62 | initColorSpace = this.props.colorSpace 63 | } else { 64 | /* eslint-disable no-console */ 65 | console.error('Incorrect props: colorSpace should be one of [HSL,HCL,Lab,RGB]'); 66 | /* eslint-enable no-console */ 67 | } 68 | } 69 | 70 | // init state 71 | this.state = { 72 | rootWidth: rootWidth, 73 | rootHeight: rootHeight, 74 | colorSpace: initColorSpace, 75 | stops: stops 76 | }; 77 | } 78 | 79 | _addHandler(mouseX) { 80 | var offset = 1.0 * mouseX / this.state.rootWidth; 81 | var midColor = this.colorScale(offset); 82 | var newStop = { 83 | idx: this.state.stops.length, 84 | x: mouseX, 85 | offset: offset, 86 | color: midColor, 87 | opacity: 1.0 88 | }; 89 | var newStops = this.state.stops.concat([newStop]); 90 | newStops.sort(CompareOffset); 91 | this.setState({ stops: newStops }); 92 | 93 | this._notifyChange(); 94 | } 95 | _removeHandler(idx) { 96 | const {stops: oldStops} = this.state; 97 | const stopIdx = _.findIndex(this.state.stops, {idx}); 98 | if (stopIdx !== -1) { 99 | this.setState({ 100 | stops: [ 101 | ...oldStops.slice(0, stopIdx), 102 | ...oldStops.slice(stopIdx + 1) 103 | ] 104 | }); 105 | } 106 | } 107 | 108 | _dragHandler(d, mouseX, colorPickerID) { 109 | // only update handler position but not state 110 | d.x = mouseX; 111 | d3.select(this).attr('x', mouseX); 112 | d3.select('#' + colorPickerID) 113 | .style('left', (d.x - ColorPickerWidth / 2) + 'px') 114 | .style('top', Height + 'px'); 115 | } 116 | 117 | _dragHandlerEnd(d) { 118 | // when the end of drag, update the state once. 119 | var newStops = _.cloneDeep(this.state.stops); 120 | var currentHandler = _.find(newStops, { 'idx': d.idx }); 121 | currentHandler.offset = 1.0 * d.x / this.state.rootWidth; 122 | currentHandler.x = d.x; 123 | this.setState({ stops: newStops }); 124 | 125 | this._notifyChange(); 126 | } 127 | 128 | _notifyChange() { 129 | if (this.props.onChange) { 130 | this.props.onChange(this.state.stops, this.colorScale); 131 | } 132 | } 133 | 134 | componentDidMount() { 135 | // TODO: get the auto-expanded comonent width 136 | var rootWidth = this.refs.root.offsetWidth; 137 | var rootHeight = this.state.rootHeight; 138 | if (this.props.width) { 139 | rootWidth = this.props.width; 140 | } 141 | var newStops = this.state.stops.map((stop) => { 142 | stop.x = stop.offset * rootWidth; 143 | return stop; 144 | }); 145 | 146 | // TODO: this is anti-pattern. should fix it soon. 147 | /* eslint-disable react/no-did-mount-set-state */ 148 | this.setState({ 149 | rootWidth: rootWidth, 150 | stops: newStops 151 | }); 152 | /* eslint-enable react/no-did-mount-set-state */ 153 | 154 | var self = this; 155 | // init canvas 156 | this.canvas = d3.select('#' + this.containerID) 157 | .append('canvas') 158 | .attr('width', rootWidth) 159 | .attr('height', 1) 160 | .style('width', rootWidth + 'px') 161 | .style('height', rootHeight + 'px'); 162 | 163 | this.svg = d3.select('#' + this.containerID) 164 | .append('svg') 165 | .attr('width', rootWidth) 166 | .attr('height', rootHeight); 167 | 168 | this.colorMap = this.svg.append('rect') 169 | .attr('id', 'gc-color-map') 170 | .attr('x', 0) 171 | .attr('y', 0) 172 | .attr('width', rootWidth) 173 | .attr('height', rootHeight) 174 | .attr('fill', 'rgba(0,0,0,0)') 175 | .on('click', function clickColorMap() { 176 | var mouseX = d3.mouse(this)[0]; 177 | self._addHandler(mouseX); 178 | }); 179 | } 180 | 181 | _refreshCanvas() { 182 | if (this.svg === null) { 183 | return; 184 | } 185 | 186 | var rootWidth = this.state.rootWidth; 187 | var rootHeight = this.state.rootHeight; 188 | 189 | // refresh canvas size 190 | this.canvas 191 | .attr('width', this.state.rootWidth) 192 | .attr('height', 1) 193 | .style('width', this.state.rootWidth + 'px') 194 | .style('height', this.state.rootHeight + 'px'); 195 | 196 | this.svg.attr('width', rootWidth) 197 | .attr('height', rootHeight); 198 | 199 | // refresh color scale 200 | var stops = this.state.stops.map(function iterator(s) { 201 | return { 202 | offset: s.offset, 203 | color: s.color, 204 | opacity: s.opacity 205 | }; 206 | }).sort(CompareOffset); 207 | var offsets = _.map(stops, 'offset'); 208 | var colors = _.map(stops, 'color'); 209 | var opacity = _.map(stops, 'opacity'); 210 | 211 | this.colorScale = d3.scale.linear() 212 | .domain(offsets) 213 | .range(colors) 214 | .interpolate(ColorSpaces[this.state.colorSpace]); 215 | 216 | this.opacityScale = d3.scale.linear() 217 | .domain(offsets) 218 | .range(opacity); 219 | 220 | var _localColorScale = this.colorScale; 221 | var _localOpacityScale = this.opacityScale; 222 | 223 | this.colorMap 224 | .attr('width', rootWidth) 225 | .attr('height', rootHeight); 226 | 227 | this.canvas.each(function renderCanvas() { 228 | var context = this.getContext('2d'), 229 | image = context.createImageData(rootWidth, 1); 230 | for (var i = 0, j = -1, c, a; i < rootWidth; ++i) { 231 | c = d3.rgb(_localColorScale(i * 1.0 / rootWidth)); 232 | a = _localOpacityScale(i * 1.0 / rootWidth); 233 | image.data[++j] = c.r; 234 | image.data[++j] = c.g; 235 | image.data[++j] = c.b; 236 | image.data[++j] = a*255; 237 | } 238 | context.putImageData(image, 0, 0); 239 | }) 240 | // refresh gradient 241 | var gradientID = this.containerID + '_gc-gradient'; 242 | this.gradient = this.svg.select('#' + gradientID) 243 | .attr('x2', rootWidth) 244 | .selectAll('stop') 245 | .data(this.state.stops); 246 | 247 | // enter stops 248 | this.gradient.enter() 249 | .append('stop') 250 | .attr('offset', function offsetAccessor(d) { 251 | return (d.offset * 100) + '%'; 252 | }) 253 | .attr('stop-color', function colorAccessor(d) { 254 | return d.color; 255 | }); 256 | // update existing stops 257 | this.gradient 258 | .attr('offset', (d) => (d.offset * 100).toString() + '%') 259 | .attr('stop-color', (d) => d.color); 260 | 261 | // remove non-exist stops 262 | this.gradient.exit().remove(); 263 | 264 | // refresh handlers 265 | this.handlers = this.svg.selectAll('.gc-handler') 266 | .data(this.state.stops); 267 | 268 | // enter new handlers 269 | var self = this; 270 | var dragCallback = function dragCallback(d) { 271 | var newX = d3.event.x; 272 | if (newX >= 0 && newX <= self.state.rootWidth) { 273 | self._dragHandler.call(this, d, newX, ColorPickerID(self.containerID, d.idx)); 274 | } 275 | } 276 | var drag = d3.behavior.drag() 277 | .origin(Object) 278 | .on('drag', dragCallback) 279 | .on('dragend', this._dragHandlerEnd.bind(this)); 280 | 281 | this.handlers.enter() 282 | .append('rect') 283 | .attr('class', 'gc-handler') 284 | .attr('x', function xPos(d) { 285 | return d.x - HandlerWidth / 2; 286 | }.bind(this)) 287 | .attr('y', '0') 288 | .attr('width', HandlerWidth) 289 | .attr('height', rootHeight) 290 | .call(drag); 291 | 292 | // update existing handlers 293 | this.handlers 294 | .attr('x', function xPos(d) { 295 | return d.x - HandlerWidth / 2; 296 | }.bind(this)); 297 | 298 | // remove non-exist handlers 299 | this.handlers.exit().remove(); 300 | 301 | // refresh the color pickers 302 | this.state.stops.forEach(function iterator(s) { 303 | d3.select('#' + ColorPickerID(this.containerID, s.idx)) 304 | .style('left', (s.x - ColorPickerWidth / 2) + 'px') 305 | .style('top', Height + 'px'); 306 | }.bind(this)); 307 | } 308 | 309 | render() { 310 | this._refreshCanvas(); 311 | 312 | var colorChangeCallback = function colorChangeCallback(color, opacity, idx) { 313 | var newStops = _.cloneDeep(this.state.stops); 314 | var currentHandler = _.find(newStops, { 'idx': idx }); 315 | currentHandler.color = color; 316 | currentHandler.opacity = 1.0 * opacity / 100; 317 | this.setState({ stops: newStops }); 318 | 319 | // notify change 320 | if (this.props.onChange) { 321 | this.props.onChange(this.state.stops, this.colorScale); 322 | } 323 | }.bind(this); 324 | var colorpickers = this.state.stops.map(function iterator(s) { 325 | let pickerId = ColorPickerID(this.containerID, s.idx); 326 | let removeCallback = () => this._removeHandler(s.idx); 327 | let callback = (c) => colorChangeCallback(c.color, c.alpha, s.idx); 328 | var style = { 329 | left: (s.x - ColorPickerWidth / 2) + 'px', 330 | top: Height + 'px' 331 | } 332 | return ( 333 |
334 | < div id = { pickerId } 335 | key = { pickerId } > 336 | < ColorPicker animation = "slide-up" 337 | color = { s.color } 338 | alpha = {s.opacity * 100} 339 | onChange = { callback } 340 | placement = "bottomLeft" / > 341 | < /div> 342 |
x
344 |
345 | ); 346 | }.bind(this)); 347 | return ( < div className = "gc-container" 348 | ref = "root" > { colorpickers } < div className = "gc-canvas" 349 | id = { this.containerID } > < /div> < /div> 350 | ); 351 | } 352 | 353 | // Publid API: 354 | getColorMap() { 355 | return this.colorScale; 356 | } 357 | 358 | getColorStops() { 359 | return this.state.stops; 360 | } 361 | } 362 | 363 | ReactGradientColorPicker.propTypes = { 364 | stops: React.PropTypes.arrayOf(React.PropTypes.object), 365 | colorSpace: React.PropTypes.string, 366 | onChange: React.PropTypes.func, 367 | width: React.PropTypes.number 368 | }; 369 | 370 | module.exports = ReactGradientColorPicker; 371 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import 'node_modules/react-colors-picker/assets/index'; 2 | 3 | .gc-container { 4 | width: 100%; 5 | margin: 0 0; 6 | padding: 0 0; 7 | position: relative; 8 | } 9 | 10 | .gc-container .react-colorpicker-trigger { 11 | width: 10px; 12 | height: 10px; 13 | } 14 | 15 | .gc-container .gc-canvas { 16 | width: 100%; 17 | height: 20px; 18 | background-color: white; 19 | } 20 | .gc-container svg { 21 | position: absolute; 22 | top: 0px; 23 | left: 0px; 24 | } 25 | 26 | .gc-container canvas { 27 | position: absolute; 28 | top: 0px; 29 | left: 0px; 30 | } 31 | 32 | .gc-container .gc-handler { 33 | fill: white; 34 | stroke: gray; 35 | fill-opacity: 0.7; 36 | cursor: move; 37 | } 38 | 39 | .gc-container .gc-colorpicker { 40 | position: absolute; 41 | } 42 | 43 | .gc-colorpicker .remove-btn { 44 | position: relative; 45 | position: absolute; 46 | top: 15px; 47 | left: 4px; 48 | cursor: pointer; 49 | opacity: 0; 50 | } 51 | .gc-colorpicker:hover .remove-btn { 52 | opacity: 1; 53 | } 54 | .react-colorpicker { 55 | border: 1px solid #999; 56 | padding: 0px; 57 | border-radius: 2px; 58 | } 59 | 60 | .react-colorpicker-trigger { 61 | border: 0px; 62 | margin-bottom: -2px; 63 | } -------------------------------------------------------------------------------- /tests/boilerplate_test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { 3 | // renderIntoDocument, 4 | // findRenderedDOMComponentWithClass, 5 | // findRenderedDOMComponentWithTag, 6 | // Simulate 7 | //} from 'react-addons-test-utils'; 8 | 9 | describe('Boilerplate', function() { 10 | it('should do boilerplate things', function() { 11 | // TODO: test something now 12 | expect(true).to.equal(true); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import webpack from 'webpack'; 4 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 5 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 6 | import SystemBellPlugin from 'system-bell-webpack-plugin'; 7 | import CleanPlugin from 'clean-webpack-plugin'; 8 | import merge from 'webpack-merge'; 9 | import React from 'react'; 10 | import ReactDOM from 'react-dom/server'; 11 | 12 | import renderJSX from './lib/render.jsx'; 13 | import App from './demo/App.jsx'; 14 | import pkg from './package.json'; 15 | 16 | const RENDER_UNIVERSAL = true; 17 | const TARGET = process.env.npm_lifecycle_event; 18 | const ROOT_PATH = __dirname; 19 | const config = { 20 | paths: { 21 | dist: path.join(ROOT_PATH, 'dist'), 22 | src: path.join(ROOT_PATH, 'src'), 23 | demo: path.join(ROOT_PATH, 'demo'), 24 | tests: path.join(ROOT_PATH, 'tests') 25 | }, 26 | filename: 'boilerplate', 27 | library: 'Boilerplate' 28 | }; 29 | const CSS_PATHS = [ 30 | config.paths.demo, 31 | path.join(ROOT_PATH, 'style.css'), 32 | path.join(ROOT_PATH, 'node_modules/purecss'), 33 | path.join(ROOT_PATH, 'node_modules/highlight.js/styles/github.css'), 34 | path.join(ROOT_PATH, 'node_modules/react-ghfork/gh-fork-ribbon.ie.css'), 35 | path.join(ROOT_PATH, 'node_modules/react-ghfork/gh-fork-ribbon.css') 36 | ]; 37 | const STYLE_ENTRIES = [ 38 | 'purecss', 39 | 'highlight.js/styles/github.css', 40 | 'react-ghfork/gh-fork-ribbon.ie.css', 41 | 'react-ghfork/gh-fork-ribbon.css', 42 | './demo/main.css', 43 | './style.css' 44 | ]; 45 | 46 | process.env.BABEL_ENV = TARGET; 47 | 48 | const demoCommon = { 49 | resolve: { 50 | extensions: ['', '.js', '.jsx', '.css', '.png', '.jpg'] 51 | }, 52 | module: { 53 | preLoaders: [ 54 | { 55 | test: /\.jsx?$/, 56 | loaders: ['eslint'], 57 | include: [ 58 | config.paths.demo, 59 | config.paths.src 60 | ] 61 | } 62 | ], 63 | loaders: [ 64 | { 65 | test: /\.png$/, 66 | loader: 'url?limit=100000&mimetype=image/png', 67 | include: config.paths.demo 68 | }, 69 | { 70 | test: /\.jpg$/, 71 | loader: 'file', 72 | include: config.paths.demo 73 | }, 74 | { 75 | test: /\.json$/, 76 | loader: 'json', 77 | include: path.join(ROOT_PATH, 'package.json') 78 | } 79 | ] 80 | }, 81 | plugins: [ 82 | new SystemBellPlugin() 83 | ] 84 | }; 85 | 86 | if (TARGET === 'start') { 87 | module.exports = merge(demoCommon, { 88 | devtool: 'eval-source-map', 89 | entry: { 90 | demo: [config.paths.demo].concat(STYLE_ENTRIES) 91 | }, 92 | plugins: [ 93 | new webpack.DefinePlugin({ 94 | 'process.env.NODE_ENV': '"development"' 95 | }), 96 | new HtmlWebpackPlugin(Object.assign({}, { 97 | title: pkg.name + ' - ' + pkg.description, 98 | template: 'lib/index_template.ejs', 99 | 100 | inject: false 101 | }, renderJSX(__dirname, pkg))), 102 | new webpack.HotModuleReplacementPlugin() 103 | ], 104 | module: { 105 | loaders: [ 106 | { 107 | test: /\.css$/, 108 | loaders: ['style', 'css'], 109 | include: CSS_PATHS 110 | }, 111 | { 112 | test: /\.jsx?$/, 113 | loaders: ['babel?cacheDirectory'], 114 | include: [ 115 | config.paths.demo, 116 | config.paths.src 117 | ] 118 | } 119 | ] 120 | }, 121 | devServer: { 122 | historyApiFallback: true, 123 | hot: true, 124 | inline: true, 125 | progress: true, 126 | host: process.env.HOST, 127 | port: process.env.PORT, 128 | stats: 'errors-only' 129 | } 130 | }); 131 | } 132 | 133 | function NamedModulesPlugin(options) { 134 | this.options = options || {}; 135 | } 136 | NamedModulesPlugin.prototype.apply = function(compiler) { 137 | compiler.plugin('compilation', function(compilation) { 138 | compilation.plugin('before-module-ids', function(modules) { 139 | modules.forEach(function(module) { 140 | if(module.id === null && module.libIdent) { 141 | var id = module.libIdent({ 142 | context: this.options.context || compiler.options.context 143 | }); 144 | 145 | // Skip CSS files since those go through ExtractTextPlugin 146 | if(!id.endsWith('.css')) { 147 | module.id = id; 148 | } 149 | } 150 | }, this); 151 | }.bind(this)); 152 | }.bind(this)); 153 | }; 154 | 155 | if (TARGET === 'gh-pages' || TARGET === 'gh-pages:stats') { 156 | module.exports = merge(demoCommon, { 157 | entry: { 158 | app: config.paths.demo, 159 | vendors: [ 160 | 'react' 161 | ], 162 | style: STYLE_ENTRIES 163 | }, 164 | output: { 165 | path: './gh-pages', 166 | filename: '[name].[chunkhash].js', 167 | chunkFilename: '[chunkhash].js' 168 | }, 169 | plugins: [ 170 | new CleanPlugin(['gh-pages'], { 171 | verbose: false 172 | }), 173 | new ExtractTextPlugin('[name].[chunkhash].css'), 174 | new webpack.DefinePlugin({ 175 | // This affects the react lib size 176 | 'process.env.NODE_ENV': '"production"' 177 | }), 178 | new HtmlWebpackPlugin(Object.assign({}, { 179 | title: pkg.name + ' - ' + pkg.description, 180 | template: 'lib/index_template.ejs', 181 | inject: false 182 | }, renderJSX( 183 | __dirname, pkg, RENDER_UNIVERSAL ? ReactDOM.renderToString() : '') 184 | )), 185 | new NamedModulesPlugin(), 186 | new webpack.optimize.DedupePlugin(), 187 | new webpack.optimize.UglifyJsPlugin({ 188 | compress: { 189 | warnings: false 190 | } 191 | }), 192 | new webpack.optimize.CommonsChunkPlugin({ 193 | names: ['vendors', 'manifest'] 194 | }) 195 | ], 196 | module: { 197 | loaders: [ 198 | { 199 | test: /\.css$/, 200 | loader: ExtractTextPlugin.extract('style', 'css'), 201 | include: CSS_PATHS 202 | }, 203 | { 204 | test: /\.jsx?$/, 205 | loaders: ['babel'], 206 | include: [ 207 | config.paths.demo, 208 | config.paths.src 209 | ] 210 | } 211 | ] 212 | } 213 | }); 214 | } 215 | 216 | // !TARGET === prepush hook for test 217 | if (TARGET === 'test' || TARGET === 'test:tdd' || !TARGET) { 218 | module.exports = merge(demoCommon, { 219 | module: { 220 | preLoaders: [ 221 | { 222 | test: /\.jsx?$/, 223 | loaders: ['eslint'], 224 | include: [ 225 | config.paths.tests 226 | ] 227 | } 228 | ], 229 | loaders: [ 230 | { 231 | test: /\.jsx?$/, 232 | loaders: ['babel?cacheDirectory'], 233 | include: [ 234 | config.paths.src, 235 | config.paths.tests 236 | ] 237 | } 238 | ] 239 | } 240 | }) 241 | } 242 | 243 | const distCommon = { 244 | devtool: 'source-map', 245 | output: { 246 | path: config.paths.dist, 247 | libraryTarget: 'umd', 248 | library: config.library 249 | }, 250 | entry: config.paths.src, 251 | externals: { 252 | 'react': { 253 | commonjs: 'react', 254 | commonjs2: 'react', 255 | amd: 'React', 256 | root: 'React' 257 | } 258 | }, 259 | module: { 260 | loaders: [ 261 | { 262 | test: /\.jsx?$/, 263 | loaders: ['babel'], 264 | include: config.paths.src 265 | } 266 | ] 267 | }, 268 | plugins: [ 269 | new SystemBellPlugin() 270 | ] 271 | }; 272 | 273 | if (TARGET === 'dist') { 274 | module.exports = merge(distCommon, { 275 | output: { 276 | filename: config.filename + '.js' 277 | } 278 | }); 279 | } 280 | 281 | if (TARGET === 'dist:min') { 282 | module.exports = merge(distCommon, { 283 | output: { 284 | filename: config.filename + '.min.js' 285 | }, 286 | plugins: [ 287 | new webpack.optimize.UglifyJsPlugin({ 288 | compress: { 289 | warnings: false 290 | } 291 | }) 292 | ] 293 | }); 294 | } 295 | --------------------------------------------------------------------------------