├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo ├── codepen-template │ ├── codepen.js │ └── package.json ├── css │ ├── styles.css │ └── styles.pcss ├── gulpfile.js ├── index.html ├── package-lock.json └── package.json ├── index.js ├── index.test.js ├── lib ├── colorStops.js └── helpers.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | .DS_Store 5 | 6 | # Browserify bundles 7 | *.bundle.js 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | .gitignore 3 | .editorconfig 4 | 5 | node_modules/ 6 | npm-debug.log 7 | yarn.lock 8 | 9 | *.test.js 10 | .travis.yml 11 | 12 | # Demo and assets folder 13 | demo 14 | assets 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | sudo: false 5 | branches: 6 | only: 7 | - master 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andreas Larsen 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 | # PostCSS Easing Gradients 2 | 3 | [![NPM Version][npm-img]][npm] 4 | [![NPM Monthly Downloads][dm-img]][npm] 5 | [![Build Status][ci-img]][ci] 6 | [![Dependency status][dpd-img]][dpd] 7 |
8 | [![MIT License][mit-img]][mit] 9 | [![Code Style: Prettier][prt-img]][prt] 10 | [![Follow Larsenwork on Twitter][twt-img]][twt] 11 | 12 | [PostCSS](https://github.com/postcss/postcss) plugin to create smooth linear-gradients that approximate easing functions. 13 | Visual examples and online editor on [larsenwork.com/easing-gradients](https://larsenwork.com/easing-gradients/) 14 | 15 | ## Code Examples 16 | 17 | ```css 18 | .cubic-bezier { 19 | background: linear-gradient(to bottom, black, cubic-bezier(0.48, 0.3, 0.64, 1), transparent); 20 | /* => */ 21 | background: linear-gradient( 22 | to bottom, 23 | hsl(0, 0%, 0%), 24 | hsla(0, 0%, 0%, 0.94505) 7.9%, 25 | hsla(0, 0%, 0%, 0.88294) 15.3%, 26 | hsla(0, 0%, 0%, 0.81522) 22.2%, 27 | hsla(0, 0%, 0%, 0.7426) 28.7%, 28 | hsla(0, 0%, 0%, 0.66692) 34.8%, 29 | hsla(0, 0%, 0%, 0.58891) 40.6%, 30 | hsla(0, 0%, 0%, 0.50925) 46.2%, 31 | hsla(0, 0%, 0%, 0.42866) 51.7%, 32 | hsla(0, 0%, 0%, 0.34817) 57.2%, 33 | hsla(0, 0%, 0%, 0.2693) 62.8%, 34 | hsla(0, 0%, 0%, 0.19309) 68.7%, 35 | hsla(0, 0%, 0%, 0.12126) 75.2%, 36 | hsla(0, 0%, 0%, 0.05882) 82.6%, 37 | hsla(0, 0%, 0%, 0.01457) 91.2%, 38 | hsla(0, 0%, 0%, 0) 39 | ); 40 | } 41 | 42 | .ease { 43 | background: linear-gradient(green, ease, red); 44 | /* => */ 45 | background: linear-gradient( 46 | hsl(120, 100%, 25.1%), 47 | hsl(88.79, 100%, 24.28%) 7.8%, 48 | hsl(69.81, 100%, 23.14%) 13.2%, 49 | hsl(53.43, 100%, 24.55%) 17.6%, 50 | hsl(42.52, 100%, 28.9%) 21.7%, 51 | hsl(34.96, 100%, 32.64%) 25.8%, 52 | hsl(29.1, 100%, 35.96%) 30.2%, 53 | hsl(24.26, 100%, 38.94%) 35.1%, 54 | hsl(20.14, 100%, 41.56%) 40.6%, 55 | hsl(16.47, 100%, 43.87%) 46.9%, 56 | hsl(13.13, 100%, 45.83%) 54.1%, 57 | hsl(10.07, 100%, 47.42%) 62.2%, 58 | hsl(7.23, 100%, 48.62%) 71.1%, 59 | hsl(4.6, 100%, 49.43%) 80.6%, 60 | hsl(2.16, 100%, 49.87%) 90.5%, 61 | hsl(0, 100%, 50%) 62 | ); 63 | } 64 | 65 | .steps { 66 | background: linear-gradient(to right, green, steps(4, skip-none), red); 67 | /* => */ 68 | background: linear-gradient( 69 | to right, 70 | hsl(120, 100%, 25.1%), 71 | hsl(120, 100%, 25.1%) 25%, 72 | hsl(42.59, 100%, 28.87%) 25%, 73 | hsl(42.59, 100%, 28.87%) 50%, 74 | hsl(21.3, 100%, 40.82%) 50%, 75 | hsl(21.3, 100%, 40.82%) 75%, 76 | hsl(0, 100%, 50%) 75%, 77 | hsl(0, 100%, 50%) 78 | ); 79 | } 80 | 81 | .radial { 82 | background: radial-gradient(circle at top right, red, ease-in-out, blue); 83 | /* => */ 84 | background: radial-gradient( 85 | circle at top right, 86 | hsl(0, 100%, 50%), 87 | hsl(353.5, 100%, 49.71%) 7.7%, 88 | hsl(347.13, 100%, 48.89%) 14.8%, 89 | hsl(341.1, 100%, 47.69%) 21%, 90 | hsl(335.24, 100%, 46.22%) 26.5%, 91 | hsl(329.48, 100%, 44.57%) 31.4%, 92 | hsl(323.63, 100%, 42.76%) 35.9%, 93 | hsl(317.56, 100%, 40.82%) 40.1%, 94 | hsl(310.92, 100%, 38.7%) 44.2%, 95 | hsl(303.81, 100%, 36.49%) 48.1%, 96 | hsl(296, 100%, 36.55%) 52%, 97 | hsl(288.73, 100%, 38.81%) 56%, 98 | hsl(282.14, 100%, 40.92%) 60.1%, 99 | hsl(276.09, 100%, 42.84%) 64.3%, 100 | hsl(270.27, 100%, 44.64%) 68.8%, 101 | hsl(264.54, 100%, 46.28%) 73.7%, 102 | hsl(258.7, 100%, 47.74%) 79.2%, 103 | hsl(252.68, 100%, 48.92%) 85.4%, 104 | hsl(246.32, 100%, 49.72%) 92.5%, 105 | hsl(240, 100%, 50%) 106 | ); 107 | } 108 | ``` 109 | 110 |
111 | 112 | ## Syntax 113 | 114 | Currently a subset of the [full syntax](https://github.com/w3c/csswg-drafts/issues/1332#issuecomment-299990698) is supported: 115 | 116 | ```xml 117 | linear-gradient( 118 | [ ,]? 119 | , 120 | , 121 | 122 | ) 123 | ``` 124 | 125 | The steps syntax is also being figured out and currently [this](https://github.com/w3c/csswg-drafts/issues/1680#issuecomment-361550637) is supported. 126 | 127 |
128 | 129 | ## Usage 130 | 131 | ```js 132 | postcss([require('postcss-easing-gradients')]) 133 | ``` 134 | 135 | See [PostCSS Usage](https://github.com/postcss/postcss#usage) docs for examples for your environment. 136 | 137 |
138 | 139 | ## Options 140 | 141 | ### colorStops: 15 142 | 143 | is the default. A lower number creates a more "low poly" gradient with less code but a higher risk of banding. 144 | 145 | ### alphaDecimals: 5 146 | 147 | is the default. A lower number can result in banding. 148 | 149 | ### colorMode: 'lrgb' 150 | 151 | is the default color space used for interpolation and is closest to what most browsers use. Other options are `'rgb', 'hsl', 'lab' and 'lch'` as per [chromajs documentation](http://gka.github.io/chroma.js/#chroma-mix) 152 | 153 | [ci-img]: https://img.shields.io/travis/larsenwork/postcss-easing-gradients.svg?branch=master&longCache=true&style=flat-square 154 | [ci]: https://travis-ci.org/larsenwork/postcss-easing-gradients 155 | [npm-img]: https://img.shields.io/npm/v/postcss-easing-gradients.svg?longCache=true&style=flat-square 156 | [npm]: https://www.npmjs.com/package/postcss-easing-gradients 157 | [dm-img]: https://img.shields.io/npm/dm/postcss-easing-gradients.svg?longCache=true&style=flat-square 158 | [dpd-img]: https://img.shields.io/david/larsenwork/postcss-easing-gradients.svg?longCache=true&style=flat-square 159 | [dpd]: https://david-dm.org/larsenwork/postcss-easing-gradients 160 | [prt-img]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?longCache=true&style=flat-square 161 | [prt]: https://github.com/prettier/prettier 162 | [mit-img]: https://img.shields.io/github/license/larsenwork/postcss-easing-gradients.svg?longCache=true&style=flat-square 163 | [mit]: https://github.com/larsenwork/postcss-easing-gradients/blob/master/LICENSE 164 | [twt-img]: https://img.shields.io/twitter/follow/larsenwork.svg?label=follow+larsenwork&longCache=true&style=flat-square 165 | [twt]: https://twitter.com/larsenwork 166 | -------------------------------------------------------------------------------- /demo/codepen-template/codepen.js: -------------------------------------------------------------------------------- 1 | const plugin = require('postcss-easing-gradients') 2 | const styles = document.head.getElementsByTagName('style') 3 | 4 | const updateStyle = style => { 5 | plugin.process(style.textContent).then(result => { 6 | style.textContent = result.css 7 | }, console.error) 8 | } 9 | 10 | if (styles.length) { 11 | Array.prototype.forEach.call(styles, updateStyle) 12 | } 13 | 14 | new MutationObserver(mutations => 15 | mutations.forEach(mutation => 16 | Array.prototype.filter 17 | .call(mutation.addedNodes || [], node => node.nodeName === 'STYLE') 18 | .forEach(updateStyle) 19 | ) 20 | ).observe(document.head, { 21 | childList: true, 22 | }) 23 | -------------------------------------------------------------------------------- /demo/codepen-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codepen-template", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "codepen.js", 6 | "scripts": { 7 | "browserify": 8 | "browserify codepen.js | babel --presets=env | uglifyjs --compress --mangle > codepen.bundle.js" 9 | }, 10 | "author": "Andreas", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "babel-cli": "^6.24.1", 14 | "babel-preset-env": "^1.4.0", 15 | "browserify": "^14.3.0", 16 | "postcss-easing-gradients": "^1.2.2", 17 | "uglify-js": "^2.8.22" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/css/styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | padding: 0; 5 | margin: 0; 6 | } 7 | 8 | body { 9 | /*background-image: linear-gradient(to right, hsl(100, 100%, 50%) 49%, hsl(200, 100%, 50%) 50%);*/ 10 | } 11 | 12 | .gradient { 13 | background: linear-gradient(to right, hsl(120, 100%, 25.1%), hsl(120, 100%, 25.1%) 25%, hsl(42.59, 100%, 28.87%) 25%, hsl(42.59, 100%, 28.87%) 50%, hsl(21.3, 100%, 40.82%) 50%, hsl(21.3, 100%, 40.82%) 75%, hsl(0, 100%, 50%) 75%, hsl(0, 100%, 50%)); 14 | background-position: center; 15 | background-repeat: no-repeat; 16 | background-size: cover; 17 | height: 50vh; 18 | width: 100vw; 19 | } 20 | 21 | .gradient--easing { 22 | background: radial-gradient(circle at top right, hsl(0, 100%, 50%), hsl(351.5, 100%, 49.51%) 9.99%, hsl(343.03, 100%, 48.11%) 19.07%, hsl(334.18, 100%, 45.93%) 27.44%, hsl(324.5, 100%, 43.03%) 35.26%, hsl(313.41, 100%, 39.49%) 42.72%, hsl(300, 100%, 35.36%) 50%, hsl(286.59, 100%, 39.49%) 57.28%, hsl(275.5, 100%, 43.03%) 64.74%, hsl(265.82, 100%, 45.93%) 72.56%, hsl(256.97, 100%, 48.11%) 80.93%, hsl(248.5, 100%, 49.51%) 90.01%, hsl(240, 100%, 50%)); 23 | width: 100vw; 24 | opacity: 1; 25 | } 26 | -------------------------------------------------------------------------------- /demo/css/styles.pcss: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | padding: 0; 5 | margin: 0; 6 | } 7 | 8 | body { 9 | /*background-image: linear-gradient(to right, hsl(100, 100%, 50%) 49%, hsl(200, 100%, 50%) 50%);*/ 10 | } 11 | 12 | .gradient { 13 | background: linear-gradient(to right, green, steps(4, skip-none), red); 14 | background-position: center; 15 | background-repeat: no-repeat; 16 | background-size: cover; 17 | height: 50vh; 18 | width: 100vw; 19 | } 20 | 21 | .gradient--easing { 22 | background: radial-gradient(circle at top right, red, ease-in-out, blue); 23 | width: 100vw; 24 | opacity: 1; 25 | } 26 | -------------------------------------------------------------------------------- /demo/gulpfile.js: -------------------------------------------------------------------------------- 1 | const browserSync = require('browser-sync').create() 2 | const gulp = require('gulp') 3 | const gulpPostcss = require('gulp-postcss') 4 | const gulpRename = require('gulp-rename') 5 | const easingGradient = require('postcss-easing-gradients') 6 | 7 | const paths = { 8 | base: './', 9 | styles: { 10 | src: './css/*.pcss', 11 | dest: './css', 12 | }, 13 | html: { 14 | src: './*.html', 15 | }, 16 | } 17 | 18 | function serve(done) { 19 | browserSync.init({ 20 | server: { baseDir: paths.base }, 21 | open: false, 22 | }) 23 | done() 24 | } 25 | 26 | function reload(done) { 27 | browserSync.reload() 28 | done() 29 | } 30 | 31 | function watch() { 32 | gulp.watch(paths.styles.src, gulp.series(css, reload)) 33 | gulp.watch(paths.html.src).on('change', browserSync.reload) 34 | } 35 | 36 | function css() { 37 | return gulp 38 | .src(paths.styles.src) 39 | .pipe( 40 | gulpPostcss([ 41 | easingGradient({ 42 | // Default settings 43 | precision: 0.1, 44 | alphaDecimals: 5, 45 | }), 46 | ]) 47 | ) 48 | .pipe(gulpRename({ extname: '.css' })) 49 | .pipe(gulp.dest(paths.styles.dest)) 50 | } 51 | 52 | gulp.task('default', gulp.series(css, serve, watch)) 53 | 54 | gulp.task('style', gulp.series(css)) 55 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CSS Easing Gradients 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pretty_css_gradients", 3 | "version": "2.0.0", 4 | "description": "Using postcss-easing-gradients to create the smoothest gradients.", 5 | "main": "index.js", 6 | "scripts": { 7 | "gulp": "./node_modules/gulp/bin/gulp.js" 8 | }, 9 | "author": "Andreas Larsen", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "browser-sync": "^2.24.7", 13 | "easing-coordinates": "^2.0.0", 14 | "gulp": "^4.0.0", 15 | "gulp-postcss": "^8.0.0", 16 | "gulp-rename": "^1.4.0", 17 | "postcss-easing-gradients": "^3.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const easingCoordinates = require('easing-coordinates') 2 | const postcss = require('postcss') 3 | const valueParser = require('postcss-value-parser') 4 | const getColorStops = require('./lib/colorStops.js') 5 | const helpers = require('./lib/helpers.js') 6 | 7 | /** 8 | * The easing gradient function is a postcss plugin which supports the in /.helpers mentioned gradient types. 9 | */ 10 | module.exports = postcss.plugin('easing-gradient', (options = {}) => { 11 | if (!options.stops) { 12 | options.stops = 13 13 | } 14 | return function(css) { 15 | css.walkRules(rule => { 16 | rule.walkDecls(decl => { 17 | // If declaration value contains a -gradient. 18 | if (decl.value.includes('-gradient')) { 19 | // Parse the declaration and walk through the nodes - https://github.com/TrySound/postcss-value-parser. 20 | const parsedValue = valueParser(decl.value) 21 | parsedValue.walk(node => { 22 | // Only modify gradient as the value can contain more e.g. 'linear-gradient(black, pink) center'. 23 | if (node.value === 'linear-gradient' || node.value === 'radial-gradient') { 24 | // Get a sensible array of gradient parameters where e.g. a function is split into multiple array items 25 | const gradientParams = valueParser 26 | .stringify(helpers.divToSemiColon(node.nodes)) 27 | .split(';') 28 | .map(str => str.trim()) 29 | 30 | gradientParams.forEach((param, i) => { 31 | if (helpers.isTimingFunction(param)) { 32 | try { 33 | const colors = [gradientParams[i - 1], gradientParams[i + 1]] 34 | const coordinates = easingCoordinates.easingCoordinates( 35 | param, 36 | options.stops - 1 37 | ) 38 | const colorStops = getColorStops( 39 | colors, 40 | coordinates, 41 | options.alphaDecimals, 42 | options.colorMode 43 | ) 44 | // Update node 45 | node.type = 'word' 46 | // Assume if it has 4 params it's because the first one is the direction 47 | if (gradientParams.length === 4) { 48 | node.value = `${node.value}(${gradientParams[0]}, ${colorStops.join(', ')})` 49 | } else { 50 | node.value = `${node.value}(${colorStops.join(', ')})` 51 | } 52 | // Update our declaration value 53 | decl.value = parsedValue.toString() 54 | } catch (error) { 55 | console.log(helpers.errorMsg(decl.value)) 56 | } 57 | } 58 | }) 59 | } 60 | }) 61 | } 62 | }) 63 | }) 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss') 2 | const plugin = require('./') 3 | 4 | function run(input, output, opts) { 5 | return postcss([plugin(opts)]) 6 | .process(input, { from: null }) 7 | .then(result => { 8 | expect(result.css).toEqual(output) 9 | expect(result.warnings()).toHaveLength(0) 10 | }) 11 | } 12 | 13 | /* eslint-disable max-len */ 14 | 15 | /** 16 | * Default output 17 | */ 18 | it('create a steps gradient with direction', () => { 19 | return run( 20 | 'a{ background: linear-gradient(to right, green, steps(4, skip-none), red); }', 21 | 'a{ background: linear-gradient(to right, hsl(120, 100%, 25.1%), hsl(120, 100%, 25.1%) 25%, hsl(42.59, 100%, 28.87%) 25%, hsl(42.59, 100%, 28.87%) 50%, hsl(21.3, 100%, 40.82%) 50%, hsl(21.3, 100%, 40.82%) 75%, hsl(0, 100%, 50%) 75%, hsl(0, 100%, 50%)); }', 22 | {} 23 | ) 24 | }) 25 | it('create a cubic bezier gradient with direction', () => { 26 | return run( 27 | 'a{ background: linear-gradient(to right, black, cubic-bezier(0.48, 0.30, 0.64, 1.00), transparent); }', 28 | 'a{ background: linear-gradient(to right, hsl(0, 0%, 0%), hsla(0, 0%, 0%, 0.9173) 11.36%, hsla(0, 0%, 0%, 0.82176) 21.57%, hsla(0, 0%, 0%, 0.71719) 30.81%, hsla(0, 0%, 0%, 0.60741) 39.26%, hsla(0, 0%, 0%, 0.49624) 47.09%, hsla(0, 0%, 0%, 0.3875) 54.5%, hsla(0, 0%, 0%, 0.28501) 61.66%, hsla(0, 0%, 0%, 0.19259) 68.74%, hsla(0, 0%, 0%, 0.11406) 75.94%, hsla(0, 0%, 0%, 0.05324) 83.43%, hsla(0, 0%, 0%, 0.01395) 91.39%, hsla(0, 0%, 0%, 0)); }', 29 | {} 30 | ) 31 | }) 32 | it('create an ease gradient with direction', () => { 33 | return run( 34 | 'a{ background: linear-gradient(to right, green, ease, red); }', 35 | 'a{ background: linear-gradient(to right, hsl(120, 100%, 25.1%), hsl(95.38, 100%, 24.58%) 5.79%, hsl(78.24, 100%, 23.69%) 10.88%, hsl(60.53, 100%, 22.47%) 15.63%, hsl(45.6, 100%, 27.55%) 20.37%, hsl(35.49, 100%, 32.35%) 25.46%, hsl(27.94, 100%, 36.66%) 31.25%, hsl(21.9, 100%, 40.44%) 38.08%, hsl(16.79, 100%, 43.67%) 46.3%, hsl(12.26, 100%, 46.31%) 56.25%, hsl(8.08, 100%, 48.29%) 68.29%, hsl(4.05, 100%, 49.55%) 82.75%, hsl(0, 100%, 50%)); }', 36 | {} 37 | ) 38 | }) 39 | it('create an ease radial-gradient', () => { 40 | return run( 41 | 'a{ background: radial-gradient(circle at top right, red, ease-in-out, blue); }', 42 | 'a{ background: radial-gradient(circle at top right, hsl(0, 100%, 50%), hsl(351.5, 100%, 49.51%) 9.99%, hsl(343.03, 100%, 48.11%) 19.07%, hsl(334.18, 100%, 45.93%) 27.44%, hsl(324.5, 100%, 43.03%) 35.26%, hsl(313.41, 100%, 39.49%) 42.72%, hsl(300, 100%, 35.36%) 50%, hsl(286.59, 100%, 39.49%) 57.28%, hsl(275.5, 100%, 43.03%) 64.74%, hsl(265.82, 100%, 45.93%) 72.56%, hsl(256.97, 100%, 48.11%) 80.93%, hsl(248.5, 100%, 49.51%) 90.01%, hsl(240, 100%, 50%)); }', 43 | {} 44 | ) 45 | }) 46 | 47 | /** 48 | * Output with custom settings 49 | */ 50 | it('create a cubic bezier gradient with 1 alphaDecimal', () => { 51 | return run( 52 | 'a{ background: linear-gradient(black, cubic-bezier(0.48, 0.30, 0.64, 1.00), transparent); }', 53 | 'a{ background: linear-gradient(hsl(0, 0%, 0%), hsla(0, 0%, 0%, 0.9) 11.36%, hsla(0, 0%, 0%, 0.8) 21.57%, hsla(0, 0%, 0%, 0.7) 30.81%, hsla(0, 0%, 0%, 0.6) 39.26%, hsla(0, 0%, 0%, 0.5) 47.09%, hsla(0, 0%, 0%, 0.4) 54.5%, hsla(0, 0%, 0%, 0.3) 61.66%, hsla(0, 0%, 0%, 0.2) 68.74%, hsla(0, 0%, 0%, 0.1) 75.94%, hsla(0, 0%, 0%, 0.1) 83.43%, hsla(0, 0%, 0%, 0) 91.39%, hsla(0, 0%, 0%, 0)); }', 54 | { alphaDecimals: 1 } 55 | ) 56 | }) 57 | it('create a cubic-bezier gradient with 5 stops', () => { 58 | return run( 59 | 'a{ background: linear-gradient(black, cubic-bezier(0.48, 0.30, 0.64, 1.00), transparent); }', 60 | 'a{ background: linear-gradient(hsl(0, 0%, 0%), hsla(0, 0%, 0%, 0.71719) 30.81%, hsla(0, 0%, 0%, 0.3875) 54.5%, hsla(0, 0%, 0%, 0.11406) 75.94%, hsla(0, 0%, 0%, 0)); }', 61 | { stops: 5 } 62 | ) 63 | }) 64 | it('create an ease gradient with hsl colorMode', () => { 65 | return run( 66 | 'a{ background: linear-gradient(to right, green, ease, red); }', 67 | 'a{ background: linear-gradient(to right, hsl(120, 100%, 25.1%), hsl(115.04, 100%, 26.08%) 5.79%, hsl(106.9, 100%, 27.84%) 10.88%, hsl(96.08, 100%, 30%) 15.63%, hsl(83.71, 100%, 32.75%) 20.37%, hsl(69.61, 100%, 35.49%) 25.46%, hsl(55.71, 100%, 38.43%) 31.25%, hsl(41.52, 100%, 41.37%) 38.08%, hsl(28.53, 100%, 44.12%) 46.3%, hsl(16.96, 100%, 46.47%) 56.25%, hsl(8.05, 100%, 48.24%) 68.29%, hsl(2.13, 100%, 49.61%) 82.75%, hsl(0, 100%, 50%)); }', 68 | { colorMode: 'hsl' } 69 | ) 70 | }) 71 | /** 72 | * Ignore incorrect/unsuported input 73 | */ 74 | it('ignore unsuported gradients', () => { 75 | return run( 76 | 'a{ background: linear-gradient(black, funky-ease, transparent); }', 77 | 'a{ background: linear-gradient(black, funky-ease, transparent); }', 78 | {} 79 | ) 80 | }) 81 | it('ignore gradients with color stop locations set', () => { 82 | return run( 83 | 'a{ background: linear-gradient(black 20px, cubic-bezier(0.48, 0.30, 0.64, 1.00), transparent); }', 84 | 'a{ background: linear-gradient(black 20px, cubic-bezier(0.48, 0.30, 0.64, 1.00), transparent); }', 85 | {} 86 | ) 87 | }) 88 | 89 | /** 90 | * Fallback to linear gradient when incorrect transition functions 91 | */ 92 | it('ignore gradients with incorrect transition function syntax set', () => { 93 | return run( 94 | 'a{ background: linear-gradient(black, cubic-bezier(0.48, 0.30, 0.64), transparent); }', 95 | 'a{ background: linear-gradient(black, cubic-bezier(0.48, 0.30, 0.64), transparent); }', 96 | {} 97 | ) 98 | }) 99 | -------------------------------------------------------------------------------- /lib/colorStops.js: -------------------------------------------------------------------------------- 1 | const chroma = require('chroma-js') 2 | const helpers = require('./helpers.js') 3 | 4 | /** 5 | * Calculate the color stops based on start+stopColor in an array and easingType 6 | * @param {array} colors Two colors in an array 7 | * @param {array} coordinates An array of coordinates (object with x and y keys) 8 | * @param {number} alphaDecimals How many decimals should be on the returned color values 9 | * @param {string} colorMode Color space used for color interpolation http://gka.github.io/chroma.js/#chroma-mix 10 | * @returns {array} An array of colorstops (a string with color and position) 11 | */ 12 | module.exports = (colors, coordinates, alphaDecimals = 5, colorMode = 'lrgb') => { 13 | const colorStops = [] 14 | colors = helpers.transparentFix(colors) 15 | coordinates.forEach(coordinate => { 16 | const ammount = coordinate.y 17 | const percent = coordinate.x * 100 18 | let color = chroma.mix(colors[0], colors[1], ammount, colorMode).css('hsl') 19 | color = helpers.roundHslAlpha(color, alphaDecimals) 20 | if (Number(coordinate.x) !== 0 && Number(coordinate.x) !== 1) { 21 | colorStops.push(`${color} ${+percent.toFixed(2)}%`) 22 | } else { 23 | colorStops.push(color) 24 | } 25 | }) 26 | return colorStops 27 | } 28 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | const chroma = require('chroma-js') 2 | 3 | // https://developer.mozilla.org/docs/Web/CSS/single-transition-timing-function 4 | const stepsFunction = 'steps' 5 | const cubicFunction = 'cubic-bezier' 6 | const easeShorthands = ['ease', 'ease-in', 'ease-out', 'ease-in-out'] 7 | const timingFunctions = [...easeShorthands, cubicFunction, stepsFunction] 8 | 9 | /** 10 | * Test if a string has a parenthesis 11 | * @param {String} str A text string 12 | * @returns {Boolean} If a string has a "(" 13 | */ 14 | const hasParenthesis = str => str.indexOf('(') !== -1 15 | 16 | /** 17 | * Get substring before first parenthesis in a string 18 | * @param {String} str A text string 19 | * @returns {String} The substring if the provided string has a parenthesis. Otherwise the string. 20 | */ 21 | const getBeforeParenthesisMaybe = str => 22 | hasParenthesis(str) ? str.substring(0, str.indexOf('(')) : str 23 | 24 | /** 25 | * Test if a string matches a css timing function 26 | * @param {String} str A text string 27 | * @returns {Boolean} If the string is a timing function 28 | */ 29 | exports.isTimingFunction = str => timingFunctions.includes(getBeforeParenthesisMaybe(str)) 30 | 31 | /** 32 | * Get insides of a parenthesis 33 | * @param {String} str A text string 34 | * @returns {String} The substring within the parenthesis 35 | * Note: It's also used in this file so declared first and exported after 36 | */ 37 | const getParenthesisInsides = str => str.match(/\((.*)\)/).pop() 38 | exports.getParenthesisInsides = getParenthesisInsides 39 | 40 | /** 41 | * If a color is transparent then convert it to a hsl-transparent of the paired color 42 | * @param {Array} colors An array with two colors 43 | * @returns {Object} A color as chroma object 44 | */ 45 | exports.transparentFix = colors => 46 | colors.map((color, i) => { 47 | return color === 'transparent' 48 | ? chroma(colors[Math.abs(i - 1)]) 49 | .alpha(0) 50 | .css('hsl') 51 | : color 52 | }) 53 | 54 | /** 55 | * Change value to ';' on child objects of type 'div' 56 | * @param {Array.} obj An array of objects 57 | * @returns {Array.} An array of objects 58 | */ 59 | exports.divToSemiColon = obj => { 60 | obj.map(childObj => { 61 | if (childObj.type === 'div') { 62 | childObj.value = ';' 63 | } 64 | return childObj 65 | }) 66 | return obj 67 | } 68 | 69 | /** 70 | * Round alpha in hsl colors to alphaDecimals 71 | * @param {String} color As an hsl value 72 | * @param {Number} alphaDecimals An integer specifying max number of deicamals 73 | * @returns {String} A alpha value rounded version of the hsl color string 74 | */ 75 | exports.roundHslAlpha = (color, alphaDecimals) => { 76 | const prefix = getBeforeParenthesisMaybe(color) 77 | const values = getParenthesisInsides(color) 78 | .split(',') 79 | .map( 80 | string => 81 | string.indexOf('%') === -1 ? +Number(string).toFixed(alphaDecimals) : string.trim() 82 | ) 83 | color = `${prefix}(${values.join(', ')})` 84 | return color 85 | } 86 | 87 | /** 88 | * Wrap a string telling the user we couldn't parse it 89 | * @param {String} input A string 90 | * @returns {String} The full error message wrapped around the string 91 | */ 92 | exports.errorMsg = input => { 93 | return `Couldn't parse: 94 | ${input} 95 | Check the syntax to see if it's correct/supported.` 96 | } 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-easing-gradients", 3 | "version": "3.0.1", 4 | "engines": { 5 | "node": ">=6.0.0" 6 | }, 7 | "description": "PostCSS plugin to create smooth linear-gradients that approximate easing functions.", 8 | "scripts": { 9 | "test": "jest && eslint *.js", 10 | "lintfix": "eslint --fix --ext .js --ignore-path .gitignore ." 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/larsenwork/postcss-easing-gradients.git" 15 | }, 16 | "keywords": [ 17 | "postcss", 18 | "postcss-plugin", 19 | "css", 20 | "gradients" 21 | ], 22 | "author": "Andreas Larsen (https://larsenwork.com)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/larsenwork/postcss-easing-gradients/issues" 26 | }, 27 | "homepage": "https://github.com/larsenwork/postcss-easing-gradients", 28 | "dependencies": { 29 | "chroma-js": "^1.3.7", 30 | "easing-coordinates": "^2.0.0", 31 | "postcss": "^7.0.2", 32 | "postcss-value-parser": "^3.3.0" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^5.5.0", 36 | "eslint-config-logux": "^25.0.1", 37 | "eslint-config-postcss": "^3.0.6", 38 | "eslint-config-prettier": "^3.0.1", 39 | "eslint-config-standard": "^12.0.0", 40 | "eslint-plugin-import": "^2.14.0", 41 | "eslint-plugin-jest": "^21.22.0", 42 | "eslint-plugin-node": "^7.0.1", 43 | "eslint-plugin-prettier": "^2.6.2", 44 | "eslint-plugin-promise": "^4.0.0", 45 | "eslint-plugin-security": "^1.4.0", 46 | "eslint-plugin-standard": "^4.0.0", 47 | "jest": "^23.5.0", 48 | "prettier": "^1.14.2", 49 | "prettier-eslint": "^8.8.2" 50 | }, 51 | "eslintConfig": { 52 | "plugins": [ 53 | "prettier" 54 | ], 55 | "env": { 56 | "jest": true 57 | }, 58 | "parserOptions": { 59 | "ecmaVersion": 6 60 | }, 61 | "extends": [ 62 | "plugin:prettier/recommended" 63 | ], 64 | "rules": { 65 | "prettier/prettier": [ 66 | "error", 67 | { 68 | "singleQuote": true, 69 | "trailingComma": "es5", 70 | "parser": "flow" 71 | } 72 | ], 73 | "max-len": [ 74 | "error", 75 | 120 76 | ], 77 | "func-style": [ 78 | "error", 79 | "declaration", 80 | { 81 | "allowArrowFunctions": true 82 | } 83 | ] 84 | } 85 | }, 86 | "prettier": { 87 | "semi": false, 88 | "singleQuote": true, 89 | "trailingComma": "es5", 90 | "printWidth": 100 91 | } 92 | } 93 | --------------------------------------------------------------------------------