├── .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 | [](https://www.npmjs.com/package/philter) [](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 = ``
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