├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── LICENSE.md ├── README.md ├── docs ├── docs.js ├── docs.scss └── index.html ├── filter.jpg ├── package-lock.json ├── package.json ├── publish-docs.sh ├── source ├── ImageFilter.jsx └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-object-rest-spread" 4 | ], 5 | "presets": [ 6 | "es2015", 7 | "react" 8 | ], 9 | "env": { 10 | "development": { 11 | "presets": [ 12 | "react-hmre" 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Config helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | # We recommend you to keep these unchanged 9 | indent_style = space 10 | indent_size = 2 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | eslint-config-airbnb 3 | 4 | parser: 5 | babel-eslint 6 | 7 | settings: 8 | ecmascript: 6 9 | 10 | ecmaFeatures: 11 | jsx: true 12 | modules: true 13 | destructuring: true 14 | classes: true 15 | forOf: true 16 | blockBindings: true 17 | arrowFunctions: true 18 | 19 | env: 20 | browser: true 21 | 22 | rules: 23 | arrow-body-style: 0 24 | arrow-parens: 0 25 | class-methods-use-this: 0 26 | func-names: 0 27 | indent: 2 28 | new-cap: 0 29 | no-plusplus: 0 30 | max-len: ["error", { "code": 120 }] 31 | no-return-assign: 0 32 | quote-props: 0 33 | template-curly-spacing: [2, "always"] 34 | comma-dangle: ["error", { 35 | "arrays": "always-multiline", 36 | "objects": "always-multiline", 37 | "imports": "always-multiline", 38 | "exports": "always-multiline", 39 | "functions": "never" 40 | }] 41 | jsx-quotes: [2, "prefer-single"] 42 | react/forbid-prop-types: 0 43 | react/jsx-curly-spacing: [2, "always"] 44 | react/jsx-filename-extension: 0 45 | react/jsx-boolean-value: 0 46 | react/prefer-stateless-function: 0 47 | react/no-unescaped-entities: 0 48 | import/extensions: 0 49 | import/no-extraneous-dependencies: 0 50 | import/no-unresolved: 0 51 | import/prefer-default-export: 0 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | dist 4 | .module-cache 5 | *.log* 6 | _nogit 7 | lib 8 | dist-docs 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | dist 4 | dist-docs 5 | docs 6 | publish-docs.sh 7 | .module-cache 8 | *.log* 9 | _nogit 10 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v8.17.0 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - today Stanko Tadić 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Image Filter 2 | 3 | [![npm version](https://img.shields.io/npm/v/react-image-filter.svg?style=flat-square)](https://www.npmjs.com/package/react-image-filter) 4 | [![npm downloads](https://img.shields.io/npm/dm/react-image-filter.svg?style=flat-square)](https://www.npmjs.com/package/react-image-filter) 5 | 6 | Lightweight React component, for applying color filters on images, 7 | works in all modern browsers plus IE10+ and Edge. 8 | Made because CSS filters don't work in IE and Edge :( 9 | 10 | Component is written as ES module, so it will work with webpack and other module bundlers (which is standard for React apps anyway). Tested with `react-create-app` and my boilerplate, [Marvin](https://github.com/workco/marvin). 11 | 12 | 13 | ## Demo 14 | 15 | Check the [interactive demo](https://muffinman.io/react-image-filter/). 16 | 17 | [![Interactive demo](filter.jpg)](https://muffinman.io/react-image-filter/) 18 | 19 | ## Quick start 20 | 21 | Get it from [npm](https://www.npmjs.com/package/react-image-filter) 22 | 23 | ``` 24 | $ npm install --save react-image-filter 25 | ``` 26 | 27 | Import and use it in your React app. 28 | 29 | ```javascript 30 | import React, { Component } from 'react'; 31 | import ImageFilter from 'react-image-filter'; 32 | 33 | class Example extends Component { 34 | render() { 35 | return ( 36 | 42 | ); 43 | } 44 | } 45 | ``` 46 | 47 | ## Table of contents 48 | 49 | * [Props](#user-content-props) 50 | * [Browser support](#user-content-browser-support) 51 | * [License](#user-content-license) 52 | 53 | ## Props 54 | 55 | * **image** string, *required* 56 | 57 | Your image (URL or base64 encoded) 58 | 59 | * **filter** string or array, *required* 60 | 61 | Color filter to be applied, passed to [feColorMatrix](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix). 62 | 63 | This is array of 20 numbers, example: 64 | 65 | ```javascript 66 | [ 67 | 0.3, 0.45, 0.1, 0, 0, 68 | 0.2, 0.45, 0.1, 0, 0, 69 | 0.1, 0.3, 0.1, 0, 0, 70 | 0, 0, 0, 1, 0, 71 | ]; 72 | ``` 73 | 74 | Following presets are available (strings): 75 | 76 | * `duotone` - if you selected duotone, you have to pass `colorOne` and `colorTwo` as well, check beneath 77 | * `invert` 78 | * `grayscale` 79 | * `sepia` 80 | 81 | If you have ideas for new presets, please open an issue titled `Preset: `, so I can add it. Cheers! 82 | 83 | * **colorOne** array 84 | 85 | The first color for duotone filter, array of three numbers which are RED / GREEN / BLUE color values, example: 86 | 87 | ```javascript 88 | [40, 50, 200] 89 | ``` 90 | 91 | * **colorTwo** array 92 | 93 | The second color for duotone filter. Same as `colorOne`. 94 | 95 | * **preserveAspectRatio** string, default: 'none', *required* 96 | 97 | Aspect ratio string, passed to image's [preserveAspectRatio]( https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio). 98 | 99 | You can pass string `cover` to apply the same effect as CSS `background-size: cover`. 100 | 101 | 102 | * **className** string 103 | 104 | CSS class name (it will be applied along with `ImageFilter` class name). 105 | 106 | * **style** object 107 | 108 | CSS style to be applied on wrapper div. Please note that this will override style applied in component. 109 | 110 | * **svgStyle** object 111 | 112 | CSS style to be applied on the SVG element. Please note that this will override style applied in component. 113 | 114 | * **svgProps** object 115 | 116 | Other props to be passed to SVG, example: 117 | 118 | ```javascript 119 | { 120 | 'aria-label': 'My awesome image', 121 | tabIndex: -1, 122 | } 123 | ``` 124 | 125 | * **onChange** function 126 | 127 | Callback to be called on filter change. 128 | 129 | 130 | ## Browser support 131 | 132 | Modern browsers and IE10+. 133 | 134 | ## License 135 | 136 | Released under [MIT License](LICENSE.md). 137 | -------------------------------------------------------------------------------- /docs/docs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ImageFilter from '../source/index'; 4 | import './docs.scss'; 5 | 6 | const NONE = [ 7 | 1, 0, 0, 0, 0, 8 | 0, 1, 0, 0, 0, 9 | 0, 0, 1, 0, 0, 10 | 0, 0, 0, 1, 0, 11 | ]; 12 | 13 | const Example = class extends React.Component { 14 | constructor() { 15 | super(); 16 | 17 | const values = [...NONE]; 18 | 19 | const labels = [ 20 | 'Red to Red', 21 | 'Green to Red', 22 | 'Blue to Red', 23 | 'Alpha to Red', 24 | 'Add to Red', 25 | 'Red to Green', 26 | 'Green to Green', 27 | 'Blue to Green', 28 | 'Alpha to Green', 29 | 'Add to Green', 30 | 'Red to Blue', 31 | 'Green to Blue', 32 | 'Blue to Blue', 33 | 'Alpha to Blue', 34 | 'Add to Blue', 35 | 'Red to Alpha', 36 | 'Green to Alpha', 37 | 'Blue to Alpha', 38 | 'Alpha to Alpha', 39 | 'Add to Alpha', 40 | ]; 41 | 42 | this.state = { 43 | values, 44 | labels, 45 | filter: values, 46 | applyFilter: true, 47 | colorOne: null, 48 | colorTwo: null, 49 | key: new Date().getTime(), 50 | }; 51 | 52 | this.handleToggleFilter = this.handleToggleFilter.bind(this); 53 | } 54 | 55 | handleChange(index, value) { 56 | const { 57 | values, 58 | } = this.state; 59 | 60 | const newValues = [...values]; 61 | newValues[index] = value; 62 | 63 | this.setState({ 64 | values: newValues, 65 | filter: newValues, 66 | }); 67 | } 68 | 69 | handleToggleFilter() { 70 | const { applyFilter } = this.state; 71 | 72 | this.setState({ 73 | applyFilter: !applyFilter, 74 | }); 75 | } 76 | 77 | renderSliders() { 78 | const { 79 | values, 80 | labels, 81 | } = this.state; 82 | 83 | return values.map((value, index) => { 84 | return ( 85 |
86 | { labels[index] }: { parseFloat(value).toFixed(2) } 87 | this.handleChange(index, e.target.value) } 95 | /> 96 |
97 | ); 98 | }); 99 | } 100 | 101 | render() { 102 | const { 103 | filter, 104 | applyFilter, 105 | values, 106 | colorOne, 107 | colorTwo, 108 | key, 109 | } = this.state; 110 | 111 | return ( 112 |
113 |
114 | this.setState({ values: m }) } 123 | /> 124 |
125 |
126 | { this.renderSliders() } 127 |
128 | 129 |
130 |

Presets

131 | 137 | 143 | 149 | 155 | 166 | 177 | 188 | 198 |
199 | 200 |

Misc

201 | 202 | 208 | 214 | 215 |
216 | Please note that Unsplash will sometime return the same image. 217 |
218 | 219 |

Applied props

220 | { typeof filter === 'object' ? 221 |
222 |           const filter = [
223 | {' '}{ values[0] }, { values[1] }, { values[2] }, { values[3] }, { values[4] },
224 | {' '}{ values[5] }, { values[6] }, { values[7] }, { values[8] }, { values[9] },
225 | {' '}{ values[10] }, { values[11] }, { values[12] }, { values[13] }, { values[14] },
226 | {' '}{ values[15] }, { values[16] }, { values[17] }, { values[18] }, { values[19] },
227 | ]; 228 |
: 229 |
230 |             const filter = '{ filter }';
231 | { filter === 'duotone' && 232 | 233 | const colorOne = [{ colorOne[0] }, { colorOne[1] }, { colorOne[2] }];
234 | const colorTwo = [{ colorTwo[0] }, { colorTwo[1] }, { colorTwo[2] }]; 235 |
} 236 |
} 237 |
238 | ); 239 | } 240 | }; 241 | 242 | 243 | ReactDOM.render(, document.getElementById('demo')); 244 | -------------------------------------------------------------------------------- /docs/docs.scss: -------------------------------------------------------------------------------- 1 | $main-color: #e9463f; 2 | 3 | @mixin md() { 4 | @media (min-width: 600px) { 5 | @content; 6 | } 7 | } 8 | 9 | @mixin lg() { 10 | @media (min-width: 1000px) { 11 | @content; 12 | } 13 | } 14 | 15 | body { 16 | background: #fdfdfd; 17 | padding-bottom: 40px; 18 | } 19 | 20 | a, 21 | a:active, 22 | a:visited, 23 | a:focus { 24 | color: $main-color; 25 | } 26 | 27 | a:hover { 28 | color: darken($main-color, 20); 29 | } 30 | 31 | h2, 32 | h3 { 33 | margin-top: 50px; 34 | color: $main-color; 35 | } 36 | 37 | .Header { 38 | border-bottom: 1px solid #ddd; 39 | margin-bottom: 20px; 40 | background: #fff; 41 | padding: 20px 0; 42 | } 43 | 44 | .Content { 45 | position: relative; 46 | margin: 0 auto; 47 | padding: 0 20px; 48 | max-width: 1000px; 49 | } 50 | 51 | .ImageWrapper { 52 | padding-bottom: percentage(8/12); 53 | position: relative; 54 | background: #eee; 55 | border-radius: 5px; 56 | overflow: hidden; 57 | } 58 | 59 | .ImageFilter { 60 | position: absolute !important; 61 | top: 0; 62 | left: 0; 63 | height: 100%; 64 | width: 100%; 65 | } 66 | 67 | .Controls { 68 | margin: 20px 0; 69 | font-size: 14px; 70 | 71 | @include md { 72 | display: -webkit-box; 73 | display: -ms-flexbox; 74 | display: flex; 75 | -ms-flex-wrap: wrap; 76 | flex-wrap: wrap; 77 | -webkit-box-pack: justify; 78 | -ms-flex-pack: justify; 79 | justify-content: space-between; 80 | } 81 | } 82 | 83 | .Control { 84 | padding: 4px 10px; 85 | border: 1px solid #e1e1e8; 86 | border-radius: 5px; 87 | margin-bottom: 10px; 88 | 89 | @include md { 90 | -ms-flex-preferred-size: 19%; 91 | flex-basis: 19%; 92 | max-width: 19%; 93 | width: 19%; 94 | } 95 | 96 | &:hover { 97 | background: #f7f7f9; 98 | } 99 | } 100 | 101 | .Control-input { 102 | // -webkit-appearance: none; 103 | width: 100%; 104 | height: 16px; 105 | accent-color: $main-color; 106 | // background: transparent; 107 | // padding: 0; 108 | // margin: 0; 109 | } 110 | 111 | // .Control-input::-webkit-slider-thumb { 112 | // -webkit-appearance: none; 113 | // } 114 | 115 | // .Control-input:focus { 116 | // outline: none; 117 | 118 | // &::-webkit-slider-runnable-track { 119 | // height: 16px; 120 | // } 121 | // } 122 | 123 | // .Control-input::-webkit-slider-thumb { 124 | // -webkit-appearance: none; 125 | // border: none; 126 | // position: relative; 127 | // top: 50%; 128 | // height: 12px; 129 | // margin-top: -6px; 130 | // width: 32px; 131 | // border-radius: 5px; 132 | // background: $main-color; 133 | // cursor: pointer; 134 | // } 135 | 136 | // .Control-input::-webkit-slider-runnable-track { 137 | // width: 100%; 138 | // cursor: pointer; 139 | // background: #F7F7F9; 140 | // border: none; 141 | // height: 8px; 142 | // border-radius: 5px; 143 | // border: 1px solid #E1E1E8; 144 | // transition: height 0.25s; 145 | // } 146 | 147 | .btn { 148 | margin-right: 5px; 149 | background: #111; 150 | 151 | &:hover { 152 | background: darken($main-color, 20); 153 | } 154 | 155 | &:focus { 156 | background: $main-color; 157 | outline: none; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Image Filter - React component 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | My blog 24 | GitHub 25 | npm 26 |
27 |
28 | 29 |
30 |

React Image Filter

31 | 32 |

33 | ImageFilter - React filter component. 34 |

35 | 36 |

37 | Unfortunately CSS filters don't work in IE and Edge. 38 | This is React component that used SVG filters instead, 39 | and it works in all modern browsers plus IE10+ and Edge 40 | For props and presets check the detailed documentation on 41 | GitHub. 42 |

43 | 44 |

Demo

45 | 46 |

47 | Change feColorMatrix values by using sliders. 48 |

49 |
50 | 51 |
52 | 53 |
54 | Images from Unsplash. 55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /filter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stanko/react-image-filter/0930a4c5bde8248e6bfbf730e8bbb80e79b22e81/filter.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-image-filter", 3 | "version": "0.1.2", 4 | "private": false, 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "description": "Lightweight React component, for applying color filters on images, works in all modern browsers plus IE10+ and Edge.", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "start": "webpack-dev-server", 11 | "build": "rm -rf ./lib && NODE_ENV=\"production\" babel ./source --out-dir ./lib", 12 | "build-docs": "rm -rf ./dist-docs && EXAMPLE=\"true\" NODE_ENV=\"production\" webpack", 13 | "publish-to-npm": "npm run build && npm publish", 14 | "publish-docs": "sh publish-docs.sh", 15 | "publish-all": "npm run publish-to-npm && npm run publish-docs", 16 | "lint-break-on-errors": "eslint ./source/js ./webpack.config.js -f table --ext .js --ext .jsx", 17 | "lint": "eslint ./source/js ./webpack.config.js -f table --ext .js --ext .jsx || true", 18 | "preview": "rm -rf ./dist && NODE_ENV=\"production\" webpack-dev-server" 19 | }, 20 | "devDependencies": { 21 | "babel-cli": "^6.24.1", 22 | "babel-core": "^6.7.2", 23 | "babel-eslint": "^7.1.0", 24 | "babel-loader": "^7.1.2", 25 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 26 | "babel-plugin-transform-runtime": "^6.6.0", 27 | "babel-preset-es2015": "^6.22.0", 28 | "babel-preset-react": "^6.5.0", 29 | "babel-preset-react-hmre": "^1.1.1", 30 | "babel-runtime": "^6.6.1", 31 | "css-loader": "^0.28.4", 32 | "eslint": "^3.10.1", 33 | "eslint-config-airbnb": "^13.0.0", 34 | "eslint-plugin-import": "^2.2.0", 35 | "eslint-plugin-jsx-a11y": "^2.2.3", 36 | "eslint-plugin-react": "^6.7.1", 37 | "extract-text-webpack-plugin": "^3.0.0", 38 | "html-webpack-plugin": "^2.24.1", 39 | "node-sass": "^4.5.3", 40 | "postcss-loader": "^2.0.6", 41 | "react": "^16.0.0", 42 | "react-dom": "^16.0.0", 43 | "sass-loader": "^6.0.5", 44 | "style-loader": "^0.18.2", 45 | "uglify-js": "^2.8.29", 46 | "webpack": "^3.5.5", 47 | "webpack-dev-server": "^2.2.1" 48 | }, 49 | "dependencies": { 50 | "prop-types": "^15.5.8" 51 | }, 52 | "peerDependencies": { 53 | "react": ">=15.6.2", 54 | "react-dom": ">=15.6.2" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "git+ssh://git@github.com/Stanko/react-image-filter.git" 59 | }, 60 | "keywords": [ 61 | "react", 62 | "filter", 63 | "invert" 64 | ], 65 | "author": "Stanko", 66 | "bugs": { 67 | "url": "https://github.com/Stanko/react-image-filter/issues" 68 | }, 69 | "homepage": "https://github.com/Stanko/react-image-filter#readme" 70 | } 71 | -------------------------------------------------------------------------------- /publish-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build docs 4 | npm run build-docs 5 | # Switch to gh-pages branch 6 | git checkout gh-pages 7 | # Copy everything from build to root 8 | cp ./dist-docs/* ./ 9 | # Commit with current time 10 | git commit -a -m "Demo updated `date +'%Y-%m-%d %H:%M:%S'`" 11 | # Pull changes, if any 12 | git pull --rebase origin gh-pages 13 | # Push changes 14 | git push origin gh-pages 15 | # Switch back to the branch 16 | git checkout - 17 | -------------------------------------------------------------------------------- /source/ImageFilter.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const WRAPPER_STYLE = { 5 | position: 'relative', 6 | }; 7 | 8 | const IMAGE_STYLE = { 9 | display: 'block', 10 | visibility: 'hidden', 11 | width: '100%', 12 | }; 13 | 14 | const SVG_STYLE = { 15 | display: 'block', 16 | height: '100%', 17 | width: '100%', 18 | overflow: 'hidden', 19 | }; 20 | 21 | const SVG_ABSOLUTE_STYLE = { 22 | left: 0, 23 | position: 'absolute', 24 | top: 0, 25 | }; 26 | 27 | const NONE = [ 28 | 1, 0, 0, 0, 0, 29 | 0, 1, 0, 0, 0, 30 | 0, 0, 1, 0, 0, 31 | 0, 0, 0, 1, 0, 32 | ]; 33 | 34 | const INVERT = [ 35 | -1, 0, 0, 0, 1, 36 | 0, -1, 0, 0, 1, 37 | 0, 0, -1, 0, 1, 38 | 0, 0, 0, 1, 0, 39 | ]; 40 | 41 | const GRAYSCALE = [ 42 | 1, 0, 0, 0, 0, 43 | 1, 0, 0, 0, 0, 44 | 1, 0, 0, 0, 0, 45 | 0, 0, 0, 1, 0, 46 | ]; 47 | 48 | const SEPIA = [ 49 | 0.3, 0.45, 0.1, 0, 0, 50 | 0.2, 0.45, 0.1, 0, 0, 51 | 0.1, 0.3, 0.1, 0, 0, 52 | 0, 0, 0, 1, 0, 53 | ]; 54 | 55 | function omit(object, keysToOmit) { 56 | const result = {}; 57 | 58 | Object.keys(object).forEach(key => { 59 | if (keysToOmit.indexOf(key) === -1) { 60 | result[key] = object[key]; 61 | } 62 | }); 63 | 64 | return result; 65 | } 66 | 67 | const types = { 68 | DUOTONE: 'duotone', 69 | INVERT: 'invert', 70 | GRAYSCALE: 'grayscale', 71 | SEPIA: 'sepia', 72 | }; 73 | 74 | export default class ImageFilter extends Component { 75 | constructor(props) { 76 | super(props); 77 | 78 | this.state = { 79 | id: `${ new Date().getTime() }${ Math.random() }`.replace('.', ''), 80 | filter: this.getMatrix(props), 81 | }; 82 | } 83 | 84 | componentWillReceiveProps(nextProps) { 85 | const { 86 | filter, 87 | colorOne, 88 | colorTwo, 89 | } = this.props; 90 | 91 | if ( 92 | filter !== nextProps.filter || 93 | (nextProps.filter === 'duotone' && (colorOne !== nextProps.colorOne || colorTwo !== nextProps.colorTwo)) 94 | ) { 95 | this.setState({ 96 | filter: this.getMatrix(nextProps, true), 97 | }); 98 | } 99 | } 100 | 101 | getMatrix(props, triggerCallback = false) { 102 | let filter = props.filter; 103 | 104 | if (filter === types.GRAYSCALE) { 105 | filter = GRAYSCALE; 106 | } else if (filter === types.INVERT) { 107 | filter = INVERT; 108 | } else if (filter === types.SEPIA) { 109 | filter = SEPIA; 110 | } else if (filter === types.DUOTONE) { 111 | filter = this.convertToDueTone(props.colorOne, props.colorTwo); 112 | } 113 | 114 | if (triggerCallback && props.onChange && typeof props.onChange === 'function') { 115 | props.onChange(filter); 116 | } 117 | 118 | return filter; 119 | } 120 | 121 | convertToDueTone(color1, color2) { 122 | return [ 123 | (color2[0] / 256) - (color1[0] / 256), 0, 0, 0, color1[0] / 256, 124 | (color2[1] / 256) - (color1[1] / 256), 0, 0, 0, color1[1] / 256, 125 | (color2[2] / 256) - (color1[2] / 256), 0, 0, 0, color1[2] / 256, 126 | 0, 0, 0, 1, 0, 127 | ]; 128 | } 129 | 130 | render() { 131 | const { 132 | image, 133 | preserveAspectRatio, 134 | className, 135 | style, 136 | svgStyle, 137 | svgProps, 138 | } = this.props; 139 | 140 | const { 141 | id, 142 | filter, 143 | } = this.state; 144 | 145 | const aspectRatio = preserveAspectRatio === 'cover' ? 'xMidYMid slice' : preserveAspectRatio; 146 | const renderImage = preserveAspectRatio === 'none'; 147 | 148 | const svgMergedStyle = renderImage ? { 149 | ...SVG_STYLE, 150 | ...SVG_ABSOLUTE_STYLE, 151 | ...svgStyle, 152 | } : { 153 | ...SVG_STYLE, 154 | ...svgStyle, 155 | }; 156 | 157 | const otherProps = omit(this.props, [ 158 | 'image', 159 | 'filter', 160 | 'preserveAspectRatio', 161 | 'className', 162 | 'style', 163 | 'svgStyle', 164 | 'svgProps', 165 | 'colorOne', 166 | 'colorTwo', 167 | ]); 168 | 169 | return ( 170 |
175 | { renderImage && 176 | } 183 | 188 | 192 | 196 | 197 | 206 | 207 |
208 | ); 209 | } 210 | } 211 | 212 | ImageFilter.propTypes = { 213 | image: PropTypes.string.isRequired, 214 | filter: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired, 215 | // Check https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio 216 | preserveAspectRatio: PropTypes.string.isRequired, 217 | className: PropTypes.string, 218 | style: PropTypes.object, 219 | svgStyle: PropTypes.object, 220 | svgProps: PropTypes.object, 221 | colorOne: PropTypes.array, 222 | colorTwo: PropTypes.array, 223 | onChange: PropTypes.func, // eslint-disable-line react/no-unused-prop-types 224 | }; 225 | 226 | ImageFilter.defaultProps = { 227 | filter: NONE, 228 | preserveAspectRatio: 'none', 229 | className: '', 230 | style: {}, 231 | svgStyle: {}, 232 | svgProps: {}, 233 | }; 234 | -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | import ImageFilter from './ImageFilter'; 2 | 3 | export default ImageFilter; 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | 7 | const NODE_ENV = process.env.NODE_ENV || 'development'; 8 | const EXAMPLE = process.env.EXAMPLE || 'false'; 9 | const isExample = EXAMPLE === 'true'; 10 | const isProduction = NODE_ENV === 'production'; 11 | 12 | const distPath = path.join(__dirname, './dist'); 13 | const exampleDistPath = path.join(__dirname, './dist-docs'); 14 | const sourcePath = path.join(__dirname, './source'); 15 | const docsPath = path.join(__dirname, './docs'); 16 | 17 | const outputDistPath = isExample ? exampleDistPath : distPath; 18 | 19 | 20 | // Common plugins 21 | const plugins = [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': { 24 | NODE_ENV: JSON.stringify(NODE_ENV), 25 | }, 26 | }), 27 | ]; 28 | 29 | const entry = {}; 30 | 31 | // Common rules 32 | const rules = [ 33 | { 34 | test: /\.(js|jsx)$/, 35 | exclude: /node_modules/, 36 | use: [ 37 | 'babel-loader', 38 | ], 39 | }, 40 | // { 41 | // test: /\.(png|gif|jpg|svg)$/, 42 | // include: imgPath, 43 | // use: 'url-loader?limit=20480&name=assets/[name]-[hash].[ext]', 44 | // }, 45 | ]; 46 | 47 | if (isProduction) { 48 | // Production entry points 49 | entry['docs.min'] = path.join(docsPath, 'docs.js'); 50 | 51 | // Production plugins 52 | plugins.push( 53 | new webpack.optimize.UglifyJsPlugin({ 54 | include: /\.min\.js$/, 55 | compress: { 56 | warnings: false, 57 | screw_ie8: true, 58 | conditionals: true, 59 | unused: true, 60 | comparisons: true, 61 | sequences: true, 62 | dead_code: true, 63 | evaluate: true, 64 | if_return: true, 65 | join_vars: true, 66 | }, 67 | output: { 68 | comments: false, 69 | }, 70 | }), 71 | new ExtractTextPlugin('demo.css') 72 | ); 73 | 74 | if (isExample) { 75 | plugins.push( 76 | new HtmlWebpackPlugin({ 77 | template: path.join(docsPath, 'index.html'), 78 | path: outputDistPath, 79 | filename: 'index.html', 80 | }) 81 | ); 82 | } 83 | 84 | // Production rules 85 | rules.push( 86 | { 87 | test: /\.scss$/, 88 | loader: ExtractTextPlugin.extract({ 89 | fallback: 'style-loader', 90 | use: 'css-loader!sass-loader', 91 | }), 92 | } 93 | ); 94 | } else { 95 | // Development entry points 96 | entry.docs = path.join(docsPath, 'docs.js'); 97 | 98 | // Development plugins 99 | plugins.push( 100 | new webpack.HotModuleReplacementPlugin(), 101 | new HtmlWebpackPlugin({ 102 | template: path.join(docsPath, 'index.html'), 103 | path: outputDistPath, 104 | filename: 'index.html', 105 | }) 106 | ); 107 | 108 | // Development rules 109 | rules.push( 110 | { 111 | test: /\.scss$/, 112 | exclude: /node_modules/, 113 | use: [ 114 | 'style-loader', 115 | 'css-loader?sourceMap', 116 | 'sass-loader?sourceMap', 117 | ], 118 | } 119 | ); 120 | } 121 | 122 | module.exports = { 123 | devtool: isProduction ? false : 'source-map', 124 | context: isProduction ? outputDistPath : sourcePath, 125 | entry, 126 | output: { 127 | path: outputDistPath, 128 | // publicPath: '/', 129 | filename: '[name].js', 130 | }, 131 | module: { 132 | rules, 133 | }, 134 | resolve: { 135 | extensions: ['.webpack-loader.js', '.web-loader.js', '.loader.js', '.js', '.jsx'], 136 | modules: [ 137 | path.resolve(__dirname, 'node_modules'), 138 | sourcePath, 139 | ], 140 | }, 141 | plugins, 142 | devServer: { 143 | contentBase: isProduction ? distPath : sourcePath, 144 | historyApiFallback: true, 145 | compress: isProduction, 146 | inline: !isProduction, 147 | hot: !isProduction, 148 | host: '0.0.0.0', 149 | disableHostCheck: true, 150 | stats: { 151 | assets: true, 152 | children: false, 153 | chunks: false, 154 | hash: false, 155 | modules: false, 156 | publicPath: false, 157 | timings: true, 158 | version: false, 159 | warnings: true, 160 | colors: { 161 | green: '\u001b[32m', 162 | }, 163 | }, 164 | }, 165 | }; 166 | --------------------------------------------------------------------------------