├── .travis.yml
├── .gitignore
├── .npmignore
├── demo
├── index.html
├── codepen-template
│ ├── package.json
│ └── codepen.js
├── package.json
├── css
│ ├── styles.pcss
│ └── styles.css
└── gulpfile.js
├── LICENSE
├── lib
├── colorStops.js
└── helpers.js
├── package.json
├── index.js
├── index.test.js
└── README.md
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - stable
4 | sudo: false
5 | branches:
6 | only:
7 | - master
8 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | CSS Easing Gradients
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.