├── .npmignore ├── .gitignore ├── test ├── test.svg └── test.js ├── lib ├── svg │ ├── color.svg │ ├── duotone.svg │ ├── anaglyph.svg │ ├── vintage-3.svg │ ├── vintage-5.svg │ ├── vintage-4.svg │ ├── vintage-6.svg │ ├── vintage-1.svg │ └── vintage-2.svg └── index.js ├── bower.json ├── package.json ├── LICENSE ├── CHANGELOG.md ├── bin └── philter.js ├── README.md └── dist ├── philter.min.js └── philter.js /.npmignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test/test.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/svg/color.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/svg/duotone.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "philter", 3 | "description": "Philter is a JS plugin giving you the power to control CSS filters with HTML attributes", 4 | "main": "dist/philter.js", 5 | "moduleType": "globals", 6 | "keywords": [ 7 | "filters", 8 | "css", 9 | "html", 10 | "js", 11 | "javascript", 12 | "philter", 13 | "plugin" 14 | ], 15 | "authors": [ 16 | "Liudas Dzisevicius " 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "bower_components", 21 | "CONTRIBUTING.md", 22 | "CHANGELOG.md" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /lib/svg/anaglyph.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/svg/vintage-3.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "philter", 3 | "version": "1.5.0", 4 | "description": "Philter is a JS plugin giving you the power to control CSS filters with HTML data attributes.", 5 | "main": "lib/index.js", 6 | "keywords": [ 7 | "philter", 8 | "filter", 9 | "css", 10 | "svg", 11 | "html" 12 | ], 13 | "bin": { 14 | "philter": "bin/philter.js" 15 | }, 16 | "scripts": { 17 | "test": "mocha --reporter spec -c" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/Specro/Philter.git" 22 | }, 23 | "author": "Liudas Dzisevicius", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/Specro/Philter/issues" 27 | }, 28 | "homepage": "http://specro.github.io/Philter/", 29 | "dependencies": { 30 | "bluebird": "^3.4.6", 31 | "cheerio": "^0.22.0", 32 | "commander": "^2.9.0", 33 | "fs-extra": "^6.0.1", 34 | "hex-rgb": "^3.0.0", 35 | "is-html": "^1.0.0", 36 | "lodash": "^4.17.10" 37 | }, 38 | "devDependencies": { 39 | "chai": "^4.1.2", 40 | "mocha": "^5.2.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Liudas Dzisevicius 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 | 23 | -------------------------------------------------------------------------------- /lib/svg/vintage-5.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/svg/vintage-4.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/svg/vintage-6.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/svg/vintage-1.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/svg/vintage-2.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | ## [Unreleased] 3 | 4 | ## [1.5.0] - 2018-06-03 5 | ### Added 6 | - Duotone filter 7 | 8 | ### Changed 9 | - Moved SVG files in the browser plugin into JS to remove unneeded requests 10 | 11 | ### Fixed 12 | - Custom filter paths resolving wrong 13 | 14 | ## [1.4.1] - 2017-07-05 15 | ### Added 16 | - Experimental anaglyph filter 17 | 18 | ### Fixed 19 | - Custom filter directory not recognizing relative paths 20 | - CLI nesting self closing tags when injecting SVG into HTML 21 | 22 | ## [1.4.0] - 2016-11-08 23 | ### Added 24 | - Custom filters 25 | 26 | ### Fixed 27 | - Color filter not merging with source graphic 28 | - Uploading browser dist to npm 29 | 30 | ## [1.3.1] - 2016-10-26 31 | ### Added 32 | - Error if first argument is not an array or a string 33 | - Tests 34 | 35 | ### Changed 36 | - Compressed SVG filters to one line 37 | 38 | ### Fixed 39 | - Default SVG filter generating wrong CSS 40 | - Empty SVG element returned if no SVG was generated 41 | - CLI: Saving SVG when no SVG was generated 42 | 43 | ## [1.3.0] - 2016-10-25 44 | ### Added 45 | - Node module 46 | - CLI 47 | 48 | ### Removed 49 | - jQuery version 50 | - CSS class version 51 | 52 | ## [1.2.0] - 2016-03-03 53 | ### Added 54 | - 6 new vintage SVG filters (vanilla JS version only) 55 | 56 | ### Changed 57 | - Transition CSS rules are now applied only to Philter elements 58 | 59 | ### Fixed 60 | - Inconsistent height on SVG filters 61 | - CSS rules being applied to the first selector of the element and breaking on elements with same selectors 62 | - SVG adding to body height 63 | - Filter count increasing faster than data is being returned from the server causing wrong filter ids 64 | 65 | ## [1.1.2] - 2016-02-28 66 | ### Added 67 | - Bower compatibility 68 | 69 | ## [1.1.1] - 2015-12-15 70 | ### Added 71 | - Plugin version in vanilla JavaScript 72 | - Option to remove 'philter' from data attributes to make markup shorter 73 | 74 | ### Fixed 75 | - False element width or height being set on custom SVG filters 76 | 77 | ## [1.1.0] - 2015-11-07 78 | ### Changed 79 | - Changed classes to data attributes to describe filters 80 | 81 | ### Removed 82 | - Describing filters with classes The old version still can be found in dist directory 83 | -------------------------------------------------------------------------------- /bin/philter.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const philter = require('../lib/index') 4 | const cheerio = require('cheerio') 5 | const fs = require('fs-extra') 6 | const path = require('path') 7 | const program = require('commander') 8 | 9 | function list(val) { 10 | return val.split(',') 11 | } 12 | 13 | program 14 | .version('1.5.0') 15 | .usage('[options] ') 16 | .option('-n, --no-tag', 'No "philter" in data attributes') 17 | .option('-s, --svg ', 'SVG directory or svg/html file to append to') 18 | .option('-c, --css ', 'CSS directory or css/html file to append to') 19 | .option('-H, --html', 'Pass HTML instead of filenames') 20 | .option('-D, --custom-filter-dir ', 'Custom SVG filter directory') 21 | .option('-F, --custom-filters ', 'Comma-separated custom filter list', list) 22 | .parse(process.argv) 23 | 24 | if (program.customFilters && !program.customFilterDir) { 25 | throw new Error('Philter: Custom filter directory not found') 26 | } 27 | let html = program.html?program.args[0]:program.args 28 | let customFilterDir = program.customFilterDir?program.customFilterDir:'' 29 | let customFilters = program.customFilters?program.customFilters:[] 30 | philter(html, {tag:!program.noTag, customFilterDir:customFilterDir, customFilters:customFilters}, (css, svg) => { 31 | saveData(program.css, css, 'css', (dir) => { 32 | console.log(`CSS saved to ${dir}`) 33 | if (svg) { 34 | saveData(program.svg, svg, 'svg', (dir) => { 35 | console.log(`SVG saved to ${dir}`) 36 | }) 37 | } 38 | }) 39 | }) 40 | 41 | function saveData(dir, data, type, cb) { 42 | if (dir) { 43 | if(path.parse(dir).ext && path.parse(dir).ext === '.html') { 44 | fs.readFile(dir, 'utf-8', (err, file) => { 45 | if (err) { 46 | throw err 47 | } 48 | let $ = cheerio.load(file, {recognizeSelfClosing: true}) 49 | if (type === 'svg') { 50 | if ($('body').find('#philter-svg').length) { 51 | $('#philter-svg').replaceWith(data) 52 | } else { 53 | $('body').append(data) 54 | } 55 | } else { 56 | if ($('head').find('#philter-css').length) { 57 | $('#philter-css').replaceWith(``) 58 | } else { 59 | $('head').append(``) 60 | } 61 | } 62 | fs.writeFile(dir, $.html(), (err) => { 63 | if (err) { 64 | throw err 65 | } 66 | cb(dir) 67 | }) 68 | }) 69 | } else if(path.parse(dir).ext) { 70 | fs.appendFile(dir, data, (err) => { 71 | if (err) { 72 | throw err 73 | } 74 | cb(dir) 75 | }) 76 | } else { 77 | fs.ensureDir(dir, (err) => { 78 | if (err) { 79 | throw err 80 | } 81 | fs.writeFile(dir + 'philter.' + type, data, (err) => { 82 | if (err) { 83 | throw err 84 | } 85 | cb(dir + 'philter.' + type) 86 | }) 87 | }) 88 | } 89 | } else { 90 | fs.writeFile('philter.' + type, data, (err) => { 91 | if (err) { 92 | throw err 93 | } 94 | cb('philter.' + type) 95 | }) 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Philter v1.5.0 2 | [![npm](https://img.shields.io/npm/v/philter.svg)](https://www.npmjs.com/package/philter) [![dependencies](https://david-dm.org/specro/philter.svg)](https://david-dm.org/specro/philter) 3 | 4 | Philter is a JS plugin giving you the power to control CSS filters with HTML data attributes. 5 | Visit the [Demo page](http://specro.github.io/Philter/) for examples. 6 | 7 | ## Installation 8 | Since version 1.3.0 Philter comes as vanilla js plugin or npm package. 9 | Download the plugin and move the `philter` directory to your `js` directory, then include it in your page: 10 | ```html 11 | 12 | ``` 13 | or with NPM: 14 | ```shell 15 | npm install philter 16 | ``` 17 | ## Usage 18 | ### Node 19 | ```js 20 | const philter = require('philter') 21 | 22 | philter(['index.html', 'post.html'], { tag: true, customFilterDir: '', customFilters: [] } (css, svg) => { 23 | console.log('CSS: ', css) 24 | console.log('SVG: ', svg) 25 | }) 26 | ``` 27 | You can also pass 3 parameters to philter: 28 | * tag - boolean - This enables the 'philter' part in data-philter-. If you don't use any plugins which use data attributes or they won't collide with Philter, you can set this to false to omit this part and shorten your markup. 29 | * customFilterDir - string - Directory where custom filters are stored. 30 | * customFilters - array - Array of custom filter names. 31 | 32 | ### Browser 33 | I highly recommend using Philter in Node and use the plugin version only for development and demonstration purposes, since the browser version doesn't support all the listed filters. 34 | 35 | First initiate the plugin: 36 | ```html 37 | 43 | ``` 44 | You can pass 3 parameters to Philter: 45 | * transitionTime - The hover transition time of default CSS filters 46 | * tag - This enables the 'philter' part in data-philter-. If you don't use any plugins which use data attributes or they won't collide with Philter, you can set this to false to omit this part and shorten your markup. 47 | 48 | ### CLI 49 | ```shell 50 | philter index.html post.html -c index.html -s index.html 51 | ``` 52 | 53 | ``` 54 | Usage: philter [options] 55 | 56 | Options: 57 | 58 | -h, --help output usage information 59 | -V, --version output the version number 60 | -n, --no-tag No "philter" in data attributes 61 | -s, --svg SVG directory or svg/html file to append to 62 | -c, --css CSS directory or css/html file to append to 63 | -H, --html Pass HTML instead of filenames 64 | -D, --custom-filter-dir Custom SVG filter directory 65 | -F, --custom-filters Custom filters 66 | 67 | ``` 68 | 69 | ## Format 70 | 71 | Now you can start using the filters. The plugin uses this kind of syntax format: 72 | ```html 73 | data-philter-="" 74 | ``` 75 | or 76 | ```html 77 | data-philter-=" " 78 | ``` 79 | You give an element the data attribute for a specific filter and then a value for it. You can also add another value that the filter will use when hovering on that element. 80 | For example: 81 | ```html 82 |
83 | ``` 84 | This element would be blured in 5px radius. If we would add another value, like this: 85 | ```html 86 |
87 | ``` 88 | The element would unblur when hovered over with the mouse. 89 | With filters that use more than one value you have to specify every value for hover too. 90 | You can add more than one filter onto an element by using the same method: 91 | ```html 92 |
93 | ``` 94 | Philter even supports custom SVG filters: 95 | ```html 96 |
97 | ``` 98 | Where 'filter' in 'data-philter-svg' attribute is the ID of the filter. 99 | Also Philter has pre-built custom filters: 100 | ```html 101 |
102 | ``` 103 | This one would overlay the element with #00ff00 color in 50% opacity. 104 | More filters are to come in the near future. You have any suggestions or know a filter that certainly has to be present in Philter? Just contact me or Elephento team. 105 | 106 | ## More info on filters 107 | Here's a list of filters that you can use and their limitations in Philter. 108 | * blur 109 | * grayscale 110 | * hue-rotate 111 | * saturate 112 | * sepia 113 | * contrast 114 | * invert 115 | * opacity 116 | * brightness 117 | * drop-shadow - Requires 4 values. In the browser the 4th value instead of color is opacity 0 to 100%, color is locked to black. 118 | * svg - Custom SVG filter. Requires 1 value - filter ID. 119 | * color - Requires 2 values. Color and opacity. 120 | * vintage - Requires an integer from 1 to 6. 121 | * duotone - Requires 2 values. 2 colors in hex. 122 | * custom - Requires a string - custom filter name. 123 | * anaglyph - Experimental - Requires an anaglyph offset value. 124 | 125 | Drop shadow filter in browser supports only black color because with it's already long class it would be even longer with rgba implementation. 126 | 127 | ### Vintage 128 | There are 6 vintage filters: 129 | * Rises contrast. Brings out details and colors. 130 | * Washes out the image with light brown sepia. 131 | * Raises the brightness and gives a green/cyan look. 132 | * Close to 3 but a bit less brightness and more green. 133 | * Close to 2 but mixed with violet. Gives a sweet/daydream look. 134 | * Grayscale but better (IMO :)) 135 | 136 | ### Duotone 137 | Duotone filter maps shadows and highlights of the image to 2 different colors. First color is mapped to the shadow and second to the highlights. Colors for this filter should be provided in hex format e.g., #123456. 138 | 139 | ### Experimental 140 | This section contains filters that are experimental i.e. work only in one browser, have image breaking bugs (even though this whole SVG thing is pretty buggy) etc. These filters will be showcased here: [Philter experimental](https://specro.github.io/Philter/experimental). 141 | 142 | Anaglyph filter tries to imitate the [3D anaglyph effect](https://en.wikipedia.org/wiki/Anaglyph_3D) that is widely used for 3D images and graphic effects. So far this should work 100% only on Chrome. Also I use feBlend to cut off the offset part which means that the bigger the offset is the more of the image is being cut off the left side. 143 | 144 | ### Custom 145 | You can use filters that you wrote by yourself in NPM/CLI version by using the custom tag like this: 146 | ```html 147 | data-philter-cutom="" 148 | ``` 149 | If using custom filters you must supply the directory where they're stored and custom filter names in the options. The file of the filter must have that name and its id must be that same name. 150 | 151 | ## Compatibility 152 | Philter was developed and tested on Chrome 46+, Firefox 41+, Opera 34+ and Edge 20+. The default CSS filters should be compatible with most versions of browsers that support filters. The custom filters are supported only by Firefox, Chrome and Opera. You may notice glitching on Edge when more than one hover element is on the page and loss of some filters when they are stacked on one element. 153 | 154 | ## Issues 155 | This is mainly due to SVG filter limitations or complexities. It may be solved in the future... or it may not. 156 | * On my recent tests with Chrome SVG filters stack with other filters but as always you may encounter bugs. 157 | * SVG filters don't support CSS transitions. 158 | * SVG filters actually know what to do on hover but ^ and you may encounter other bugs (like flickering and so on). Especially on Edge and IE browsers. 159 | 160 | ## WIP 161 | I'm working on all sorts of stuff that involves this plugin and doesn't. So please bear with the way I develop Philter. If you have any suggestions ideas or just wanna say something you can send me an email at liudas.dzisevicius@gmail.com or tweet @baldassertation. 162 | * Gulp (I work with Gulp, so there will be no Grunt here. Sorry.) 163 | * Webpack 164 | * More custom SVG filters 165 | 166 | ## License 167 | Philter is licensed under MIT License. 168 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const philter = require('../lib/index') 3 | 4 | describe('Module', () => { 5 | it('should return css and svg as strings', function() { 6 | philter('
', (css, svg)=> { 7 | expect(css).to.be.a('string') 8 | expect(svg).to.be.a('string') 9 | }) 10 | }) 11 | it('should return css and svg as strings with tag: false', function() { 12 | philter('
', {tag: false}, (css, svg)=> { 13 | expect(css).to.be.a('string') 14 | expect(svg).to.be.a('string') 15 | }) 16 | }) 17 | it('should throw an Error when no HTML or files are given', function() { 18 | expect(philter.bind(philter, (css, svg) => {})).to.throw('Philter: No HTML or files given') 19 | }) 20 | it('should throw an Error when HTML doesn\'t contain philter data attributes', function() { 21 | expect(philter.bind(philter, '
', (css, svg) => {})).to.throw('Philter: No philter data attributes found') 22 | }) 23 | it('should throw an Error when no callback is given', function() { 24 | expect(philter.bind(philter, '
')).to.throw('Philter: Callback must be a function') 25 | }) 26 | it('should throw an Error when no custom filter directory is given', function() { 27 | expect(philter.bind(philter, '
', {customFilters: ['test']}, (css, svg) => {})).to.throw('Philter: No custom filter directory found') 28 | }) 29 | }) 30 | 31 | describe('Filters', () => { 32 | it('blur', function() { 33 | philter('
', (css, svg)=> { 34 | expect(css).to.equal('[data-philter-blur="10"]{filter:blur(10px);}') 35 | expect(svg).to.equal('') 36 | }) 37 | }) 38 | it('grayscale', function() { 39 | philter('
', (css, svg)=> { 40 | expect(css).to.equal('[data-philter-grayscale="10"]{filter:grayscale(10%);}') 41 | expect(svg).to.equal('') 42 | }) 43 | }) 44 | it('hue-rotate', function() { 45 | philter('
', (css, svg)=> { 46 | expect(css).to.equal('[data-philter-hue-rotate="10"]{filter:hue-rotate(10deg);}') 47 | expect(svg).to.equal('') 48 | }) 49 | }) 50 | it('saturate', function() { 51 | philter('
', (css, svg)=> { 52 | expect(css).to.equal('[data-philter-saturate="10"]{filter:saturate(10%);}') 53 | expect(svg).to.equal('') 54 | }) 55 | }) 56 | it('sepia', function() { 57 | philter('
', (css, svg)=> { 58 | expect(css).to.equal('[data-philter-sepia="10"]{filter:sepia(10%);}') 59 | expect(svg).to.equal('') 60 | }) 61 | }) 62 | it('contrast', function() { 63 | philter('
', (css, svg)=> { 64 | expect(css).to.equal('[data-philter-contrast="10"]{filter:contrast(10%);}') 65 | expect(svg).to.equal('') 66 | }) 67 | }) 68 | it('invert', function() { 69 | philter('
', (css, svg)=> { 70 | expect(css).to.equal('[data-philter-invert="10"]{filter:invert(10%);}') 71 | expect(svg).to.equal('') 72 | }) 73 | }) 74 | it('opacity', function() { 75 | philter('
', (css, svg)=> { 76 | expect(css).to.equal('[data-philter-opacity="10"]{filter:opacity(10%);}') 77 | expect(svg).to.equal('') 78 | }) 79 | }) 80 | it('brightness', function() { 81 | philter('
', (css, svg)=> { 82 | expect(css).to.equal('[data-philter-brightness="10"]{filter:brightness(10%);}') 83 | expect(svg).to.equal('') 84 | }) 85 | }) 86 | it('drop-shadow', function() { 87 | philter('
', (css, svg)=> { 88 | expect(css).to.equal('[data-philter-drop-shadow="10 10 10 black"]{filter:drop-shadow(10px 10px 10px black);}') 89 | expect(svg).to.equal('') 90 | }) 91 | }) 92 | it('svg', function() { 93 | philter('
', (css, svg)=> { 94 | expect(css).to.equal('[data-philter-svg="filter"]{filter:url(#filter);}') 95 | expect(svg).to.equal('') 96 | }) 97 | }) 98 | it('color', function() { 99 | philter('
', (css, svg)=> { 100 | expect(css).to.equal('[data-philter-color="black 10"]{filter:url(#color-1);}') 101 | expect(svg.replace(/\r?\n|\r/g, '')).to.equal('') 102 | }) 103 | }) 104 | it('vintage', function() { 105 | philter('
', (css, svg)=> { 106 | expect(css).to.equal('[data-philter-vintage="3"]{filter:url(#vintage-3);}') 107 | expect(svg.replace(/\r?\n|\r/g, '')).to.equal('') 108 | }) 109 | }) 110 | it('duotone', function() { 111 | philter('
', (css, svg) => { 112 | expect(css).to.equal('[data-philter-duotone="#000000 #ffffff"]{filter:url(#duotone-1);}') 113 | expect(svg.replace(/\r?\n|\r/g, '')).to.equal('') 114 | }) 115 | }) 116 | it('anaglyph', function() { 117 | philter('
', (css, svg)=> { 118 | expect(css).to.equal('[data-philter-anaglyph="1"]{filter:url(#anaglyph-1);}') 119 | expect(svg.replace(/\r?\n|\r/g, '')).to.equal('') 120 | }) 121 | }) 122 | it('custom', function() { 123 | philter('
', {customFilterDir: './test/', customFilters: ['test']}, (css, svg) => { 124 | expect(css).to.equal('[data-philter-custom="test"]{filter:url(#test);}') 125 | expect(svg.replace(/\r?\n|\r/g, '')).to.equal('') 126 | }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const _ = require('lodash') 4 | const cheerio = require('cheerio') 5 | const Promise = require('bluebird') 6 | const isHtml = require('is-html') 7 | const rgb = require('hex-rgb') 8 | 9 | Promise.promisifyAll(fs) 10 | 11 | function philter(files, options, cb) { 12 | if (!_.isString(files) && !_.isArray(files)) { 13 | throw new Error('Philter: No HTML or files given') 14 | } 15 | if (_.isFunction(options)) { 16 | cb = options 17 | options = {} 18 | } 19 | if (!_.isFunction(cb)) { 20 | throw new Error('Philter: Callback must be a function') 21 | } 22 | options = _.defaults(options, { 23 | tag: true, 24 | customFilterDir: '', 25 | customFilters: [] 26 | }) 27 | 28 | if (isHtml(files)) { 29 | let $ = cheerio.load(files) 30 | parseElements($, options, cb) 31 | } else { 32 | let html = '' 33 | let promises = [] 34 | _.forEach(files, (file) => { 35 | promises.push( 36 | fs.readFileAsync(file, 'utf-8').then((data) => { 37 | html += data 38 | }).catch((err) => { 39 | throw err 40 | }) 41 | ) 42 | }) 43 | Promise.all(promises).then(() => { 44 | let $ = cheerio.load(html) 45 | parseElements($, options, cb) 46 | }) 47 | } 48 | } 49 | 50 | /** 51 | * Parse HTML and return elements with philter attributes 52 | * @param {Object} $ Cheerio object 53 | * @param {Object} filters All possible filters 54 | * @param {Boolean} tag Is 'philter' tag active 55 | */ 56 | function getElements($, filters, tag) { 57 | let elements = []; 58 | _.forEach(filters, (unit, filter) => { 59 | let query; 60 | if (tag) { 61 | query = $(`[data-philter-${filter}]`) 62 | } else { 63 | query = $(`[data-${filter}]`) 64 | } 65 | if (query) { 66 | _.forEach(query, (element) => { 67 | if (!_.includes(elements, element)) { 68 | elements.push(element) 69 | } 70 | }) 71 | } 72 | }) 73 | 74 | return elements 75 | } 76 | 77 | /** 78 | * Parse elements and pass generated CSS and SVG to the callback 79 | * @param {Object} $ Cheerio object 80 | * @param {Object} options Philter options 81 | * @param {Function} cb Callback 82 | */ 83 | function parseElements($, options, cb) { 84 | let filters = { 85 | 'blur': 'px', 86 | 'grayscale': '%', 87 | 'hue-rotate': 'deg', 88 | 'saturate': '%', 89 | 'sepia': '%', 90 | 'contrast': '%', 91 | 'invert': '%', 92 | 'opacity': '%', 93 | 'brightness': '%', 94 | 'drop-shadow': (h, v, blur, color) => `${h}px ${v}px ${blur}px ${color}`, 95 | 'svg': (url) => `url(#${url})`, 96 | 'color': (nr) => `url(#color-${nr})`, 97 | 'vintage': (nr) => `url(#vintage-${nr})`, 98 | 'duotone': (nr) => `url(#duotone-${nr})`, 99 | 'anaglyph': (nr) => `url(#anaglyph-${nr})`, 100 | 'custom': {} 101 | } 102 | let filterCount = { 103 | 'color': 0, 104 | 'anaglyph': 0, 105 | 'duotone': 0, 106 | 'vintage-1': 0, 107 | 'vintage-2': 0, 108 | 'vintage-3': 0, 109 | 'vintage-4': 0, 110 | 'vintage-5': 0, 111 | 'vintage-6': 0 112 | } 113 | let css = '' 114 | let svg = '' 115 | let promises = [] 116 | if (!_.isEmpty(options.customFilters)) { 117 | if (!options.customFilterDir) { 118 | throw new Error('Philter: No custom filter directory found') 119 | } 120 | _.forEach(options.customFilters, (value) => { 121 | filters.custom[value] = `url(#${value})` 122 | filterCount[value] = 0 123 | }) 124 | } 125 | let elements = getElements($, filters, options.tag) 126 | 127 | if (_.isEmpty(elements)) { 128 | throw new Error('Philter: No philter data attributes found') 129 | } 130 | 131 | _.forEach(elements, (element) => { 132 | let selector = '' 133 | let rule = { 134 | default: '', 135 | hover: '' 136 | } 137 | _.forEach($(element).data(), (value, key) => { 138 | value = value.toString() 139 | let values = value.split(' ') 140 | key = _.replace(_.kebabCase(key), /(philter-)/g, '') 141 | selector += `[data-${options.tag?'philter-':''}${key}="${value}"]` 142 | switch (key) { 143 | case 'color': 144 | filterCount.color++ 145 | rule.default += filters.color(filterCount.color) 146 | getColorFilter(values[0], values[1], filterCount.color, promises, (filter) => { 147 | svg += filter 148 | }) 149 | if (values[2] && values[3]) { 150 | filterCount.color++ 151 | rule.hover += filters.color(filterCount.color) 152 | getColorFilter(values[2], values[3], filterCount.color, promises, (filter) => { 153 | svg += filter 154 | }) 155 | } 156 | break 157 | case 'anaglyph': 158 | filterCount.anaglyph++ 159 | rule.default += filters.anaglyph(filterCount.anaglyph) 160 | getAnaglyphFilter(values[0], filterCount.anaglyph, promises, (filter) => { 161 | svg += filter 162 | }) 163 | if (values[1]) { 164 | filterCount.anaglyph++ 165 | rule.hover += filters.anaglyph(filterCount.anaglyph) 166 | getAnaglyphFilter(values[1], filterCount.anaglyph, promises, (filter) => { 167 | svg += filter 168 | }) 169 | } 170 | break 171 | case 'duotone': 172 | filterCount.duotone++ 173 | rule.default += filters.duotone(filterCount.duotone) 174 | getDuotoneFilter(values[0], values[1], filterCount.duotone, promises, (filter) => { 175 | svg += filter 176 | }) 177 | if (values[2] && values[3]) { 178 | filterCount.duotone++ 179 | rule.hover += filters.duotone(filterCount.duotone) 180 | getDuotoneFilter(values[2], values[3], filterCount.duotone, promises, (filter) => { 181 | svg += filter 182 | }) 183 | } 184 | break 185 | case 'vintage': 186 | if (!_.has(filterCount, `vintage-${values[0]}`)) { 187 | throw new Error(`Philter: No such filter: vintage-${values[0]}`); 188 | } 189 | filterCount[`vintage-${values[0]}`]++; 190 | rule.default += filters.vintage(values[0]) 191 | if (filterCount[`vintage-${values[0]}`] === 1) { 192 | getVintageFilter(values[0], promises, (filter) => { 193 | svg += filter 194 | }) 195 | } 196 | if (values[1]) { 197 | filterCount[`vintage-${values[1]}`]++; 198 | rule.hover += filters.vintage(values[1]) 199 | if (filterCount[`vintage-${values[1]}`] === 1) { 200 | getVintageFilter(values[1], promises, (filter) => { 201 | svg += filter 202 | }) 203 | } 204 | } 205 | break 206 | case 'drop-shadow': 207 | rule.default += `${key}(${filters[key](values[0], values[1], values[2], values[3])})` 208 | if (values[4] && values[5] && values[6] && values[7]) { 209 | rule.hover += `${key}(${filters[key](values[4], values[5], values[6], values[7])})` 210 | } 211 | break 212 | case 'svg': 213 | rule.default += filters.svg(values[0]) 214 | if (values[1]) { 215 | rule.hover += filters.svg(values[1]) 216 | } 217 | break 218 | case 'custom': 219 | filterCount[values[0]]++ 220 | rule.default += filters.custom[values[0]] 221 | if (filterCount[values[0]] === 1) { 222 | getCustomFilter(path.join(options.customFilterDir, values[0]), promises, (filter) => { 223 | svg += filter 224 | }) 225 | } 226 | if (values[1]) { 227 | filterCount[values[1]] 228 | rule.hover += custom.filters[values[1]] 229 | if (filterCount[values[1]] === 1) { 230 | getCustomFilter(path.join(options.customFilterDir, values[1]), promises, (filter) => { 231 | svg += filter 232 | }) 233 | } 234 | } 235 | break 236 | default: 237 | // setup default rule 238 | rule.default += `${key}(${values[0]+filters[key]})` 239 | 240 | // setup hover rule 241 | if (values[1]) { 242 | rule.hover += `${key}(${values[1]+filters[key]})` 243 | } 244 | } 245 | }) 246 | css += `${selector}{filter:${rule.default};}` 247 | if (rule.hover) { 248 | css += `${selector}:hover{filter:${rule.hover};}` 249 | } 250 | 251 | selector = '' 252 | rule.default = '' 253 | rule.hover = '' 254 | }) 255 | Promise.all(promises).then(() => { 256 | let svgWrapper = '' 257 | if (svg) { 258 | svgWrapper = `${svg}` 259 | } else { 260 | svgWrapper = '' 261 | } 262 | 263 | cb(css, svgWrapper) 264 | }) 265 | } 266 | 267 | /** 268 | * Parse color filter and pass it to the callback 269 | * @param {String} color Filter color 270 | * @param {String} opacity Filter opacity 271 | * @param {Integer} id Filter ID 272 | * @param {Array} promises List of readFileAsync promises 273 | * @param {Function} cb Callback 274 | */ 275 | function getColorFilter(color, opacity, id, promises, cb) { 276 | let filter = '' 277 | promises.push( 278 | fs.readFileAsync(`${__dirname}/svg/color.svg`, 'utf-8').then((data) => { 279 | let $ = cheerio.load(data, {recognizeSelfClosing: true}); 280 | $('filter').attr('id', 'color-' + id) 281 | $('feflood').attr('flood-color', color).attr('flood-opacity', opacity/100) 282 | filter = $.html() 283 | cb(filter) 284 | }).catch((err) => { 285 | throw err 286 | }) 287 | ) 288 | } 289 | 290 | /** 291 | * Parse anaglyph filter and pass it to the callback 292 | * @param {String} offset Filter offset 293 | * @param {Integer} id Filter ID 294 | * @param {Array} promises List of readFileAsync promises 295 | * @param {Function} cb Callback 296 | */ 297 | function getAnaglyphFilter(offset, id, promises, cb) { 298 | let filter = '' 299 | promises.push( 300 | fs.readFileAsync(`${__dirname}/svg/anaglyph.svg`, 'utf-8').then((data) => { 301 | let $ = cheerio.load(data, {recognizeSelfClosing: true}); 302 | $('filter').attr('id', 'anaglyph-' + id) 303 | $('feoffset').attr('dx', offset/100) 304 | $('feblend').attr('x', offset/100) 305 | filter = $.html() 306 | cb(filter) 307 | }).catch((err) => { 308 | throw err 309 | }) 310 | ) 311 | } 312 | 313 | /** 314 | * Parse duotone filter and pass it to the callback 315 | * @param {String} color1 First filter color 316 | * @param {String} color2 Second filter color 317 | * @param {Integer} id Filter ID 318 | * @param {Array} promises List of readFileAsync promises 319 | * @param {Function} cb Callback 320 | */ 321 | function getDuotoneFilter(color1, color2, id, promises, cb) { 322 | let filter = '' 323 | color1 = rgb(color1); 324 | color2 = rgb(color2); 325 | promises.push( 326 | fs.readFileAsync(`${__dirname}/svg/duotone.svg`, 'utf-8').then((data) => { 327 | let $ = cheerio.load(data, {recognizeSelfClosing: true}); 328 | $('filter').attr('id', 'duotone-' + id) 329 | $('fefuncr').attr('tablevalues', color1.red/255+' '+color2.red/255) 330 | $('fefuncg').attr('tablevalues', color1.green/255+' '+color2.green/255) 331 | $('fefuncb').attr('tablevalues', color1.blue/255+' '+color2.blue/255) 332 | filter = $.html() 333 | cb(filter) 334 | }).catch((err) => { 335 | throw err 336 | }) 337 | ) 338 | } 339 | 340 | /** 341 | * Read vintage filter and pass it to the callback 342 | * @param {Integer} id Filter ID 343 | * @param {Array} promises List of readFileAsync promises 344 | * @param {Function} cb Callback 345 | */ 346 | function getVintageFilter(id, promises, cb) { 347 | let filter = '' 348 | promises.push( 349 | fs.readFileAsync(`${__dirname}/svg/vintage-${id}.svg`, 'utf-8').then((data) => { 350 | cb(data) 351 | }).catch((err) => { 352 | throw err 353 | }) 354 | ) 355 | } 356 | 357 | /** 358 | * Load custom SVG filter 359 | * @param {String} file SVG file directory 360 | * @param {Array} promises List of readFileAsync promises 361 | * @param {Function} cb Callback 362 | */ 363 | function getCustomFilter(dir, promises, cb) { 364 | promises.push( 365 | fs.readFileAsync(dir + '.svg', 'utf-8').then((data) => { 366 | cb(data) 367 | }).catch((err) => { 368 | throw err 369 | }) 370 | ) 371 | } 372 | 373 | module.exports = philter 374 | -------------------------------------------------------------------------------- /dist/philter.min.js: -------------------------------------------------------------------------------- 1 | /* Philter v1.5.0 | (c) 2015-2018 Liudas Dzisevicius | MIT License */ 2 | !function(){"use strict";function i(e,t,n){switch(t[0]){case"drop-shadow":e[0]=e[0]+t[0]+"("+t[1]+n+" "+t[2]+n+" "+t[3]+n+" rgba(0,0,0,"+.01*t[4]+")) ",t[5]&&t[6]&&t[7]&&t[8]?e[1]=e[1]+t[0]+"("+t[5]+n+" "+t[6]+n+" "+t[7]+n+" rgba(0,0,0,"+.01*t[8]+")) ":e[1]=e[1]+t[0]+"("+t[1]+n+" "+t[2]+n+" "+t[3]+n+" rgba(0,0,0,"+.01*t[4]+")) ";break;case"svg":e[0]=e[0]+"url("+n+t[1]+") ",t[2]?e[1]=e[1]+"url("+n+t[2]+") ":e[1]=e[1]+"url("+n+t[1]+") ";break;case"color":++this.filterCount.color,r.call(this,t[1],t[2],this.filterCount.color),e[0]=e[0]+"url("+n+"color-"+this.filterCount.color+") ",t[3]&&t[4]&&(++this.filterCount.color,r.call(this,t[3],t[4],this.filterCount.color)),e[1]=e[1]+"url("+n+"color-"+this.filterCount.color+") ";break;case"duotone":++this.filterCount.duotone,l.call(this,t[1],t[2],this.filterCount.duotone),e[0]=e[0]+"url("+n+"duotone-"+this.filterCount.duotone+") ",t[3]&&t[4]&&(++this.filterCount.duotone,l.call(this,t[3],t[4],this.filterCount.duotone)),e[1]=e[1]+"url("+n+"duotone-"+this.filterCount.duotone+") ";break;case"vintage":0==this.filterCount["vintage-"+t[1]]&&a.call(this,t[1]),++this.filterCount["vintage-"+t[1]],e[0]=e[0]+"url("+n+"vintage-"+t[1]+") ",t[2]?(0==this.filterCount["vintage-"+t[2]]&&a.call(this,t[2]),++this.filterCount["vintage-"+t[1]],e[1]=e[1]+"url("+n+"vintage-"+t[2]+") "):e[1]=e[1]+"url("+n+"vintage-"+t[1]+") ";break;default:e[0]=e[0]+t[0]+"("+t[1]+n+") ",t[2]?e[1]=e[1]+t[0]+"("+t[2]+n+") ":e[1]=e[1]+t[0]+"("+t[1]+n+") "}return e}function r(e,t,n){var r=document.getElementById("svg");r||((r=document.createElement("div")).setAttribute("id","svg"),r.innerHTML='',document.body.appendChild(r),r=document.getElementById("svg")),t*=.01;var l='';l=l.replace("color","color-"+n),r.querySelector("defs").innerHTML+=l,s(r.querySelector('filter[id="color-'+n+'"]').children[0],{"flood-opacity":t,"flood-color":e})}function l(e,t,n){var r=document.getElementById("svg");r||((r=document.createElement("div")).setAttribute("id","svg"),r.innerHTML='',document.body.appendChild(r),r=document.getElementById("svg")),e=f(e),t=f(t);var l='';l=l.replace("duotone","duotone-"+n),r.querySelector("defs").innerHTML+=l,s(r.querySelector('filter[id="duotone-'+n+'"] fefuncr'),{tableValues:e.r/255+" "+t.r/255}),s(r.querySelector('filter[id="duotone-'+n+'"] fefuncg'),{tableValues:e.g/255+" "+t.g/255}),s(r.querySelector('filter[id="duotone-'+n+'"] fefuncb'),{tableValues:e.b/255+" "+t.b/255})}function a(e){var t=document.getElementById("svg");t||((t=document.createElement("div")).setAttribute("id","svg"),t.innerHTML='',document.body.appendChild(t),t=document.getElementById("svg"));var n="";switch(e){case"1":n='';break;case"2":n='';break;case"3":n='';break;case"4":n='';break;case"5":n='';break;case"6":n=''}t.querySelector("defs").innerHTML+=n}function o(e){var t={blur:"px","hue-rotate":"deg","drop-shadow":"px",svg:"#",color:"#",vintage:"#",duotone:"#",default:"%"};return t[e]?t[e]:t.default}function s(e,t){for(var n in t)e.setAttribute(n,t[n])}function f(e){var t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return t?{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}:null}window.Philter=function(){var e={transitionTime:.5,tag:!0},t=document.createElement("style");this.filterCount={color:0,duotone:0,"vintage-1":0,"vintage-2":0,"vintage-3":0,"vintage-4":0,"vintage-5":0,"vintage-6":0},this.filters=["blur","grayscale","hue-rotate","saturate","sepia","contrast","invert","opacity","brightness","drop-shadow","svg","color","duotone","vintage"],this.elements=[],this.styleString="",this.transitionString="",arguments[0]&&"object"==typeof arguments[0]?this.options=function(e,t){var n;for(n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}(e,arguments[0]):this.options=e,function(){for(var e=0;e